Časovače, Java a CzechIdM – 1. část

Toto je první část příspěvku zaměřeného na časovače a časové služby v programovacím jazyce Java. V dnešním příspěvku si na úvod uvedeme dva příklady, kde najdou časovače své uplatnění. První je spíše obecného charakteru, druhý je již specifický a popisuje použití časovačů v našem systému CzechIdM. Poté si ukážeme, jak časovat úlohy v Java SE a Java EE 5 a 6 s použitím EJB kontejneru.

Příští příspěvek bude zaměřen více specificky, a to na jBPM časovače a na projekt Quartz. Zároveň si jednotlivé možnosti shrneme a porovnáme. Nezbývá než začít.

 

Příklady použití časovačů

Prakticky každý obchodní informační systém generuje v nějaké formě přehledy a statistiky, které jsou jejich uživateli vyžadovány. Příkladem mohou být například skladové systémy, kde se v pravidelných intervalech mohou generovat například skladové přehledy. Tyto systémy pracují s velkým objemem dat, a proto vygenerování reportů může systém a příslušnou databázi dost zatěžovat. Tato výkonnostní ztráta může způsobovat reakční prodlevu při klasické uživatelské práci, což jistě není pro uživatele příjemné. Proto by bylo lepší je generovat v době, kdy není systém využíván uživateli. A právě pro spouštění procesu generování reportů se uplatní časovače.

 

Dalším příkladem využití časovačů může být náš identity manager CzechIdM, kde se časovače používají pro:

  • spouštění workflow (procesy, jejichž zdrojové kódy jsou uloženy v databázi a vykonávají specifické úkony; dají se v průběhu přerušit, přičemž je aktuální stav uložen do databáze),
  • synchronizaci a propagaci informací o uživatelích z autoritativního zdroje do CzechIdM a koncových systémů,
  • rekonciliaci, tj. porovnání identit v CzechIdM s účty na koncových systémech dle korelačních pravidel.

 

Podívejme se na konkrétní příklad použití časovače v Identity Manageru. Ve společnosti dojde k ukončení pracovního vztahu se zaměstnancem. Personalista zanese tuto skutečnost do personálního systému (berme zde jako autoritativní zdroj informací). CzechIdM dá všechny účty daného uživatele na koncových systémech do karantény. Karanténa označuje stav, kdy jsou účty zablokovány, ale nejsou ještě smazány. To se provádí z toho důvodu, aby mohly být případně v daném časovém intervalu obnoveny (například v případě chybného ukončení pracovního vztahu). Po vypršení karantény mohou být účty již trvale smazány. A právě spuštěním procesu odstraňujícího účty po vypršení karantény je pověřen příslušný časovač. Dost již bylo příkladů, nyní zpět k hlavnímu tématu.

Nejjednodušší časovače v Javě SE

Od verze J2SE 1.3 jsou v Javě obsaženy třídy java.util.Timer a java.util.TimerTask. Tyto třídy slouží právě k jednoduchému časování úloh. Podívejme se nejdříve na vzorový příklad.

package eu.bcvsolutions.main;

import java.util.Calendar;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Třída pro generovaní statistik.
 *
 * @author Jarda Mlejnek
 *
 */
public class StatisticGenerator extends TimerTask {

	public void run() {
		System.out.println("Executing a time consuming task.");

		//TODO Application code ...
	}

}

/**
 * Hlavní třída aplikace
 *
 * @author Jarda Mlejnek
 *
 */
class MainApplication {

	public static void main(String[] argv) {
		// Vytvoříme časovač
		Timer timer = new Timer();

		// Určíme čas vypršení časovače - pátek, 22:30:00
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
		cal.set(Calendar.HOUR, 10);
		cal.set(Calendar.MINUTE, 30);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.AM_PM, Calendar.PM);

		// Vytvoříme rozvrh (spouštění tasku definovaného třídou "StatisticGenerator", každý pátek v 22:30).
		timer.schedule(new StatisticGenerator(), cal.getTime(), 1000 * 60 * 60 * 24 * 7);
	}

}

Třída StatisticGenerator rozšiřuje abstraktní třídu TimerTask, přičemž překrývá metodu run. Zde se zapisuje kód, který se má provézt po vypršení časovače.  Dále je potřeba vytvořit samotný časovač, nastavit potřebný timeout a přiřadit mu instanci naší třídy StatisticGenerator, tj. vytvořit rozvrh. K tomu slouží metoda schedule, kde posledním parametrem je v milisekundách uvedená perioda opakování časované úlohy. V našem vzorovém případě je perioda týden. Signatur metod schedule je více. Bližší informace lze dohledat v JavaDocu příslušné verze Javy.

 

