Embedding Tomcat in Eclipse with Maven, JNDI et tutti quanti!

Context

I work at « Etat de Vaud » in Switzerland and we have a lot of j2ee Servlet applications which run on Tomcat 6.0.x, that are multi-modules Maven projects.

My main concern is about development time, about the time the developer spend (or loose) compiling, packaging, deploying and running the application between 2 « tests »

Of course, we use unit tests extensively here, to develop Services, DAOs and other classes, but when it comes to pure web development (understand JSP) the « round-trip » time can become huge.

For example, on one of our project, when the developer change code in a controller (Spring MVC), it must

  • Run maven to compile the code and package the war (~30-60 seconds)
  • Copy the war to webapps of the Tomcat server (or have a symlink if on Linux)
  • Then tomcat redeploy the application, including the Spring context (DAOs, Services, SessionFactory) which takes around 30 seconds

One code modification, round-trip time = ~1 minute…

Compare that to PHP, Ruby on Rails or even Grails which are quite instantaneous!

Multi-modules Maven projects

If you have single modules Maven projects, this is not a problem. Say we have:

myapp/webapp

which contains JSP and static contents:

myapp/webapp/css/...
myapp/webapp/images/...
myapp/webapp/WEB-INF/jsp/...

You have all your code in one location, say

myapp/src/main/java/...

then Eclipse can output compiled classes to

myapp/webapp/WEB-INF/classes/...

and have your libs in

myapp/webapp/WEB-INF/lib/...

Then you can start tomcat on

myapp/webapp/

And that’s all.

But when you have Multi-modules Maven projects, then you have Java source files in 2-3 places and Eclipse is unable to write all the compiled classes in one place (myapp/webapp/WEB-INF/classes)

Embedding Tomcat in Eclipse to the rescue!

The idea is to embedd Tomcat in Eclipse, so Tomcat is in the same JVM/ClassLoader of Eclipse and Tomcat « sees » all the projects, and also all the compiled classes and all the libs that are configured in Eclipse.

