Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring 3.1 WebApplicationInitializer & Embedded Jetty 8 AnnotationConfiguration

I'm trying to create a simple webapp without any XML configuration using Spring 3.1 and an embedded Jetty 8 server.

However, I'm struggling to get Jetty to recognise my implementaton of the Spring WebApplicationInitializer interface.

Project structure:

src  +- main      +- java      |   +- JettyServer.java      |   +- Initializer.java      |       +- webapp          +- web.xml (objective is to remove this - see below). 

The Initializer class above is a simple implementation of WebApplicationInitializer:

import javax.servlet.ServletContext; import javax.servlet.ServletException;  import org.springframework.web.WebApplicationInitializer;  public class Initializer implements WebApplicationInitializer {      @Override     public void onStartup(ServletContext servletContext) throws ServletException {         System.out.println("onStartup");     } } 

Likewise JettyServer is a simple implementation of an embedded Jetty server:

import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext;  public class JettyServer {      public static void main(String[] args) throws Exception {           Server server = new Server(8080);          WebAppContext webAppContext = new WebAppContext();         webAppContext.setResourceBase("src/main/webapp");         webAppContext.setContextPath("/");         webAppContext.setConfigurations(new Configuration[] { new AnnotationConfiguration() });         webAppContext.setParentLoaderPriority(true);          server.setHandler(webAppContext);         server.start();         server.join();     } } 

My understanding is that on startup Jetty will use AnnotationConfiguration to scan for annotated implementations of ServletContainerInitializer; it should find Initializer and wire it in...

However, when I start the Jetty server (from within Eclipse) I see the following on the command-line:

2012-11-04 16:59:04.552:INFO:oejs.Server:jetty-8.1.7.v20120910 2012-11-04 16:59:05.046:INFO:/:No Spring WebApplicationInitializer types detected on classpath 2012-11-04 16:59:05.046:INFO:oejsh.ContextHandler:started o.e.j.w.WebAppContext{/,file:/Users/duncan/Coding/spring-mvc-embedded-jetty-test/src/main/webapp/} 2012-11-04 16:59:05.117:INFO:oejs.AbstractConnector:Started [email protected]:8080 

The important bit is this:

No Spring WebApplicationInitializer types detected on classpath 

Note that src/main/java is defined as a source folder in Eclipse, so should be on the classpath. Also note that the Dynamic Web Module Facet is set to 3.0.

I'm sure there's a simple explanation, but I'm struggling to see the wood for the trees! I suspect the key is with the following line:

... webAppContext.setResourceBase("src/main/webapp"); ... 

This makes sense with a 2.5 servlet using web.xml (see below), but what should it be when using AnnotationConfiguration?

NB: Everything fires up correctly if I change the Configurations to the following:

... webAppContext.setConfigurations(new Configuration[] { new WebXmlConfiguration() }); ... 

In this case it finds the web.xml under src/main/webapp and uses it to wire the servlet using DispatcherServlet and AnnotationConfigWebApplicationContext in the usual way (completely bypassing the WebApplicationInitializer implementation above).

This feels very much like a classpath problem, but I'm struggling to understand quite how Jetty associates itself with implementations of WebApplicationInitializer - any suggestions would be most appreciated!

For info, I'm using the following:

Spring 3.1.1 Jetty 8.1.7 STS 3.1.0

like image 353
Duncan Avatar asked Nov 04 '12 19:11

Duncan


People also ask

What is WebApplicationInitializer in Spring?

In Spring, WebApplicationInitializer is an Interface and it is Servlet 3.0+ implementation to configure ServletContext programmatically in comparison to the traditional way to do this using the web. xml file. This interface is used for booting Spring web applications.

What is the use of AbstractAnnotationConfigDispatcherServletInitializer?

Mostly, developers use AbstractAnnotationConfigDispatcherServletInitializer , which is an implementation of the WebApplicationInitializer , to create Spring web applications. Traditionally, Java web applications based on Servlets were using web. xml file to configure a Java web application.

What is the use of WebMvcConfigurer?

Interface WebMvcConfigurer. Defines callback methods to customize the Java-based configuration for Spring MVC enabled via @EnableWebMvc . @EnableWebMvc -annotated configuration classes may implement this interface to be called back and given a chance to customize the default configuration.

