Spring AOP高级-如果我们自己实现一个AOP?

从 0 开始深入学习 Spring 专栏目录总览

在前面学习 IOC 高级的 BeanPostProcessor 中,咱就了解到 BeanPostProcessor 的 postProcessAfterInitialization 方法可以用于创建代理对象,那 SpringFramework 本身是如何实现的呢?在研究这个问题之前,咱先不要着急直接翻源码,咱自己试着思考一下,如果让我们来实现这个 AOP 的效果,我们可以如何来搞。

提示:本节的内容是为了后面的 AOP 原理做铺垫,小伙伴们不要沉迷于自行实战 AOP 本身呀。。。

1. 前期分析

咱先分析一下,一个完整的 AOP 机制需要的支撑组件和它们的作用分别都是什么。

1.1 生成代理对象的时机

最先想到的,肯定是普通的 bean 在初始化阶段,被 BeanPostProcessor 影响后,在 postProcessAfterInitialization 方法中生成代理对象吧!这也是 AOP 的实现机制中最重要的环节之一。

1.2 解析切入点表达式的时机

再想一个问题,BeanPostProcessor 怎么知道哪些 bean 在创建时需要织入通知,生成代理对象呢?可能是在 bean 的初始化逻辑中检查的吧,可是检查的依据是什么呢?依据是那些被 @Aspect 标注的切面类,里面定义的 pointcut 方法定义的切入点表达式吧!但是什么时候解析这些切入点表达式呢?

  • Aspect 切面类初始化的时候再解析?晚了吧,万一在解析之前已经有好多 bean 已经创建完了呢?那岂不是错过时机了?
  • Aspect 切面类加载进 BeanFactory 后再解析?哎这个可以,此时所有的 bean 还都没有创建,AOP 可以大展身手,但是再想一个问题:如果此时已经把 AOP 的东西全部准备好了,那回头后置处理器的初始化阶段咋办?这个通知是织入还是不织入呢?所以看来这个时机虽然很靠前了,但还是有点尴尬;
  • 负责 AOP 的核心后置处理器初始化的时候再去 BeanFactory 中解析?哎这个看样子就能同时顾及到两方面了,此时所有的 BeanDefinition 都加载进 BeanFactory 了,而且后置处理器都初始化好了,普通 bean 也都没有创建,所以这个时机点是相对最合适的。

所以我们得出结论:在 AOP 核心后置处理器的初始化阶段,解析容器中的所有切面类中的切入点表达式

1.3 通知织入的方式

接下来,通知如何织入呢?既然是动态代理,那就应该区分到底是使用 jdk 动态代理,还是 Cglib 动态代理了。

除此之外, InvocationHandler 或者 MethodInterceptor 的逻辑应该怎么写,这也是个问题,因为一个 bean 可能会被多个切面同时织入通知,所以这些因素我们也要考虑进去。

2. 开始编码吧!

好了,咱暂时就想到这么多,下面咱自己搞一个后置处理器,把这个思路实际运用一下。

小册提示:再啰嗦一遍。。。小伙伴们千万不要觉得思路都理清楚了,就要直接造一个简易版的出来,真的没必要,人家 SpringFramework 写的比咱好太多了,咱理解思路就好啦,后面马上咱研究原理就好,这只是个铺垫。

本章源码均在 com.linkedbear.spring.aop.h_imitate 。

2.1 代码准备

还是跟往常一样,咱先准备好代码:

@Service
public class UserService {
    
    public void get(String id) {
        System.out.println("获取id为" + id + "的用户。。。");
    }
    
    public void save(String name) {
        System.out.println("保存name为" + name + "的用户。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.h_imitate.service.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect beforePrint ......");
    }
}

注意,这次不需要配置类了,因为不需要声明 @EnableAspectJAutoProxy 注解,我们通过自己编写的后置处理器,一样可以达到效果(理论上)。

2.2 基本的后置处理器编写

接下来的核心就是编写后置处理器,咱声明一个 AopProxyPostProcessor 吧:

@Component
public class AopProxyPostProcessor implements BeanPostProcessor {
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        
        return bean;
    }
}

这里面只需要实现 postProcessAfterInitialization 方法即可( before 主要做属性赋值 & 初始化等动作)。

2.3 获得所有切面和切入点表达式

咱上面分析了,后置处理器的初始化阶段需要获取到容器中现有的所有切面和切入点表达式,而且要注意这个过程中不要把这些切面给初始化了(也就是不要调用 getBean 方法)。具体的步骤咱分解来看:

2.3.1 获得BeanFactory

实现上述逻辑的操作,需要提前获取到 BeanFactory ,所以咱在这里需要借助回调注入的机制,把 BeanFactory 注入进来:

@Component
public class AopProxyPostProcessor implements BeanPostProcessor, BeanFactoryAware {
    
    private ConfigurableListableBeanFactory beanFactory;
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        
        return bean;
    }
    
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        // 这里强转为比较高级的类型,方便接下来拿BeanDefinition
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }
}

2.3.2 筛选出切面类

然后,要获取这里面所有的切面类,就要先获取到 BeanFactory 中已经存在的所有 BeanDefinition ,而这个动作也比较简单,咱放到初始化方法的回调中做吧:

@PostConstruct
public void initAspectAndPointcuts() {
    // 取到BeanFactory中的所有BeanDefinition
    String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        // 检查BeanDefinition对应的class上是否标注了@Aspect注解
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
        String beanClassName = beanDefinition.getBeanClassName();
        if (!StringUtils.hasText(beanClassName)) {
            continue;
        }
        Class<?> clazz = ClassUtils.resolveClassName(beanClassName, ClassUtils.getDefaultClassLoader());
        if (!clazz.isAnnotationPresent(Aspect.class)) {
            continue;
        }
        // 到此为止,说明当前BeanDefinition对应的bean是一个切面类,解析方法
        
    }
}

2.3.3 解析切入点表达式

接下来的工作就是解析 Aspect 切面类了,既然是切面类,那就需要解析这个类上的每个方法,在 SpringFramework 中提供了一个很方便的反射类,它可以很快速的对类中的方法进行访问和过滤:

    ReflectionUtils.doWithMethods(clazz, method -> {
        // 此处要实现切入点表达式的解析
    }, method -> {
        // 此处只过滤出通知方法
        return method.isAnnotationPresent(Before.class)
                || method.isAnnotationPresent(After.class)
                || method.isAnnotationPresent(AfterReturning.class)
                || method.isAnnotationPresent(AfterThrowing.class)
                || method.isAnnotationPresent(Around.class);
    });

剩下的就是怎么解析这些注解了,这里面我不打算写的特别复杂,也不写那些乱七八糟的处理逻辑了,咱还是以 @Before 注解为例来演示。

    // 借助AspectJ中的切入点表达式解析器来搞定
    PointcutParser pointcutParser = PointcutParser.
            getPointcutParserSupportingAllPrimitivesAndUsingContextClassloaderForResolution();
    // 到此为止,说明当前BeanDefinition对应的bean是一个切面类,解析方法
    ReflectionUtils.doWithMethods(clazz, method -> {
        Before before = method.getAnnotation(Before.class);
        if (before != null) {
            String pointcutExp = before.value();
            // 借助pointcutParser解析切入点表达式
            PointcutExpression pointcutExpression = pointcutParser.parsePointcutExpression(pointcutExp);
            
        }
    }, method -> { ... });

到这里,咱就可以把切面类中的切入点表达式都拿出来了,并且还封装了一个 PointcutExpression 。

2.3.4 缓存切入点表达式和通知方法

表达式有了,怎么保存为好呢?很简单,放到一个 Map 中就好:

private Map<PointcutExpression, Method> beforePointcutMethodMap = new ConcurrentHashMap<>();

@PostConstruct
public void initAspectAndPointcuts() {
    // ......
        // 到此为止,说明当前BeanDefinition对应的bean是一个切面类,解析方法
        PointcutParser pointcutParser = PointcutParser.
                getPointcutParserSupportingAllPrimitivesAndUsingContextClassloaderForResolution();
        ReflectionUtils.doWithMethods(clazz, method -> {
            Before before = method.getAnnotation(Before.class);
            if (before != null) {
                String pointcutExp = before.value();
                // 借助pointcutParser解析切入点表达式
                PointcutExpression pointcutExpression = pointcutParser.parsePointcutExpression(pointcutExp);
                // 放入对应的缓存区
                beforePointcutMethodMap.put(pointcutExpression, method);
            }
        }, method -> { ... });
    }
}

注意这里面为了编写简单快速演示,我这里只用了一个 Map 保存所有的前置通知,但实际上的通知类型有 5 种,它们需要分别保存,或者封装为类似于 Metadata 的元信息,这里咱就不演示了。

2.4 代理对象的创建

接下来,咱就要给普通的 bean 对象构造代理对象了。AspectJ 提供的 PointcutExpression 已经足以搞定切入点表达式与目标类的匹配,所以我们编写起来会很容易。

具体来看,可以把这个部分的内容分为下面几个小步骤。

2.4.1 排除的项

首先!切面类不要代理了,否则会一直循环调用直到栈溢出:

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    // 切面不增强
    if (bean.getClass().isAnnotationPresent(Aspect.class)) {
        return bean;
    }
    // ......
}

2.4.2 切入点的匹配

然后,对于普通 bean 而言,要看看有没有能切入当前 bean 的切面,如果没有,则不需要创建代理对象:

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    // 切面不增强
    if (bean.getClass().isAnnotationPresent(Aspect.class)) {
        return bean;
    }
    // 检查类是否能被切入点表达式切入
    List<Method> proxyMethods = new ArrayList<>();
    beforePointcutMethodMap.forEach((pointcutExpression, method) -> {
        if (pointcutExpression.couldMatchJoinPointsInType(bean.getClass())) {
            proxyMethods.add(method);
        }
    });
    // 没有能匹配的切面,则直接返回普通对象
    if (proxyMethods.isEmpty()) {
        return bean;
    }
    // ......

2.4.3 织入通知生成代理对象

接下来,就是如何织入通知了,这里由于我们只演示了前置通知,这里只需要在方法调用之前反射执行切面类的前置通知方法即可:

    // 需要织入通知
    return Enhancer.create(bean.getClass(), (MethodInterceptor) (proxy, method, args, methodProxy) -> {
        // 依次执行前置通知
        for (Method proxyMethod : proxyMethods) {
            Object aspectBean = beanFactory.getBean(proxyMethod.getDeclaringClass());
            proxyMethod.invoke(aspectBean);
        }
        return methodProxy.invokeSuper(proxy, args);
    });
}

这个实现方式说实话很粗糙,小伙伴们只需要领会其中的思想即可。

2.5 测试运行

编写测试启动类,由于没有配置类,所以只需要用包扫描驱动 IOC 容器即可:

public class ImitateAopApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.linkedbear.spring.aop.h_imitate");
        UserService userService = ctx.getBean(UserService.class);
        userService.get("abc");
    }
}

运行 main 方法,控制台可以打印前置通知,说明实现是可以执行的:

LogAspect beforePrint ......
获取id为abc的用户。。。

这样我们就完成了一个自行实现的 AOP 后置处理器,小伙伴们要体会其中的思想,接下来的几章 AOP 原理中,会涉及到这里面的思想,以及更复杂的概念。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP高级-如果我们自己实现一个AOP?