Monday, January 28, 2013

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:

    8.1.8.v20121106

becomes:

    7.0.34

The second change would be dependencies themselves, we will replace these lines:


    org.eclipse.jetty
    jetty-server
    ${org.eclipse.jetty.version}

     

    org.eclipse.jetty
    jetty-webapp
    ${org.eclipse.jetty.version
 

with these ones:


    org.apache.tomcat.embed
    tomcat-embed-core
    ${org.apache.tomcat}

  

    org.apache.tomcat.embed
    tomcat-embed-logging-juli
    ${org.apache.tomcat}

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,c
xf,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.

Source code: https://github.com/reta/spring-one-jar/tree/tomcat-embedded

21 comments:

Damien MARIAGE said...

Great post, could be really useful to create clean deistributions.

Not tested yet, but how do you stop your server ?

Andriy Redko said...

Thanks a lot. The simplest way to stop the server is just by sending a kill signal to terminate process gracefully (Ctrl+C also works). But you are right, I should have covered this aspect as well. The implementation I see right now is to involve another thread to handle Jetty shutdown properly (by calling stop() method on server instance).

Tim said...

Hi Andi. Why you using vendor specific classes (org.apache.catalina.*)? Much simpler to use new Servlet spec for this. This approach will work on any modern container with no additional coding.

Andriy Redko said...

Hi Tim,

Thank you for your comments. The vendor-specific classes here are necessary to run the vendor's server instance (in this case, our vendor is Apache Tomcat packaged under org.apache.catalina.*).

Thank you.

Best Regards,
Andriy Redko

Tim said...

Hi Andriy.

A strange decision to run container from your code. Why you need this?

Andriy Redko said...

Hi Tim,

I am glad you're asking this question. In short, there are several interesting use cases where embedding the servlet container is an excellent alternative. To number a few: one or single jar deployment (just run it), single jar distribution (just copy it), ease of testing + speed up of the development (no need to download or/and install anything at all).

Thank you.

Best Regards,
Andriy Redko

Al said...

Hi Andriy,

I found a problem with your Tomcat embedded code from https://github.com/reta/spring-one-jar/tree/tomcat-embedded

On JDK7 the program will randomly fail. It seems to be the order in which the Bus is initialized.

The workaround is to call the SpringBus first, before the Jaxrs gets initialized.

SpringBus bus = cxf();

JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint(jaxRsApiApplication(), JAXRSServerFactoryBean.class);

factory.setBus(bus);


The error is totally random. It fails with the error "WARNING: Can't find the the request for http://localhost:8080/rest/api/people's Observer" whenever cxf is last in the list.

So from the logs, it fails when cxf is the last component. I do not (yet) understand why this happens - especially when it's a random failure.


INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@d88c8f: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,appConfig,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,jaxRsServer,jaxRsApiApplication,peopleRestService,peopleService,jsonProvider,cxf]; root of factory hierarchy

Al said...

BTW, the failure only seems to happen with Tomcat - I could not reproduce it with Jetty.

Andriy Redko said...

Hi Al,

Thank you very much for this excellent investigation. I will work on it and commit the fix as soon as possible. You are very right, it doesn't happen all the time but it seems sometimes it does.

Thank you again a lot!

Best Regards,
Andriy Redko

Andriy Redko said...

Hi Al,

It seems like the problem is caused by order beans are created with and could be fixed by adding @DependsOn( "cxf" ) to the jaxRsServer bean definition (AppConfig.java). I've committed the fix to all branches and was not able to reproduce the issue since then. Please pull the latest changes, I hope the problem is really solved.

Thank you a lot for your very valuable help!

Best Regards,
Andriy Redko

Al McLean said...

Hi Andriy,

This works a treat, but how can I access the underlying Spring context ? I tried using the WebApplicationContextUtils to load this : context.contextHandler.getServletContext()

But it always fails with an IllegalArgumentException.

Andriy Redko said...

