Afscheid van de applicatieserver?

Als je een project start op basis van Java EE of Spring, dan kies je eerst een applicatieserver. Wordt het Tomcat, WildFly of WebSphere? Het maken van deze keuze lijkt vanzelfsprekend, maar wij denken dat het, anno 2014, tijd is om ons af te vragen of dat nog wel zo is. Er zijn allerlei recente ontwikkelingen die invloed hebben op die keuze, zoals de opkomst van de cloud en microservices, de populariteit van single page applicaties en de procesveranderingen die worden teweeggebracht door Continuous Delivery en DevOps. Gaan we binnenkort afscheid nemen van de applicatieserver?

De applicatieserver is een vast onderdeel van Java EE 7. Je verpakt je artifact in een WAR of EAR, plaatst deze in de deploymentfolder van Tomcat of JBoss/WildFly en draait je applicatie in de veilige en comfortabele omgeving van de container. Deze container levert een heleboel services, zoals het beheren van  databasetransacties, het zorgen dat HTTP-requests in de juiste vorm op de juiste plek in de Javacode van het project terechtkomen, het afhandelen van authenticatie en autorisatie, enzovoorts. Op deze manier hoeft een developer de complexe logica voor al die services niet zelf te bedenken en te coderen. Omdat de Java EE specificatie bovendien een breed gedragen standaard is, worden in alle projecten bij iedere klant bijvoorbeeld de client-server-communicatie op dezelfde manier afgehandeld. Het platform zorgt ervoor dat je je kunt concentreren op het schrijven van code die het project dichter bij de einddatum brengt. De container zorgt ervoor dat het allemaal robuust, schaalbaar en veilig is.

 

In dit artikel laten we zien dat applicatieservers, buiten de hierboven genoemde voordelen, ook een aantal nadelen hebben die soms haaks staan op de tijdgeest. We betogen dat we afscheid moeten nemen van de applicatieserver in zijn huidige vorm. Aan de hand van frameworks zoals Spring Boot en Dropwizard laten we zien hoe je wél kunt genieten van de lusten maar niet de lasten van de applicatieserver hoeft te dragen.

Applicatie en server: een onafscheidelijk duo

Ten eerste willen we steeds sneller onze wijzigingen aan de code op productie hebben staan, uiteraard zonder regressiefouten. Snel naar productie kunnen gaan betekent dat je artifacts eenvoudig horen te zijn: één pakketje dat door de hele deployment pipeline gaat en op alle omgevingen (lokaal, systeemtest, acceptatie, productie) gemakkelijk en snel kan worden geïnstalleerd. Als je applicatie bestaat uit een WAR of EAR die je op een applicatieserver moet installeren, dan breek je dat concept: je hebt nu niet meer één maar twee pakketjes, die meestal op een heel verschillende manier door de deployment pipeline gaan. Sterker nog, updates en wijzigingen aan de applicatieserver gaan soms zelfs via een andere afdeling. Je zou kunnen beargumenteren dat de applicatieserver hoort bij de infrastructuur, net als je OS en de Java runtime, maar dat is niet zo: je applicatieserver is een essentieel onderdeel van je applicatie. Je kunt dat goed zien aan de scope “provided” die je aan je Maven-dependencies kunt toevoegen. Met “provided” geef je aan dat een deel van je dependencies ergens anders vandaan komen, namelijk vanuit de applicatieserver.

 

Ten tweede introduceert de applicatieserver een version lock-in. Je bent in feite gebonden aan de versie van de dependencies die je applicatieserver toevallig levert. Stel dat WildFly geleverd wordt met versie x van Hibernate en je hebt vanwege een belangrijke bugfix juist versie y nodig, dan loop je tegen allerlei problemen aan. Je kunt Hibernate zelf packagen in je WAR-file, maar dan krijg je gegarandeerd te maken met allerlei classloading issues. Je kunt ook proberen om de Hibernate-versie van je applicatieserver bij te werken naar versie y, maar dat is riskant. De versies van de modules in de applicatieserver zijn immers vaak op elkaar afgestemd. Tenzij je een hoog niveau van automatisering hebt in je hele deployment pipeline, betekent een wijziging van je applicatieserver handwerk op verschillende servers, door verschillende mensen. Dat probleem doet zich niet alleen voor bij wijzigingen die je zelf doet, maar ook bij een upgrade naar een nieuwe versie van de applicatieserver. Dit zorgt ervoor dat een upgrade van de applicatieserver vaak wordt uitgesteld, en projecten noodgedwongen met verouderde componenenten moeten blijven werken. De applicatieserver beperkt zo de snelheid waarmee je software kan veranderen.

 