Where is dispatcher servlet in Spring?

Overview. The DispatcherServlet is the front controller in Spring web applications. It's used to create web applications and REST services in Spring MVC. In a traditional Spring web application, this servlet is defined in the web.


2 Answers

The problem is that Jetty's AnnotationConfiguration class does not scan non-jar resources on the classpath (except under WEB-INF/classes).

It finds my WebApplicationInitializer's if I register a subclass of AnnotationConfiguration which overrides configure(WebAppContext) to scan the host classpath in addition to the container and web-inf locations.

Most of the sub-class is (sadly) copy-paste from the parent. It includes:

  • an extra parse call (parseHostClassPath) at the end of the configure method;
  • the parseHostClassPath method which is largely copy-paste from AnnotationConfiguration's parseWebInfClasses;
  • the getHostClassPathResource method which grabs the first non-jar URL from the classloader (which, for me at least, is the file url to my classpath in eclipse).

I am using slightly different versions of Jetty (8.1.7.v20120910) and Spring (3.1.2_RELEASE), but I imagine the same solution will work.

Edit: I created a working sample project in github with some modifications (the code below works fine from Eclipse but not when running in a shaded jar) - https://github.com/steveliles/jetty-embedded-spring-mvc-noxml

In the OP's JettyServer class the necessary change would replace line 15 with:

