Spring AOP 无法应用于由自定义类加载器加载的类?

huangapple go评论85阅读模式
英文:

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

问题

我正在学习如何实现一个类似Tomcat的服务器,并尝试将Spring AOP应用于这个项目中。下面是当我尝试通过aop将通知指向一个方法时遇到的异常:

警告:在上下文初始化期间遇到异常 - 取消刷新尝试:org.springframework.beans.factory.BeanCreationException: 错误在 URL [jar:file:/Users/chaozy/Desktop/CS/projects/java/TomcatDIY/lib/TomcatDIY.jar!/uk/ac/ucl/catalina/conf/Service.class] 中定义的名为 'service' 的 bean:
通过构造函数实例化 Bean 失败;嵌套异常是 org.springframework.beans.BeanInstantiationException: 无法实例化 [uk.ac.ucl.catalina.conf.Service]:构造函数引发异常;
嵌套异常是 java.lang.ClassCastException: 类 com.sun.proxy.$Proxy31 无法转换为类 uk.ac.ucl.catalina.conf.Connector(com.sun.proxy.$Proxy31 和 uk.ac.ucl.catalina.conf.Connector 在加载器 uk.ac.ucl.classLoader.CommonClassLoader @78308db1 的未命名模块中)

下面是我在Bootstrap::main中设置CommonClassLoader为主类加载器的部分:

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

        Thread.currentThread().setContextClassLoader(commonClassLoader);

        // 在类 Server 中调用 init() 方法
        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);
    }

下面是Server::init方法,其中使用Spring处理Service类:

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

下面是Service::start方法,方法中的connectors也是由Spring生成的:

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

下面是我的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);
    }

pointcut位于Connector类的某个方法中,该类是由自定义类加载器CommonClassLoader加载的(实现了java.lang.ClassLoader)。

我在网上没有找到类似的问题。其中一个可能有用的是此帖子的顶部答案,其中说“作者的分析是正确的,因为JarClassLoader必须是当前线程的主要类加载器。” 但我不确定我的问题是否与那个问题相同。

在我的情况下,如果我不使用自定义类加载器, 默认的类加载器将是ApplicationClassLoader。那么这是否意味着如果我想应用spring aop,我必须使用默认的类加载器呢?

更新

我在BootStrap::main方法中添加了System.out.println(serverClass.getClassLoader());,结果显示为uk.ac.ucl.classLoader.CommonClassLoader@78308db1Connector类也是一样的。

以下是CommonClassLoader,它将/lib目录下的所有jars添加到文件和资源的URL列表中,其中包括一个打包了所有已编译.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();
                }
            }
        }
    }
}

