Spring Boot 环境中的 Bootstrap Weld

Jos*_*eta 1 tomcat cdi weld spring-boot

有谁知道在带有嵌入式 Tomcat 的 Spring Boot Jar 应用程序中引导 Weld 的方法。

我尝试将 org.jboss.weld.environment.servlet.Listener 与

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();
  }
}
Run Code Online (Sandbox Code Playgroud)

但我收到以下错误:

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
Run Code Online (Sandbox Code Playgroud)

Jos*_*eta 5

最后,我设法在使用嵌入式 Tomcat 的 Boot Spring 应用程序中引导 Weld。

存在一些与 Weld 使用的 Tomcat 容器以及 Boot Spring 生成的 jar 中的 BOOT-INF 条目的管理相关的问题。有些问题看起来像是错误,其他问题则与 Spring Boot 生成应用程序 jar 文件的方式有关。

Weld 使用 Java 服务注册一个扩展类,org.jboss.weld.environment.servlet.AbstractContainer该类用于将 Weld 注释处理器附加到相应的 servlet 容器。对于 Tomcat,此类创建一个替代servlet 容器使用的org.apache.tomcat.InstanceManager标准的类。InstanceManager

新的InstanceManager是类org.jboss.weld.environment.tomcat.WeldForwardingInstanceManager,这个类有一个方法:

private static StandardContext getStandardContext(ServletContext context)
Run Code Online (Sandbox Code Playgroud)

StandardContext使用内省从传递的私有字段中获取 a ServletContext。在 Tomcat Embedded 的情况下,传递的 ServletContext 是一个org.apache.catalina.core.StandardContext.NoPluggabilityServletContext没有相同字段的实例,并且不是ApplicationContextFacade此方法所期望的。

我修改了这个方法来应对这种情况。所以我编写了一个新WeldForwardingInstanceManager类,MyWeldForwardingInstanceManager更改了getStandardContext(ServletContext context)方法并添加了两个方法getContextFieldValue(E obj)来getContextFieldValue(String fieldname, E obj)代替现有getContextFieldValue(E obj, Class<E> clazz)方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

SecurityAction 类必须复制到同一个包中,因为它具有包可见性。

并且需要一个新的 TomcatContainer:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后需要添加一个服务来在启动时加载EmbeddedTomcatContainer。这是通过将一个具有名称的文件添加org.jboss.weld.environment.servlet.Container到应用程序文件夹services的文件夹中来完成的。META-INF该文件的内容是 MyWeldForwardingInstanceManager 类的完全限定名称。在这种情况下:

mypackage.MyWeldForwardingInstanceManager
Run Code Online (Sandbox Code Playgroud)

这些更改允许引导 Weld,在 Eclipse 中运行该应用程序没有任何问题,但在尝试从repackage使用spring-boot-maven-plugin.

为了使其在使用打包的 jar 文件时工作,您必须更改 Weld 的两个类。

第一个是org.jboss.weld.environment.deployment.discovery.FileSystemBeanArchiveHandler。getUrl()include 类 ZipFileEntry 的方法似乎存在错误,因为它在构建嵌入式 jar 文件的 URL 时没有添加 Jar 分隔符。所以需要将其更改为:

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

第二个是org.jboss.weld.environment.util.Files,这个类有一个方法filenameToClassname必须修改,以考虑到 Spring Boot 项目的类放置在该文件夹内BOOT-INF/classes,并且 Boot Spring 从该文件夹中加载它们,但 Weld 代码认为这些类是从根加载。修改后,该方法如下所示:

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;
}
Run Code Online (Sandbox Code Playgroud)

完成所有这些修改后,Weld 启动没有问题,并且所有 CDI 注释都在 Jar 打包的 Spring Boot 应用程序中工作。

编辑:

为了避免像 Omnifaces 2.x 这样使用 Weld 并在 JSF 启动时初始化的库出现问题,最好使用 servlet 容器初始化程序而不是 servlet 上下文侦听器来初始化 Weld,如 Omnifaces 作者 @BalusC 所建议的那样。看到这个答案。