Featured image of post Java工程师 SpringAOP编码实现

Java工程师 SpringAOP编码实现

🌏Java工程师 SpringAOP原理实现 🎯 这篇文章用于记录 对SpringAOP原理实现的实践 包括 1️⃣原理分析 分析在使用Spring框架的时候 AOP是如何为我们工作的 分析AOP的实现原理 2️⃣流程分析 分析整个AOP容器的工作流程 3️⃣过程实现 编码模拟实现AOP容器

🎄AOP原理分析

🍭概述

Java工程师 SpringAOP原理实现I的实战中,我们通过编写Aspect切面类,将其中的三个方法依次通过@Before、@AfterReturning、@After注解(使用切面表达式)“插入”到littleBabyDuck对象的swim()方法执行前后;然后还使用了注解的方式使用AOP的功能 仅仅是编写了一个切面类和注解 然后给目标方法加上这个注解 就能控制切面方法插入到目标方法的前后执行

这些到底是如何实现的❓ 先说结论 IoC + BeanPostProcessor + 动态代理(动态代理使用反射)

关于动态代理 可见这篇文章:Java编程思想 动态代理

关于IoC 可见这两篇:IoC原理实现I IoC原理实现II

关于BeanPostProcessor 就在这篇文章进行讲述

如何来验证上面AOP使用了BeanPostProcessor的结论?追溯源码!

🍭@EnableAspectJAutoProxy源码分析

接下来 对 Java工程师 SpringAOP实战 中的源码进行分析 清楚了其中的过程 才可以在后面编码模拟实现其原理 主要分析的是@EnableAspectJAutoProxy注解的源码

🌴验证@EnableAspectJAutoProxy注解的重要功能

从@EnableAspectJAutoProxy注解追溯 大家是否还记得我在Java工程师 SpringAOP实战中使用到的AppConfiguration配置类 这里我将它列出来:

/**
@author Liu Xianmeng
@createTime 2023/9/16 17:32
@instruction
*/

@Configuration
@ComponentScan("bbm.com") // Spring扫描bbm.com包下的所有类 将符合条件的类注入容器进行管理
🎯@EnableAspectJAutoProxy
public class AppConfiguration {
    @Bean
    public Duck returnADuck(){
        return new Duck("littleBabyDuck");
    }
}

这个配置类中有一个非常重要的注解 🎯@EnableAspectJAutoProxy 这个注解支持了Spring中AOP功能得以开启 如果将这个注解去掉 如下所示

重新启动项目

浏览器访问

查看控制台日志 这里我想看到的结果是切面方法不会执行 但切面方法依然执行了 这是为什么❓

这是因为我是使用了SpringBoot,Spring Boot项目默认支持@EnableAspectJAutoProxy 也就是说就算不加这个注解 也能支持AOP, Spring Boot已经进行了自动配置

那应该如何验证🎯@EnableAspectJAutoProxy 这个注解支持了Spring中AOP功能得以开启?在application.yml中加上以下配置即可:

spring:
  aop:
    auto: false # 取消aop功能的默认支持

然后重新启动项目:

浏览器测试:

查看控制台:这个时候我们发现,只有Duck类的fly方法得到了执行 说明@EnableAspectJAutoProxy和AOP的功能密切相关

这个时候我们发现,只有Duck类的fly方法得到了执行

🌴查看@EnableAspectJAutoProxy的源码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
🎯@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
    /* proxyTargetClass属性,默认值为false,表示使用Java动态代理技术(基于接口的代理)
       如果想要使用CGLIB代理技术(基于类的代理),可以将proxyTargetClass属性设置为true */
    boolean proxyTargetClass() default false;
    /* exposeProxy属性 默认值为false 表示不暴露代理对象
       如果需要在切面中获取当前代理对象 可以将exposeProxy属性设置为true */
    boolean exposeProxy() default false;
}

其中有一行🎯@Import({AspectJAutoProxyRegistrar.class}) 非常重要 接下来进行分析

借助@Import({AspectJAutoProxyRegistrar.class})注解,Spring容器将实例化AspectJAutoProxyRegistrar类,并通过它自动将AspectJ切面和通知注册为Spring的bean,并自动将它们包装成代理对象。这样,在使用AspectJ定义的切面和通知时,Spring会自动创建代理对象,并根据定义的切点和通知类型在目标对象方法执行前、执行后等特定时机触发相应的切面逻辑

