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".
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).
