Bootstrap Weld in a Spring Boot environment

自古美人都是妖i 提交于 2019-12-25 12:55:25

问题


Does anyone know a way to bootstrap Weld in a Spring Boot Jar application with embedded Tomcat.

I have tried to use org.jboss.weld.environment.servlet.Listener with

import org.jboss.weld.environment.servlet.Listener; 

@SpringBootApplication
public class MyApplication
{
  public static void main(String[] args)
  {
    SpringApplication.run(MyApplication.class, args);
  }

  @Bean
  public Listener weldListener()
  {
    return new Listener();
  }
}

But I get the following error:

java.lang.RuntimeException: WELD-ENV-001104: Cannot get StandardContext from ServletContext.
    at org.jboss.weld.environment.tomcat.WeldForwardingInstanceManager.getStandardContext(WeldForwardingInstanceManager.java:104) ~[weld-servlet-2.4.6.Final.jar:2.4.6.Final]
...
Caused by: java.lang.ClassCastException: org.apache.catalina.core.StandardContext$NoPluggabilityServletContext cannot be cast to org.apache.catalina.core.ApplicationContextFacade
    at org.jboss.weld.environment.tomcat.WeldForwardingInstanceManager.getStandardContext(WeldForwardingInstanceManager.java:101) ~[weld-servlet-2.4.6.Final.jar:2.4.6.Final]
... 13 common frames omitted

回答1:


Finally I managed to bootstrap Weld in a Boot Spring application that uses an embedded Tomcat.

There are some problems related to both the Tomcat container that Weld uses and the management of the BOOT-INF entries in the Boot Spring generated jar. Some of the problems seem bugs and the other ones are related to the way Spring Boot generates the application jar file.

Weld uses Java Services to register a class that extends org.jboss.weld.environment.servlet.AbstractContainer that is used to attach the Weld annotations processor to the corresponding servlet container. In the case of Tomcat this class creates a org.apache.tomcat.InstanceManager that replaces the standard InstanceManager that the servlet container uses.

The new InstanceManager is the class org.jboss.weld.environment.tomcat.WeldForwardingInstanceManager, this class has a method:

private static StandardContext getStandardContext(ServletContext context)

that uses introspection to obtain a StandardContext from a private field of the passed ServletContext. In the case of Tomcat Embedded the passed ServletContext is an instance of org.apache.catalina.core.StandardContext.NoPluggabilityServletContext that no has the same field and is not an ApplicationContextFacade that is what this method expect.

I modified this method to deal with this situation. So I wrote a new WeldForwardingInstanceManagerclass, MyWeldForwardingInstanceManager, changing the getStandardContext(ServletContext context) method and adding two methods getContextFieldValue(E obj) and getContextFieldValue(String fieldname, E obj) instead of the existing getContextFieldValue(E obj, Class<E> clazz) method:

private static StandardContext getStandardContext(ServletContext context)
{
    try
    {
        // Hack into Tomcat to replace the InstanceManager using
        // reflection to access private fields
        try
        {
            ApplicationContext appContext = (ApplicationContext)getContextFieldValue(context);
            return (StandardContext)getContextFieldValue(appContext);
        }
        catch (NoSuchFieldException e)
        {
            ServletContext servletContext = (ServletContext)getContextFieldValue("sc", context);
            ApplicationContext appContext = (ApplicationContext)getContextFieldValue(servletContext);
            return (StandardContext)getContextFieldValue(appContext);
        }
    }
    catch (Exception e)
    {
        throw TomcatLogger.LOG.cannotGetStandardContext(e);
    }
}

private static <E> Object getContextFieldValue(E obj) throws NoSuchFieldException, IllegalAccessException
{
    return getContextFieldValue(CONTEXT_FIELD_NAME, obj);
}

private static <E> Object getContextFieldValue(String fieldname, E obj) throws NoSuchFieldException, IllegalAccessException
{
    Field field = SecurityActions.lookupField(obj.getClass(), fieldname);
    SecurityActions.ensureAccessible(field);
    return field.get(obj);
}

The SecurityAction class has to be copied to the same package because it has package visibility.

And a new TomcatContainer is needed:

package mypackage;

import org.jboss.weld.environment.servlet.*;
import org.jboss.weld.environment.servlet.logging.TomcatLogger;

public class EmbeddedTomcatContainer extends AbstractContainer
{
    public static final Container INSTANCE = new EmbeddedTomcatContainer();

    private static final String TOMCAT_REQUIRED_CLASS_NAME = "org.apache.catalina.connector.Request";

    @Override
    protected String classToCheck()
    {
        return TOMCAT_REQUIRED_CLASS_NAME;
    }

    @Override
    public void initialize(ContainerContext context)
    {
        try
        {
            MyWeldForwardingInstanceManager.replaceInstanceManager(context.getServletContext(), context.getManager());
            if (Boolean.TRUE.equals(context.getServletContext().getAttribute(EnhancedListener.ENHANCED_LISTENER_USED_ATTRIBUTE_NAME)))
            {
                TomcatLogger.LOG.allInjectionsAvailable();
            }
            else
            {
                TomcatLogger.LOG.listenersInjectionsNotAvailable();
            }
        }
        catch (Exception e)
        {
            TomcatLogger.LOG.unableToReplaceTomcat(e);
        }
    }
}

And then is necessary to add a Service to load the EmbeddedTomcatContainer at startup. This is done adding a file with name org.jboss.weld.environment.servlet.Container to the services folder of the META-INF folder of your applicaction. The contents of this file is the fully qualified name of the MyWeldForwardingInstanceManager class. In this case:

mypackage.MyWeldForwardingInstanceManager

These changes allow to bootstrap Weld, running the application inside Eclipse works with no problem, but fails when trying to run he application from a jar file packaged using the repackage goal of the spring-boot-maven-plugin.

To make it work when using the packaged jar file you have to change two classes of Weld.

The first one is org.jboss.weld.environment.deployment.discovery.FileSystemBeanArchiveHandler. It seems that there is a bug in the getUrl() method of the include class ZipFileEntry because it no adds a Jar separator when building the URL for an embedded jar file. So its needed to change it to:

@Override
public URL getUrl() throws MalformedURLException
{
    return new URL(archiveUrl + (archiveUrl.endsWith(".jar") ? JAR_URL_SEPARATOR : "") + name);
}

The second one is org.jboss.weld.environment.util.Files, this class has a method filenameToClassname that has to be modified to taking into account that the classes of the Spring Boot project are placed inside the folder BOOT-INF/classes and the Boot Spring loads them from it, but the Weld code thinks that these classes are loaded from the root. Once modified the method looks like:

public static String filenameToClassname(String filename)
{
    filename = filename.substring(0, filename.lastIndexOf(CLASS_FILE_EXTENSION)).replace('/', '.').replace('\\', '.');
    if (filename.startsWith("BOOT-INF.classes."))
        filename = filename.substring(BOOT_INF_CLASSES.length());
    return filename;
}

After all these modifications have been done Weld starts with no problem and all the CDI annotations work in a Jar packaged Spring Boot application.

EDIT:

In order to avoid problems with libraries like Omnifaces 2.x that use Weld and are initialized when JSF starts, is better to initialize Weld using a servlet container initializer instead of a servlet context listener as suggested by @BalusC the author of Omnifaces. See this answer.




回答2:


It's not that complicated. Through a lot of trial and error, just include the javassist.jar and putting all my application files in a jar so there is nothing in BOOT-INF/classes ended up working for me. I used weld 3.0.1



来源:https://stackoverflow.com/questions/48054288/bootstrap-weld-in-a-spring-boot-environment

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