为了使类由我自己的类加载器加载而不是ApplicationClassLoader加载,只需要BootstrapCommonClassLoader来启动服务器,这两个类将由ApplicationClassLoader加载,其他类将由CommonClassLoader加载。使用以下启动文件:

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 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 &#39;service&#39; 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&lt;?&gt; serverClass = commonClassLoader.loadClass(&quot;uk.ac.ucl.catalina.conf.Server&quot;);
        Constructor&lt;?&gt; constructor = serverClass.getConstructor();
        Object serverObject = constructor.newInstance();
        Method m = serverClass.getMethod(&quot;init&quot;);
        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(&quot;beans.xml&quot;);
        service = ApplicationContextHolder.getBean(&quot;service&quot;);
        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(&quot;execution(void uk.ac.ucl.catalina.conf.Connector.init(..))&quot;)
    public void initConnector(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        int port = (int)args[0];
        Logger logger = LogManager.getLogger(&quot;ServerXMLParsing&quot;);
        logger.info(&quot;Initializing ProtocolHandler [http-bio-{}]&quot;, 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&#39;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(&quot;user.dir&quot;));
        File libDir = new File(workDir, &quot;lib&quot;);
        File[] jarFiles = libDir.listFiles();

        for (File file : jarFiles) {
            if (file.getName().endsWith(&quot;.jar&quot;)){
                try {
                    URL url = new URL(&quot;file:&quot; + 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

答案1

得分: 3

我可以使用第二个GitHub仓库重现您的问题,以及您提供的关于如何启动服务器的信息。因此,非常感谢您提供的最小完整可复现示例(MCVE)。🙂

您并没有像我们两个人猜测的那样存在类加载问题。解释要简单得多:您遇到了一个Spring AOP配置问题,这是一个典型的初学者错误。

看一下这个类:

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

我们可以看到,这是一个实现了接口的类。Spring AOP自动代理的默认行为是使用JRE动态代理。例如,当像这样创建bean时:

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

Spring将创建一个动态代理,实现目标类所实现的所有接口。在这种情况下,只有Runnable接口。换句话说,代理对象只会有来自该接口的方法,并且只能转换为该接口类型。这解释了为什么您无法将代理转换为Connector类型。

如果您希望Spring直接通过CGLIB创建类的代理,您需要在beans.xml中更改配置为:

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

然后,服务器将正常启动,因为CGLIB代理是Connector的直接子类,这也意味着赋值会按预期进行。

有关更多信息,请参阅Spring手册


**结语与教训:**这个问题是为什么一个MCVE比一组不连贯的代码片段更好、更强大的完美例子,没有构建文件、配置、包名、导入等等:

  • 您的问题描述了您使用自定义类加载器的不寻常方法,因此您自然而然地认为问题的根本原因与该方法有关。
  • 您详细描述了问题,以至于让我和可能的其他读者也能够理解和解决问题。这导致与您一样,根据所提供的信息,我有点“想要”在那里查看和解决问题,也因为关于转换问题的错误消息类似于确实存在类加载器问题的情况。
  • 您的问题中提供了很多信息,但没有包含beans.xml中的自动代理配置,这对于复现问题非常关键。
  • 在第一个存储库中,项目无法编译,因此我无法复现问题,因此也无法“尝试”解决问题,以便找出更多信息。
  • 第二个存储库的项目在我的机器上编译通过,因此我可以运行具有许多类的应用程序并复现问题。但直到我添加了一些显示Spring代理父类和实现的接口的调试语句,我才认识到问题出在哪里。因此,我检查了Spring XML配置,并很容易修复了它。
  • 即使没有MCVE,我可能也能够怀疑自动代理配置可能是问题所在,这是因为错误class ...$Proxy31 cannot be cast to class ...Connector,如果我拥有Connector类,我会看到它实际上是一个类,而不是一个接口,并且根据$Proxy[number]类名的典型形式推断出这是一个JDK动态代理,因为CGLIB代理的名称类似于Connector$$EnhancerByCGLIB$$[number]。但仅仅看到源代码,而不能运行它,有可能会忽略这个微妙的信息,因为我会把注意力集中在自定义类加载器上。毕竟,我的大脑不是JVM。

因此,当在Stack Overflow上提问或作为一名软件开发人员在寻找调试帮助时,总是努力通过MCVE来使问题可以复现。您可能认为您大致知道问题的大致位置,甚至可以用自己的偏见选择共享的信息来提供一些合理的解释。但您可能是错误的,通过在帮助者的思维中也创建相同的偏见,无意中进一步混淆了真正的问题,可能会延长而不是缩短解决方案的搜索时间。

总之:**MCVE就是MCVE,MCVE非常重要!**准备一个MCVE在大多数情况下不是,像许多拒绝这样做的人认为的那样,浪费时间和精力,而是在大多数情况下节省了大量时间,甚至可能成为解决问题与永远被卡住之间的区别。

英文:

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(&quot;prototype&quot;)
@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, i.e. when creating the bean like this

Connector connector = ApplicationContextHolder.getBean(&quot;connector&quot;);

Spring will create a dynamic proxy implementing all interfaces the target class also implements. In this case this is Runnable only. I.e. 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

&lt;aop:aspectj-autoproxy proxy-target-class=&quot;true&quot;/&gt;

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.

huangapple
  • 本文由 发表于 2020年9月28日 16:43:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/64098763.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定