mercredi 26 septembre 2012

Vaadin 6 + GAE + Guice-Servlet - TUTORIAL

How on earth are we supposed to create a Vaadin 6 application which is both managed by Guice and hosted on Google App Engine ? The solution is actually quite simple, but it can take a while to discover it.

The solution I present here also solves a common problem with Vaadin + GAE projects, which require different servlets to be deployed in production and development environments.

First, let me go through the steps I assume you have already completed :
  • Setup Eclipse with GPE
  • Created a GAE project
  • Resolved all dependencies :
    • vaadin.jar
    • guice-3.0.jar
    • guice-servlet-3.0.jar
    • aopalliance.jar
  • Successfully ran the application without Guice injection
(Note that I created the project with the GPE, and then added the vaadin JAR. I found it easier that way than creating the project with the Vaadin plugin)
If you have trouble with these steps, check around the web, there's already many resources that can help you. If still in trouble, post a comment and I'll describe those steps too.

So you have your Vaadin + GAE project, but you want to earn the benefits of Guice and Guice Servlet. In order to do that, you will have to let Guice handle the instanciation of your servlets. So let's configure your web.xml file with the Guice Servlet filter :

  <filter>
    <filter-name>guiceFilter</filter-name>
    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>guiceFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>com.yourcompany.yourproject.guice.Bootstrap</listener-class>
  </listener>
So far, nothing special, it's exactly what you can find on the Guice wiki. Go take a look if you're not familiar with it, it's worth its weight in gold :
http://code.google.com/p/google-guice/wiki/Servlets

In the web.xml, we reference a Bootstrap listener class. So let's code that class :
public class Bootstrap extends GuiceServletContextListener {

 @Override
 protected Injector getInjector() {
  return Guice.createInjector(new MyServletModule(), new DomainModule());
 }
}
Again, nothing new here, just standard Guice Servlet config. But hold on, now is the time to have fun. Look at the code of our ServletModule
public class MyServletModule extends ServletModule {

 @Override
 protected void configureServlets() {

  // Disable Vaadin debug mode in production
  if (GaeEnvironment.isProduction())
   getServletContext().setAttribute("productionMode", "true");

  // Set the application class name
  Map<String, String> params = new HashMap<String, String>();
  params.put("application", MainApplication.class.getName());
  
  // Deploy the servlet which is right for the current environment
  if (GaeEnvironment.isProduction()) {
   bind(ProdApplicationServlet.class).asEagerSingleton();
   serve("/app/*", "/VAADIN/*").with(ProdApplicationServlet.class, params);
  } else {
   bind(DevApplicationServlet.class).asEagerSingleton();
   serve("/app/*", "/VAADIN/*").with(DevApplicationServlet.class, params);
  }

  // Bind your Application implementation
  bind(Application.class).to(MainApplication.class).in(ServletScopes.SESSION);

  // OPTIONAL : In case you user Objectify 4
  bind(ObjectifyFilter.class).in(Singleton.class);
  filter("/*").through(ObjectifyFilter.class);

 }

}

OK, that's a lot of code. Let us review it chunk by chunk :
 // Disable Vaadin debug mode in production
  if (GaeEnvironment.isProduction())
   getServletContext().setAttribute("productionMode", "true");
As you might already be aware of, the debug feature of Vaadin is activated by default. This is not the kind of feature we want users to be able to use, so we want to make sure that it is automatically disabled when the application is in the cloud.
The GaeEnvironment class is only here to clarify the intent of the code, have a look at the implementation :
public class GaeEnvironment {

    public static boolean isProduction() {
        return SystemProperty.environment.value() == SystemProperty.Environment.Value.Production;
    }

    public static boolean isDevelopment() {
        return !isProduction();
    }

}

In the next piece of code, we first create a map of properties that we use to initialize the servlets.
// Set the application class name
  Map<String, String> params = new HashMap<String, String>();
  params.put("application", MainApplication.class.getName());
  
  // Deploy the servlet which is right for the current environment
  if (GaeEnvironment.isProduction()) {
   bind(ProdApplicationServlet.class).asEagerSingleton();
   serve("/app/*", "/VAADIN/*").with(ProdApplicationServlet.class, params);
  } else {
   bind(DevApplicationServlet.class).asEagerSingleton();
   serve("/app/*", "/VAADIN/*").with(DevApplicationServlet.class, params);
  }
And again, we check if we are in a Production or Local/Dev environment. The reason is that the GAE Production environment is different to an everyday servlet container, and Vaadin provides a specific implementation for it.
In the example above, our web application will be available at http://localhost:8888/app/ and http://localhost:8888/VAADIN/. You can get rid of the first one, which is only here to help you make nice looking URLs, but do NOT remove the second one, or you will break the integration.

Now let's look at the code of our ProdApplicationServlet :
@SuppressWarnings ("serial")
@Singleton
public class ProdApplicationServlet extends GAEApplicationServlet {

 private final Provider<Application> applicationProvider;

 @Inject
 public ProdApplicationServlet(Provider<Application> applicationProvider) {
  this.applicationProvider = applicationProvider;
 }

 @Override
 protected Application getNewApplication(HttpServletRequest request) throws ServletException {
  return applicationProvider.get();
 }

}
As you can see, this class is a simple extension of GAEApplicationServlet, which is provided by Vaadin. Since it does not support dependency injection, we have to subclass it and override its getNewApplication() method so that it returns a Guice-managed instance of Application. That way, our implementation of Application can itself be injected.

The DevApplicationServlet is very similar, the only difference is that it extends a non-GAE-specific servlet.
@SuppressWarnings ("serial")
@Singleton
public class DevApplicationServlet extends ApplicationServlet {

 private final Provider<Application> applicationProvider;

 @Inject
 public DevApplicationServlet(Provider<Application> applicationProvider) {
  this.applicationProvider = applicationProvider;
 }

 @Override
 protected Application getNewApplication(HttpServletRequest request) throws ServletException {
  return applicationProvider.get();
 }

}

Don't forget to bind the implementation of Application you want to use :
// Bind your Application implementation
  bind(Application.class).to(MainApplication.class).in(ServletScopes.SESSION);

By now, you might have realized something is wrong. Since the resolution of which implementation of Application to instantiate is made by Guice, there shouldn't be any need to pass its name as an initialization parameter to the servlets. And you would be right, however the servlets follow a FAIL FAST strategy, and they need a valid Application class name on initialization, even if it is different from the actual returned implementation.

I hope you got it to work too, and please give some feedback in the comments if there is anything missing.

P.S.: If you don't know what to do next, just put an @Inject annotation on your MainApplication's constructor so that the parameters get injected.

2 commentaires:

  1. I have a question. I have a servlet module where I am registering the servlet class with a url pattern. The module extends the ServletModule and I am overriding the conffigureServlets() method which goes like this..
    @Override
    public void configureServlets() {
    serve("/urlpattern").with(Myservlet.class);
    }

    My question is how can I unit test this? Before going to the previous question, is there any need to unit test this? If yes, how can I do it? Thanks.

    RépondreSupprimer