Hi Al,

Not sure I understood well where you would like to access Spring's application context. If you need it in any bean, just do @Inject private ApplicationContext context; (or @Autowired), it should be available for you. Please let me know if I haven't answered your question and you need help with that, a small code snippet would make me understand better :)

Thank you.

Best Regards,
Andriy Redko

Thank you.

Al McLean said...

Hi,

So from your Jetty example, if I wanted to access the Servlets' spring Application context from the Starter class I might do this :

ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(contextHandler.getServletContext());

But this always throws an IllegalArgument exception. You can find more of a code sample here :

http://stackoverflow.com/questions/16038589/problems-with-spring-3-1-java-configured-webapplicationcontext-and-jetty?noredirect=1#comment22883921_16038589

Regards,
Al.

Andriy Redko said...

Hi Al,

Got it now, thanks. It's not possible to get the Spring's application context within Starter class. The only purpose of this class is to run Jetty + Spring + JAX-RS and not being a part of running application. It's by design and if you really need it there, the application should be designed in a different way.

Thank you.

Best Regards,
Andriy Redko

Al McLean said...

Ok that's fine - guess I'll need to do some redesigning. Thanks again for the post though and thanks very much for the prompt responses.

Regards,
Alistair.

mobiusinversion said...

Hi Andriy,

I tried to make a connection pool as a bean and injecting it with @Inject like I saw in the peopleRestService. But I dont seem to get the actual object when I inject it, instead an NPE. Do you know how I can get access to the ben from another class?

@Bean
public ComboPooledDataSource comboPooledDataSource() {

ComboPooledDataSource pool = new ComboPooledDataSource();
// configure here
}
Then in another class:

public class DatabaseQuery {

@Inject private ComboPooledDataSource comboPooledDataSource;

private Connection getConnection() {
try {
return comboPooledDataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
From some debugging statements I can see the connection pool is successfully created, but when I use the comboPooledDataSource, I get a NullPointerException. How to I get the bean and use it? Am I doing this wrong?

Andriy Redko said...

Hi David,

Your DatabaseQuery class is not a bean, that's why the instances of other beans are not being injected, causing NPE.

Instantiate it as a bean, or add @ComponentScan and @Component/@Named annotations so Spring will manage its lifecycle.

Thank you.

Best Regards,
Andriy Redko

mobiusinversion said...

Hi Andriy, thanks for the reply. Can I ask you for an example of how to do that?

Right now I have

@Component
public class DatabaseQuery {

@Inject private ComboPooledDataSource comboPooledDataSource;

private Connection getConnection() {
try {
return comboPooledDataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
...
}

Then in my AppConfig:

@Bean
@Named
public ComboPooledDataSource comboPooledDataSource() {
ComboPooledDataSource pool = new ComboPooledDataSource();


I am getting an NPE on getConnection(). Am I using @Named incorrectly?

Andriy Redko said...

Hi,

Hm ... It seems right to me. I assume you are *not* calling private Connection getConnection() in constructor of DatabaseQuery class. And your AppConfig has something like that:

@Bean
public DatabaseQuery databaseQuery() {
return new DatabaseQuery();
}

Thanks.

Best Regards,
Andriy Redko

mobiusinversion said...

I think I might be missing something subtle. This worked, though is very explicit:


http://stackoverflow.com/questions/18346099/how-to-inject-a-bean-into-a-java-class/18361100#18361100

You can get it by name if you use the javax.inject @Named annotation:

ComboPooledDataSource comboPooledDataSource = ctx.getBean("myDataSource");

Then in AppConfig:

@Bean
@Named("myDataSource")
public ComboPooledDataSource comboPooledDataSource() {

Andriy Redko said...

Right, the problem is not related to ComboPooledDataSource but to the class in which you what to inject it: DatabaseQuery. This class ( DatabaseQuery) must also be declared and used as a bean.

Thanks.

Best Regards,
Andriy Redko