Ten derde is het de bedoeling dat in een applicatieserver meerdere applicaties naast elkaar draaien. Dat heeft twee nadelen. Om te beginnen zijn die verschillende applicaties niet meer van elkaar geïsoleerd. Je kunt JAX-RS niet bij de ene applicatie wél upgraden naar een nieuwe versie en bij de andere applicatie niet. Dit levert nog meer weerstand tegen het upgraden van de applicatieserver. Je zult nu in één klap  alle applicaties moeten upgraden en volledig moeten testen op regressiefouten. Daarnaast is ook de opkomst van de cloud nadelig voor het idee om meerdere applicaties in een applicatieserver te stoppen. In de cloud wil je makkelijk kunnen op- en neerschalen door goedkope servers met beperkte hardwarespecificaties aan of uit te zetten. In het geval dat applicatie A veel bezoekers trekt, wil je alleen applicatie A opschalen en niet ook applicaties B en C die toevallig ook op je applicatieserver zijn gedeployed, omdat dat onnodig resources verspilt. Een oplossing is natuurlijk om maar één applicatieserver per applicatie in te zetten. Maar als je dat doet, is het eigenlijk nog beter om de applicatieserver meteen helemaal in de applicatie te stoppen, zodat er nog maar één artifact overblijft en de deployment hierdoor makkelijker is.

 

Tot slot zorgt het splitsen van het opstarten van je applicatieserver en de deployment van je software voor meer complexiteit tijdens het ontwikkelen. Je kunt je IDE zodanig configureren dat met één druk op de knop én je project wordt gebouwd én het artifact met een hot deploy op de applicatieserver wordt geplaatst. Echter, in de praktijk blijkt dat vaak toch wat minder makkelijk te configureren. Het resultaat is dat een developer in de console zijn build maakt en, al dan niet zonder scriptje, een WAR of een EAR naar een mapje in de applicatieserver moet verplaatsen. Welbeschouwd is dat eigenlijk een hele rare constructie; je voegt al je bestanden samen tot een WAR, die je vervolgens in de deploymentfolder van de applicatieserver zet, zodat de applicatieserver de WAR weer kan uitpakken om de applicatie te draaien.

 

Kortom, er zijn redenen genoeg waarom je de applicatieserver als een integraal onderdeel van de applicatie kunt beschouwen. Hierna zullen we laten zien hoe bovengenoemde problemen met behulp van embedded applicatieservers kunnen worden opgelost.

Embedded applicatieservers

In de meeste andere programmeertalen en frameworks is de applicatieserver al een vast onderdeel van de applicatie. Dat betekent dat je de applicatie rechtstreeks opstart met de bijbehorende interpreter of server: een project met het Play! framework kan je starten met play run, bij Vert.x doe je vertx run MyApp.java en Ruby on Rails start je met rails server. Wat we graag willen bereiken is dat je dat bij Java webapplicaties ook kunt doen: java -jar application.jar en klaar.

 

Het idee om ook bij klassieke Java webapplicaties de applicatieserver te embedden in de applicatie raakt steeds meer in zwang. Er zijn verschillende mogelijkheden om servletcontainers zoals Jetty en Tomcat, of zelfs meer volledige Java EE-containers zoals GlassFish en WildFly als een library op te nemen in je project.

 

Het is niet moeilijk om dit zelf te doen. Een standalone webapplicatie op basis van Jetty kun je schrijven met slechts een paar regels code. Je kunt naar wens bouwstenen toevoegen om te kunnen werken met REST, dependency injection, security, enzovoorts. Voor RESTful webservices op basis van JAX-RS kun je bijvoorbeeld Jersey en Jackson gebruiken, zoals je in codevoorbeeld 1 kunt zien. In dit codevoorbeeld wordt de ServletContainer van Jersey als Servlet aan de Jetty container toegevoegd. Door middel van een servlet init parameter geven we aan dat de package com.example.rest gescand moet worden op JAX-RS resources, waarvan de getoonde HelloWorldResource een voorbeeld is. Het laatste stukje van de puzzel is het omzetten van POJO’s naar JSON, dat we in dit geval met Jackson doen. Hiervoor is geen configuratie nodig; het is voldoende om de Jersey-Jackson Media Module op je classpath te zetten. Het laatste paneel in codevoorbeeld 1 toont het Gradle build script waarmee je dit voorbeeld zelf in Eclipse kunt uitproberen. Als je nu de main methode uitvoert en in een browser naar http://localhost:8080/hello/Java-magazine gaat, wordt je op passende wijze begroet.

 


