问题
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 WeldForwardingInstanceManager
class, 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