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.AutowiredAnnotationBeanPostProcessorINFO: 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
23 comments:
Great post, could be really useful to create clean deistributions.
Not tested yet, but how do you stop your server ?
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).
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.
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
Hi Andriy.
A strange decision to run container from your code. Why you need this?
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
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
BTW, the failure only seems to happen with Tomcat - I could not reproduce it with Jetty.
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
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
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.
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.
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.
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
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.
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?
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
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?
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
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() {
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
Can't make it run. It gives me the following output and exits:
$ java -jar target/spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar
Dec 04, 2015 5:49:49 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2f4d3709: startup date [Fri Dec 04 17:49:49 CET 2015]; root of context hierarchy
Dec 04, 2015 5:49:50 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@7106e68e: 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$ImportAwareBeanPostProcessor#0,simpleBean]; root of factory hierarchy
Called from single JAR!
Hi Luis,
Thanks for the comment. There are few branches in this Git repo, you should do git checkout tomcat-embedded once you cloned the repo. Thanks!
Best Regards,
Andriy Redko
Post a Comment