Spring 3.1 WebApplicationInitializer & Embedded Jetty 8 AnnotationConfiguration

回眸只為那壹抹淺笑 提交于 2019-11-28 18:13:26

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();                 
            }
        }
    });

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);
        }
    }
});

Jetty 9.0.1 contains an enhancement which allows for scanning of annotations of non-jar resources (ie classes) on the container classpath. See comment #5 on the following issue for how to use it:

https://bugs.eclipse.org/bugs/show_bug.cgi?id=404176#c5

Jan

The code below did the trick in my maven project:

public static void main(String[] args) throws Exception {
    Server server = new Server();
    ServerConnector scc = new ServerConnector(server);
    scc.setPort(Integer.parseInt(System.getProperty("jetty.port", "8080")));
    server.setConnectors(new Connector[] { scc });

    WebAppContext context = new WebAppContext();
    context.setServer(server);
    context.setContextPath("/");
    context.setWar("src/main/webapp");
    context.getMetaData().addContainerResource(new FileResource(new File("./target/classes").toURI()));
    context.setConfigurations(new Configuration[]{
            new WebXmlConfiguration(),
            new AnnotationConfiguration()
    });

    server.setHandler(context);

    try {
        System.out.println(">>> STARTING EMBEDDED JETTY SERVER, PRESS ANY KEY TO STOP");
        System.out.println(String.format(">>> open http://localhost:%s/", scc.getPort()));
        server.start();
        while (System.in.available() == 0) {
            Thread.sleep(5000);
        }
        server.stop();
        server.join();
    } catch (Throwable t) {
        t.printStackTrace();
        System.exit(100);
    }

}

Based on my testing and this thread http://forum.springsource.org/showthread.php?127152-WebApplicationInitializer-not-loaded-with-embedded-Jetty I don't think it works at the moment. If you look in AnnotationConfiguration.configure:

   parseContainerPath(context, parser);
   // snip comment
   parseWebInfClasses(context, parser);
   parseWebInfLib (context, parser);

it seems coupled to a war-like deployment rather than embedded.

Here is an example using Spring MVC and embedded Jetty that might be more useful:

http://www.jamesward.com/2012/08/13/containerless-spring-mvc

It creates the Spring servlet directly rather then relying on annotations.

To those experiencing this lately, it appears this gets around the issue:

@Component
public class Initializer implements WebApplicationInitializer {

    private ServletContext servletContext;

    @Autowired
    public WebInitializer(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    @PostConstruct
    public void onStartup() throws ServletException {
        onStartup(servletContext);
    }

    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("onStartup");
    }
}

To make it work on Jetty 9 set attribute AnnotationConfiguration.CLASS_INHERITANCE_MAP on WebAppContext

webAppContext.setAttribute(AnnotationConfiguration.CLASS_INHERITANCE_MAP, createClassMap());

And here is how to create this map:

private ClassInheritanceMap createClassMap() {
    ClassInheritanceMap classMap = new ClassInheritanceMap();
    ConcurrentHashSet<String> impl = new ConcurrentHashSet<>();
    impl.add(MyWebAppInitializer.class.getName());
    classMap.put(WebApplicationInitializer.class.getName(), impl);
    return classMap;
}

I placed that solution on gitHub

What about just setting the context attribute that tells the scanner which things belong on the container classpath that need to be scanned?

context attribute: org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern ./servlet-api-[^/].jar$

It is designed to be used with jar names, but you could just match everything.

You'd need to use the WebInfConfiguration as well as the AnnotationConfiguration classes.

cheers Jan

In our case these lines helped in Jetty startup code:

    ClassList cl = Configuration.ClassList.setServerDefault(server);
    cl.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", "org.eclipse.jetty.annotations.AnnotationConfiguration");

Jetty 9 version of "magomarcelo" answer:

        context.setConfigurations(
            new org.eclipse.jetty.webapp.Configuration[] { new WebXmlConfiguration(), new AnnotationConfiguration() {
                @Override
                public void preConfigure(WebAppContext context) throws Exception {
                    final ClassInheritanceMap map = new ClassInheritanceMap();
                    final ConcurrentHashSet<String> set = new ConcurrentHashSet<>();
                    set.add(MyWebAppInitializer.class.getName());
                    map.put(WebApplicationInitializer.class.getName(), set);
                    context.setAttribute(CLASS_INHERITANCE_MAP, map);
                    _classInheritanceHandler = new ClassInheritanceHandler(map);
                }
            } });