webAppContext.setConfigurations (new Configuration [] {         new AnnotationConfiguration()          {             @Override             public void configure(WebAppContext context) throws Exception             {                 boolean metadataComplete = context.getMetaData().isMetaDataComplete();                 context.addDecorator(new AnnotationDecorator(context));                     AnnotationParser parser = null;                 if (!metadataComplete)                 {                     if (context.getServletContext().getEffectiveMajorVersion() >= 3 || context.isConfigurationDiscovered())                     {                         parser = createAnnotationParser();                         parser.registerAnnotationHandler("javax.servlet.annotation.WebServlet", new WebServletAnnotationHandler(context));                         parser.registerAnnotationHandler("javax.servlet.annotation.WebFilter", new WebFilterAnnotationHandler(context));                         parser.registerAnnotationHandler("javax.servlet.annotation.WebListener", new WebListenerAnnotationHandler(context));                     }                 }                  List<ServletContainerInitializer> nonExcludedInitializers = getNonExcludedInitializers(context);                 parser = registerServletContainerInitializerAnnotationHandlers(context, parser, nonExcludedInitializers);                  if (parser != null)                 {                     parseContainerPath(context, parser);                     parseWebInfClasses(context, parser);                     parseWebInfLib (context, parser);                     parseHostClassPath(context, parser);                 }                               }              private void parseHostClassPath(final WebAppContext context, AnnotationParser parser) throws Exception             {                 clearAnnotationList(parser.getAnnotationHandlers());                 Resource resource = getHostClassPathResource(getClass().getClassLoader());                                   if (resource == null)                     return;                  parser.parse(resource, new ClassNameResolver()                 {                     public boolean isExcluded (String name)                     {                                    if (context.isSystemClass(name)) return true;                                                    if (context.isServerClass(name)) return false;                         return false;                     }                      public boolean shouldOverride (String name)                     {                         //looking at webapp classpath, found already-parsed class of same name - did it come from system or duplicate in webapp?                         if (context.isParentLoaderPriority())                             return false;                         return true;                     }                 });                  //TODO - where to set the annotations discovered from WEB-INF/classes?                     List<DiscoveredAnnotation> annotations = new ArrayList<DiscoveredAnnotation>();                 gatherAnnotations(annotations, parser.getAnnotationHandlers());                                  context.getMetaData().addDiscoveredAnnotations (annotations);             }              private Resource getHostClassPathResource(ClassLoader loader) throws IOException             {                 if (loader instanceof URLClassLoader)                 {                     URL[] urls = ((URLClassLoader)loader).getURLs();                     for (URL url : urls)                         if (url.getProtocol().startsWith("file"))                             return Resource.newResource(url);                 }                 return null;                                 }         },     }); 

Update: Jetty 8.1.8 introduces internal changes that are incompatible with the code above. For 8.1.8 the following seems to work:

webAppContext.setConfigurations (new Configuration []     {         // This is necessary because Jetty out-of-the-box does not scan         // the classpath of your project in Eclipse, so it doesn't find         // your WebAppInitializer.         new AnnotationConfiguration()          {             @Override             public void configure(WebAppContext context) throws Exception {                    boolean metadataComplete = context.getMetaData().isMetaDataComplete();                    context.addDecorator(new AnnotationDecorator(context));                         //Even if metadata is complete, we still need to scan for ServletContainerInitializers - if there are any                    AnnotationParser parser = null;                    if (!metadataComplete)                    {                        //If metadata isn't complete, if this is a servlet 3 webapp or isConfigDiscovered is true, we need to search for annotations                        if (context.getServletContext().getEffectiveMajorVersion() >= 3 || context.isConfigurationDiscovered())                        {                            _discoverableAnnotationHandlers.add(new WebServletAnnotationHandler(context));                            _discoverableAnnotationHandlers.add(new WebFilterAnnotationHandler(context));                            _discoverableAnnotationHandlers.add(new WebListenerAnnotationHandler(context));                        }                    }                     //Regardless of metadata, if there are any ServletContainerInitializers with @HandlesTypes, then we need to scan all the                    //classes so we can call their onStartup() methods correctly                    createServletContainerInitializerAnnotationHandlers(context, getNonExcludedInitializers(context));                     if (!_discoverableAnnotationHandlers.isEmpty() || _classInheritanceHandler != null || !_containerInitializerAnnotationHandlers.isEmpty())                    {                                   parser = createAnnotationParser();                         parse(context, parser);                         for (DiscoverableAnnotationHandler h:_discoverableAnnotationHandlers)                            context.getMetaData().addDiscoveredAnnotations(((AbstractDiscoverableAnnotationHandler)h).getAnnotationList());                          }              }              private void parse(final WebAppContext context, AnnotationParser parser) throws Exception             {                                    List<Resource> _resources = getResources(getClass().getClassLoader());                  for (Resource _resource : _resources)                 {                     if (_resource == null)                         return;                      parser.clearHandlers();                     for (DiscoverableAnnotationHandler h:_discoverableAnnotationHandlers)                     {                         if (h instanceof AbstractDiscoverableAnnotationHandler)                             ((AbstractDiscoverableAnnotationHandler)h).setResource(null); //                     }                     parser.registerHandlers(_discoverableAnnotationHandlers);                     parser.registerHandler(_classInheritanceHandler);                     parser.registerHandlers(_containerInitializerAnnotationHandlers);                      parser.parse(_resource,                                   new ClassNameResolver()                     {                         public boolean isExcluded (String name)                         {                             if (context.isSystemClass(name)) return true;                             if (context.isServerClass(name)) return false;                             return false;                         }                          public boolean shouldOverride (String name)                         {                             //looking at webapp classpath, found already-parsed class of same name - did it come from system or duplicate in webapp?                             if (context.isParentLoaderPriority())                                 return false;                             return true;                         }                     });                 }             }              private List<Resource> getResources(ClassLoader aLoader) throws IOException             {                 if (aLoader instanceof URLClassLoader)                 {                     List<Resource> _result = new ArrayList<Resource>();                     URL[] _urls = ((URLClassLoader)aLoader).getURLs();                                           for (URL _url : _urls)                         _result.add(Resource.newResource(_url));                      return _result;                 }                 return Collections.emptyList();                              }         }     }); 
like image 147
Stevie Avatar answered Sep 29 '22 11:09

Stevie


I was able to resolve in an easier but more limited way by just providing explicitly to the AnnotationConfiguration the implementation class (MyWebApplicationInitializerImpl in this example) that I want to be loaded like this:

webAppContext.setConfigurations(new Configuration[] {     new WebXmlConfiguration(),     new AnnotationConfiguration() {         @Override         public void preConfigure(WebAppContext context) throws Exception {             MultiMap<String> map = new MultiMap<String>();             map.add(WebApplicationInitializer.class.getName(), MyWebApplicationInitializerImpl.class.getName());             context.setAttribute(CLASS_INHERITANCE_MAP, map);             _classInheritanceHandler = new ClassInheritanceHandler(map);         }     } }); 
like image 26
magomarcelo Avatar answered Sep 29 '22 11:09

magomarcelo