简体   繁体   中英

Spring AOP cannot be applied to class loaded by custom classloader?

I am learning how to implement a Tomcat-like server and I try to apply Spring AOP into this project. And this the exception I got when I tried to point my advices to a method by aop :

WARNING: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'service' defined in URL [jar:file:/Users/chaozy/Desktop/CS/projects/java/TomcatDIY/lib/TomcatDIY.jar!/uk/ac/ucl/catalina/conf/Service.class]:
 Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [uk.ac.ucl.catalina.conf.Service]: Constructor threw exception; 
nested exception is java.lang.ClassCastException: class com.sun.proxy.$Proxy31 cannot be cast to class uk.ac.ucl.catalina.conf.Connector (com.sun.proxy.$Proxy31 and uk.ac.ucl.catalina.conf.Connector are in unnamed module of loader uk.ac.ucl.classLoader.CommonClassLoader @78308db1)

So this is the Bootstrap::main where I set the CommonClassLoader to the primary class loader:

    public static void main(String[] args)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        CommonClassLoader commonClassLoader = new CommonClassLoader();

        Thread.currentThread().setContextClassLoader(commonClassLoader);

        // Invoke the init() method in class Server
        Class<?> serverClass = commonClassLoader.loadClass("uk.ac.ucl.catalina.conf.Server");
        Constructor<?> constructor = serverClass.getConstructor();
        Object serverObject = constructor.newInstance();
        Method m = serverClass.getMethod("init");
        m.invoke(serverObject);
    }

This is the Server::init method, which uses Spring to handle Service class.

public class Server{
    private void init() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        service = ApplicationContextHolder.getBean("service");
        service.start();
    }
}

This is the Service::start method, the connectors in the method are also generated by Spring.

public class Service{
    public void start() {
        for (Connector connector : connectors) {
            connector.setService(this);
            connector.init(connector.getPort());
        }
    }
}

This is my advice :

    @Before("execution(void uk.ac.ucl.catalina.conf.Connector.init(..))")
    public void initConnector(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        int port = (int)args[0];
        Logger logger = LogManager.getLogger("ServerXMLParsing");
        logger.info("Initializing ProtocolHandler [http-bio-{}]", port);
    }

The pointcut is located at one of the methods in the class Connector , which is loaded in by a custom classloader CommonClassLoader (implement java.lang.ClassLoader ).

I didn't find many similar questions online. One might be useful if The top answer of this post , which says The author's analysis is correct as the JarClassLoader must be the primary classloader of the current thread. But I am not sure if my problem is the same as that one.

In my case the default classloader would be ApplicationClassLoader if I don't use a custom one. So does it mean I have to use the default classloader if I want to apply spring aop ?

UPDATE

I put System.out.println(serverClass.getClassLoader()); in the BootStrap::main method and it showed uk.ac.ucl.classLoader.CommonClassLoader@78308db1 . And same for Connector class.

Here is the CommonClassLoader , it adds all of the jars under /lib to the url list of files and resources. This includes a file which packed all of the compiled .classes .

public class CommonClassLoader extends URLClassLoader {
    public CommonClassLoader() {
        super(new URL[]{});

        File workDir = new File(System.getProperty("user.dir"));
        File libDir = new File(workDir, "lib");
        File[] jarFiles = libDir.listFiles();

        for (File file : jarFiles) {
            if (file.getName().endsWith(".jar")){
                try {
                    URL url = new URL("file:" + file.getAbsolutePath());
                    this.addURL(url);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

In order to make the classes loaded by my own classloader instead of applicationClassLoader . Only Bootstrap and CommonClassLoader are needed to start the server, this two classes will be loaded by ApplicationClassLoader , the others will be loaded by CommonClassLoader . this startup file is used:

rm -f bootstrap.jar

jar cvf0 bootstrap.jar -C target/classes uk/ac/ucl/Bootstrap.class -C target/classes uk/ac/ucl/classLoader/CommonClassLoader.class

rm -f lib/TomcatDIY.jar

cd target/classes

jar cvf0 ../../lib/MyTomcat.jar *

cd ..
cd ..


java -cp bootstrap.jar uk.ac.ucl.Bootstrap

I can reproduce your problem with the second GitHub repo and your information how to start the server. So thank you for the MCVE. 😀

You do not have a class-loading problem like we both suspected. The explanation is much simpler: You have a Spring AOP configuration problem, a typical beginner's mistake.

Looking at this class

@Component
@Scope("prototype")
@Setter @Getter
public class Connector implements Runnable {
  // (...)
}

we see that this is a class implementing an interface. The default for Spring AOP auto-proxying is that it uses JRE dynamic proxies, ie when creating the bean like this

Connector connector = ApplicationContextHolder.getBean("connector");

Spring will create a dynamic proxy implementing all interfaces the target class also implements. In this case this is Runnable only. Ie the proxy object will only have methods from that interface and can only be cast to that interface. This explains why you cannot cast the proxy to Connector .

If you want Spring to create proxies for classes directly via CGLIB, you need to change your configuration in beans.xml to

<aop:aspectj-autoproxy proxy-target-class="true"/>

Then the server will start up normally because the CGLIB proxy is a direct Connector subclass, which also means that the assignment works as intended.

See the Spring manual for more information.


Epilogue & lesson learned: This question is a perfect example for why an MCVE is so much better and more powerful than just a set of incoherent code snippets without build files, configuration, package names, imports etc.:

  • Your question described your unusual approach with the custom class loader, so you naturally assumed that the problem's root cause was connected to that approach.
  • You described it in enough detail to also make it plausible to me and possibly to other readers. The effect was that just like you with the information given I kind of wanted to see and solve the problem there, also because the error message about the casting problem is similar to cases where there are indeed class loader problems.
  • There was a lot of information in your question, but not the beans.xml with the auto-proxy configuration which was absolutely vital for reproducing the problem.
  • In your first repository the project did not compile, so I could not reproduce the problem, hence also not "play" with it in order to find out more.
  • The project from the second repository compiled on my machine, so I could run the application with its many classes and reproduce the problem. But it wasn't until I added some debug statements showing the Spring proxy's parent class and implemented interfaces, that I recognised what was going wrong. So I checked the Spring XML configuration and could easily fix it.
  • I might have been able to suspect the auto-proxy configuration to be the problem even without the MCVE due to the error class ...$Proxy31 cannot be cast to class ...Connector if I had had the Connector class at my disposal, seeing that it actually is a class and not an interface and concluding from the typical $Proxy[number] class name that this was a JDK dynamic proxy because CGLIB proxies have a name like Connector$$EnhancerByCGLIB$$[number] . But just seeing the source code and not being able to run it, chances are that I would have overlooked this subtle piece of information, my focus being the custom class loader. My brain is not a JVM, after all.

So when asking questions on SO or looking for debugging help as a software developer in general, always try to make the problem reproducible for your helpers by means of an MCVE. You might think you know where approximately the problem is and even provide some plausible explanation with your own biased selection of shared information. But you could be wrong and make things worse by also creating the same bias in your helpers' minds, unintentionally further obscuring the real problem and probably lengthening instead of shortening the search for a solution.

Bottom line: An MCVE is an MCVE is an MCVE - and MCVEs rule! Preparing an MCVE is not, as many people refusing to do so like to think, a waste of time and effort, but in most cases saves a ton of time and even makes the difference between solving the problem or being stuck forever.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM