JavaSvet - otvorena java zajednica

JavaSvet on Facebook
 
glavna stranica arr2javasvet  english version arr2java.net
Sadržaj:

Command pattern u EJB okruženju

Dejan Krsmanović
2 Feb 2005

Ovaj članak ima za cilj da pokaže kako se korišćenjem Command paterna može znatno olakšati razvoj u Enerprise JavaBeans okruženju  i eliminisati neke od često navođenih mana EJB razvoja. Ideje su delom preuzete iz knjige EJB Design Patterns koju je napisao Floyd Marinescu, konkretno EJB Command Pattern, koje su zatim razrađene prilikom njihovog korišćenja na više EJB projekata. Iako je polazni EJB Command Patern bio primenljiv i u EJB 1.0 specifikaciji, pojedine ideje iz ovog članka zahtevaju 2.0 ili 2.1 specifikacije. 

Tekst nema za cilj detaljniju analizu samog Command paterna pogotovo ne onako kako je izvorno definisan u čuvenoj knjizi Design Patterns: Elements of Reusable Object-Oriented Software. Takođe, od čitalaca se podrazumeva elementarno poznavanje EJB specifikacije.

Loše i dobre strane Enterprise JavaBeans specifikacije

Enterprise JavaBeans specifikacija je od svoje prve verzije doživljavala brojne kritike zbog svoje arhitekture, ograničenja i komplikovanog razvoja. Iako se vremenom delimično poboljšavala, mnogi problemi su i dalje prisutni i uticali su na pojavu brojnih alternativnih rešenja u vidu light-weight kontejnera. Ovaj vid konkurencije je očigledno pozitivno uticao na razvoj EJB specifikacije s obzirom na najave radikalnih promena u EJB 3.0. Na žalost, EJB 3.0 je još daleko, tako da se u međuvremenu moramo zadovoljiti ovime što imamo u aktuelnim verzijama.

O manama EJB specifikacije se u poslednje vreme dosta priča tako da ih i nema mnogo smisla ponavljati. Uglavnom se navode problemi oko sporog i kompleksnog razvoja zbog potrebe održavanja nekoliko različitih interfejsa i deskriptora (ovaj problem se uglavnom može rešiti korišćenjem XDoclet-a), nemogućnost testiranja van kontejnera... Mane Entity Bean-ova ne vredi ni pominjati jer ih je mnogo! Za više informacija o ovoj temi preporučujem neku knjigu Roda Johnsona. 

Međutim, EJB specifikacija ima i dosta dobrih strana. Stateless Session Bean-ovi  (SLSB) i Message Driven Bean-ovi (MDB) su i dalje prilično popularni među programerima. Oni omogućuju jednostavniji rad sa distribuiranim aplikacijama, deklarativnu kontrolu transakcija, asinhrono izvršavanje i sl. Entity Bean-ovi su veoma loše rešenje i treba ih izbegavati u širokom luku. Umesto njih je mnogo bolje koristiti neki ORM alat poput Hibernate. Time se praktično ništa ne gubi (Entity bean-ove ionako treba koirstiti samo preko Session Facade) a mnogo se dobija na fleksibilnosti. Statefull Session Bean-ove (SSB) prati priča da su prilično neskalabilni te ih treba izbegavati ako je to moguće. Lično nisam imao mnogo dodira sa njima pogotovo ne u aplikacijama sa velikim brojem korisnika, pa ne mogu tvrditi da li je to tačno ili ne.

Dakle, od EJB specifikacije vredi koristiti samo SLSB i MDB. Međutim, rad sa njima takođe nije jednostavan. Svi gore pomenuti problemi u EJB razvoju i dalje postoje. Pre nego što objasnimo kako ih je moguće rešiti samo ukratko ćemo opisati Command patern. 

Osnove Command paterna

Kao što je rečeno, patern koji ovde opisujemo predstavlja modifikovanu verziju originalnog Command paterna i mnogo je bliži EJB Command paternu koji je opisan u knjizi EJB Design Patterns Floyda Marinescu-a.   

