Uruchomienie cyklicznego zadania w EJB 3.1 ze @Schedule

Z Jacek Laskowski - Wiki Projektanta Java EE

Pomyślałby kto, że uruchomienie zadania w EJB 3.1 co 5 sekund wymaga jedynie stworzenia bezstanowego ziarna sesyjnego z metodą opatrzoną adnotacją @Schedule(second = "*/5", info="Every 5 seconds") i gotowe. Nic bardziej mylnego! Okazuje się, że domyślne wartości dla atrybutów @Schedule to zerowa sekunda, minuta i godzina, więc brak ich określenia sprawi, że kolejne uruchomienie będzie o północy następnego dnia. Można się nieźle przejechać podczas wdrożenia.

W tym artykule zestawię kompletny projekt zarządzany przez Apache Maven 3.0.3 i z wykorzystaniem wbudowanego kontenera EJB 3.1 (w tej roli GlassFish Server Open Source Edition 3.1.1) uruchomię zadanie - metodę ziarna bezstanowego - co zadany interwał, w naszym przykładzie będzie to "co 5 sekund".

Spis treści

Stworzenie struktury projektowej z Apache Maven - mvn archetype:generate

Zacznę standardowo - skorzystam z Apache Maven i jego zadania archetype:generate (byłbym wdzięczny za każdą wskazówkę, jak to usprawnić, a jednocześnie nie nakładać na siebie karbów instalacji i poznawania kombajnu w stylu IDE - może coś w stylu Gradle?).

$ mvn -v
Apache Maven 3.0.3 (r1075438; 2011-02-28 18:31:09+0100)
Maven home: /Users/jacek/apps/maven
Java version: 1.6.0_26, vendor: Apple Inc.
Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
Default locale: en_US, platform encoding: MacRoman
OS name: "mac os x", version: "10.6.8", arch: "x86_64", family: "mac"

Uruchamiam polecenie mvn archetype:generate z opcją -B - utworzenia projektu w trybie wsadowym (nieinteraktywnym).

mvn archetype:generate -B \
    -DarchetypeGroupId=org.codehaus.mojo.archetypes \
    -DarchetypeArtifactId=ejb-javaee6 \
    -DgroupId=pl.japila.jee6 \
    -DartifactId=ejb31-timer-examples \
    -Dversion=1.0

Do utworzenia projektu skorzystałem z archetypu org.codehaus.mojo.archetypes.ejb-javaee6, co znacznie uprościło odpowiednią konfigurację Mavena. W zasadzie wszystko, co konieczne już jest w pom.xml (sercu projektu mavenowego) - packaging, JUnit, project.build.sourceEncoding, maven-compiler-plugin oraz najważniejsza konfiguracja maven-ejb-plugin.

W ten sposób utworzyłem strukturę projektu ejb31-timer-examples i on, od tej pory, staje się katalogiem bieżącym.

$ cd ejb31-timer-examples/

Deklaracja zależności - GlassFish 3.1.1

Zmieniam zawartość pom.xml o zależności GlassFish 3.1.1 ze wskazaniem na repozytorium, w którym się znajdują. Skorzystałem z Embeddable EJB 3.1 z GlassFish 3.1 i NetBeans IDE 7.0, a szczególnie rozdziałów, w których opisałem zależności projektowe oraz samo uruchomienie testu z wbudowanym kontenerem EJB 3.1, którym jest GlassFish 3.1.1.

