找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1230

积分

0

好友

174

主题
发表于 昨天 21:23 | 查看: 1| 回复: 0

你是否曾遇到过这样的场景?一个UserService并未实现Loggable接口,却能直接调用其log()方法。这背后的核心机制便是Spring Introduction——它能够为已有的Bean动态地扩展新的接口。本文将从源码层面深入拆解这一过程的实现逻辑,揭示Spring如何“悄无声息”地为Bean增添新能力。

一、Spring Introduction的三大核心组件

理解Introduction,关键在于掌握其协作的“三驾马车”:

1. IntroductionAdvisor:定义扩展接口的“蓝图”

IntroductionAdvisor是Spring用于描述“要为Bean添加何种接口”的组件。其核心方法是getIntroductionClass(),用于返回待扩展的接口类型。例如,若需为Bean添加Loggable接口,则需要如下自定义Advisor:

// 自定义IntroductionAdvisor
public class LoggableAdvisor implements IntroductionAdvisor {
    @Override
    public Class<?> getIntroductionClass() {
        return Loggable.class; // 指定要扩展的接口
    }
    @Override
    public boolean matches(Class<?> clazz) {
        // 仅对UserService及其子类应用此扩展
        return UserService.class.isAssignableFrom(clazz);
    }
    // 其他方法省略...
}

其中的matches()方法扮演着过滤器角色,确保扩展只作用于符合条件的目标Bean。

2. IntroductionInterceptor:实现扩展行为的“执行器”

IntroductionInterceptor扩展接口的具体功能实现者,它需要同时实现IntroductionInterceptor接口以及目标扩展接口(如Loggable)。当代理对象调用扩展接口方法时,最终执行的是此处定义的逻辑:

// 自定义IntroductionInterceptor
public class LoggableInterceptor implements IntroductionInterceptor, Loggable {
    @Override
    public void log(String message) {
        System.out.println("[Log] " + message); // 实现具体的日志功能
    }
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 判断当前调用的方法是否属于扩展接口
        if (Loggable.class.isAssignableFrom(invocation.getMethod().getDeclaringClass())) {
            // 若是,则执行本Interceptor中的实现
            return invocation.getMethod().invoke(this, invocation.getArguments());
        }
        // 否则,执行目标Bean原有的方法逻辑
        return invocation.proceed();
    }
    // 其他方法省略...
}

invoke()方法是核心枢纽,它决定了调用流量是流向扩展实现还是原生Bean。

3. DefaultAdvisorAutoProxyCreator:生成代理的“发动机”

DefaultAdvisorAutoProxyCreator是Spring提供的自动代理创建器。它会扫描容器中所有的Advisor,当发现匹配的IntroductionAdvisor时,便触发动态代理的生成过程。其核心逻辑封装在getProxy()方法中:

// DefaultAdvisorAutoProxyCreator的核心方法(简化逻辑)
protected Object getProxy(Class<?> beanClass, String beanName) {
    // 1. 寻找所有适用于当前Bean的Advisor(包含IntroductionAdvisor)
    List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
    if (advisors.isEmpty()) {
        return bean; // 若无匹配的Advisor,则直接返回原Bean
    }
    // 2. 创建动态代理(根据情况选择JDK动态代理或CGLIB)
    return createProxy(beanClass, beanName, advisors, new SingletonTargetSource(bean));
}

findEligibleAdvisors()方法找到了我们定义的LoggableAdvisor,Spring便会为UserService创建相应的代理对象。

二、源码追踪:Bean的动态扩展流程

我们可以通过以下UML时序图来梳理Spring Introduction的完整工作流程(图中步骤与源码关键节点对应):

Spring Introduction时序图

沿着源码,我们可以梳理出以下几个关键步骤:

步骤1:Bean获取触发代理创建

当通过applicationContext.getBean(UserService.class)获取Bean时,Spring会咨询DefaultAdvisorAutoProxyCreator。该创建器会检查UserService是否符合LoggableAdvisormatches()方法设定的条件。若匹配,则启动代理创建流程。

步骤2:生成JDK动态代理

由于UserService是一个接口,Spring默认会使用JdkDynamicAopProxy来创建代理。在构建代理时,LoggableAdvisor所关联的LoggableInterceptor被整合到调用链中:

// JdkDynamicAopProxy的构造函数
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
    this.advised = config;
    // 将Advisor中的Interceptor整合到代理配置中
    this.interceptors = config.getInterceptorsAndDynamicInterceptionAdvice(null, null);
}
步骤3:执行扩展接口的逻辑

