Hlavní navigace

JBoss: EJB a transakce

18. 5. 2009
Doba čtení: 11 minut

Sdílet

Transakce je pojem relativně často zmiňovaný a má to svůj dobrý důvod. Transakce by měla být základním kamenem J2EE aplikací. Tento díl představuje transakce v aplikačním serveru JBoss z pohledu business komponent. Ukážeme si, jakým způsobem můžeme využít server pro automatické řízení běhu transakcí.

Co to je, když se řekne transakce

V poslední době jsem měl možnost položit tuto otázku širokému okruhu vývojářů a překvapilo mě, jak málo z nich na ni dokázalo uspokojivě odpovědět. Začněme tedy napřed s tím, co to transakce je. Z intuitivního úhlu pohledu je transakce posloupnost operací vykonaných jako jedna jediná. Nejčastější použití je aktualizace databáze. Například přesun peněz z jednoho účtu na jiný. Odebrání částky na jednom a přidání na druhý účet musí proběhnout jako jedna jediná celistvá operace. Nesmí dojít k přerušení uprostřed, po odebrání částky z prvního účtu. Z formálního hlediska musí transakce splňovat čtyři vlastnosti označované často jako ACID:

  • Atomicita (Atomicity) – buď se provedou všechny operace z transakce, nebo žádná z nich
  • Konzistence (Consistency) – běh transakce zachovává konzistenci zdrojů (např. databáze splňuje integritní omezení), v průběhu transakce může dojít k nekonzistenci, nikoliv však po jejím ukončení
  • Izolovanost (Isolation) – ačkoliv může více transakcí běžet naráz, nesmí o sobě navzájem vědět a musí před ostatními skrývat dočasné mezivýsledky (míra tohoto skrývání může být různá a hovoříme zde o úrovních transakční izolovanosti)
  • Trvanlivost (Durability) – po úspěšném ukončení transakce jsou všechny změny trvale uloženy a uloženy zůstanou i při následném výpadku systému

Záměrně jsem ve druhém bodu napsal zdroje. Transakce totiž může řídit práci s libovolnými zdroji, které to podporují. Například Java Message Service umožňuje zaručené zasílání zpráv v rámci transakce. Můžeme například poslat několik zpráv do fronty v rámci transakce, ale příjemce je uvidí až ve chvíli, kdy transakci potvrdíme – v tuto chvíli se změny provedené transakcí stanou trvalé. Kdyby doručení jedné zprávy do fronty selhalo, nedošlo by k doručení žádné z nich a transakce by byla odvolána (rollback).

Pokud má libovolný zdroj, komponenta, služba podporovat standardní transakce, měla by implementovat potřebná rozhraní z Java Transaction API. JTA dokonce podporuje distribuované transakce a běh jedné transakce nad různými zdroji (databáze, JMS, EJB…). JBoss AS používá jako implementaci JTA knihovnuJBoss Transactions, původní projekt společností Arjuna Technologies a HP. V tuto chvíli nebudu zabíhat do detailů, protože tomuto projektu bude určitě věnován samostatný díl seriálu.

Transakce z pohledu EJB

Činnost business komponent může samozřejmě také probíhat v rámci transakce. Pomocí anotací můžeme označit jednotlivé metody business komponent a říct tak EJB kontejneru (ta část aplikačního serveru, která řídí business komponenty), jak má pro danou metodu nastavit transakční prostředí. Zde se ukazuje, jak výhodné je použít session bean pro práci s entitami – můžeme měnit více entit v rámci jedné transakce a máme zaručeno, že budou provedeny buď všechny změny entity beans, nebo žádné. Transakce je totiž automaticky zrušena, jakmile business metoda vyvolá runtime výjimku (výjimku, která je potomkem RuntimeException), nebo výjimku označenou anotací @ApplicationEx­ception s atributem rollback nastaveným na true, a tuto výjimku nezachytí.

Ještě bych chtěl zmínit, že automatické chování funguje v případě, kdy transakce řídí EJB kontejner (tzv. container managed transactions). Ja samozřejmě možné transakce řídit ručně. Ukázku ručního řízení ale ponechám až do dílu o JBoss Transactions.

Zpět k business komponentám. Každá metoda session a entity bean může běžet v transakci. Toto transakční zpracování nám zajistí EJB kontejner na základě anotací @TransactionAt­tributena úrovni třídy nebo metody. Možné hodnoty jsou:

  • REQUIRED (výchozí) – pokud je metoda zavolána v rámci transakce, je její kód v této transakci vykonán, pokud transakce neexistuje, vytvoří se nová a kód je v ní vykonán, tato transakce končí s návratem z metody
  • REQUIRES_NEW  – vždy je vytvořena nová transakce a kód metody je v ní vykonán, stávající transakce je odložena, nová transakce končí s návratem z metody a dojde k obnovení původní transakce
  • SUPPORTS  – kód metody je vykonán v existující transakci, pokud transakce neexistuje, vykoná se mimo transakci
  • NOT_SUPPORTED  – kód metody je vykonán vždy mimo transakci, pokud nějaká již existuje, je odložena a po návratu z metody obnovena
  • NEVER  – kód metody musí být vykonán zásadně mimo transakci, pokud již nějaká existuje, dojde k výjimce  EJBException
  • MANDATORY  – kód metody musí být vykonán v rámci existující transakce, pokud taková neexistuje, dojde k výjimce TransactionRe­quiredExcepti­on

Tento popis a více podrobností můžete také najít v JavaEE 5 Tutorial.

Nyní si vytvoříme jednoduchou bezestavovou session bean a na ní si demonstrujeme chování při použití různých hodnot v anotaci @TransactionAttribute. Můžete si prohlédnout celé zdrojové kódy [.tar.gz] tentokrát doplněné i o projekt v NetBeans. Příklad jsem spouštěl s AS verze 5.0.1.GA, což by mohlo mít vliv na formát ukazovaných výstupů.

@Stateless
public class TransactorBean implements Transactor {

    @TransactionAttribute(REQUIRED)
    public String transactionRequired() throws SystemException {
        Transactor transactor = getTransactor();
        StringBuilder sb = new StringBuilder();

        sb.append("this = " + this.getClass().getCanonicalName() + "\n");
        sb.append("Transactor trans = " + transactor.getClass().getCanonicalName() + "\n\n");

        sb.append(">>> REQUIRED >>>\n");
        sb.append("Transakce: " + tm.getTransaction() + "\n\n");

        sb.append("REQUIRED -> REQUIRES_NEW...\n");
        sb.append(transactor.transactionRequiresNew());

        sb.append("REQUIRED -> SUPPORTS...\n");
        sb.append(transactor.transactionSupports());

        sb.append("REQUIRED -> NOT_SUPPORTED...\n");
        sb.append(transactor.transactionNotSupported());

        sb.append("REQUIRED -> NEVER...\n");
        try {
            sb.append(transactor.transactionNever());
            sb.append("CHYBA, sem jsme se neměli dostat!\n\n");
        } catch (EJBException e) {
            sb.append("Očekávaná výjimka: " + e + "\n\n");
        }

        sb.append("REQUIRED -> MANDATORY...\n");
        sb.append(transactor.transactionMandatory());

        sb.append("<<< REQUIRED <<<\n\n");
        return sb.toString();
    }

    ...

    @TransactionAttribute(MANDATORY)
    public String transactionMandatory() throws SystemException {
        StringBuilder sb = new StringBuilder();

        sb.append(">>> MANDATORY >>>\n");
        sb.append("Transakce: " + tm.getTransaction());
        sb.append("\n<<< MANDATORY <<<\n\n");

        return sb.toString();
    }


    @TransactionAttribute(NEVER)
    public String transactionNever() throws SystemException {
        StringBuilder sb = new StringBuilder();

        sb.append(">>> NEVER >>>\n");
        sb.append("Transakce: " + tm.getTransaction());
        sb.append("\n<<< NEVER <<<\n\n");

        return sb.toString();
    }
}

Abychom mohli zjistit informace o aktuální transakci, budeme potřebovat instanci třídy implementující rozhraní TransactionMa­nager. Je několik způsobů, jak jej získat. Nejvíc systémový způsob je jeho získání pomocí JNDI, které je použito i v ukázkové aplikaci. To je možné buď přímým voláním JNDI, nebo pomocí dependency injection. Další způsoby vyžadují znalost implementačních detailů knihovny pro řízení transakcí. Na ukázku je jeden takový způsob v komentáři společně s metodou getTransactionManager(). Podotýkám, že jméno, pod kterým je transakční manažer v JNDI navázaný, není v JEE 5 standardizováno.

@Resource(mappedName = "java:/TransactionManager")
private TransactionManager tm;

Abychom si ukázali, jak se transakce vytváří, odkládají a obnovují, budeme volat jednotlivé metody session bean mezi sebou. Možná by někdo teď mohl mít nutkání zavolat z metody v session bean jinou její metodu a očekávat, že všechno – zejména tedy anotace @TransactionAt­tribute – bude fungovat tak, jako když jsme metodu volali z klienta. Podívejme se, jaký objekt jsme dostali na straně klienta z JNDI, a jaký objekt je v proměnné this na straně serveru uvnitř metody session bean.

    // u klienta
    System.out.println("Transactor trans = " + transactor.getClass().getCanonicalName() + "\n\n");

    // na serveru
    sb.append("this = " + this.getClass().getCanonicalName() + "\n");
    sb.append("Transactor trans = " + transactor.getClass().getCanonicalName() + "\n\n");

Co se ve skutečnosti v proměnných nachází uvidíme ve výstupu na straně klienta, který zobrazí:

=== Klient ===
Transactor trans = $Proxy2

=== Server ===
this = cz.root.jboss.transactions.TransactorBean
Transactor trans = $Proxy194

Zde vzniká kámen úrazu, který způsobí, že anotace se jaksi nebere v potaz. Ve skutečnosti na straně klienta získáme instanci objektu java.lang.reflec­t.Proxy. Tento objekt se navenek tváří jako objekt implementující požadované rozhraní. Jeho úkolem je delegování volání metod na server. Na serveru toto volání zachytí InvocationHandler a ten může před zavoláním dané session bean inicializace prostředí včetně transakcí (více o vnitřním fungování EJB volání naleznete v dalším dílu). V proměnné this uvnitř session bean máme skutečnou instanci třídy, která metody implementuje, tedy TransactorBean. Nemůžeme tedy očekávat, že server bude cokoliv řídit, pokud budeme další metody této třídy přímo volat. Toto je naprosto v souladu se specifikacíEJB3 – JSR-220 (soubor EJB Core, strana 343):

The following subsections define the responsibilities of the container for managing the invocation of an enterprise bean business method when the method is invoked via the enterprise bean’s business interface (and/or home or component interface), or web service endpoint. The container’s res­ponsibilities depend on the value of the transaction attribute.