Podívejme se ještě na některé technické aspekty těchto časovačů. Jelikož je Java multiplatformní , tak není možné garantovat skutečnost, že na všech platformách bude časovaná akce spuštěna ve stejný čas. Úlohy (TimerTask) jsou implementovány jako Runable objekty. To znamená, že jsou jsou uspány. Timer je poté v daný čas probudí, ale přesný čas, kde se spustí akce definované v metodě run, záleží na rozvrhovací politice JVM a na tom, kolik vláken čeká na procesoru na vykonání. Z tohoto důvodu se tyto časovače nehodí pro real-time systémy, kde je potřeba pracovat s časem přesně. To je však víceméně problém všech zde popsaných technologií. Při použití časovačů dle výše popsaných příkladů není potřeba pracovat na milisekundy přesně.

Zpoždění může být způsobeno dvěma hlavními problémy:

  1. velké množství vláken čeká na vykonání,
  2. zpoždění může být způsobeno garbage collectorem.
Závěrem bychom měli ještě konstatovat, že tyto časovače startují jako vlákna běžící na pozadí prováděné aplikace. To znamená, že není možné je používat v řízeném prostředí (např. kontejnerech aplikačních serverů Java EE), protože nejsou v rozsahu daného kontejneru. Zároveň nepodporují žádným implicitním způsobem persistenci. Uplatní se tedy pouze v jednoduchých desktopových aplikacích, případně mohou být použity pro časování interakcí v GUI.

Časové služby poskytované EJB v Java EE 5

Pokud potřebujeme použít časovače v aplikaci postavené na technologii EJB (od verze 2.1), tak můžeme využít přímo služby poskytované Javou EE 5. Verze EJB 3 přicházející s dependency injection a anotacemi dále zjednodušuje vytváření a práci s těmito časovači. Podporovány jsou opět i intervalové časovače s danou periodou opakování (stejně jako o předchozí technologie). Hlavním rozdílem oproti časovačům v Java SE je to, že tyto časovače běží na úrovni aplikačního serveru. Dále je možné nastavit persistenci těchto časovačů s tím, že pokud dojde k pádu aplikačního serveru a timeout časovače vyprší během výpadku serveru, tak se časovaná akce provede ihned po nastartování aplikačního serveru.

 

Podívejme se opět na příklad použití. Předpokládejme, že aplikačním serverem je JBoss AS 5.1 a je použito EJB 3.1.

package eu.bcvsolutions.examples.ejbtimer;

import java.io.Serializable;
import java.util.Date;

import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerHandle;
import javax.ejb.TimerService;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;

@Stateless
public class SchedulerBean implements Scheduler {

	protected static Log log = Logging.getLog(SchedulerBean.class);

	@Resource
	protected TimerService timerService;

	@Timeout
	@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
	public void onTimeout(Timer timer) {

                Serializable info = timer.getInfo();
		if (info instanceof Integer[]) {

                      // Spustíme příslušnou úlohu
                      Integer schedulerTask = ((Integer[]) info)[0];
                      executeSchedulerTask(schedulerTaskId, timer);

                } else {

                      // Zrušíme časovač a zaloguje chybu.
                      timer.calcel();
                      log.wand("Timer: Bad object type.")

                }
	}

        protected void disableTimer(SchedulerTask task) {
		Timer timer = getTimerFromTask(task);
		if (timer != null) {
			timer.cancel();
		}
		task.setTimerHandle(null);
	}

	protected void createTimer(SchedulerTask task) {
                Integer[] taskId = task.getTaskId();
                long intervalDuration = task.getIntervalDuration();
                Date initialExpiration = task.getInitialExpiration();

                Timer timer = null;

                if (intervalDuration != null && intervalDuration > 0) {
                        timer = this.timerService.createTimer(
					initialExpiration, intervalDuration, taskId
			);
                } else {
                        timer = this.timerService.createTimer(
					initialExpiration, taskId
			);
                }

                task.setTimerHandle(timer.getHandle());
	}

        private Timer getTimerFromTask(SchedulerTask task) {
                return task.getTimerHandle().getTimer();
        }

}

