Så hvad er nyt? De største nyheder i Servlets 3.0 er
- Konfiguration, herunder annotationer og dynamisk registrering
- Pluggability
- Asynkronitet
Det har været fokus for hele Java EE 6, at udvikling skal være nemmere og mere fleksibelt, samt at det skal være mere ligetil at anvende Java EE med andre frameworks. Det er i nogen grad lykkedes med Servlets, om end man kan have en bekymring for hvorvidt fleksibilitet og smidighed leder til uoverskuelighed for større systemer. Mere om dette senere.
Konfiguration vha annotationer
Som den sidste dreng i klassen har Servlets nu også taget annotationer til sig. Hvor det tidligere kun var muligt at konfigurere Servlets i web.xml kan vi nu annotere os ud af konfiguration. Dermed følger Servlets i hælene på eksempelvis EJB og JPA, der også ”møder programmøren hvor han er”, nemlig i Java-koden og ikke i en ekstern og temmelig sikkert ret uoverskuelig xml-fil
Det er som sagt ikke nyt hverken i Java EE eller Java SE for den sags skyld. Der er ingen tvivl om at flere og flere konstruktioner i Java for fremtiden bliver annotationsbaserede. Vi kan nu integrere konfiguration i vores Servlet-kode:
@WebServlet( name = "MyServletsName", urlPatterns = {"/simple"}, initParams = { @WebInitParam(name="one", value="aValue"), @WebInitParam(name="two", value ="anotherValue")}, loadOnStartup = 1 ) public class MySimpleServlet extends HttpServlet { }
og Filter-kode:
@WebFilter( urlPatterns="/allAndNone", servletNames="MyServletsName", dispatcherTypes=FORWARD ) public class MyFilter implements Filter { }
Samt den noget mere beskedne @WebListener med en enkelt attribut – value – til beskrivelse af listeneren.
Det er stort set det samme vi kan angive vha annotationer, som vi tidligere udelukkende kunne skrive i web.xml. En undtagelse er asyncSupported som vi kommer til senere (og som naturligvis også optræder i den nye web.xml). En anden undtagelse er servletNames attributten i @WebFilter, som gør det muligt at knytte filtre til navngivne servlets frem for til url-patterns. Her bliver name attributten fra @WebServlet eller <servlet-name> fra web.xml pludselig relevant som andet end et internt navn, der forbinder servlet med url-mapping i deployment descriptoren. En lille, men brugbar, forbedring.
Servlets kan ikke køre udenfor web containeren, men den nye attribut asyncSupported gør faktisk, at vi kan sende Request- og Response-objektet ud af web containeren og behandle dem i en asynkron kontekst. Hæng på, for nu kommer forklaringen snart.
Konfiguration vha dynamisk registrering
En anden ny måde at konfigurere sine Serlvets på er vha dynamisk registrering. Umiddelbart lyder det svært løfterigt og undertegnede så straks for sig hvordan servlets og jsp-sider på runtime kreerede og registrerede nye servlets, der kunne kalde op til web services og og … men det kan man ikke!
Man kan kreere og registrere servlets programmatisk og man kan gøre det runtime, men det kan udelukkende ske når ServletContext initialiseres og altså ikke i vild dynamisk interaktion med klienter og programmer. Det betyder ikke, at det ikke er brugbart. Den dynamiske kode skal ligge i en ServletContextListener eller en implementation af det nye interface ServletContainerInitializer og den eksekveres som sagt når ServletContext initialiserer. Det giver os mulighed for at undersøge deploy-miljøet og konfigurere web komponenterne i forhold til dette på runtime. En fin feature, men den skæmmes lidt af denne udviklers skuffelse over, at man ikke kan jonglere noget mere med registreringen.
Hvorom alting er, i pakken javax.servlet introduceres nu følgende interfaces:
- ServletRegistration
- ServletRegistration.Dynamic
- FilterRegistration
- FilterRegistration.Dynamic
Implementationer af disse leveres af containeren og vi får fat i dem via ServletContext.
@WebListener public class MyContentListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { ServletContext sc = sce.getServletContext(); ServletRegistration.Dynamic srd = sc.addServlet("DynamicServlet","dk.lb.webapp.DynamicServlet"); srd.addMapping("/DynamicServlet"); srd.setAsyncSupported(true); srd.setInitParameter("ParamOne", "Foo"); ServletRegistration sr = sc.getServletRegistration("NonDynamicServlet"); Collection<String> col = sr.getMappings(); sr.addMapping("/OneMoreServlet"); } }
Forskellen på ServletRegistration og den indre interface SerlvetRegistration.Dynamic er, at førstnævnte kan anvendes på alle registrerede Servlets, dvs også dem, som vi ikke har registreret dynamisk, mens sidstnævnte udelukkende kan anvendes i forbindelse med Servlets, som er tilføjet dynamisk. På den måde undgår man at den dynamiske registrering konflikter med det, som er skrevet i web.xml, idet ServletRegistration kun tilbyder get-metoder, med to undtagelser. Vi kan via ServletRegistration tilføje urlPattern og initParameters til en Servlet, der allerede er konfigureret i web.xml.
Den dynamiske lillebror ServletRegistration.Dynamic tilbyder os fuld konfiguration af en given servlet, men vi får altså kun fingre i et objekt af denne type, ved at benytte os af addServlet(..) metoden på ServletContext, dvs der vil altid være tale om en ny og ikke tidligere registreret Servlet. Her kan vi til gengæld bruge hele paletten og konfigurere servletten på samme måde som i deployment descriptor eller vha web.xml. Der er visse udfordringer ved dette. Tilføjer man fx en Servlet dynamisk under et navn som konflikter med et servletnavn i konfigurationen i web.xml vil man få fejl på deploy-time.
ServletContextListeneren er ikke et nyt bekendtskab, men det er derimod ServletContainerInitializer interfacet. Her begynder det at blive spændende og elegant. Et eksempel:
@HandlesTypes(HttpServlet.class) public class MyInitializer implements ServletContainerInitializer{ public void onStartup(Set<Class<?>> classes, ServletContext ctx) throws ServletException { for(Class servletClass:classes){ ServletRegistration.Dynamic srd = ctx.addServlet(servletClass.getName(), servletClass); srd.addMapping("/" + servletClass.getSimpleName()); } } }
Det der sker er, at jeg vha annotationen @HandlesTypes angiver hvilke typer min ServletContainerInitializer skal håndtere. Når containeren initialiseres (eller applikationen deployes) vil de klasser der opfylder kriterierne i @HandlesTypes blive injected via metoden onStartup(..). På samme vis kan jeg anvende mine egne annotationer:
public @interface MyAnnotation {} @MyAnnotation public class FirstDynamicServlet extends HttpServlet{ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println("Hello from FirstDynamicServlet"); } }
og få injected alle klasser med min annotation:
@HandlesTypes(MyAnnotation.class) public class AnotherInitializer implements ServletContainerInitializer{ public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException { for(Class servletClass:c){ // kun klasser med annotationen @MyAnnotation bliver injected // Det vil sige at det er simpelt at gruppere sine klasser // med konfiguration for øje assert servletClass.hasAnnotation(MyAnnotation.class); ServletRegistration.Dynamic srd = ctx.addServlet(servletClass.getName(), servletClass); srd.addMapping("/" + servletClass.getSimpleName()); } } }
En let og elegant måde at anvende egne annotationer uden at bøvle med refleksion, samt at konfigurere grupper af klasser på runtime. Her stopper elegancen imidlertid, for nu er det tid til at fortælle serveren hvad jeg gerne vil. Jeg skal have registreret min ServletContextInitializer, hvilket ikke sker via annotationer, ej heller programmatisk eller i deployment descriptor. Nej, det jeg skal gøre er at lægge en fil navngivet efter interfacet i mappen META-INF og i filen skrive fully qualified name på de implementerende klasser (sic..):
Det er ikke særlig smidigt i forhold til refaktorering, hvilket virker meget kontra målsætningen for Java EE 6.
Fragmenteret konfigurering
En af målsætningerne for Java EE 6 er, at det skal være nemmere at integrere med andre frameworks. Her kommer pluggability ind i billedet. Udfordringen er, at Servlets ofte bruges med frameworks, der bygger ovenpå Servlet spec’en. Et framework kommer med sin egen konfigurationskode, som web app-udvikleren skal inkludere i sin web.xml for at få framework og egen web applikation til at spille sammen og deploye korrekt på serveren. Det er bøvlet for såvel framework-udvikleren som web app-udvikleren.
Den nye feature, der løser problemet er web-fragment.xml. Et web-fragment.xml-dokument kan indeholde stort set det samme som web.xml og bruges til at konfigurere fx et framework. Når frameworket inkluderes i en war og deployes på serveren, vil containeren scanne efter web-fragment.xml dokumenter og merge indholdet med war’ens web.xml. Frameworket vil typisk være pakket i en jar-fil hvor også web-fragment.xml ligger. Det betyder altså, at web-app-udvikleren blot kan inkludere jar-filen i sin war og overlade til serveren at merge web-fragment.xml og web.xml. Det betyder også at framework-udvikleren kan bekymre sig mindre om at gøre sin xml spiselig og tilgængelig for andre udviklere. Han kan opnå bedre indkapsling ved at inkludere sin konfigurationskode i frameworkets jar-fil og lade det være op til serveren at finde, læse og merge koden.
Opsamling på konfiguration
Vores gammelkendte deployment descriptor web.xml bliver altså udfordret af annotationer, nye API’er og fragmenter. En udredning af magtforhold er på sin plads. Som alle andre steder i Java EE (EJB, JPA mv) overstyrer web.xml al anden konfiguration. To konfliktende fragmenter vil lede til fejl på deploytime. Som noget nyt kan man angive load-rækkefølge af fragmenter relativt til andre fragmenter. Den smidige og fleksible konfiguration kommer altså med rygsækken fyldt med detaljerede regler og præsedens.
Asynkronitet
Og nu kommer det! Den store performance nyhed i Servlets 3.0. Asynkron behandling af request. Det første vi skal have slået fast er, hvor asynkroniteten optræder. Lad os sige, at vi har en klient, der kalder op til en server og rammer en Servlet. Klienten beder måske om information om en kunde, som serveren skal hente fra en database. Kaldet til database går forholdsvist langsomt og indtil det returnerer er web containerens tråd blokeret.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) { Customer customer = CustDao.getCustomer(1, "Bill Joy", "Disc Drive 1"); req.setAttribute("customer", customer); req.getRequestDispatcher("/showCustomer.jsp").forward(req, resp); }
I disse web 2.0 tider med Ajax og partielle requests og konstante request til vores hårdtarbejdende web container, kan vi meget nemt komme til at opleve thread starvation med ovenstående model. Det scenario vi godt kunne tænke os er følgende:
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp){ new Thread(new Runnable(){ public void run(){ Customer customer = CustDao.getCustomer(1, "Bill Joy", "Disc Drive 1"); req.setAttribute("customer", customer); req.getRequestDispatcher("/showCust.jsp").forward(req, resp); }).start(); }
I ovenstående bliver der oprettet en ny tråd, der tager sig af databaseadgangen. Det vil sige at doGet(…) returnerer forholdsvist hurtigt og web containerens tråd bliver frigivet til at tage sig af det næste request. Problemet med ovenstående er, at det ikke virker. En web container har lov til at optimere performance ved at genbruge HttpServletRequest- hhv -Response objekterne. I det øjeblik doGet(..) metoden returnerer (eller rettere sagt – øjeblikket efter service(..) metoden returnerer og svaret dispatches til den kaldende klient) vil de to objekter blive renset for klient specifikke detaljer og lagt tilbage i object-poolen, klar til brug for næste klient. Det sker uanset om en ny tråd, som vi har oprettet, stadig har en reference til objekterne, som i ovenstående kode. Derfor kan en kørsel af ovenstående resultere i uforudsigelige resultater.
Udfordringen er altså at få Request- og Response-objekterne til at overleve doGet(..) metoden med uændret tilstand. Det er præcis hvad asynkron requestbehandling handler om. Betragt nedenstående:
protected void doGet(HttpServletRequest req, HttpServletResponse resp){ AsyncContext asyncContext = req.startAsync(); asyncContext.start(new Runnable() { run(){ Customer customer = CustDao.getCustomer(1,"Bill Joy","Disc Drive 1"); asyncContext.getRequest().setAttribute("customer", customer); asyncContext.dispatch("/showCustomer.jsp"); }); }
Umiddelbart adskiller ovenstående kode sig ikke meget fra mit hjemmestrikkede eksempel på asynkron behandling fra før, hvor jeg selv oprettede en tråd og et Runnable-objekt, der fik referencer til Request- og Response-objekterne. Forskellen er, at ovenstående kode virker! Når vi anvender det nye interface AsyncContext, får vi lov at bevare en reference til Request og Response, der rækker ud over doGet(..)-metodens levetid.
For at køre asynkron kode i en Servlet skal vi angive at den understøtter asynkronitet, hvilket kan gøres vha annotationer
@WebServlet (asyncSupported=true) public class MyServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse res){ // doAsyncronicStuff }
programmatisk
ServletContext sc = … ServletRegistration.Dynamic srd = sc.addServlet("DynamicServlet", myServlet) srd.setAsyncSupported(true);
eller i web.xml
</servlet> <servlet-name>MyServlet</servlet-name> … <async-supported>true</async-supported> </servlet>
Det, der sker når vi starter den asynkrone process er, at web containerens tråd frigives uden at dispatche et svar til klienten. Tråden kan dermed bruges til at håndtere det næste klient-request. Alt imens er request og response blevet overdraget til en anden tråd, som kører den asynkrone process (dette sker med kaldet asyncContext.start(…). Den tråd kan komme hvor som helst fra og det er her serveren kan optimere performance, da workload bliver lettet for web containerens tråde.
Ovenstående er naturligvis et meget simplificeret overbliksbillede over asynkronitet. Der er mange flere detaljer og anvendelsesmuligheder, som ikke bliver berørt her. Kald i asynkron kontekst kan eks. bruges til streaming, ajax polling eller simuleret server-push. Vi behøver ikke at afslutte vores response hver gang vi låner en tråd fra tråd-pool’en. I stedet kan vi blot aflevere vores tråd og stille os bag i køen igen. Specielt ved kald der lægger op til en uendelig løkke, der sender (pusher) opdateringer til klienten, vil asynkronitet være en vigtig performancebooster.
Opsamling
Ovenstående er en stærkt reduceret og højst partisk gennemgang af nyhederne i Servlet 3.0. Der er sket meget mere, små og halvstore forbedringer, som jeg desværre ikke har plads til at komme ind på her.
Generelt er det højst relevante forbedringer man ser i Servlets 3.0. Omkring konfiguration kan man dog indvende, at smidighed og fleksibilitet risikerer at gøre konfigurationen helt uoverskuelig. Hvor man før havde al konfiguration samlet i web.xml ligger det nu i annotationer, ServletContainerInitializer-implementationer, ServletContextListeners og fragmenter. Deployment descriptoren som vi kendte den fra Serlvets 2.5 var nok stor og bøvlet, men i det mindste indeholdt den al information. I yderste konsekvens forestiller jeg mig en frustreret udvikler, der skriver sit eget refleksionsværktøj, som kan scanne al koden og .xml-filer og samle konfiguration i én overbliksfil (som man evt kunne navngive … hmm … fx web.xml). Omvendt kan man sige, at andre frameworks, eks. JBoss Seam og Spring 2+, har haft konfiguration vha. både XML og annotationer længe, og overblikket faciliteres blot vha. IDE-plugins og tools, så mon ikke vi snart ser IDE-plugins til web app konfiguration også?
Med hensyn til asynkronitet, så ser jeg et stort potentiale for performance optimering. Dog med den krølle, at man her, som alle steder, skal kende sit system godt og bruge den nye feature klogt. Når man lader web containeren dispathce request og response til en anden tråd er det ikke uden omkostninger, så man bør kun gøre det hvis der er en reel risiko for thread starvation på web containeren.