Důležitá je ona část zmiňující povinnosti (EJB) kontejneru v případě, že jsou business metody volány pomocí business rozhraní. Nic se zde neříká o povinnostech kontejneru při přímém volání jedné metody z jiné a je potřeba to mít na paměti. Z tohoto důvodu také uvniř naší session bean získáváme toto business rozhraní pomocí JNDI. Není možné zde provést dependency injection, protože ve chvíli, kdy se server snaží tyto závislosti injektovat, není ještě session bean inicializovaná a nemůže tudíž být pro injekci použita.

    private Transactor getTransactor() {
        try {
            InitialContext ic = new InitialContext();
            Transactor t = (Transactor) ic.lookup("TransactorBean/remote");
            return t;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

Nyní již máme dostatek indicií, abychom se podívali, jak se transakce vlastně chovají. Čísla transakcí nebudete mít patrně nikdy shodná s těmi, co jsou ukázána zde. Jde ale především o to, jestli jsou nebo nejsou mezi jednotlivými metodami stejná.

Required

Metoda transactionRe­quired() je volána z klienta, aby jsme si ukázali vytvoření nové transakce. Z ní pak budeme volat většinu ostatních metod.

>>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
...
<<< REQUIRED <<<

Requires New

Metoda transactionRequiresNew() je volána z metody transactionReuired(), aby jsme ověřili, že skutečně dojde k vytvoření nové transakce a k obnovení té původní po návratu z volající metody.

>>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >

REQUIRED -> REQUIRES_NEW...
>>> REQUIRES_NEW >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b8 status: ActionStatus.RUNNING >
<<< REQUIRES_NEW <<<

Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >

Všiměte si, že číslo transakce je skutečně jiné a po návratu je k dispozici opět ta původní.

Supports

Metoda transactionSupports() je volána také dvakrát. Jednak z metody transactionRe­quired(), aby se použila stávající transakce, ale také z metody transactionNotSupported(), aby se ověřilo, že nebude vytvořena nová transakce.

>>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
...
REQUIRED -> SUPPORTS...
>>> SUPPORTS >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
<<< SUPPORTS <<<
...
>>> NOT_SUPPORTED >>>
Transakce: null

NOT_SUPPORTED -> SUPPORTS...
>>> SUPPORTS >>>
Transakce: null
<<< SUPPORTS <<<

V prvním případu došlo k použití stejné transakce. V tom druhém, kdy v metodě transactionNotSupported() skutečně žádná transakce neběžela, se ani žádná nová nevytvořila.

Not Supported

Metoda transactionNotSupported() je volána z metody transactionReuired() a ukazuje odložení stávající transakce a neexistenci transakce v průběhu jejího provádění. Tento výstup jsme již viděli v předchozí části.

 >>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
...
REQUIRED -> NOT_SUPPORTED...
>>> NOT_SUPPORTED >>>
Transakce: null
...
<<< NOT_SUPPORTED <<<

Never

Metoda transactionNever() je volána z metody transactionRe­quired(), kde očekáváme výjimku EJBException, protože metoda byla zavolána během existence aktivní transakce. Dále je zavolána z metody transactionNotSupported(). Zde již dojde k jejímu vykonání, neboť existující transakce byla odložena.

>>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
...
REQUIRED -> NEVER...
Očekávaná výjimka: javax.ejb.EJBException: Transaction present on server in Never call (EJB3 13.6.2.6)
...

A druhé volání…

>>> NOT_SUPPORTED >>>
Transakce: null
...
NOT_SUPPORTED -> NEVER ...
>>> NEVER >>>
Transakce: null
<<< NEVER <<<
...
<<< NOT_SUPPORTED <<<

Mandatory

Metoda transactionMandatory() je opět volána z metody transactionReuired(). Vše proběhne bez problémů, protože máme aktivní transakci. Poté ji zkusíme zavolat i z metody transactionNotSupported(), kde očekáváme výjimku TransactionRe­quiredExcepti­on, protože aktivní transakce neexistuje.

>>> REQUIRED >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
...
REQUIRED -> MANDATORY...
>>> MANDATORY >>>
Transakce: TransactionImple < ac, BasicAction: a222163:d60e:4a015a05:1b7 status: ActionStatus.RUNNING >
<<< MANDATORY <<<

A druhé volání…

>>> NOT_SUPPORTED >>>
Transakce: null
...
NOT_SUPPORTED -> MANDATORY...
Očekávaná výjimka: javax.ejb.EJBTransactionRequiredException: ...

Bonusový úkol

Zbývá ověřit poslední fakt – volání metody transactionRe­quired() z jiné metody s aktivní transakcí, jako důkaz toho, že nebude vytvořena nová transakce. Tento úkol ponechávám již čtenářům.

root_podpora

Možná vás teď napadá otázka, jak transakční manažer zjistí aktuální transakci. Kde by mohla být tato informace uložena a jak ji svázat s konkrétním vykonáním metody? Každý kód ve virtuálním stroji Javy (JVM) musí být vykonán v rámci nějakého vlákna. Ať už to je hlavní vlákno aplikace, nebo jakékoliv jiné. Bez vlákna by to zkrátka nešlo. A aktuální vlákno je právě to, k čemu máme přístup v každém místě programu pomocí volání Thread.currentThread(). Jak do něj ale uložit potřebné informace? Java pro to nabízí speciální proměnné označované jako lokální proměnné vlákna (thread local) a pro manipulaci s nimi slouží třída ThreadLocal. Přesně tímto způsobem je k danému vláknu uložena i aktuální transakce a odtud ji také transakční manažer zjišťuje.

Závěr

Tento díl poodhalil další možnosti Enterprise Javy, a to převážně kontejnerem ovládané transakce v entity a session beans. Jak jsem naznačil již na začátku, příště nahlédneme pod pokličku implementace EJB3 v aplikačním serveru JBoss a povíme si o dynamickém proxy objektu a InvocationHan­dler u.

Byl pro vás článek přínosný?