Lottery JMS, Message Driven Bean, JMX, Singleton Session Bean Óbudai Egyetem, Java Enterprise Edition Műszaki Informatika szak Labor 7 Bedők Dávid 2016.10.04. v0.9
JBoss management console ● http://localhost:9990/ ● Létre kell hozni egy management user-t ehhez. ○ [JBOSS-HOME]/bin/add-user.[bat|sh] ○ Management User (enter) ○ Username: admin (are you sure? yes) ○ Password: AlmafA11# ○ Enter, Yes, Yes, Enter ● http://localhost:9990/ ○ Try Again és Login (BASIC AUTH)
> /jboss-eap-6.4/bin/add-user.sh admin AlmafA11# 2
Feladat Készítsünk egy szolgáltatást, mely tárolja az ötös lottóhúzás eredményeit, ezek időpontját, nyereményalapját és a számot kihúzó személy nevét (legyen kinek megköszönni). A kihúzott számokat egy JMS queue interface-en* keresztül fogja a szolgáltatás megkapni (machine-to-machine interface). RESTful interface-en keresztül adjunk lehetőséget arra, hogy lekérdezzük az aktuális, illetve az összes sorsolás adatát. * itt most interface alatt két rendszer közötti kommunikációs réteget értve 3
Kiegészítés RESTful interface-en keresztül lehessen 5 szám megadásával “megkapni” a nyereményt! Minden sorsolásnak van egy nyereményalapja, melyet 100%-ban kiosztanak a nyertesek között. A szolgáltatás tartsa karban, hogy az aktuális “jogszabályok” szerint e nyereményalap mennyi százaléka jár az 1, 2, 3, 4 és 5 találatos játékosoknak. Megjegyzés: Mindez természetesen egyszerűsítés és nem is garantálja a helyes szétosztást, hiszen nem számol azzal, hogy pl. mennyi öt találatos szelvény volt.
A nyereményeloszlás százalékos paramétereit szabvány management felületen (JMX) lehessen konfigurálni!
4
Ismeretszerzés ● Java Message Service (JMS) ○ ○ ○ ○
JMS Queue JMS Topic (feladat nem érinti) Message Driven Bean (listener) JMS Client
● Java Management eXtension (JMX) ○ Standard Management Bean (MBean) ○ jconsole
● Singleton Session Bean ○ speciális Statefull Session Bean
● EntityManager ○ persist művelet ○ JOIN FETCH
5
Project struktúra lottery (root project) ● lot-jmsclient (JMS client alkalmazás) ● lot-ejbservice (EJB service réteg) ● lot-persistence (persistence service réteg) ● lot-webservice (RESTfull service réteg) Része az EAR-nak: sárga Standalone alkalmazás: kék A jms client alkalmazás classpathához jelenleg nem lesz szükség pl. egy lot-ejbserviceclient jar-ra, mivel a kommunikáció szabványos (nem kell pl. remote interface), és szöveges üzeneteket fogunk küldeni (nem kellenek stub-ok). Utóbbi megléte esetén megjelenne egy lot-ejbserviceclient jar is, mely része lenne mind az EAR mind a JMS client classpathának.
6
Adatbázis oldal create-schema.sql CREATE TABLE event ( event_id SERIAL NOT NULL, event_puller CHARACTER VARYING(100) NOT NULL, event_prizepool INTEGER NOT NULL, event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, CONSTRAINT PK_EVENT_ID PRIMARY KEY (event_id) ); CREATE TABLE drawnnumber ( drawnnumber_id SERIAL NOT NULL, drawnnumber_event_id INTEGER NOT NULL, drawnnumber_value INTEGER NOT NULL, CONSTRAINT PK_DRAWNNUMBER_ID PRIMARY KEY (drawnnumber_id), CONSTRAINT FK_DRAWNNUMBER_EVENT FOREIGN KEY (drawnnumber_event_id) REFERENCES event (event_id) MATCH SIMPLE ON UPDATE RESTRICT ON DELETE RESTRICT );
7
Legfrissebb és az összes sorsolás RESTful services LotteryRestService ● ●
List<EventStub> getAllEvents() throws AdaptorException; ○ http://localhost:8080/lottery/api/service/event/list EventStub getLatestEvent() throws AdaptorException; ○ http://localhost:8080/lottery/api/service/event/latest
LotteryFacade ● ●
List<EventStub> getAllEvents() throws AdaptorException; EventStub getLatestEvent() throws AdaptorException;
EventService ●
Event readLatest() throws PersistenceServiceException; ○
●
SELECT e FROM Event e JOIN FETCH e.numbers ORDER BY e.date DESC
○ SetMaxResult(1) List<Event> readAll() throws PersistenceServiceException; ○
SELECT e FROM Event e JOIN FETCH e.numbers ORDER BY e.prizePool
EventConverter ● ●
EventStub to(Event event); List<EventStub> to(List<Event> events);
8
JOIN FETCH server.log SELECT event0_.event_id AS event_id1_1_0_,
A Set LAZY módon van kapcsolva az numbers1_.drawnnumber_id AS drawnnum1_0_1_, Event-hez. A JOIN beköti a event0_.event_date AS event_da2_1_0_, select-be a táblát (ez esetben a event0_.event_prizepool AS event_pr3_1_0_, lekérdezés azonos lesz a bemutatottal), de nem event0_.event_puller AS event_pu4_1_0_, attacholja az entitásokat (ha numbers1_.drawnnumber_event_id AS drawnnum3_0_1_, bejárjuk (pl. size()), akkor külön numbers1_.drawnnumber_value AS drawnnum2_0_1_, select-ben lekéri és attacholja. numbers1_.drawnnumber_event_id AS drawnnum3_1_0__, A JOIN FETCH e plusz lekérdezés nélkül attacholja is, numbers1_.drawnnumber_id AS drawnnum1_0_0__ és ez a hatékony megoldás ez esetben! FROM event event0_ INNER JOIN drawnnumber numbers1_ ON event0_.event_id=numbers1_.drawnnumber_event_id ORDER BY event0_.event_date DESC 9
Új sorsolás adatainak rögzítése
10
Java Message Service (JMS) ●
● ● ●
Üzenetküldés alapú kommunikáció ○ “lazán” kötődő komponensek (beékelődik a kommunikációba az üzeneteket kezelő/tároló komponens) Message-Oriented Middleware (MOM) JMS 1.1 (2002, JSR914, JEE6), JMS 2.0 (2013, JSR343, JEE7) Típusai ○ point-to-point (queue) ■ producer üzeneteket küld a queue-ba ■ consumer üzenetet kiolvas a queue-ból ■ egy üzenetet egy fogadó dolgoz fel (ack küldés is van) ■ producer és consumer nem kell hogy egy időben “online” legyen ○ publish-subscribe (topic) ■ publish üzeneteket küld a topic-ba ■ subscriber(ek) megkapják a topic-ba küldött üzenetet ■ egy üzenet több fogadó is feldolgoz(hat) ■ publisher és subscriber között van időbeli függés, “tankönyvi” eset szerint a komponensek egyszerre “online”-ok (de vannak speciális feliratkozások)
11
JBoss 6.4 - MOM, JMS provider ● HornetQ messaging ○ ○ ○ ○
http://hornetq.jboss.org/ Deprecated, JBoss 7.x-től JBoss A-MQ váltja fel http://www.jboss.org/products/amq/overview/ JMS 1.1 és JMS 2.0 támogatás
● Verzió: 2.3.25.Final (JBoss 6.4 esetén)
12
JMS Queue létrehozása
lotteryqueue-jms.xml
<messaging-deployment xmlns="urn:jboss:messaging-deployment:1.0"> <jms-destinations> <jms-queue name="lotteryqueue"> <entry name="jms/queue/lotteryqueue" /> <entry name="java:jboss/exported/jms/queue/lotteryqueue" />
local JNDI name
remote JNDI name
JNDI név “szabványok”: java:/jms/queue/lotteryqueue lesz a valós JNDI név. Remote JMS client a java:jboss/exported/ előtagot automatikusan fogja használni (JBoss JMS Client jar használata esetén)
A file nevének *-jms.xml-nek kell lennie, és a deployments könyvtárba másolással “létrehozható”, de pl. standalone.xml-ben is lehet defininálni, illetve programozottan runtime is létrehozható. 13
Lottery Listener Message Driven Bean package hu.qwaevisz.lottery.ejbservice.listener;
LotteryListener.java
@MessageDriven(name = "LotteryListener", activationConfig = { @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "destination", propertyValue = "lotteryqueue"), @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge") })
public class LotteryListener implements MessageListener { @EJB private LotteryFacade facade; @PostConstruct public void initialize() { ... } @Override public void onMessage( final Message message) { ...
Amikor a “lotteryqueue”-ba üzenet érkezik, a LotteryListener MDB aktiválódik, és az onMessage() metódusa meghívásra kerül a feldolgozandó üzenettel. Ha kivételt dob a metódus, az üzenet feldolgozás rollback-elődik, és nem kerül ki a sorból!
} }
14
Lottery Listener - onMessage() Message Driven Bean
● ● ● ● ●
BytesMessage MapMessage ObjectMessage StreamMessage TextMessage
LotteryListener.java public void onMessage( final Message message) { try { final Queue destination = (Queue) message.getJMSDestination(); final String queueName = destination.getQueueName(); LOGGER.debug("New JMS message arrived into " + queueName + " queue (correlation id: " + message.getJMSCorrelationID() + ")"); if (message instanceof TextMessage) { final TextMessage textMessage = (TextMessage) message; String content = textMessage.getText(); [..] // parse content to int[] numbers this.facade.createNewEvent(numbers); } else { LOGGER.error("..."); }
queue name: kinyerhető (hasznos, ha egy listener több queue-ra figyel egyidejűleg (erre van lehetőség) correlation id: tipikusan kliens hozza létre, és elküldi a JMS message-el, hogy később pl. egy async válasz során azonosítani tudja a “választ”.
} catch (final JMSException | AdaptorException | NumberFormatException e) { LOGGER.error(e, e); } }
15
JMS Client Application Távolról JMS üzenetet küld a lotteryqueue-ba, melyet az elindított JBoss EAS által indított HornetQ mint JMS MOM fog fogadni. Ahhoz, hogy meg tudjuk szólítani ezt a szolgáltatást, az alábbiak szükségesek: ●
●
JBoss initial context factory ○ osztály neve: org.jboss.naming.remote.client.InitialContextFactory ○ a classpath-on legyen elérhető ez az osztály ■ compile group: 'org.jboss.as', name: 'jboss-as-jms-client-bom', version: '7.2.0.Final' JBoss EAS host-ja (localhost) és remote portja (def: 4447) ○
● ● ● ●
stanalone.xml | socket-binding-group | <socket-binding name="remoting" port="4447"/>
JMS Connection Factory JNDI neve (jms/RemoteConnectionFactory) Egy min. guest role-lal rendelkező user authentikációja (username és password) A cél queue JNDI neve (jms/queue/lotteryqueue) Ha TextMessage helyett pl. ObjectMessage-et küldünk, akkor szükség volna egy “serviceclient.jar”-ra, mely tartalmazza a Serializable DTO-kat (hasonlóan az ejb client-nél alkalmazottak szerint). 16
JMS user létrehozása
> \jboss-eap-6.4\bin\add-user.sh -a -u jmstestuser -p User#70365 -g guest
17
JMS Client Application A csatlakozás lényegi részeit kiemelve final Properties environment = new Properties();
SimpleClient.java
environment.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory"); environment.put(Context.PROVIDER_URL,
"remote://localhost:4447");
environment.put(Context.SECURITY_PRINCIPAL,
"jmstestuser");
environment.put(Context.SECURITY_CREDENTIALS,
"User#70365");
final Context context = new InitialContext(environment); final ConnectionFactory connectionFactory = (ConnectionFactory) context.lookup( "jms/RemoteConnectionFactory"); final Destination destination = (Destination) context.lookup( "jms/queue/lotteryqueue"); Connection connection = connectionFactory.createConnection( "jmstestuser", "User#70365"); final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); final MessageProducer producer = session.createProducer(destination); connection.start(); final TextMessage textMessage = session.createTextMessage( "1, 2, 3, 4, 5"); producer.send(textMessage);
18
JMS (Remote) Connection Factory A standalone-full.xml-ből előre konfigrálva kapjuk standalone(-full).xml <subsystem xmlns="urn:jboss:domain:messaging:1.4"> <jms-connection-factories> <entries> <entry name="java:jboss/exported/ jms/RemoteConnectionFactory"/>
A “java:jboss/exported” prefix-et a ClassPath-on lévő JBoss JMS Client jar “adja hozzá” a JNDI névhez.
19
Session Beans
Concurrency Management CMC - Container-Managed Concurrency (def.) @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
BMC - Bean-Managed Concurrency @ConcurrencyManagement(ConcurrencyManagementType.BEAN)
● Kizárólag Singleton Session Bean-ek esetén van értelmezve a Bean-Managed Concurrency! ● Utóbbi esetén engedélyezett pl. a synchronized és volatile kulcsszavak használata. 20
Singleton Session Bean Az EJB container garantálja, hogy a Singleton Session Bean-ből ugyanazt a példányt fogja minden szálon használni. Természetesen nem arról van szó, hogy minden a SSB-t használó klienst szépen sorbaállít a container (ez bottleneck-je lenne az egész rendszernek). Vannak READ és vannak WRITE lock-kal rendelkező metódusai (kizólag CMC esetén használható). ● READ: párhuzamosan több szálon is futhat (állapot olvasás) ● WRITE (def.): kizárólag egy szálon futhat (állapot módosítás) 21
StateHolder Sorsoló és a nyereményalap lekérdezése StateHolderImpl.java package hu.qwaevisz.lottery.ejbservice.holder; @Singleton(mappedName = "ejb/lotteryState") @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) public class LotteryStateHolderImpl implements LotteryStateHolder { private String puller; @PostConstruct public void initialize() { this.puller = "Juanita A. Jenkins"; } @Override @Lock(LockType.READ) public String getCurrentPuller() { return this.puller; } @Override @Lock(LockType.WRITE) public void setCurrentPuller(String name) { this.puller = name; }
A prizePool tárolása és getter/setter üzleti metódusa mindezzel teljesen azonosan elkészíthető. A SSB-nek természetesen illendő interface-t készíteni (LotteryStateHolder ), mely jelen esetben a @Local annotációt megkapja.
} 22
LotteryFacade kiegészítése LotteryListener hívja LotteryFacadeImpl.java package hu.qwaevisz.lottery.ejbservice.facade; @Stateless(mappedName = "ejb/lotteryFacade") public class LotteryFacadeImpl implements LotteryFacade { @EJB Az EventService a persistence private EventService eventService; rétegben egy tranzakción belül @EJB be kell hogy insertálja az új event sort, illetve az 5 új private LotteryStateHolder stateHolder; drawnumber sort ehhez az eseményhez.
@Override public void createNewEvent(int[] numbers) throws AdaptorException { try { this.eventService.create(this.stateHolder.getCurrentPuller(), this.stateHolder.getCurrentPrizePool(), numbers); } catch (final PersistenceServiceException e) { LOGGER.error(e, e); throw new AdaptorException(e.getLocalizedMessage()); } } } 23
EventService kiegészítése Persistence réteg implementációja
Fontos! Az Event entitás Set numbers field-je @OneToMany annotációjában a cascade értéke CascadeType.ALL vagy PERSIST legyen!
EventServiceImpl.java
package hu.qwaevisz.lottery.persistence.service; public class EventServiceImpl implements EventService { @PersistenceContext(unitName = "lot-persistence-unit") private EntityManager entityManager;
@Override public void create(String puller, Integer prizePool, int[] numbers) throws PersistenceServiceException { try { final Event event = new Event(puller, prizePool); for (final int number : numbers) { public void addNumber(Integer event.addNumber(number); number) { } this.numbers.add(new this.entityManager.persist(event); DrawnNumber(number, this)); } } catch (final Exception e) { throw new PersistenceServiceException("Unknown error when fetching Events! " + e.getLocalizedMessage(), e); } persist: egy új (vagy egy törlésre jelölt) merge: egy detached (nem managed) entitás } entitás létrehozása, és egyben létrehozása (a metódus visszaadja a managed } managed állapotba hozása entitást, az átadott detached nem bántja) 24
Java Management eXtension ● A JMX technológia a JavaSE része, és természetesen a JEE is támogatja, szerver oldali komponensek monitorozására is használható. ● Managed Bean-ek létrehozása szükséges hozzá (MBean), melyeket az MBean server észlel és kezel. ● JMX klienst könnyedén írhatunk, de a szabvány csatorna lévén erre legtöbbször nincsen szükség (pl. jconsole egy Java SE-vel szállított kliens alkalmazás). ● Az MBean-eknek követniük kell a JMX specifikációban leírt szabályokat (JMX kliensek szabvány elérése ezáltal garantált). ● Simple Network Management Protocol (SNMP) 25
MBean készítésének szabályai ● Ha az implementáció Something class, akkor az interface SomethingMBean kell hogy legyen. ● Az MBean-ben műveleteket (operations) és attribútumokat (attributes) definiálhatunk. ○ ○ ○
Read-only A típusú xyz attribútum esetén léteznie kell egy A getXyz() metódusnak. Írható/olvasható A típusú xyz attribútum esetén létezni kell egy A getXyz() és void setXyz( A ) metódusnak. Minden olyan metódus, mely nem getter illetve setter, automatikusan műveletnek számít.
○
Nem lehet a getter/setter-t másra használni, nem lehet azonos névvel overload-olt gettert készíteni, nem lehet más az összetartozó getter/setter paraméterezése/visszatérési értékének típusa.
○
Egyszerű esetben a használható/javasolt típusok a java primitívek, tömbök, String-ek legyenek, de létezik komplexebb típus is (pl. TabularData). 26
LotteryMonitor JMX MBean készítése LotteryMonitor.java package hu.qwaevisz.lottery.ejbservice.management; public class LotteryMonitor implements LotteryMonitorMBean { @EJB A getPrizePool() és private LotteryStateHolder stateHolder; setPrizePool() implementációja @Override a puller alapján egyértelmű. public String getPuller() { return this.stateHolder.getCurrentPuller(); } @Override public void setPuller(String name) { this.stateHolder.setCurrentPuller(name); } public void start() throws Exception { A start() és a stop() LOGGER.info("Start Lottery MBean"); metódusok a JMX MBean életciklusa során } meghívódnak. public void stop() throws Exception { Használatuk opcionális. LOGGER.info("Stop Lottery MBean"); } 27
MBean regisztrációja EJBService project
src/main/resources/jboss-service.xml <server xmlns="urn:jboss:service:7.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:service:7.0 jboss-service_7_0.xsd"> <mbean code=" hu.qwaevisz.lottery.ejbservice.management.LotteryMonitor" name="lottery.mbean:service=LotteryMonitorMBean"> lottery.mbean lesz a topológiában a helye, LotteryMonitorMBean pedig ezen belül az MBean neve
A code értékénél az osztályt kell megadni, mely megfelel mindenben a JMX MBean szabványoknak!
Ez egy JBoss specifikus állomány, neve kötelezően jboss-service.xml kell hogy legyen.
28
jconsole [JRE HOME]/bin/jconsole.[bat|sh] DE: JBoss esetén a jconsole classpath-ához hozzá kell fűzni további osztályokat (pl. a “jboss-cli-client.jar”-t), ezért a [JBOSS HOME]/bin/jconsole.[bat|sh] parancsal indítsuk el (mely hivatkozik a [JRE HOME]-ban lévőre.
A JBoss AS látszódni fog a Local process-ek között (de ugyanezen klienssel Remote JVM-hez is tudunk csatlakozni). Megjegyzés: MAC OS-en előfordul(hat) hogy a JBoss nem találja meg a jconsole.sh futtatásakor a JRE HOME-ot, ilyenkor lefutattva a JBoss alatti jconsole.sh-t a CLASSPATH-t beállítjuk a terminálban, és elindítjuk ugyanebben a terminálban mi a 29 JRE HOME alatti jconsole-t.
Nyeremény ellenőrzése és kiszámolása Új ismeretet nem tartalmazó üzleti metódus implementációja, mely során a kliens RESTful interface-en keresztül beküld 5 számot, és az alkalmazás ellenőrzi hogy az aktuális sorsolás során e számok “jók-e”, avagy sem, és a nyereményalapot és a nyeremények aktuális “jogszbályban” definiált eloszlási százalékuk ismeretében visszaadja a nyeremény összegét. A nyereményalap adatbázisból olvasható ki, míg az eloszlási százalékok MBean-en keresztül írhatóak/olvashatóak (MBean operations).
30
Gradle - Deploy to JBoss gradle.build
ext { deployLocation = '/jboss-eap-6.4/standalone/deployments/' } task deployClean ( type: Delete ) { delete deployLocation + "${project.name}-${version}.ear" sleep(2000) } task deployEar ( type: Copy ) { dependsOn 'deployClean' from "build/libs/${project.name}-${version}.ear" into deployLocation }
gradle clean build deployEar 31