For Jetty 9, if you have webjars, the solution provided does not work straight away as those Jars need to be on the classpath and the JAR contents need to be available as resources for your webapp. So, for that to work together with webjars, the config would have to be:

context.setExtraClasspath(pathsToWebJarsCommaSeparated);
context.setAttribute(WebInfConfiguration.WEBINF_JAR_PATTERN, ".*\\.jar$");
context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*\\.jar$");
context.setConfigurations(
        new org.eclipse.jetty.webapp.Configuration[] { 
        new WebInfConfiguration(), new MetaInfConfiguration(),
        new AnnotationConfiguration() {
            @Override
            public void preConfigure(WebAppContext context) throws Exception {
                final ClassInheritanceMap map = new ClassInheritanceMap();
                final ConcurrentHashSet<String> set = new ConcurrentHashSet<>();
                set.add(MyWebAppInitializer.class.getName());
                map.put(WebApplicationInitializer.class.getName(), set);
                context.setAttribute(CLASS_INHERITANCE_MAP, map);
                _classInheritanceHandler = new ClassInheritanceHandler(map);
            }
        } });

The order here is important (WebInfConfiguration has to come before MetaInf).

Solution that worked for me and does not involve scanning, but uses WebApplicationInitializer class that you provide. Jetty version: 9.2.20

public class Main {

public static void main(String... args) throws Exception {
    Properties properties = new Properties();
    InputStream stream = Main.class.getResourceAsStream("/WEB-INF/application.properties");
    properties.load(stream);
    stream.close();
    PropertyConfigurator.configure(properties);

    WebAppContext webAppContext = new WebAppContext();
    webAppContext.setResourceBase("resource");
    webAppContext.setContextPath(properties.getProperty("base.url"));
    webAppContext.setConfigurations(new Configuration[] {
        new WebXmlConfiguration(),
        new AnnotationConfiguration() {
            @Override
            public void preConfigure(WebAppContext context) {
                ClassInheritanceMap map = new ClassInheritanceMap();
                map.put(WebApplicationInitializer.class.getName(), new ConcurrentHashSet<String>() {{
                    add(WebInitializer.class.getName());
                    add(SecurityWebInitializer.class.getName());
                }});
                context.setAttribute(CLASS_INHERITANCE_MAP, map);
                _classInheritanceHandler = new ClassInheritanceHandler(map);
            }
        }
    });

    Server server = new Server(Integer.parseInt(properties.getProperty("base.port")));
    server.setHandler(webAppContext);
    server.start();
    server.join();
}
}

The source (in russian) of this code snippet is here: https://habrahabr.ru/post/255773/

did a simple maven project to demonstrate how it can be done cleanly.

    public class Console {
    public static void main(String[] args) {
        try {
            Server server = new Server(8080);

            //Set a handler to handle requests.
            server.setHandler(getWebAppContext());

            //starts to listen at 0.0.0.0:8080
            server.start();
            server.join();
        } catch (Exception e) {
            log.error("server exited with exception", e);
        }
    }

    private static WebAppContext getWebAppContext() {
        final WebAppContext webAppContext = new WebAppContext();

        //route all requests via this web-app.
        webAppContext.setContextPath("/");

        /*
         * point to location where the jar into which this class gets packaged into resides.
         * this could very well be the target directory in a maven development build.
         */
        webAppContext.setResourceBase("directory_where_the_application_jar_exists");

        //no web inf for us - so let the scanning know about location of our libraries / classes.
        webAppContext.getMetaData().setWebInfClassesDirs(Arrays.asList(webAppContext.getBaseResource()));

        //Scan for annotations (servlet 3+)
        final AnnotationConfiguration configuration = new AnnotationConfiguration();
        webAppContext.setConfigurations(new Configuration[]{configuration});

        return webAppContext;
    }
}

and that's all - the spring WebApplicationInitializer that you use will get detected without explicitly letting jetty server know about the existence of such an app initializer.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!