🌴追查AspectJAutoProxyRegistrar的源码

1️⃣查看AspectJAutoProxyRegistrar类的源码

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    AspectJAutoProxyRegistrar() {}
    // 顾名思义 —> 注册Bean的定义信息(可见我的IoC实现II那一篇文章 里面有模拟实现这个方法)
    public void 🎯registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        //代码省略...
    }
}

2️⃣其中有一个🎯registerBeanDefinitions方法 查看其源码:

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    // 顾名思义 —> 注册切面注解自动代理对象
    AopConfigUtils.🎯registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
    // 代码省略...
}

3️⃣其中有一个🎯registerAspectJAnnotationAutoProxyCreatorIfNecessary方法 查看其源码:

@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
    return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null);
}

4️⃣继续查看其registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null)方法的内部:

@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
    return registerOrEscalateApcAsRequired(🎯AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

其中有一个方法叫registerOrEscalateApcAsRequired 其中传入了一个参数🎯AnnotationAwareAspectJAutoProxyCreator.class 见名知意 这个类是 基于注解的切面自动代理创建器

5️⃣查看AnnotationAwareAspectJAutoProxyCreator类的类图:

注意到这个类有一个叫BeanPostProcessor顶层接口 在模拟实现AOP功能的时候会使用这个接口来模拟实现

🌴查看分析BeanPostProcessor接口的源码

BeanPostProcessor是Spring框架中的一个接口,用于扩展Bean的初始化和销毁过程。它提供了两个方法:postProcessBeforeInitialization()和postProcessAfterInitialization()

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // postProcessBeforeInitialization()方法在Bean的初始化之前被调用
        // 允许开发者在Bean进行初始化之前对其进行一些定制操作 通常可以在这个方法中修改和增强Bean的属性
        return bean;
    }
    @Nullable
    default Object 🎯postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // postProcessAfterInitialization()方法在Bean的初始化之后被调用
        // 允许开发者在Bean进行初始化之后对其进行一些定制操作
        // 通常可以在这个方法中进行一些资源引用的清理工作或者增加额外的功能
        return bean;
    }
}

AOP的切面类的切面方法会在🎯postProcessAfterInitialization中执行的操作中插入到目标方法的前后 很显然 只有只有Bean被初始化时候才能被切面方法切入 这个方法中会使用动态代理来生成目标方法类的代理对象 从而把要执行的切面方法插入到目标方法的前后

到此为止AOP的实现原理分析完成 后面我们来实现AOP的代码✨

🎄AOP代码实现

有了前面的IoC、动态代理、@EnableAspectJAutoProxy源码的追溯的铺垫 现在就开始尝试模拟编码实现AOP啦😎 再开始之前需要说明一下 AOP编码的实现将基于我的 IoC原理实现II 这篇文章写的源码 接下来直接开始!

🌈第1阶段 BeanPostProcessor准备

🍭 编写BeanPostProcessor接口

模拟Spring的BeanPostProcessor接口 新建一个自己的BeanPostProcessor接口 我将它放在bbm.com.component包下:

package bbm.com.component;

/**
@author Liu Xianmeng
@createTime 2023/9/18 21:57
@instruction 仿照Spring的`BeanPostProcessor`接口 新建一个自己的`BeanPostProcessor`接口
*/
public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

🍭 编写BeanPostProcessor接口的实现类

package bbm.com.component;

/**
@author Liu Xianmeng
@createTime 2023/9/18 22:00
@instruction BigBigMengBeanPostProcessor 具有实际功能的后置处理器
*/
@Component // 后置处理器也是一个Bean 交给IoC容器进行管理
public class BigBigMengBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // 添加一句日志 表示此方法被执行 打印出当前处理的bean
        System.out.println("C BigBigMengBeanPostProcessor M 初始化前 -> bean = " + bean);
        return bean; // 处理后返回这个bean
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // 添加一句日志 表示此方法被执行 打印出当前处理的bean
        System.out.println("C BigBigMengBeanPostProcessor M 初始化后 -> bean = " + bean);
        return bean; // 处理后返回这个bean
    }
}

到了这里 有一个东西要拿出来说 那就是postProcessBeforeInitialization和postProcessAfterInitialization方法是对相对Initialization方法执行的 那Initialization方法是什么?从何而来?有什么用呢?它的源码如下:

// InitializingBean是Spring框架中的一个接口
package org.springframework.beans.factory;

public interface InitializingBean {
    // 用于在bean属性设置完成后进行自定义初始化操作
    void afterPropertiesSet() throws Exception;
}

所以接下来需要编写一个InitializingBean接口 和Spring框架提供的上面的这个接口一样

🍭 编写InitializingBean接口

package bbm.com.component;

/**
@author Liu Xianmeng
@createTime 2023/9/19 10:11
@instruction 
*/

@SuppressWarnings({"all"})
public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

🍭 实现InitializingBean接口

给需要afterPropertiesSet()方法的Bean🎯实现InitializingBean接口 这里我只给DuckDao类实现这个方法 便于后面运行观察日志 如果有需要 可以给任意的Bean实现这个接口

/**
@author Liu Xianmeng
@createTime 2023/9/15 0:03
@instruction DuckDao将作为一个组件被放入IoC容器 名字为duckDao
*/
...
public class DuckDao 🎯implements InitializingBean {
    ...
    🎯@Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("C DuckDao M afterPropertiesSet() -> initializing");
    }
}

🍭修改BigBigMengAnnotationApplicationContext类

给createBean(BeanDefinition beanDefinition)函数加上一个参数 -> createBean(🎯String beanName, BeanDefinition beanDefinition) 修改后 其他有关联的地方会报错 全部进行修改即可

/**
 * createBean方法完成Bean对象的创建
 *
 * 🎯@param beanName          传入Bean的名字(目前用不到这个看参数 这个参数会在实现AOP的时候用到)
 * @param beanDefinition    传入Bean的定义信息
 * @return                  返回创建的Bean对象
 */
private Object createBean(🎯String beanName, BeanDefinition beanDefinition) {...}

添加BeanPostProcessor集合 -> 管理后置处理器

public class BigBigMengAnnotationApplicationContext { // IoC容器
    //将bean处理器对象 放在ArrayList
    private List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();
 	...   
}

修改beanDefinitionsByScan函数 添加🎯BeanPostProcessor的处理部分

...
if(clazz.isAnnotationPresent(Component.class)) {
    System.out.println("C IoC M beanDefinitionsByScan() -> " + classFullName + "是一个Spring bean, 将其注入IoC容器进行管理");

    /** 🎯20230919 如果该Component实现了BeanPostProcessor接口 
     *  则创建其对象并加入将其加入beanPostProcessorList  **/
    if (BeanPostProcessor.class.isAssignableFrom(clazz)) {
        BeanPostProcessor beanPostProcessor = (BeanPostProcessor) clazz.newInstance();
        beanPostProcessorList.add(beanPostProcessor);
        // 不需要将BeanPostProcessor的信息封装到beanDefinitionMap 也不需要放入单例池
        // 执行完后扫描下一个Bean进行处理
        continue;
    }

    /*********** 将其信息封装到beanDefinitionMap ***********/
    ...
}

修改createBean函数 添加🎯BeanPostProcessor和🎯InitializingBean的处理部分 添加完这样一个结构之后 就形成了 BeforeInitialization -> Initializing -> AfterInitialization的完整结构

		...代码省略
            
		// 进行组装
        field.setAccessible(true); // 因为属性都是修饰的所以需要设置可访问
        field.set(newInstance, fieldBean);
    }
}

/**
 * 🎯调用后置处理器BeforeInitialization方法
 */
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
    newInstance = beanPostProcessor.postProcessBeforeInitialization(newInstance, beanName);
}

/**
 * 🎯如果 bean 实现了 InitializingBean 则执行afterPropertiesSet()方法
 */