import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.*;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.servlet.ServletContainer;

public class Main {

public static void main(String[] args) throws Exception {
Server server = new Server(8080);
ServletContextHandler handler =
new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);

ServletHolder holder = new ServletHolder(new ServletContainer());
holder.setInitParameter(ServerProperties.PROVIDER_PACKAGES, "com.example.rest");
handler.addServlet(holder, "/*");

server.start();
server.join();
}
}

—————————————————————————————————————————————————————–


import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/hello")
public class HelloWorldResource {

@GET
@Path("/{name}")
@Produces(MediaType.APPLICATION_JSON)
public Greeting getGreeting(@PathParam("name") String name) {
return new Greeting(name);
}

public class Greeting {
private String greeting;
public Greeting(String name) { this.greeting = "Hello, " + name + "!"; }
public String getGreeting() { return greeting; }
public void setGreeting(String greeting) { this.greeting = greeting; }
}
}

—————————————————————————————————————————————————————–


apply plugin: 'java'
apply plugin: 'eclipse'

repositories {
mavenCentral()
}

dependencies {
compile 'org.eclipse.jetty:jetty-servlet:9.2.3.v20140905'
compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.13'
compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.13'
}

Listing1: Een REST-applicatie met Jetty, Jersey en Jackson

Dropwizard

Wil je toch aan de slag met een iets completere stack, dan kun je gebruik maken van één van de lichtgewicht frameworks die de laatste jaren zijn ontstaan. Een van de opties is Dropwizard, een microframework dat is samengesteld uit een klein aantal goede Java-libraries, zoals Jersey, Jackson, Guava en Logback. Je kunt Dropwizard zien als een iets uitgebreidere, opinionated versie van codevoorbeeld 1. Dropwizard is van mening is dat moderne webapplicaties een REST-API met JSON hebben en DevOps-vriendelijk moeten zijn. Daarom biedt het out-of-the-box goede faciliteiten voor configuratie, logging en metrieken, naast de basisfuncties om een webapplicatie op te zetten. Door middel van de Metrics library kun je via (onder andere) een REST-API statistieken opvragen over de gezondheid van je applicatie: hoe staat het met het geheugengebruik van de JVM, welke HTTP statuscodes worden er zoal door je REST-endpoints teruggegeven, hoeveel ERROR-events worden per seconde naar de logs geschreven, en noem maar op. Je wordt ook aangemoedigd om zelf health checks toe te voegen. Health checks zijn kleine tests die laten zien dat je applicatie goed werkt in de productieomgeving. Deze tests controleren bijvoorbeeld of er een databaseconnectie beschikbaar is en of de applicatie schrijfrechten heeft op de juiste mappen.

 

Een complete Dropwizard-applicatie met health check zie je in codevoorbeeld 2. Als je hiervan de main methode uitvoert, kun je weer http://localhost:8080/hello/Java-magazine in de browser opvragen. Daarnaast krijg je een extra ingang op poort 8081 om statistieken op te vragen. Op http://localhost:8081/healthcheck zie je bijvoorbeeld het resultaat van de health check die in het codevoorbeeld is toegevoegd.

 

Omdat Dropwizard al keuzes voor je gemaakt heeft, kun je snel productief zijn. Aan de andere kant geef je wel weer controle over je libraries uit handen. Naar onze mening bestaat de waarde van Dropwizard vooral uit de inspiratie die je eruit kunt opdoen: de code in het framework waarmee de libraries aan elkaar zijn geplakt is zó eenvoudig, dat je hem gemakkelijk kan forken om zelf je eigen voorkeurslibraries te gebruiken. Ook als je besluit om Dropwizard niet te gebruiken, kun je je voordeel doen met de opinies die in het framework worden uitgedragen.


import com.codahale.metrics.health.HealthCheck;

import io.dropwizard.*;
import io.dropwizard.setup.*;

public class HelloWorldApplication extends Application<Configuration> {

public static void main(String[] args) throws Exception {
new HelloWorldApplication().run(args);
}

@Override
public void run(Configuration configuration, Environment environment) {
final HelloWorldResource resource = new HelloWorldResource();
environment.jersey().register(resource);

// Voeg een health check toe.
// In een ‘echte’ applicatie zou je hier bijvoorbeeld kunnen controleren
// of je databaseconnectie nog werkt.
HealthCheck healthCheck = new HealthCheck() {
@Override protected Result check() throws Exception {
return resource.getGreeting("example").getGreeting().contains("example")
? Result.healthy()
: Result.unhealthy("De hello world resource groet niet persoonlijk.");
}
};
environment.healthChecks().register("hello-world-resource", healthCheck);
}

@Override
public void initialize(Bootstrap<Configuration> bootstrap) { }
}