Sam patern je prilično jednostavan. Poslovna logika se nalazi unutar jednostavnih, POJO klasa, koje samo treba da implementiraju jednostavan Command interfejs:

package net.javasvet.command
import java.io.Serializable;
public interface Command extends Serializable {
   
public void execute() throws CommandException;
}

Command interfejs nasleđuje Serializable interfejs, čime svi objekti koji implementiraju Command interfejs indirektno postaju Serializable. Ovo je potrebno iz prostog razloga što se Command objekti prenose kao parametri u remote pozivima pa moraju biti Serializable. Videćemo kasnije da će nam to koristiti i za druge upotrebe ovog paterna.

Command interfejs definiše samo execute() metodu koju konkretne Command klase moraju implementirati. Kao što se može videti execute() ne prima nikakve parametre i ne vraća ništa. Ukoliko imamo parametre potrebne za rad metode, njih nećemo prenositi kao parametre metode, već ćemo ih definisati kao promenljive instace. Njih je potrebno setovati pre poziva execute metode. Da li će se to raditi preko seter metoda ili u konstruktoru, stvar je ličnih preferenci. Rezultati operacija se vraćaju takođe kao promenljive instace. U ovom slučaju moguće je koristiti samo getter metode (ili eventualno public promenljive).

Šta može biti implementirano u execute() metodi? Pa praktično sve. Kao što se može primetiti Command klase predstavljaju apstrakciju klasičnih metoda, gde jedna Command klasa odgovara jednoj metodi, execute metoda odgovara telu klasične metode, a svojstva (property) klase odgovaraju parametrima metode i povratnoj vrednosti. Sve što može biti implementirano u običnoj metodi može biti i u Command klasi. Šta se onda time dobija? Pa pre svega, mogućnost da različite komponente mogu izvršavati istu Command klasu. Izvršioci komandi nisu vezani za konkretnu implementaciju jer sve što oni znaju  je Command interfejs preko koga komuniciraju sa konkretnim Command objektom.

Upotreba (EJB) Command paterna

Command objekat moguće je instancirati i izvršavati na razne načine, između ostalog i unutar JUnit testova. Preduslov je da ne koristimo EntityBean-ove za pristup bazi, već neki od alata koji može da radi i bez kontejnera (Hibernate, neka JDO implementacija, JDBC...). Ovo Command objekte čini mnogo lakšim za ravoj od klasičnih Enterprise Java Bean-ova. Međutim, upotreba Command paterna ni u kom slučaju ne isključuje korišćenje EJB-ova. U narednom delu biće prikazano nekoliko različitih izvršilaca Commandi u EJB okruženju

Session Bean kao izvršilac

Prvo ćemo razmatrati varijantu koja se najčešće navodi kao EJB Command patern, a to je slučaj kada je izvršilac Command objekata Session Bean. Na ovaj način, omogućava se korišćenje transakcija kojima upravlja kontejner (CMT - Container Manager Transaction). Za ovo, potreban nam je jedan Session bean koji se koristi za sve Command objekte. Implementacija ovog Session bean-a bi izgledala otprilike ovako:

public class CommandExecutorBean extends BaseSessionBean
       
implements SessionBean {
   
...
   
public Command executeCommand(Command command) throws CommandException {
       
command.execute();
       
return command;
   
}

}

Očigledno, ovakav EJB je veoma generički definisan i nema ništa specifično za problem koji je implementiran u Command objektima. Pa ipak, command objekti koji se izvršavaju u ovakvom EJB-u, imaju sve ono zbog čega se EJB i koristi - deklarativne transakcije, security, distribuiranost... Činjenica da nam je potreban samo jedan EJB znatno olakšava razvoj s obzirom da nema potrebe stalno održavati sve one klase i fajlove koje je je neophodno održavati u razvoju EJB komponenti.

Korišćenje Command objekata je prilično jednostavno:

