Servlet filter access to managed beans in single WAR deployment

My application uses a custom servlet filter added to the web.xml of the Cuba Web Client block. This needs access to the Cuba managed service beans such as DataManager, so to perform the injection I have the following code in the filter init() method:

AppContext.getApplicationContext().getAutowireCapableBeanFactory().autowireBean(this);

However when switching deployment to single WAR mode and adding the same filter definition to single-war-web.xml, the autowireBean() call above fails to inject any managed bean references. I also tried using AppBeans.get() to obtain managed bean services by class or name, and this always returns null.

I inspected com.haulmont.cuba.web.sys.singleapp.SingleAppWebServletListener, which is referenced in single-war-web.xml, and realised that it performs the job of registering the Cuba servlets and filters in single WAR mode programmatically instead of declaratively in web.xml. I tried this approach with my own ServletListener but again the managed beans were inaccessible from the context/classloader of the ServletListener.
So, the question is how can I obtain Cuba managed bean references from my servlet filter in single WAR deployment mode?

Hi Matt,

All filters and servlets in a single WAR deployment should be registered programmatically. It’s necessary for correct class loading for core and web modules.
The correct way is:

  • Create AppWebContextLoader that extends com.haulmont.cuba.web.sys.singleapp.SingleAppWebContextLoader. Register filter here. E.g:
    
    public class CustomSingleAppWebContextLoader extends SingleAppWebContextLoader {
        @Override
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            super.contextInitialized(servletContextEvent);
            ServletContext servletContext = servletContextEvent.getServletContext();
            registerHttpFilter(servletContext);
        }
    
        protected void registerHttpFilter(ServletContext servletContext) {
            TestFilter testFilter = new TestFilter();
            FilterRegistration.Dynamic cubaHttpFilterReg = servletContext.addFilter("TestFilter", testFilter);
            cubaHttpFilterReg.setAsyncSupported(true);
            cubaHttpFilterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
        }
    }
    
  • Create ServletListener that extends com.haulmont.cuba.web.sys.singleapp.SingleAppWebServletListener. Class should be create extended AppContextLoader
    
    public class CustomSingleAppWebServletListener extends SingleAppWebServletListener {
        protected Object appContextLoader;
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            try {
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                //need to put the following class to WebAppClassLoader, to share it between for web and core
                contextClassLoader.loadClass("com.haulmont.cuba.core.sys.remoting.LocalServiceDirectory");
    
                ServletContext servletContext = sce.getServletContext();
                String dependenciesFile;
                try {
                    dependenciesFile = IOUtils.toString(servletContext.getResourceAsStream("/WEB-INF/web.dependencies"), "UTF-8");
                } catch (IOException e) {
                    throw new RuntimeException("An error occurred while loading dependencies file", e);
                }
    
                String[] dependenciesNames = dependenciesFile.split("\\n");
                URL[] urls = Arrays.stream(dependenciesNames)
                        .map((String name) -> {
                            try {
                                return servletContext.getResource("/WEB-INF/lib/" + name);
                            } catch (MalformedURLException e) {
                                throw new RuntimeException("An error occurred while loading dependency " + name, e);
                            }
                        })
                        .toArray(URL[]::new);
                URLClassLoader webClassLoader = new CubaSingleAppClassLoader(urls, contextClassLoader);
    
                Thread.currentThread().setContextClassLoader(webClassLoader);
                Class<?> appContextLoaderClass = webClassLoader.loadClass("com.company.singlefilter.web.CustomSingleAppWebContextLoader");
                appContextLoader = appContextLoaderClass.newInstance();
    
                Method setJarsNamesMethod = ReflectionUtils.findMethod(appContextLoaderClass, "setJarNames", String.class);
                ReflectionUtils.invokeMethod(setJarsNamesMethod, appContextLoader, dependenciesFile);
    
                Method contextInitializedMethod = ReflectionUtils.findMethod(appContextLoaderClass, "contextInitialized", ServletContextEvent.class);
                ReflectionUtils.invokeMethod(contextInitializedMethod, appContextLoader, sce);
    
                Thread.currentThread().setContextClassLoader(contextClassLoader);
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                throw new RuntimeException("An error occurred while starting single WAR application", e);
            }
        }
    }
    
  • Register ServletListener in web.xml instead of SingleAppWebServletListener
You can see sources in the attached archive. Also, we'll try to simplify the creation of AppContextLoader in the platform version 6.7. Now, we need to copy/paste ServletListener.

Thanks,
Andrey Subbotin

web.zip (3.6K)

Thanks very much Andrey for this answer - I can see how doing this should overcome my problem.

But I am not very comfortable with copying the implementation of SingleAppWebServletListener.contextInitialized() into my class because it could very easily result in problems in the future where my copy no longer matches changes made to the Cuba platform. It is probably safest for me to revert to deploying as separate app and app-core WAR files for now.

Matt.

The issue is fixed for release 6.6. Try the new servlet context parameter webServletContextListener (see the example in the linked YouTrack issue).

Thanks Konstantin,

I tested this with platform 6.6.2, adding the webServletContextListener param to single-war-web.xml and implementing a custom ServletContextListener to programmatically add the ServletFilter as per the PL-9399 example. It worked fine, and is a much more maintainable solution.

Matt.

:ticket: See the following issue in our bug tracker:

https://youtrack.cuba-platform.com/issue/PL-9399