Dodatkowo, zdefiniowałem profil glassfish-ejb-logging, który włącza komunikaty związane z wbudowanym kontenerem EJB 3.1.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>pl.japila.jee6</groupId>
  <artifactId>ejb31-timer-examples</artifactId>
  <version>1.0</version>
  <packaging>ejb</packaging>
  <name>ejb31-timer-examples EJB</name>
  <url>http://www.jaceklaskowski.pl/wiki</url>
  <properties>
    <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <glassfish.version>3.1.1</glassfish.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>javax.ejb</artifactId>
      <version>${glassfish.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.extras</groupId>
      <artifactId>glassfish-embedded-all</artifactId>
      <version>${glassfish.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <repositories>
    <repository>
      <id>glassfish-repository</id>
      <url>http://download.java.net/maven/glassfish</url>
    </repository>
  </repositories>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
          <compilerArguments>
            <endorseddirs>${endorsed.dir}</endorseddirs>
          </compilerArguments>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-ejb-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <ejbVersion>3.1</ejbVersion>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.1</version>
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>copy</goal>
            </goals>
            <configuration>
              <outputDirectory>${endorsed.dir}</outputDirectory>
              <silent>true</silent>
              <artifactItems>
                <artifactItem>
                  <groupId>javax</groupId>
                  <artifactId>javaee-endorsed-api</artifactId>
                  <version>6.0</version>
                  <type>jar</type>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
    <finalName>ejb31-timer-examples</finalName>
  </build>
  <profiles>
    <profile>
      <id>glassfish-ejb-logging</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.7.2</version>
            <configuration>
              <argLine>-Djava.util.logging.config.file=target/test-classes/logging.properties</argLine>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

Stworzenie bezstanowego ziarna sesyjnego - pl.japila.jee6.Worker

Na potrzeby moich doświadczeń stworzyłem bezstanowe ziarno sesyjne pl.japila.jee6.Worker w katalogu src/main/java/pl/japila/jee6.

package pl.japila.jee6;
 
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.ejb.Timer;
import java.util.Calendar;
 
@Stateless
public class Worker {
    @Schedule(second = "*/5", info="Every 5 seconds")
    void run(Timer timer) {
        Calendar c = Calendar.getInstance();
        System.out.format("[%tl:%tM:%tS] Working on...%n", c, c, c, timer.getInfo());
    }
}

Z użyciem adnotacji @Schedule(second = "*/5", info="Every 5 seconds") wiązałem nadzieje, że zadanie zostanie uruchomione co 5 sekund, począwszy od chwili uruchomienia ziarna przez kontener EJB 3.1.

Stworzenie klasy testującej - pl.japila.jee6.WorkerTest

Poza ziarnem sesyjnym stworzyłem jeszcze klasę testującą pl.japila.jee6.WorkerTest w katalogu src/test/java/pl/japila/jee6.

package pl.japila.jee6;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class WorkerTest {
    EJBContainer ejbContainer;
    Context ctx;
 
    @Before
    public void setUp() {
 
        // Uruchomienie wbudowanego kontenera EJB 3.1
        ejbContainer = EJBContainer.createEJBContainer();
 
        // Dostanie się do kontekstu JNDI
        ctx = ejbContainer.getContext();
    }
 
    @Test
    public void testRun() throws Exception {
        Worker calculator = (Worker) ctx.lookup("java:global/classes/Worker");
 
        // daj szansę na uruchomienie stopera - 15 sekund daje odpowiednie okienko czasowe
        Thread.sleep(1000 * 15);
    }
 
    @After
    public void tearDown() {
        try {
            if (ctx != null) {
                ctx.close();
            }
        } catch (NamingException ex) {
            // handle error
        }
        if (ejbContainer != null) {
            ejbContainer.close();
        }
    }
}

Stworzenie pliku konfiguracyjnego do śledzenia wykonania GlassFish - logging.properties

Tworzę plik konfiguracyjny logging.properties w katalogu src/test/resources, który używam w profilu glassfish-ejb-logging do obniżenia poziomu komunikatów z GlassFish'a.

handlers= java.util.logging.ConsoleHandler
 
java.util.logging.ConsoleHandler.level=ALL
javax.enterprise.system.container.ejb.level=ALL

Poziomy śledzenia wykonania GlassFish'a zaczerpnąłem z The Logger Namespace Hierarchy (z Sun GlassFish Enterprise Server 2.1 Administration Guide).

Uruchomienie testów - mvn test

Z tak przygotowanym projektem miałem nadzieję zobaczyć uruchomioną metodę void run(Timer timer) co 5 sekund od momentu uruchomienia. Jakież było moje zdumienie, kiedy podczas pierwszego uruchomienia okazało się, że GlassFish zgłosił pierwsze uruchomienie na...północ następnego dnia?! Zwróć uwagę na włączone śledzenie podając parametr -Pglassfish-ejb-logging, który włącza profil mavenowy glassfish-ejb-logging.

$ mvn -Pglassfish-ejb-logging clean test
...
Aug 20, 2011 8:16:29 PM com.sun.ejb.containers.EJBTimerService createTimer
FINE: @@@ Created timer [1@@1313864189031@@server@@gfembed6198285410428554402tmp] with the first expiration set to: Sun Aug 21 00:00:00 CEST 2011

Odpowiedź znalazłem w rozdziale 18.2.1.2 Expression Rules specyfikacji EJB 3.1 - wystarczy użyć @Schedule(second = "*/5", minute = "*", hour = "*", info="Every 5 seconds") i temat obsłużony.

$ mvn clean test
...
INFO: Portable JNDI names for EJB Worker : [java:global/classes/Worker!pl.japila.jee6.Worker, java:global/classes/Worker]
Aug 20, 2011 8:29:55 PM com.sun.jts.CosTransactions.DefaultTransactionService setServerName
INFO: JTS5014: Recoverable JTS instance, serverId = [100]
Aug 20, 2011 8:29:55 PM org.glassfish.deployment.admin.DeployCommand execute
INFO: classes was successfully deployed in 14,016 milliseconds.
PlainTextActionReporterSUCCESSDescription: deploy AdminCommandApplication deployed with name classes.
    [name=classes
[8:29:56] Working on...
[8:30:00] Working on...
[8:30:05] Working on...
[8:30:10] Working on...

Sprawdź i raportuj wszelkie niezgodności (pomijając fakt braku wyświetlania informacji o zadaniu, które najwyraźniej nie jest wspierane przez GlassFish 3.1.1).

Osobiste