Listing 2: Een applicatie in Dropwizard, met health check. De HelloWorldResource is hetzelfde als in codevoorbeeld 1.

Spring Boot

Een ander framework met een embedded applicatieserver is Spring Boot. Net zoals het Spring-framework jaren geleden liet zien hoe onhandig de Java EE-specificatie toendertijd was en dus van grote invloed was op de introductie van EJB 3.0 en JPA 1.0 in Java EE 5, zou Spring Boot nu het einde van de losse applicatieserver kunnen inleiden. Het Spring-framework is een volwaardig alternatief voor Java EE en met Spring Boot kun je alle mogelijkheden van Spring gebruiken in een standalone applicatie. Conventie boven configuratie zorgt ervoor dat je in een paar regels code een REST-service kunt schrijven (zie codevoorbeeld 3).

 

We hebben tot nu toe nog niet aangestipt hoe je met de beschreven technieken moet omgaan met de libraries op je classpath. Het draaien van de voorbeeldapplicaties is Eclipse is makkelijk, omdat de IDE je classpath beheert. Maar, zoals we aan het begin van deze sectie schreven, zouden we de applicatie ook graag willen kunnen starten met een simpel java -jar application.jar. Dus één enkel gemakkelijk te hanteren artifact, in plaats van een handjevol losse JAR’s. Voor WAR- en EAR-files is er een standaardmechanisme om je libraries te verpakken, maar dat bestaat helaas (nog) niet voor JAR-files.

 

Dropwizard gebruikt daarom shaded JAR’s: hierbij worden alle packages en classes van alle dependencies samengevoegd met je applicatieclasses tot een enkele JAR-file. Zulke bestanden kun je bijvoorbeeld met de Maven Shade Plugin maken. Omdat shaded JAR’s snel onoverzichtelijk kunnen worden en problemen hebben met gelijknamige resources uit verschillende dependencies, heeft Spring Boot een eigen mechanisme ontwikkeld om JAR-files in te bedden. Je kunt dan je dependencies, net als in een WAR, opnemen in de lib-directory van je applicatie-JAR.


@Controller
@EnableAutoConfiguration
@ComponentScan
public class Main {

public static void main(String[] args) throws Exception {
SpringApplication.run(Main.class, args);
}

@Autowired private GreeterBean greeterBean;

@RequestMapping("/")
@ResponseBody
private String home(@RequestParam(required = false) String user) {
return greeterBean.greeting(user);
}

@Service
public static class GreeterBean {
public String greeting(String user) {
return "hello " + (user != null ? user : "world");
}
}
}

Listing 3: Hello world in Spring Boot

 

Embedded applicatieservers en Java EE

Als je echt Java EE wilt gebruiken met een embedded applicatieserver, dan ben je vooralsnog op jezelf aangewezen. Maar ook hier worden de eerste stappen al gezet en wordt geëxperimenteerd met “Java EE Boot”, een alternatief voor Spring Boot op basis van Apache TomEE (referentie [5]).

Conclusie

En, gaan we afscheid nemen van de applicatieserver? Zoals we in dit artikel hebben laten zien, denken we dat het antwoord op die vraag luidt: ja, maar het is geen definitief vaarwel. In de komende jaren zal de applicatieserver in zijn huidige zelfstandige vorm langzaam uit beeld verdwijnen. In plaats daarvan ondergaat hij een metamorfose en keert terug als integraal onderdeel van je applicatie.

 

Referenties

  • [1] Eberhard Wolff – Java Application Servers Are Dead!
    http://www.slideshare.net/ewolff/java-application-servers-are-dead
  • [2] Parallel Universe – An Opinionated Guide to Modern Java, Part 3: Web Development http://blog.paralleluniverse.co/2014/05/15/modern-java-pt3/
  • [3] Dropwizard
    http://dropwizard.io/index.html
  • [4] Spring Boot
    http://projects.spring.io/spring-boot/
  • [5] Apache TomEE + Shrinkwrap = JavaEE Boot
    http://www.lordofthejars.com/2014/09/apache-tomee-shrinkwrap-javaee-boot-not.html