Jedná se o jednoduchou stateless beanu, která implementuje metody pro vytvoření časovače pro danou úlohu typu SchedulerTask (metoda createTimer), pro zrušení časovače u dané úlohy (metoda disableTimer) a nakonec pro spuštění příslušné úlohy . Pro vytvoření časovače je potřeba injektovat třídu TimerService jako zdroj. Při vytváření časovače se určí jeho id, čas expirace, případně interval do další expirace. Nakonec se dané úloze, která má být vykonána (v našem případě instance třídy SchedulerTask) přiřadí objekt typu TimerHandle, přes který lze získat zpětně referenci na přiřazený časovač. Tuto referenci bychom využili, pokud bychom chtěli zrušit timer spouštějící danou úlohu.

 

Metoda anotována anotací @OnTimeout smí být v každé třídě, ve které se pracuje s časovači, pouze jedna a vždy musí jako vstupní atribut přijímat objekt třídy javax.ejb.Timer a mít návratovou hodnotu void. Obsah této metody je spuštěn po vypršení časovače. V našem případě to znamená, že se po expiraci určí ID plánované úlohy a úloha se spustí. Pokud není ID úlohy v časovači uloženo, tak se jedná o chybu a časovač se zruší.

Konfigurace EJB timerů se provádí v souboru $JBOSS_HOME/{server_type}/deploy/ejb2-timer-service.xml.

Implicitně jsou časovače persistentní, tj. ukládají se do interní databáze JBoss aplikačního serveru, a to konkrétně do tabulky PUBLIC.TIMERS (to lze v dané konfiguračním souboru změnit). Persistence lze zrušit zakomentováním

<!--
    A persistence policy that persists timers to a database.

    The 2 supported db persistence plugins are:
      org.jboss.ejb.txtimer.GeneralPurposeDatabasePersistencePlugin
      org.jboss.ejb.txtimer.OracleDatabasePersistencePlugin

    The table name defaults to "TIMERS". It can be overriden using the
    'TimersTable' attribute if the persistence plugin supports it.
    When overriding the timers table, an optional schema can be specified
    using the syntax [schema.]table
  -->
  <mbean code="org.jboss.ejb.txtimer.DatabasePersistencePolicy" name="jboss.ejb:service=EJBTimerService,persistencePolicy=database">
    <!-- DataSourceBinding ObjectName -->
    <depends optional-attribute-name="DataSource">jboss.jca:service=DataSourceBinding,name=DefaultDS</depends>
    <!-- The plugin that handles database persistence -->
    <attribute name="DatabasePersistencePlugin">org.jboss.ejb.txtimer.GeneralPurposeDatabasePersistencePlugin</attribute>
    <!-- The timers table name -->
    <attribute name="TimersTable">TIMERS</attribute>
    <depends>jboss.jdbc:datasource=DefaultDS,service=metadata</depends>
  </mbean>

a odkomentováním

  <!-- A persistence policy that does not persist the timer
  <mbean code="org.jboss.ejb.txtimer.NoopPersistencePolicy" name="jboss.ejb:service=EJBTimerService,persistencePolicy=noop"/>
  -->

A co Java EE 6?

Java EE 6 dále rozšiřuje možnosti Javy EE 5 také v oblasti časovačů. Poskytuje dva druhy enterprise časovačů, a to:

  1. programovatelné časovače – založeny na časovačích z Java EE 5; explicitně jsou volány metody pro vytvoření časovače; již nebudeme opakovat,
  2. automatické časovače – časovače jsou automaticky vytvořeny po nasazení aplikace na aplikační server; časovače jsou vytvořeny pro metody, které jsou anotovány anotacemi java.ejb.Schedule nebo java.ejb.Schedules.
Konfigurace časovačů je popsána buď anotacemi, nebo v deskriptoru ejb-jar.xml.

Příklady automatických časovačů:

Metoda cleanupData bude spuštěna každou neděli v poledne.

@Schedule(dayOfWeek="Sun", hour="12")
public void cleanupData() { ... }

Časové výrazy se dají také kombinovat, viz. následující příklad.

@Schedules ({
    @Schedule(dayOfMonth="Last"),
    @Schedule(dayOfWeek="Fri", hour="23")
})
public void doPeriodicCleanup() { ... }

Závěrem první části

V první části jsme se podívali na základní časování úloh v Java SE a Java EE. Příště se již podíváme na specifičtější metody (to však neznamená lepší nebo horší :-)). Nenechte si to ujít.

 

 

Zdroje informací

  1. The Java EE 5 Tutorial, Dostupné online na: http://download.oracle.com/javaee/5/tutorial/doc/
  2. The Java EE 6 Tutorial, Dostupné online na: http://download.oracle.com/javaee/6/tutorial/doc/
  3. JBoss Application Server Documentation, Dostupné online na: http://docs.jboss.org/jbossas/docs/Installation_And_Getting_Started_Guide/5/html/index.html