当我们通过代理对象调用log()方法时,会触发JdkDynamicAopProxyinvoke()方法,这里是路由决策的核心:

// JdkDynamicAopProxy.invoke() 方法的关键逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 1. 检查当前方法是否属于通过Introduction扩展的接口
    IntroductionInfo introductionInfo = this.advised.getIntroductionInfo();
    if (introductionInfo != null && introductionInfo.isIntroductionMethod(method)) {
        // 2. 定位到对应的IntroductionInterceptor
        IntroductionInterceptor interceptor = (IntroductionInterceptor) this.interceptors.get(0);
        // 3. 执行Interceptor中的实现逻辑
        return method.invoke(interceptor, args);
    }
    // 4. 非扩展方法,则委派给目标Bean执行
    return method.invoke(target, args);
}

isIntroductionMethod()方法是关键判断点,它决定了调用是否应该被路由到IntroductionInterceptor

三、实战演示:为UserService添加Loggable功能

将上述组件组合起来,进行一个完整的验证:

  1. 定义Loggable接口
    public interface Loggable {
        void log(String message);
    }
  2. 配置Spring Bean(以XML配置为例):
    <bean id="userService" class="com.example.UserService"/>
    <bean id="loggableAdvisor" class="com.example.LoggableAdvisor"/>
    <bean id="loggableInterceptor" class="com.example.LoggableInterceptor"/>
    <!-- 启用自动代理 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
  3. 测试验证
    UserService userService = applicationContext.getBean(UserService.class);
    // 关键:将代理对象强制转换为扩展接口类型
    ((Loggable) userService).log("用户登录成功"); // 控制台输出:[Log] 用户登录成功

至此,神奇的效果显现:UserService本身并未实现Loggable接口,但其代理对象却能成功调用log()方法。这正是Spring Introduction机制的强大之处。

注意:如果目标Bean是类而非接口,Spring会使用CGLIB生成代理,其核心原理与JDK动态代理一致。

四、深入源码:为何代理对象能“强转”成功?

你可能会疑惑,代理对象为何能被强制转换为Loggable类型?奥秘在于Spring在生成代理类时,已经将扩展接口添加到了代理类实现的接口列表中:

// JdkDynamicAopProxy中构建代理接口列表的逻辑
private Class<?>[] getProxyInterfaces() {
    List<Class<?>> interfaces = new ArrayList<>();
    // 1. 添加目标Bean原生实现的所有接口
    interfaces.addAll(Arrays.asList(this.advised.getTargetSource().getTargetClass().getInterfaces()));
    // 2. 添加通过Introduction扩展的所有接口
    IntroductionInfo introductionInfo = this.advised.getIntroductionInfo();
    if (introductionInfo != null) {
        interfaces.addAll(introductionInfo.getIntroductions());
    }
    return interfaces.toArray(new Class<?>[0]);
}

代理类在运行时确实实现了Loggable接口,因此强制转换是合法且成功的。这便是“偷偷加接口”在底层的实现原理。

这种能力在需要对第三方库的Bean进行功能增强,而又无法修改其源码的场景下显得尤为有用,是Spring框架AOP能力的一种灵活体现。

五、核心逻辑总结

Spring Introduction机制的本质可归纳为以下四点:

  1. 定义:通过IntroductionAdvisor定义“需要添加什么接口”。
  2. 实现:通过IntroductionInterceptor具体实现“接口的功能逻辑”。
  3. 织入:借助DefaultAdvisorAutoProxyCreator自动创建“集成了扩展接口的代理Bean”。
  4. 路由:在方法调用时,代理对象将扩展方法的调用路由到对应的Interceptor执行。

简而言之,其原理是 “通过代理进行转发,实现接口的动态织入”。Spring利用动态代理技术,将新接口“嫁接”到目标Bean上,实现了无需修改原始代码即可增强功能的解耦设计。

这引发了另一个有趣的技术思考:如果一个Bean被多个IntroductionAdvisor处理,Spring会如何合并这些扩展接口?其内部会妥善处理接口集合的合并,确保代理类实现所有被引入的接口,并将方法调用正确地路由到各自的Interceptor。这进一步体现了Spring AOP在设计上的健壮性与灵活性。理解Introduction机制,也为理解@Transactional@Async等基于代理的Spring高级特性奠定了坚实的基础。




上一篇:Python辅助工具库ubelt实战:提升开发效率的实用工具集解析
下一篇:JavaScript贪吃蛇游戏开发实战:基于HTML5 Canvas的完整实现教程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 17:29 , Processed in 0.157306 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表