//kreiranje konkretnog command objekta
CreateDailyReportCommand command = new CreateDailyReportCommand();
//setovanje potrebnih parametara
command.setDate(...);
command.setUser
(...);
...
//Koristimo business delegate patern da se ne bismo stalno petljali sa JNDI
CommandExecutorDelegate commandExecutor = new CommandExecutorDelegate();
//Izvrsavamo command i dobijamo ga nazad kao povratnu vrednost (remote metode prenose parametre po vrednosti)
command = (CreateDailyReportCommand) commandExecutor.executeCommand(command);
//Koristimo rezultate obrade
List list = command.getReports();

Potrebno je instancirati odgovarajući Command objekat, setovati potrebne propertije i pozvati executeCommand metod iz SessionBean-a. U prikazanom slučaju se koristi remote interfejs kod kojeg se prenos parametara vrši po vrednosti, pa je potrebno da se Command objekat vrati kao povratna vresnost executeCommand metode. U suprotnom promene na poslanom Command objektu ne bi bile vidljive na klijentskoj strani. U slučaju da smo koristili lokalne interfejse za CommandExecutorBean, ovaj korak bi bio nepotreban jer se u tom slučaju vrši prenos parametara preko referenci, pa bi command objekat koji se šalje i command objekat koji se vraća iz metoda bili u stvari isti. 

S obzirom da je SessionBean interfejs prisutan od prve EJB specifikacije, ovu varijantu je moguće koristiti i na starijim EJB kontejnerima. To na žalost ne važi za druge dve varijante izvršavanja koje će biti pokazane u nastavku. 

Izvršavanje komandi u definisano vreme korišćenjem EJB Timera (EJB 2.1 specifikacija)

Jedna od novina koje donosi EJB 2.1 specifikacija jeste i definisanje standardnog Timer servisa. Timer servis omogućava da se određena metoda izvršava u definisano vreme. Metod koji se ovako izvršava mora da se nalazi u nekom od Enterprise Java bean-ova koji pritom implementira TimedObject interfejs. TimedObject interfejs definiše samo jedan metod -  public void ejbTimeout(javax.ejb.Timer timer) koji se izvršava nakon isteka definisanog vremena. Samo planiranje izvršavanja (scheduling)  se vrši korišćenjem jednom od createTimer metoda unutar TimerService interfejsa. 

TimerService timerService = this.getSessionContext().getTimerService();
timerService.createTimer
(firstExecution, interval, info);

Paremetar info u prethodnom primeru može biti bilo koji Serializable objekat koji se kasnije može koristiti u ejbTimeout() metodi. Postoji više različitih createTimer metoda koji primaju različite parametre i omogućavaju izvršavanje u definisano vreme sa ili bez ponavljanja na zadati interval.

Command pattern se može primeniti i u ovom kontekstu, čime se omogućava da se pojedine komande izvršavaju u unapred definisano vreme. Kao i u osnovnom EJB Command paternu i u ovom slučaju nam je potreban samo jedan EJB. Moguće je čak proširiti osnovni CommandExecutorBean tako da se omogući i scheduling. Potrebno je da naš SessionBean implementira TimedObject interfejs i definiše dva nova metoda:

...
import javax.ejb.SessionBean;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.ejb.TimerService;

public class CommandExecutorBean implements SessionBean, TimedObject {

   
...
   
public void scheduleCommand(Date startDate, long interval, Command task) {
       
TimerService timerService = this.getSessionContext().getTimerService();
        timerService.createTimer
(startDate, interval, task);

   
}

   
public void ejbTimeout(Timer timer) {
       
Command command = (Command) timer.getInfo();    //info je u stvari Command objekat
       
try {
           
this.executeCommand(command);
       
} catch (CommandException e) {
           
e.printStackTrace();
       
}

    }

   
public Command executeCommand(Command command) throws CommandException {
       
command.execute();
       
return command;
   
}


}

Kao što se vidi iz koda, u scheduleCommand metodi vršimo kreiranje Timer-a preko TimerService objekta, i kao info parametar dajemo mu Command objekat koji treba izvršiti u definisano vreme. Nakon isteka definisanog vremena, EJB kontejner će pozvati ejbTimeout metod i kao parametar će proslediti Timer objekat koji je kreiran u scheduleCommand metodi. Dalje je jednostavno - uzimamo info objekat koji je u stvari Command objekat koji treba izvršiti, i pozivamo executeCommand() metod koji vrši izvršavanje komande.  

