Spring AOP 不能应用于 class 由自定义 classloader 加载?

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

我正在学习如何实现类似 Tomcat 的服务器,并且我尝试将 Spring AOP 应用到该项目中。当我试图通过 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)

所以这是 Bootstrap::main,我将 CommonClassLoader 设置为主 class 加载器:

    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);
    }

这是Server::init方法,用Spring来处理Service class.

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 位于 class Connector 中的方法之一,由自定义 classloader CommonClassLoader 加载(实施 java.lang.ClassLoader).

网上没搜到很多类似的问题。如果 可能会有用,上面写着 The author's analysis is correct as the JarClassLoader must be the primary classloader of the current thread. 但我不确定我的问题是否与那个问题相同。

在我的例子中,如果我不使用自定义加载器,默认的 classloader 将是 ApplicationClassLoader。那么是不是意味着我要应用spring aop就必须使用默认的classloader?

更新

我把System.out.println(serverClass.getClassLoader());放在BootStrap::main方法中,它显示uk.ac.ucl.classLoader.CommonClassLoader@78308db1Connector class.

也一样

这里是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();
                }
            }
        }
    }
}

为了使 classes 由我自己的 classloader 而不是 applicationClassLoader 加载。只有 BootstrapCommonClassLoader 需要启动服务器,这两个 class 将由 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

我可以用第二个 GitHub repo 重现你的问题,以及你如何启动服务器的信息。所以谢谢你的 MCVE。

您没有我们所怀疑的 class-loading 问题。解释要简单得多:你有一个 Spring AOP 配置问题,一个典型的初学者错误。

看着这个class

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

我们看到这是一个 class 实现接口。 Spring AOP auto-proxying 的默认设置是它使用 JRE 动态代理,即当像这样创建 bean 时

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

Spring 将创建一个实现目标 class 也实现的所有接口的动态代理。在这种情况下,这只是 Runnable。 IE。代理对象将只有来自该接口的方法,并且只能转换为该接口。这解释了为什么不能将代理转换为 Connector.

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

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

然后服务器将正常启动,因为 CGLIB 代理是直接 Connector subclass,这也意味着分配按预期工作。

有关详细信息,请参阅 Spring manual


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

  • 您的问题描述了您使用自定义 class 加载程序的不寻常方法,因此您自然而然地认为问题的根本原因与该方法有关。
  • 您对它的描述足够详细,使我和其他读者也能理解它。结果就像你一样,我有点 想要 看到并解决那里的问题,也因为关于转换问题的错误消息类似于确实存在的情况class 加载程序问题。
  • 您的问题中有很多信息,但 beans.xml 和 auto-proxy 配置对重现问题绝对至关重要。
  • 在您的第一个存储库中,该项目未编译,因此我无法重现该问题,因此也没有“玩”它以了解更多信息。
  • 第二个存储库中的项目在我的机器上编译,因此我可以 运行 应用程序及其许多 classes 并重现问题。但直到我添加了一些调试语句来显示 Spring 代理的父级 class 并实现了接口,我才意识到出了什么问题。所以我检查了 Spring XML 配置并且可以轻松修复它。
  • 即使没有 MCVE,我也可能会怀疑 auto-proxy 配置是问题所在,因为错误 class ...$Proxy31 cannot be cast to class ...Connector 如果我有 Connector class 在我的支配下,看到它实际上是一个 class 而不是一个接口,并从典型的 $Proxy[number] class 名称得出结论,这是一个 JDK 动态代理,因为 CGLIB 代理有一个像 Connector$$EnhancerByCGLIB$$[number] 这样的名字。但只是看到源代码而无法 运行 它,我很可能会忽略这条微妙的信息,我的重点是自定义 class 加载程序。毕竟我的大脑不是JVM。

因此,当作为软件开发人员询问有关 SO 的问题或寻求调试帮助时,请始终尝试通过 MCVE 为您的助手 重现问题。您可能认为您知道问题大致出在哪里,甚至可以根据您自己对共享信息的偏见选择提供一些似是而非的解释。但你可能会犯错,并在帮助者的脑海中制造同样的偏见,无意中进一步掩盖真正的问题,并可能延长而不是缩短对解决方案的搜索,从而使事情变得更糟。

底线:MCVE 是 MCVE 是 MCVE - MCVE 规则!准备 MCVE 并不是像许多拒绝这样做的人想的那样是一种浪费时间和精力,但在大多数情况下可以节省大量时间,甚至可以解决问题或永远卡住。