Going REST: embedding Tomcat with Spring and JAX-RS (Apache CXF)
This post is logical continuation of the previous one. The only difference is the container we are going to use: instead of Jetty it will be our old buddy Apache Tomcat. Surprisingly, it was very easy to embed the latest Apache Tomcat 7 so let me show that now. I won’t repeat the last post in full as there are no any changes except in POM file and Starter class. Aside from those two, we are reusing everything we have done before. For a POM file, we need to remove Jetty dependencies and replace it with Apache Tomcat ones. The first change would be within properties section, we will replace org.eclipse.jetty.version with org.apache.tomcat.
So this line:
<org.eclipse.jetty.version>8.1.8.v20121106</org.eclipse.jetty.version>
becomes:
<org.apache.tomcat>7.0.34</org.apache.tomcat>
The second change would be dependencies themselves, we will replace these lines:
<dependency> <groupid>org.eclipse.jetty</groupid> <artifactid>jetty-server</artifactid> <version>${org.eclipse.jetty.version}</version> </dependency> <dependency> <groupid>org.eclipse.jetty</groupid> <artifactid>jetty-webapp</artifactid> <version>${org.eclipse.jetty.version</version> </dependency>
with these ones:
<dependency> <groupid>org.apache.tomcat.embed</groupid> <artifactid>tomcat-embed-core</artifactid> <version>${org.apache.tomcat}</version> </dependency> <dependency> <groupid>org.apache.tomcat.embed</groupid> <artifactid>tomcat-embed-logging-juli</artifactid> <version>${org.apache.tomcat}</version> </dependency>
Great, this part is done. The last part is dedicated to changes in our main class implementation, where we will replace Jetty with Apache Tomcat.
package com.example; import java.io.File; import java.io.IOException; import org.apache.catalina.Context; import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.cxf.transport.servlet.CXFServlet; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import com.example.config.AppConfig; public class Starter { private final static Log log = LogFactory.getLog( Starter.class ); public static void main(final String[] args) throws Exception { final File base = createBaseDirectory(); log.info( "Using base folder: " + base.getAbsolutePath() ); final Tomcat tomcat = new Tomcat(); tomcat.setPort( 8080 ); tomcat.setBaseDir( base.getAbsolutePath() ); Context context = tomcat.addContext( "/", base.getAbsolutePath() ); Tomcat.addServlet( context, "CXFServlet", new CXFServlet() ); context.addServletMapping( "/rest/*", "CXFServlet" ); context.addApplicationListener( ContextLoaderListener.class.getName() ); context.setLoader( new WebappLoader( Thread.currentThread().getContextClassLoader() ) ); context.addParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() ); context.addParameter( "contextConfigLocation", AppConfig.class.getName() ); tomcat.start(); tomcat.getServer().await(); } private static File createBaseDirectory() throws IOException { final File base = File.createTempFile( "tmp-", "" ); if( !base.delete() ) { throw new IOException( "Cannot (re)create base folder: " + base.getAbsolutePath() ); } if( !base.mkdir() ) { throw new IOException( "Cannot create base folder: " + base.getAbsolutePath() ); } return base; } }
The code looks pretty simple but verbose because of the fact that it seems impossible to run Apache Tomcat in embedded mode without specifying some working directory. The small createBaseDirectory() function creates a temporary folder which we are feeding to Apache Tomcat as a baseDir. Implementation reveals that we are running Apache Tomcat server instance on port 8080, we are configuring Apache CXF servlet to handle all request at /rest/* path, we are adding Spring context listener and finally we are starting server up.
After building the project as a fat or one jar, we have a full-blown server hosting our JAR-RS application:
mvn clean package java -jar target/spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar
And we should see the output like that:
Jan 28, 2013 5:54:56 PM org.apache.coyote.AbstractProtocol init INFO: Initializing ProtocolHandler ['http-bio-8080'] Jan 28, 2013 5:54:56 PM org.apache.catalina.core.StandardService startInternal INFO: Starting service Tomcat Jan 28, 2013 5:54:56 PM org.apache.catalina.core.StandardEngine startInternal INFO: Starting Servlet Engine: Apache Tomcat/7.0.34 Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_0.xsd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_1.xsd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/jsp_2_2.xsd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_1_1.dtd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_1_2.dtd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_2_0.xsd Jan 28, 2013 5:54:56 PM org.apache.catalina.startup.DigesterFactory register WARNING: Could not get url for /javax/servlet/jsp/resources/web-jsptaglibrary_2_1.xsd Jan 28, 2013 5:54:57 PM org.apache.catalina.loader.WebappLoader setClassPath INFO: Unknown loader com.simontuffs.onejar.JarClassLoader@187a84e4 class com.simontuffs.onejar.JarClassLoader Jan 28, 2013 5:54:57 PM org.apache.catalina.core.ApplicationContext log INFO: Initializing Spring root WebApplicationContext Jan 28, 2013 5:54:57 PM org.springframework.web.context.ContextLoader initWebApplicationContext INFO: Root WebApplicationContext: initialization started Jan 28, 2013 5:54:58 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh INFO: Refreshing Root WebApplicationContext: startup date [Mon Jan 28 17:54:58 EST 2013]; root of context hierarchy Jan 28, 2013 5:54:58 PM org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider registerDefaultFilters INFO: JSR-330 'javax.inject.Named' annotation found and supported for component scanning Jan 28, 2013 5:54:58 PM org.springframework.web.context.support.AnnotationConfigWebApplicationContext loadBeanDefinitions INFO: Successfully resolved class for [com.example.config.AppConfig] Jan 28, 2013 5:54:58 PM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring Jan 28, 2013 5:54:58 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@62770d2e: defining beans [org.springframework.context.annotation.internal ConfigurationAnnotationProcessor,org.springframework.context.annotation. internalAutowiredAnnotationProcessor,org.springframework.context.annotation. internalRequiredAnnotationProces sor,org.springframework.context.annotation.internalCommonAnnotationProcessor, appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor. importAwareProcessor, cxf,jaxRsServer,jaxRsApiApplication,peopleRestService,peopleService,jsonProvider]; root of factory hierarchy Jan 28, 2013 5:54:59 PM org.apache.cxf.endpoint.ServerImpl initDestination INFO: Setting the server's publish address to be /api Jan 28, 2013 5:54:59 PM org.springframework.web.context.ContextLoader initWebApplicationContext INFO: Root WebApplicationContext: initialization completed in 1747 ms Jan 28, 2013 5:54:59 PM org.apache.coyote.AbstractProtocol start INFO: Starting ProtocolHandler ['http-bio-8080']
Let’s issue some HTTP requests so to be sure everything works as we expected:
> curl http://localhost:8080/rest/api/people?page=2 [ {'email':'person+6@at.com','firstName':null,'lastName':null}, {'email':'person+7@at.com','firstName':null,'lastName':null}, {'email':'person+8@at.com','firstName':null,'lastName':null}, {'email':'person+9@at.com','firstName':null,'lastName':null}, {'email':'person+10@at.com','firstName':null,'lastName':null} ] > curl http://localhost:8080/rest/api/people -X PUT -d 'email=a@b.com' {'email':'a@b.com','firstName':null,'lastName':null}
And we are still 100% XML free! One important note though: we create a temporary folder every time but never delete it (calling deleteOnShutdown for base doesn’t work as expected for non-empty folders). Please keep it in mind (add your own shutdown hook, for example) as I decided to leave code clean.
Reference: http://aredko.blogspot.gr/2013/01/going-rest-embedding-tomcat-with-spring.html from our JCG partner Andrey Redko at the Andriy Redko {devmind} blog.