Na ovaj način, omogućili smo da CommandExecutorBean osim klasičnog izvršavanja komandi može vršiti i izvršavanje komandi u definisano vreme. 

Asinhrono izvršavanje komandi korišćenjem MDB (EJB 2.0 specifikacija)

Sada ćemo pokazati i kako je Command patern moguće primeniti i za asinhrono izvršavanje komandi. U ovu svrhu, koristimo JMS (Java Messaging Service) i Message Driven EJB. Da se podsetimo, MDB omogućava da se određena logika izvrši asinhrono, bilo korišćenjem Queue ili Topic-a. MDB reaguje na primljenu poruku, asinhrono, tako što kontejner poziva njegov onMessage metod. Message interfejs ima nekoliko različitih varijanti u zavisnosti od toga šta poruka predstavlja. Nama posebno interesantna je ObjectMessage klasa koja nosi serijalizovani objekat. Pošto Command interfejs nasleđuje Serializable, Command objekti se mogu slati u telu poruke. 

Naš MDB reaguje na poruku koja sadrži Command objekat. onMessage metod treba samo da pokupi objekat iz poruke i izvrši ga: 

...
import javax.ejb.*;
import javax.jms.*;

public class CommandMessageBean extends BaseMessageDrivenBean
   
implements     MessageDrivenBean, MessageListener {

   
public void onMessage(Message message) {
       
try {
           
//poruka je tipa ObjectMessage
           
ObjectMessage commandMessage = (ObjectMessage) message;
            Command command =
(Command) commandMessage.getObject();
            executeCommand
(command);

       
} catch (JMSException e) {
           
//handle error
       
}

    }

   
private void executeCommand(Command command) {
           
command.execute();
       
} catch (CommandException e) {
           
//handle error
       
}
    }

}

Slanje poruke sa Command objektom se vrši na isti način kao i bilo koje druge JMS poruke. Evo primera slanja poruke na queue:

    public void executeCommandAynchronously(Command command) {
       
ConnectionFactory factory = null;
        Destination destination =
null;
        Connection connection =
null;
        Session session =
null;
        MessageProducer producer =
null;
       
try {
           
Context ctx = new InitialContext();
            factory =
(ConnectionFactory) ctx.lookup("jms/QueueConnectionFactory");
            destination =
(Destination) ctx.lookup("jms/CommandQueue");
            connection = factory.createConnection
();
            session = connection.createSession
(false, Session.AUTO_ACKNOWLEDGE);
            producer = session.createProducer
(destination);

            ObjectMessage message = session.createObjectMessage
();
            message.setObject
(command);
            producer.send
(message);
       
} catch (Exception e) {
           
throw new CommandException("Error sending command to queue", e);
       
} finally {
           
if (connection != null) {
               
try {
                   
connection.close();
               
} catch (JMSException e) {
                   
e.printStackTrace();
               
}
            }
           
if (session != null) {
               
try {
                   
session.close();
               
} catch (JMSException e) {
                   
e.printStackTrace();
               
}
            }
        }
    }

Iako ovaj kod na prvi pogled može izgledati komplikovano, u pitanju je najobičniji JMS kod za slanje poruke - potpuno isti kao u JMS tutorijalu. Srećom, taj kod je dovoljno implementirati u jednom metodu, i dalje koristiti taj metod za sve asinhrono izvršavanje bilo koje komande. Ovaj metod može biti čak i deo istog CommandExecutorBean-a koji smo prethodno koristili za sinhrono izvršavanje i za scheduling. 

Prednosti i mane

Command patern, kada se koristi na jedan od (ili više) opisanih načina, ima brojne prednosti

Opisani pristup ima i određene mane:

Source kod

Oni koje je ovaj članak zainteresovao mogu preuzeti fajl sa source kodom osnovnih klasa opisanih u projektu, iako sam ubeđen da ideje mogu da se shvate i iz prikazanog koda. Sors kod sadrži XDoclet tagove tako se interfejsi i deployment deskriptori ne nalaze u arhivi.