Step by step:

  • Create a muli-modules Maven project that have, say, 2 modules : business and web (web depends on business)
  • Import them in Eclipse using the Maven plugin for Eclipse (http://m2eclipse.codehaus.org/)
  • You then have 3 projects in Eclipse: base, business and web
  • At this point, the web project sees all the classes and libs from business and web
  • Create a class to Embed Tomcat in Eclipse as I’ll show below
  • Run your webapp using that class and you have instantaneous view of your modifications

Embedded Tomcat, with JNDI data sources

Tomcat 6.0.x is totally able to Embed itself in your code. The idea, is to create a java main() in the test part of the web project (in myapp/src/test/java). It’s important to create it in the test part, so it won’t be in your generated WAR, and the needed dependencies (catalina.jar, jsp-api, …) won’t be neither in your packaged WAR.

Step by step:

  • Create a class that implements the Embedded part of Tomcat (See below)
  • Debug it as Java Application (It’s important to « Run as Debug » so Eclipse is able to « feed » the webapp with the modified code without restarting the whole application)

Embedded class:

public class TomcatRunner {

 private static final Logger log = Logger.getLogger(TomcatRunner.class);

 private Embedded embedded;
 private String catalinaHome;
 private String pwd;

 public TomcatRunner(String contextPath, String webappDir, String contextXmlPath) throws Exception {

 // Current dir => ..../myapp/web
 final File file = new File(".");
 pwd = file.getCanonicalPath();

 // Create CATALINA_HOME
 File catalinaHomePath = new File(pwd+"/target/CATALINA_HOME");
 if (!catalinaHomePath.exists()) {
 Assert.isTrue(catalinaHomePath.mkdirs());
 }
 catalinaHome = catalinaHomePath.getCanonicalPath();

 // Create an embedded server
 embedded = new Embedded();
 embedded.setCatalinaHome(catalinaHome);

 // Create an engine
 final Engine engine = embedded.createEngine();
 engine.setDefaultHost("localhost");

 // Create a default virtual host
 final Host host = embedded.createHost("localhost", pwd /*Bas dir for webapps*/);
 engine.addChild(host);

 // The context of myapp
 // myapp/default-web.xmlis the default web.xml used for the context.
 // It contains the JSP and "static content" servlets. You can find that file in apache-tomcat-6.0.18/conf/web.xml
 final Context context = createContext(contextPath, pwd+"/"+webappDir, pwd+"/"+contextXmlPath, "myapp/default-web.xml");
 host.addChild(context);

 // Install the assembled container hierarchy
 embedded.addEngine(engine);

 // Assemble and install a default HTTP connector
 final Connector connector = embedded.createConnector(""/*Default to localhost*/, 8080/*Port to listen*/, false/*Secure*/);
 embedded.addConnector(connector);
 }

 public void start() throws Exception {
 // Start the embedded server
 embedded.start();
 }

 private static Context createContext(String path, String docBase, String contextFilePath, String defaultWebXmlPath) {
 log.debug("Creating context '" + path + "' with docBase '" + docBase + "'");

 StandardContext context = new StandardContext();

 context.setDocBase(docBase);
 context.setPath(path);

 ContextConfig config = new ContextConfig();
 config.setDefaultWebXml(defaultWebXmlPath);
 config.setCustomAuthenticators(null);
 context.addLifecycleListener(config);
 // This is the config of the JDNI datasources
 if (StringUtils.isNotBlank(contextFilePath)) {
 File configFile = new File(contextFilePath);
 context.setConfigFile(configFile.getAbsolutePath());
 }
 return context;
 }

}

Then you can use it with:

 // A global property configured for the server
 System.setProperty("myapp.appDir", "appDir/devel");

 TomcatRunner runner = new TomcatRunner("/my/app", "webapp", "webapp/META-INF/context.xml");
 runner.start();

And then start it as « Debug ». That’s it!

JNDI

If you have JNDI data sources, you can configure them in the file

webapp/META-INF/context.xml

Example of context.xml:

<Context debug="true" reloadable="true" crossContext="true" antiJARLocking="true">
   <Resource name="jdbc/myAppDataSource" auth="Container" type="javax.sql.DataSource"
     maxActive="100" maxIdle="30" maxWait="10000"
     validationQuery="select sysdate from dual"
     username="WEB_USER" password="pwd" driverClassName="oracle.jdbc.OracleDriver"
     url="jdbc:oracle:thin:@<server>:1521:orcl"/>
</Context>

Usage

The goal is to be able to modifiy static content, JSPs or java code and that it is used instantaneously without needing a restart or a manual operation.

To be able to use that Embedded tomcat thing, you need to start it as debug, then browse to http://localhost:8080/my/app

You should then be able to access your application, through the embedded tomcat.

Static content (CSS, images):

Say you modify myapp/webapp/css/myapp.css, the file is directly in the webapp folder so it is reloaded next time you load a page that use that CSS. Idem for images.

JSPs

If Tomcat is configured to recompile JSPs when they are modified (which is the default), then as soon as you modify a JSP, it’s recompiled the next time you access it.

Java code

This is the greatest part. If you modify java code in Eclipse, eclipse recompiles the java file to the corresponding .class, then also feed it to the debugger so the JVM sees the new version. (That works only if you « Run as Debug » from Eclipse)

So when you reload your page, the new class is used and the modified code is used. Magic!

Limitations

There is one limitation, which is not due to this setup, eclipse or Tomcat. It’s due to the fact that Java debugger can’t be feed a class that has « changed too much ». Changing too much means that as long as you change code in a method, that’s normally fine. But if you add/remove/rename a class, a class method or a class member, then the debugger is unable to feed it to the JVM through the debugger. Eclipse warns by saying: « Hot code replaced failed », saying that your modified code is not available in the JVM (the old code stays there) and that you should restart your application.

You then need to simply stop the TomcatRunner and restart it.

Another limitation is that if the modified classes was already run and loaded, then it won’t be reloaded before a server restart. One example, is if you have a class that set up some values in a Singleton, changing the singleton code won’t change the singleton already in memory. Idem for the spring context.

Conclusion

Using this setup change the round-trip time from ~60 seconds to instantaneous, which is great. Due to the limitations above, we must restart the webapp every 3-10 minutes depending which code we are changing, but that’s acceptable. And remember we use Unit tests for all development, we use that setup only for pure web development (JSPs)

References:

http://www.onjava.com/pub/a/onjava/2002/04/03/tomcat.html

http://tomcat.apache.org/tomcat-6.0-doc/api/org/apache/catalina/startup/Embedded.html

Publicités

Laisser un commentaire

Choisissez une méthode de connexion pour poster votre commentaire:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :