Search code examples
javaspring-mvcdependency-injectionclassloader

Spring IllegalAccessException after isolating a module in a separate ClassLoader


I have had a problem involving jar clash between incompatible versions of BouncyCastle.

We have solved it by creating a bean that, using a Spring-defined ClassLoader bean injected as property, invokes services from classes not stored in official WEB-INF/lib folder.

Following are the beans definitions

<bean id="metainfJarClassloader" class="com.jdotsoft.jarloader.JarClassLoaderFactory" factory-method="create"/>
<bean id="jadesFactory" class="it.csttech.proxy.jades.JadesFactory">
      <constructor-arg index="0" ref="metainfJarClassloader"/>       
</bean>
<bean id="bouncyCastleBeanFactory" class="it.csttech.proxy.bouncyCastle.BouncyCastleBeanFactory">
      <constructor-arg index="0" ref="metainfJarClassloader"/>       
</bean>


    <bean id="timestampService" class="it.csttech.pcp.services.spring.TimestampServiceImpl" lazy-init="true">
        <property name="timestampServerConfig">
            <bean factory-bean="jadesFactory" factory-method="createTSServerCfg">
            -------------------
            </bean>
        </property>
        <property name="jadesFactory" ref="jadesFactory" />
        <property name="bouncyCastleBeanFactory" ref="bouncyCastleBeanFactory" />
        <property name="jarClassLoader" ref="metainfJarClassloader" />
    </bean>

How does that work? Certified Timestamp service is a wrapper around services that are defined in a separate JAR and are instantiated via reflection using the metaInfClassLoader. metaInfClassLoader service loads classes that are contained in JARs under META-INF/lib

E.g.

WEB-INF
  -- lib
    -- timestamp.jar (expanded below)
      -- META-INF
        -- lib
          -- it.infocert-jades-dts.jar
          -- org.bouncycastle-bcprov.jar
       -- src
          -- it/csttech/pcp/services/spring
             -- TimestampServiceImpl.java

TimestampServiceImpl will have its dependent classes loaded from that META-INF directory.

What I can't understand is why after this component is enabled, and invoked only by the certified timestamping service which is lazily-initialized, I get plenties of IllegalAccessErrors in Spring.

Specifically, I can't access anymore any private static class defined in an MVC controller.

Evidence:

org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.IllegalAccessError: it/package/NotificationsController$Dto
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:978) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) [servlet-api.jar:?]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) [servlet-api.jar:?]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292) [catalina.jar:8.0.39]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) [catalina.jar:8.0.39]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) [tomcat-websocket.jar:8.0.39]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) [catalina.jar:8.0.39]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) [catalina.jar:8.0.39]
        at it.phoenix.web.context.PhoenixFilter.doFilter(PhoenixFilter.java:89) [phoenix-web-3.5.0.15.jar:17]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240) [catalina.jar:8.0.39]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207) [catalina.jar:8.0.39]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) [spring-security-web-4.2.1.RELEASE.jar:4.2.1.RELEASE]
------------
Caused by: java.lang.IllegalAccessError: it/package/NotificationController$Dto
        at it.phoenix.web.controllers.secure.common.NotificationsController$$FastClassBySpringCGLIB$$7a88e7c5.invoke(<generated>) ~[?:?]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
        at it.phoenix.web.controllers.secure.common.NotificationsController$$EnhancerBySpringCGLIB$$5c5467a.scrollBottom(<generated>) ~[phoenix-web-3.5.0.15.jar:17]

Part 1 of the question

What does that IllegalAccessError mean? I have always defined DTOs within my MVC controller classes by putting them private static, and it always worked

Part 2 of the question

I can see no evidence that the JarClassLoader was actually involved in loading controller classes. Does Spring replace main class loader (or enhance itself with that class loader) once it finds any bean of type ClassLoader?


Solution

  • It was not a problem with Spring itself or my code, but the JarClassLoader has an issue itself. While well documented and understandable, the following line is culprit

        Thread.currentThread().setContextClassLoader(this); //loadClass method
    

    Analysis

    The author's analysis is correct as the JarClassLoader must be the primary classloader of the current thread. After you load a class from a jar resource, that class may load other classes because of reflection or simply because it provides services which reference other classes. So who loads the new classes recurisvely? The JarClassLoader of course.

    But then there is a problem with Spring, I still deem it unbelievable. Spring does not care about the custom class loader bean, but the ContextLoader class cares about the current thread to create a mapping between threads and contexts. Probably because Spring wants to isolate different contexts. Kudos!

    Eventually debugging Spring I found the odd. The Context map had the JarClassLoader instead of Tomcat's main URLClassLoader

    Solution

    Amend the JarClassLoader provided by jdotsoft so as to restore the original class loader after instantiating the class. This may not prevent further errors if classes who depend on classes who depend on classes want to use the ClassLoader from the thread rather than from getClass()

        Thread.currentThread().setContextClassLoader(this);
    

    Becomes

        ClassLoader old = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this);
    
        try {
        ---------
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }