本学习笔记将尽可能的将AOP的知识讲解的通俗易懂,先从一个典型的问题出发,引入AOP这个概念,介绍AOP的基本概念,再到Spring中的AOP的实现方案,最后进行一个简单的总结归纳。本学习笔记中不考虑cglib、也不会太关注Spring AOP如何使用,而是尽可能的简单的说清楚AOP的工作原理。
笔记中贴出的源代码均是Spring 5.1.7-RELEASE 版本
问题提出
如下代码块,现在需要统计这个方法执行的耗时情况
public void runTask() {
doSomething();
}
一次性的解决肯定非常简单,直接添加一个时间记录即可,如下代码块
public void runTask() {
long start = System.currentTimeMillis();
doSomething();
System.out.println(System.currentTimeMillis() - start);
}
- 改写原方法:就如上述直接添加时间点记录,针对一两个简单的需求这种方案是最快最高效的,但是弊端也是非常明显的。直接把非业务功能和业务功能耦合在一起、需要改动太大的业务功能、不能灵活修改,如果下一次需要把时间记录去掉,换成统计次数调用,那么所有的地方都得改动,成本非常大,稍有不慎就容易出错
- 适配包装:即把原对象通过组合的方式包装到一个代理对象中,类似于适配器模式,如下图
⚠️ 这不是说真的就按照适配器模式去开发,而是采取类似的套路。新弄一个类然后新弄一个对应的方法,在新创建的方法里面再具体调用目标对象的方法。AOP也就是为了解决这类问题所提出的一种解决方案。
AOP 的基本概念
AOP(Aspect Oriented Programming)是基于切面编程的,可无侵入的在原本功能的切面层添加自定义代码,一般用于日志收集、权限认证等场景。
在了解AOP包含的组件之前,如果是你去设计实现一套解决方案会如何设计呢?
思考几分钟得处一些必备点~
需要知道在什么地方进行切面操作
需要知道切面操作的具体内容
如果有多个切面操作,应该得有一个先后执行的顺序
事实上AOP也确实是按照这个类似的思路去实现的,先来了解下AOP包含的几个概念
- Jointpoint(连接点):具体的切面点点抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut(切入点),仅作用在方法上。
- Advice(通知): 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。
- 目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。
- AOP代理:AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。
- Weaving(织入):将增强处理添加到目标对象中,创建一个被增强的对象的过程
总结为一句话就是:在目标对象(target object)的某些方法(jointpoint)添加不同种类的操作(通知、增强操处理),最后通过某些方法(weaving、织入操作)实现一个新的代理目标对象。
动态代理
在继续学习之前有必要介绍一下动态代理。动态代理(Dynamic Proxy)是采用Java的反射技术,在运行时按照某一接口要求创建一个包装了目标对象的新的代理对象,并通过代理对象实现对目标对象的控制操作。
使用动态代理需InvocationHandler + Proxy,可看如下代码块
public class HelloInvocationHandle implements InvocationHandler {
private Object object;
public HelloInvocationHandle(Object o) {
this.object = o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method: " + method.getName() + " is invoked");
System.out.println("proxy: " + proxy.getClass().getName());
Object result = method.invoke(object, args);
// 反射方法调用
return result;
}
}
// HelloWorld 是一个接口,此处没有贴出来
Class> proxyClass = Proxy.getProxyClass(HelloWorld.class.getClassLoader(), HelloWorld.class);
Constructor cc = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler ihs = new HelloInvocationHandle(new HelloWorldImpl());
HelloWorld helloWorld = (HelloWorld) cc.newInstance(ihs);
套路就是先获取Proxy生成的class,然后获取去其中使用了InvocationHandler作为参数的构造器,使用反射newInstance 实现代理对象helloWorld的生成,当然Proxy也提供了更加方便的方法给我们使用
final InvocationHandler in = new HelloInvocationHandle(new HelloWorldImpl());
HelloWorld helloWorld = (HelloWorld) Proxy.newProxyInstance(
HelloWorld.class.getClassLoader(), // 被代理对象的类加载器
HelloWorld.class.getInterfaces(), // 被代理对象的接口(数组,可保护多个)
in); // InvocationHandler实例对象
此外可以使用ProxyGenerator.generateProxyClass方法去获取到动态生成的代理类实际内容,具体如下
继承了Proxy类,而且其构造函数传入的确实是一个InvocationHandler实例
这个方法最后调用相当于InvocationHandler.invoke
,经过代理类的保证调用链路就到了HelloInvocationHandle类的invoke方法中,再利用反射调用被代理对象的方法。
接下来就来学习和了解下Spring AOP中最关键的两个类ProxyFactory、ProxyFactoryBean学习AOP的实现原理
ProxyFactory
ProxyFactory或许会比较陌生,可是无论是使用注解的方式还是XML的方式百转千回后还是会去创建ProxyFactory对象,所以忽略Spring前面一系列的操作,利用ProxyFactory做为入口,直面感受和学习AOP代理对象是如何生成的,demo如下
// 本代码来自官方单元测试NameMatchMethodPointcutTests类中的代码
// 在引入spring aop的模块环境下可直接运行
public void setup() {
ProxyFactory pf = new ProxyFactory(new SerializablePerson()); // 1
nop = new SerializableNopInterceptor(); // 2
pc = new NameMatchMethodPointcut(); // 3
pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop)); // 4
proxied = (Person) pf.getProxy(); // 5
// proxied就是生成的AOP代理对象
}
上面的5个步骤每一个都很关键,现在就逐一进行解释
1、ProxyFactory实例化后传入的被代理对象,会被存储到TargetSource对象中,可通过getTarget方法获取到具体的被代理对象,为什么会存储到TargetSource对象中后面会说明,再一个就是获取代理对象可能存在的接口情况也被存储到interfaces列表中。
2、实例化一个SerializableNopInterceptor对象,这是一个实现了MethodInterceptor接口的类,里面的invoke方法是提供给外界触发该增强操作的入口,类图如下:
还记得上面介绍AOP的基本概念时说的Advice通知么?其实这就是一个增强器,包含了我们需要增强的功能,日志的收集、权限认证的具体代码就是写在这些增强器中的。通过调用invoke实现相关的非业务功能。后面会具体说到是谁触发了invoke方法调用。
3、实例化了一个NameMatchMethodPointcut对象,一个非常简单的基于名字匹配的切入点,通俗的说就是通过名字判断是否需要添加通知
其实现了Pointcut接口,并且Pointcut接口包含了ClassFilter getClassFilter();
和MethodMatcher getMethodMatcher();
通过这个名字也能看的出来一个是类过滤器,一个是方法过滤器,两者共同作用就可以判断添加增强器的位置。
曾经使用过Spring AOP的小伙伴们是否记得自己的代码里写过如下类似的注解代码
@Pointcut("@annotation(XXXXAnnotation)") // 匹配的是 方法添加XXXXAnnotation注解
@Pointcut("execution(public void com.XXXXX.controller.*.*(..))")
// 匹配的是 public类型 返回void并且是com.XXXXX.controller文件夹下面的所有类方法
从类名称NameMatchMethodPointcut判断是通过方法名称匹配的,可是Pointcut接口却告诉我们是有类匹配和方法匹配两种,那意味着NameMatchMethodPointcut肯定有默认了类过滤的操作,看下StaticMethodMatcherPointcut类,代码如下
public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
private ClassFilter classFilter = ClassFilter.TRUE;
// 直接就定义好了类过滤对象ClassFilter.TRUE,也就是下面的TrueClassFilter对象
public void setClassFilter(ClassFilter classFilter) {
this.classFilter = classFilter;
}
final class TrueClassFilter implements ClassFilter, Serializable {
// 这还是经典的单例写法
public static final TrueClassFilter INSTANCE = new TrueClassFilter();
private TrueClassFilter() {
}
@Override
public boolean matches(Class> clazz) {
// 重点在这,默认全部返还true
return true;
}
private Object readResolve() {
return INSTANCE;
}
}
现在知道了NameMatchMethodPointcut是调用了TrueClassFilter单例,所以每一次通过类过滤时,都会返回true,也就是都命中,从而实现了忽略类匹配的操作机制。
到现在2实现了一个通知(增强器)、3实现了一个切入点,那么现在应该需要把2和3组合起来实现切点增强,继续看4
4、pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop));
,实例化了DefaultPointcutAdvisor对象,参数传入了实例化好的通知和切入点,形成了一个Advisor添加到了ProxyFacotry的advisors列表中
在实际的spring服务中,可能存在多个通知点和切入点,需要通过各种匹配的规则组合成一系列的Advisor对象,然后添加到对应的ProxyFacotry对象中,以便后面的织入
5、实例化代理对象,pf.getProxy()
方法写的是createAopProxy().getProxy();
。大致的可以看出来是先创建一个AopProxy对象,然后调用其getProxy()方法返回。先来看看如何创建AopProxy的
// ProxyCreatorSupport 类
private AopProxyFactory aopProxyFactory;
public ProxyCreatorSupport() {
this.aopProxyFactory = new DefaultAopProxyFactory(); // 1
}
// ProxyFactory 类
public AopProxyFactory getAopProxyFactory() {
return this.aopProxyFactory;
}
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this); // 2
}
原来aopProxyFactory默认就是DefaultAopProxyFactory对象,通过其createAopProxy方法返回一个AopProxy对象,并且这传递的参数是this
,有必要贴一下ProxyFactory的UML图
如圈住的地方是一个AdvisedSupport类,也包含了当前代理类的一些信息。来到DefaultAopProxyFactory类
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
参数传递的是AdvisedSupport对象,而ProxyFactory又是继承AdvisedSupport的,所以上面的this参数是正常的。通过对optimize、proxyTargetClas、是否存在对象接口三个条件判断选择是生成JdkDynamicAopProxy还是ObjenesisCglibAopProxy。这里也就是AOP判断使用动态代理还是CGLIB的地方。
在xml配置中添加了proxy-target-class属性也就是上面说的config.isProxyTargetClass()判断操作,当设置为true时,就会使用CGLIB
继续深入,进入到JdkDynamicAopProxy的getProxy()方法中
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
Class>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
// 明确各种需要实现功能的接口
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
看到这是不是很熟悉,就是我们上面所说的动态代理Proxy.newProxyInstance方法完成代理类的实例化
到这里整个的代理对象就生成了,其实梳理一遍整个流程还是比较清晰的
代理对象调用
动态代理对象生成后调用的入口都是InvocationHandler对象的invoke方法,而且生成代理类的InvocationHandler对象参数传入就是JdkDynamicAopProxy本身
原来JdkDynamicAopProxy也实现了InvocationHandler接口,那么其invoke方法应该包含了具体的调用逻辑
// 精简了很多代码,但是并不影响主流程的学习
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
target = targetSource.getTarget();
Class> targetClass = (target != null ? target.getClass() : null);
List
1、获取增强器执行链,具体实现在DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice方法中
对切点的过滤匹配,也就是上面说的类过滤和方法过滤,调用类过滤matches方法+方法过滤matches方法,返回true添加到返回的容器中。如果是Interceptor对象则直接添加至返回的容器中。最后生成可被调用的增强器执行链
2、反射method.invoke 调用操作
3、包装成了ReflectiveMethodInvocation对象,然后调用其proceed方法
public Object proceed() throws Throwable {
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 运行到最后了执行被代理对象的方法
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
// 从增强器执行链获取一个增强器,索引值currentInterceptorIndex+1
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// 动态参数匹配,匹配后后方可执行
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
return proceed();
}
}
else {
// 一般增强器调用invoke
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
上面说的 ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
,调用的是methodinterceptor的invoke方法,这地方也就是ProxyFactory开头提的增强器调用invoke操作的调用触发点
来看看@Before注解对应的增强器是如何操作的
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
和我们设想的一样先执行了增强器的方法,然后循环调用MethodInvocation的proceed的方法,那同理肯定可以猜到@After操作肯定是先执行proceed方法,然后调用相关的增强方法。
到这里整个的AOP过程就算完成了,但是上面还留有一个疑问TargetSource是干什么用的?
上面已经提到targetsource只是包装了一下具体的被代理类,被包装成SingletonTargetSource类,每次获取实际的被代理对象都是通过targetsource.getTarget方法获取的。那我们就可以自定义targetsource改写其中的getTarget()方法,从而实现动态控制被代理对象实际对象了。其实热部署也是采用类似的原理实现的,关于热部署的更多代码可以看看官方提供的HotSwappableTargetSourceTests 单元测试代码。
ProxyFactoryBean
了解完ProxyFactory的整个过程,就很容易理解ProxyFactoryBean了,需知道ProxyFactoryBean = Proxy + FactoryBean
,是一种特殊的工厂bean,如下图是其UML类图
通过FactoryBean很自然的想到起代理类是通过getObject方法完成
public Object getObject() throws BeansException {
initializeAdvisorChain();
// 初始化增强器链,完成advisor
if (isSingleton()) {
// 依旧需要考虑是否为单例bean
return getSingletonInstance();
}
else {
if (this.targetName == null) {
logger.info("Using non-singleton proxies with singleton targets is often undesirable. " +
"Enable prototype proxies by setting the 'targetName' property.");
}
return newPrototypeInstance();
}
}
private synchronized Object getSingletonInstance() {
if (this.singletonInstance == null) {
this.targetSource = freshTargetSource();
if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
Class> targetClass = getTargetClass();
if (targetClass == null) {
throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
}
setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
}
super.setFrozen(this.freezeProxy);
this.singletonInstance = getProxy(createAopProxy());
}
return this.singletonInstance;
}
protected Object getProxy(AopProxy aopProxy) {
return aopProxy.getProxy(this.proxyClassLoader);
}
需要关注的是this.singletonInstance = getProxy(createAopProxy());
和aopProxy.getProxy(this.proxyClassLoader);
方法,对比发现其和FactoryBean所生成代理对象的方式是一模一样的,先生成AopProxy,再调用AopProxy.getProxy方法。只是其中寻找增强器和切点的逻辑存在差异。
总结
写关于Spring的学习笔记第一次尽可能的跳出Spring框架的思路,Spring AOP提供了纯XML、XML+注解、甚至于SpringBoot纯注解等多种方案,如果纠结于前期XML的解析、注解的寻找(不是说这些不重要,只是在AOP的学习上不属于重点),那将会使得整个AOP的学习体验降到最低。从官方提供的单元测试出发执行单元测试能更加精准。有些功能点虽然被忽略了,但并不影响整体的学习和了解。
从问题的提出,明确了做什么
和在什么上面做
这两个点,进而引出AOP的概念,认识到通知、连接点等概念,进一步到Spring AOP的切点和增强,组合成为增强器。采取动态代理或者CGLIB的方案实现Weaving织入的过程,进而完成了代理对象的生成。
再提一句:官方的源码包中提供了丰富的单元测试,可以借助单元测试加深对代码的理解。