Reverse Ajax s DWR, Spring, iBatis
Před nějakým časem se v Avedyi rozjížděl nový projekt. Jednalo se o klasický webový chat. Vytvořil jsem tehdy jednoduchý prototyp. Použil jsem DWR a technologii reverse ajax.
Pro tento článek jsem původní prototyp trochu rozšířil a zapojil jsem Spring Framework a jako DAO vrstvu jsem vybral iBatis.
Krátce o iBatisu
I když iBatis není plnotučné ORM s klidem bych ho nasadil na většinu projektů, na které jsem v minulosti použil Hibernate. Stručný přehled vlastností a featur iBatisu:
- Konfigurace v XML podobně jako Hibernate.
- Veškeré SQL příkazy (insert, update, select) jsou nadefinovány přímo v XML konfigurácích. V nich definujete placeholdery, které jsou při vykonávání nahrazeny skutečnými hodnotami.
- Umí relace (1:1, 1:n, m:n)
- má vlastní kešování (kongurace opět v xml)
iBatis mě nadchnul a původně jsem o něm chtěl napsat krátký článek. Ale když jsem viděl kolik zdrojů na (i českém) internetu je rozmyslel jsem si to. Seznam zdrojů o iBatisu uvádím na konci článku.
Krátce o DWR
O DWR jsem z na blogu již psal. V tomto článku bych se blížeji podíval pouze na typy reverse ajaxu, které nabízí.
Full Streaming Mode
Nejrychlejší způsob reverse ajaxu. Spojení zůstává otevřeno po dobu 60 sekund. Poté je vytvořeno nové (což se nemusí podařit pokud browser již není otevřený např). Má dvě nevýhody:
- velká zátěž serveru (při velkém počtu klientů)
- proxy, antivirové program a různé moduly webserveru mohou držet data do doby než se spojení ukončí (k zobrazování dat by pak docházelo pouze jednou za 60 sekund)
Early Closing Mode (Long Polling)
Funguje tak, že server zavře spojení s klientem a vyžádá si
spojení nové v případě, že klientovi posíláme nějaká data.
Jestliže data neposíláme je chování stejné jako u Full Streaming
modu. Spojení s klientem není uzavřeno ihned, ale lze jej konfigurovat
pomocí parametru maxWaitAfterWrite
. Typicky napstaveno na
500 ms. Server v tomto případě pošle klientovi zprávu, počtu
500 ms a poté ukončí a znovu otevře spojení s klientem.
Výhodou je odolnost vůči držení dat na straně proxy atd. Nevýhodou je opět velká zátěž serveru.
Polling Mode
Polling Mode bych pravým reverse ajaxem ani nenazýval. Spočívá v periodickém dotazování serveru klientem. Směr volání je klient → server, což je opakem předchozích dvou možností.
Full Streaming Mode je představitelem technologie Comet – tzv. spojení mezi serverem a klientem s dlouhou délkou života.
Příklad
Příkladem je jednochuchý chat. Základní usecasy jsou:
- nalogování uživatele
- zjištění aktuálního uživatele ze session
- odlogování
- poslání nového vzkazu (buď všem v chatu nebo jednomu adresátovi)
Klient
Klientem je statická html stránka (index.html), která dělá dotazy na server pomocí js souboru. V něm je volán DWR servlet. Pro komfort při práci se stránou je použita i knihovna jQuery.
Server
Serverovou částí je javovská webové aplikace běžící
v libovolném servlet kontajneru. Jediným endpointem je DWR servlet. Jako
IOC kontajner je použit Spring Framework. Obsahuje jednu remotnutou servisní
beanu – chatService. Jsou zde dva doménové objekty
– User
a Message
.
Soubor dwr.xml
obsahující konfiguraci remotnuté servisy a
bean convertor pro doménové objekty:
<dwr>
<allow>
<!-- configure service -->
<create creator="spring" javascript="ChatService" scope="application">
<param name="beanName" value="chatService"/>
</create>
<!-- configuje bean converter -->
<convert converter="bean" match="cz.vavru.test.dwr.domain.*"/>
</allow>
</dwr>
Soubor Message.xml
s iBatis mapováním:
<sqlMap namespace="Message">
<typeAlias alias="message" type="cz.vavru.test.dwr.domain.Message"/>
<resultMap id="result" class="message">
<result property="messageId" column="messageId"/>
<result property="date" column="date" />
<result property="message" column="message"/>
<result property="sender.userId" column="senderId"/>
<result property="sender.nick" column="senderNick"/>
<result property="recipient.userId" column="recipientId"/>
<result property="recipient.nick" column="recipientNick"/>
</resultMap>
<select id="getMessages" resultMap="result">
select * from Message where recipientId is null or senderId = #userId# or recipientId = #userId#
</select>
<insert id="insertMessage">
insert into Message set date = #date#, senderId = #sender.userId#, senderNick = #sender.nick#, recipientId = #recipient.userId#, recipientNick = #recipient.nick#, message = #message#
<selectKey resultClass="int" keyProperty="messageId">
select last_insert_id() as messageId
</selectKey>
</insert>
</sqlMap>
Metoda getScriptSessionsToNotifyAboutNewMessage
z třídy
cz.vavru.test.dwr.util.DwrWebContextUtil
. Úkolem
metody je vrátit kolekci objektů ScriptSession
, které mají být
notifikovány o nově příchozí zprávě. Jak je z kódu vidět
v případě, že je vybraný adresář je notifikace poslána jenom
odesílateli nebo adresátovi. Proměnná webCtx
je interface
org.directwebremoting.WebContext
obsahují např odkaz
na aktuální ScriptSession
(obdoba HttpSession). Na rozdíl od
HttpSession
vzniká nová ScriptSession
s každým obnovením stránky (F5 v prohlížeči). Ohromnou výhodou
tohoto objektu je možnost uložit si do něj libovolný atribut. Já do něj
při zalogování ukládám informace o uživateli. A tak později
přesně vím, které scriptsešný mám zavolat:
@SuppressWarnings("unchecked")
public Collection<ScriptSession> getScriptSessionsToNotifyAboutNewMessage(Message message) {
Collection<ScriptSession> pageScriptSessions = (Collection<ScriptSession>) webCtx
.getScriptSessionsByPage(currentPage);
Collection<ScriptSession> scriptSessions = new ArrayList<ScriptSession>();
if (message.getRecipient() != null) {
for (Iterator<ScriptSession> it = pageScriptSessions.iterator(); it.hasNext();) {
ScriptSession scriptSession = it.next();
User user = (User) scriptSession.getAttribute(ChatServiceImpl.SCRIPT_CHAT_USER_SES_ATTR);
if(user == null) continue;
if (message.getSender().getUserId() == user.getUserId()
|| message.getRecipient().getUserId() == user.getUserId()) {
scriptSessions.add(scriptSession);
}
}
} else {
scriptSessions = pageScriptSessions;
}
return scriptSessions;
}
Posledním fragmentem kódu je metoda addMessage
z třídy
cz.vavru.test.dwr.service.ChatServiceImpl
.
@Transactional
public void addMessage(Message message) throws ChatServiceException {
WebContext webCtx = WebContextFactory.get();
// zjisteni aktualniho usera ze session
User currentUser = (User) webCtx.getHttpServletRequest().getSession(true).getAttribute(
CHAT_USER_SES_ATTR);
// nastaveni odesilatele a data
message.setSender(currentUser);
message.setDate(new Date());
// vlozeni message do databaze
messageDao.insertMessage(message);
DwrWebContextUtil dwrUtil = new DwrWebContextUtil(WebContextFactory.get());
// zjisteni vsech scriptsession, ktere mame notifikovat o nove zprave
Collection<ScriptSession> scriptSessions = dwrUtil
.getScriptSessionsToNotifyAboutNewMessage(message);
logger.info("Sending info about new message to " + scriptSessions.size() + " users");
Util util = new Util(scriptSessions);
// timto zpusobem se vola primo kod na strane klienta
// nasledujici radek tedy zavola javascriptovou funkci addMessage (viz soubor js/chat.js) s parametrem message
// objekt message, ze samozrejme preveden do javascriptoveho sveta
util.addFunctionCall("addMessage", message);
}
Celý příklad lze stáhnout zde – dwr-ajax.zip (zip, 9.9 MB). Archiv obsahuje:
- zdrojové soubory (celý Eclipse projekt)
- dwr-ajax.sql s DDL
- war archiv
Závěr
Věřím, že se mi povedlo ukázat jak je spojení všech použitých knihoven naprosto bezbolestné a jednoduché. Při testech na mém notebooku, kdy jsem si v každém prohlížeči (IE, Opera, Mozilla) otevřel chat a vstoupil pod jiným uživatelem, byl celý chat pekelně rychlý. Zpráva byla zobrazena takřka okamžitě.
Odkazy
- iBatis SqlMaps – tak trochu opomíjený ORM
- iBatis prezentace od Dagiho
- Oficiální iBatis SqlMaps tutoriál
- Oficiální iBatis SqlMaps dokumentace
- iBatis overview
- IBatis Presentation
- Buzzword Overload
- Easing into Comet
Celý článek Přidat komentář 6. July 2008