if (newInstance instanceof InitializingBean) {
    try {
        ((InitializingBean) newInstance).afterPropertiesSet();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 🎯调用后置处理器AfterInitialization方法
 */
for (BeanPostProcessor beanPostProcessor : beanPostProcessorList) {
    newInstance = beanPostProcessor.postProcessAfterInitialization(newInstance, beanName);
}

// 所有的属性注入完成后返回当前的Bean对象
return newInstance;

... 代码省略

⚡阶段测试

📑目前包结构如下

运行Main函数测试

🌈第2阶段 实现AOP的核心机制

AOP实战的文章中 我使用了两种方式来使用切面类 一种是用切面表达式 另一种是注解 这里我选择使用注解来实现AOP

🍭编写还需要的注解

编写@Aspect注解 它标识一个类是切面类

package bbm.com.annotation;

/**
@author Liu Xianmeng
@createTime 2023/9/19 10:52
@instruction
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) // @Aspect注解修饰切面类
public @interface Aspect {
    // 目前不需要属性
}

编写Before注解

package bbm.com.annotation;

/**
@author Liu Xianmeng
@createTime 2023/9/19 10:55
@instruction
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // @Before注解作用于方法
public @interface Before {
    /**
     * 指定注解的变量名
     * 
     * 例如 @Before(value="@annotation(用于标识切面方法要作用的普通方法的注解 的变量名)")
     * 具体可见后面编写的BigBigMengAspect切面类 
     */
    String value() default "";
}

编写After注解

package bbm.com.annotation;

/**
@author Liu Xianmeng
@createTime 2023/9/19 11:02
@instruction
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
    // 作用同 @Before
    String value() default "";
}

🍭编写@TargetMethodLabel注解

@TargetMethodLabel注解用来修饰切面类要切入的目标方法 BeanPostProcessor的AfterInitialzation方法中会进行检测哪些方法被@TargetMethodLabel注解修饰 从而在该方法的前后执行切面方法

@TargetMethodLabel注解可以根据开发者的编程偏好来命名 这个注解相当于在AOP实战的文章中@ExecuteAspectBefore注解的作用 作比较 会比较通透

package bbm.com.annotation;

/**
@author Liu Xianmeng
@createTime 2023/9/20 0:46
@instruction 用于标识切入方法要切入的目标方法
*/

@SuppressWarnings({"all"})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 用于修饰方法
public @interface TargetMethodLabel {
    // 只用于标识 不需要参数
}

🍭编写BigBigMengAspect切面类

BigBigMengAspect中不实现JoinPoint切入点 如果要实现JoinPoint 逻辑将会更加复杂 这里便以静表动 以简化实现

package bbm.com.component;

import bbm.com.annotation.*;

/**
@author Liu Xianmeng
@createTime 2023/9/19 11:05
@instruction BigBigMengAspect中不实现JoinPoint切入点 简化实现使用Object对象来代替
*/

@Aspect
@Component
public class BigBigMengAspect {
    // 被TargetMethodLabel注解修饰的方法会在执行前 先执行beforeAdvice方法
    @Before("@annotation(targetMethodLabel)")
    public static void beforeAdvice(Object o) { // static方法方便在后置处理器BigBigMengBeanPostProcessor类中进行调用
        System.out.println("C BigBigMengAspect M beforeAdvice()切入到" + o.getClass() + "执行方法前");
    }
    // 被TargetMethodLabel注解修饰的方法会在执行后 执行afterAdvice方法
    @After("@annotation(targetMethodLabel)")
    public static void afterAdvice(Object o) {
        System.out.println("C BigBigMengAspect M beforeAdvice()切入到" + o.getClass() + "执行方法后");
    }
}

🍭实现AOP动态代理

使用@TargetMethodLabel修饰目标方法 这里使用@TargetMethodLabel注解修饰DuckDao类的🎯getOneByName方法

package bbm.com.component;
/**
@author Liu Xianmeng
@createTime 2023/9/15 0:03
@instruction DuckDao将作为一个组件被放入IoC容器 名字为duckDao
*/
@Component(name = "duckDao")
@Scope("singleton") // 指定为单例对象
public class DuckDao implements InitializingBean {
    ...
    🎯@TargetMethodLabel 
    public Duck getOneByName(String duckName) {
        System.out.println("C DuckDao M getOneByName()目标方法执行..");
        return data.get(duckName);
    }
    ...
}

⚡重点中的重点来了!修改BigBigMengBeanPostProcessor类的两个postProcess方法 由于DuckDao类的🎯getOneByName方法并不是实现接口进行重写的 所以这里不能用JDK提供的动态代理 要用CGLib实现动态代理 所以 需要加入CGLib需要的Maven依赖:

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
</dependency>

然后开始编写postProcessAfterInitialization方法:

package bbm.com.component;

/**
@author Liu Xianmeng
@createTime 2023/9/18 22:00
@instruction BigBigMengBeanPostProcessor 具有实际功能的后置处理器
*/

@SuppressWarnings({"all"})
@Component // 后置处理器也是一个Bean 交给IoC容器进行管理
public class BigBigMengBeanPostProcessor implements BeanPostProcessor {

    /**
     * 注入切面对象 以调用其切面方法
     */
    @Autowired
    private BigBigMengAspect bigBigMengAspect;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // 添加一句日志 表示此方法被执行 打印出当前处理的bean
        System.out.println("C BigBigMengBeanPostProcessor M postProcessBeforeInitialization -> bean = " + bean);
        return bean; // 处理后返回这个bean
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // 添加一句日志 表示此方法被执行 打印出当前处理的bean
        System.out.println("C BigBigMengBeanPostProcessor M postProcessAfterInitialization -> bean = " + bean);
        // 反射获取bean的所有方法并遍历 如果发现方法有@TargetMethodLabel修饰
        // 则创建代理对象执行目标方法 在目标方法执行的前后切入切面类的方法
        Method[] declaredMethods = bean.getClass().getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            /**
             * 如果这个方法被 @TargetMethodLabel注解修饰
             * 则使用CGLib生成当前bean的代理对象并返回
             */
            if(declaredMethod.isAnnotationPresent(TargetMethodLabel.class)) {
                // 创建代理对象执行目标方法 在目标方法执行的前后切入切面类的方法
                // 创建一个Enhancer增强器实例
                Enhancer enhancer = new Enhancer();
                // 设置增强器要增强的父类Class对象 这个地方其实就是DuckDao的Class对象
                enhancer.setSuperclass(bean.getClass());
                /**
                 * 创建MethodInterceptor方法拦截器对象
                 * @param o 使用字节码生成的代理对象 personProxy
                 * @param method 父类中原本要执行的方法
                 * @param objects 方法在调用时传入的实参数组
                 * @param methodProxy 子类中重写父类的方法 personProxy
                 * @return  在目标对象上执行了指定的方法 并返回了该方法的结果
                 * @throws Throwable
                 */
                MethodInterceptor interceptor = new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        Object resultValue = null; // 用于接收method方法执行的返回值

                        /*** 如果被代理的DuckDao对象要执行的方法名 === declaredMethod.getName() 则对原目标方法进行增强 ***/
                        if(method.getName().equals(declaredMethod.getName())) {
                            /**
                             * 判断并执行BigBigMengAspect的@Before修饰的方法 对原目标方法进行增强
                             *
                             * 因为我只写了一个Aspect类 所以将其注入到BigBigMengBeanPostProcessor后
                             * 只需要判断这一个bigBigMengAspect切面类对象 如果有更多的切面类对象 则应该放在集合中进行管理 遍历处理
                             */
                            if(BigBigMengAspect.class
                                .getDeclaredMethod("beforeAdvice", Object.class)
                                .getDeclaredAnnotation(Before.class).value()
                                .equals("@annotation(targetMethodLabel)")){

                                bigBigMengAspect.beforeAdvice(bean);
                            }

                            // 🎯执行目标方法 由代理对象执行
                            resultValue = methodProxy.invokeSuper(o, objects);

                            // 执行BigBigMengAspect的@After修饰的方法
                            if(BigBigMengAspect.class
                                .getDeclaredMethod("afterAdvice", Object.class)
                                .getDeclaredAnnotation(After.class).value()
                                .equals("@annotation(targetMethodLabel)")){

                                bigBigMengAspect.afterAdvice(bean);
                            }
                        }
                        // 返回method方法执行的返回值
                        return resultValue;
                    }
                };
                enhancer.setCallback(interceptor);
                // 返回代理对象
                Object proxy = enhancer.create();
                System.out.println("C BigBigMengBeanPostProcessor M postProcessAfterInitialization() proxy = " + proxy.getClass());
                return proxy;
            }
        }
        // 没有被 @TargetMethodLabel 注解修饰的类不需要生成代理对象
        return bean;
    }
}

🍭最终的包结构

⚡运行测试

令人兴奋的时刻!可以看到CGLib动态代理对象生成了 切面方法插入到了目标方法的前后✨

🌈总结

到此 AOP的模拟实现就完成啦 在模拟实现的过程中 使用简化的逻辑复现了 注解 + BeanPostProcessor + 动态代理 实现AOP的过程 从而更清晰地理解AOP的实现 有助于自己更通透地使用Spring提供的AOP功能