一文带你掌握Spring AOP的底层实现

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文带你掌握Spring AOP的底层实现相关的知识,希望对你有一定的参考价值。

Spring AOP底层实现

spring aop中的joinpoint

前面讲过多种类型的joinpoint,如构造方法调用、字段的设置即获取、方法调用、方法执行等,但是在spring aop中之实现了方法级别的joinpoint,确切来说是只支持方法执行类型的joinpoint。虽然spring aop仅提供方法拦截,但是实际开发过程中,这已经可以满足80%的开发需求了。

如果要使用超出spring aop之外的功能,可以借助其他aop实现产品,如aspectj

spring aop中的pointcut

spring中以接口org.springframework.aop.Pointcut作为aop框架中所有Pointcut的最顶层抽象,该接口定义了两个方法用来帮助系统捕捉相应的joinpoint,并提供一个TruePointcut类型实例,如果Pointcut类型为TruePointcut,默认会对系统中的所有对象,以及对象上所有被支持的Joinpoint匹配。

Pointcut接口定义如下:

public interface Pointcut {

	/**
	 * 匹配将被执行织入操作的类对象
	 */
	ClassFilter getClassFilter();

	/**
	 * 匹配将被执行织入操作的对象以及相应的方法
	 */
	MethodMatcher getMethodMatcher();

	Pointcut TRUE = TruePointcut.INSTANCE;

}

ClassFilter和MethodMather之所以将类型匹配和方法匹配分开定义,是因为可以重用不同级别的匹配定义,并且可以在不同的级别或者相同的级别上进行组合操作,或者强制某个子类只覆写相应的方法定义。

ClassFilter

ClassFilter接口的作用是对Joinpoint所处的对象进行Class级别的类型匹配:

@FunctionalInterface
public interface ClassFilter {

	/**
	 * 当织入的目标对象的class类型与pointcut所规定的类型相符时,matches方法将会返回true,否则返回false(意味着不会对该类型的目标对象进行织入操作)
	 */
	boolean matches(Class<?> clazz);


	/**
	 * 当我们对于类型没有要求时,那么可以使用TRUE,对系统中所有的目标类以及它们的实例进行织入
	 */
	ClassFilter TRUE = TrueClassFilter.INSTANCE;

}

MethodMatcher

相比于ClassFilter来说,MethodMatcher更加复杂,Spring主要支持的就是方法级别的拦截,其定义如下:

public interface MethodMatcher {

	/**
	 * 根据方法以及目标对象类型判断是否为要拦截的方法
	 * 当isRuntime方法返回false时,表示不会考虑连接点方法的具体参数
	 * 此时也成为StaticMethodMatcher,对于同样类型方法的匹配结果可以在内部缓存提高性能
	 */
	boolean matches(Method method, Class<?> targetClass);

	/**
	 * 返回false表示执行不考虑参数的方法匹配,StaticMethodMathcer
	 * 返回true表示执行考虑参数的方法匹配,DynamicMethodMatcher
	 */
	boolean isRuntime();

	/**
	 * 当isRuntime返回true时会调用该方法
	 * 对方法调用的参数进行匹配检查,称为DynamicMethodMatcher,每次都要检查参数所以不能缓存
	 * 匹配效率相对于StaticMethodMatcher来说要差
	 * 大部分情况下StaticMethodMathcer已经能满足需求,尽量避免使用DynamicMethodMatcher类型
	 */
	boolean matches(Method method, Class<?> targetClass, Object... args);


	/**
	 * Canonical instance that matches all methods.
	 */
	MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

我们看看Pointcut都有哪些实现(通过idea查看):

Pointcut

接下来介绍几种较为常用的Pointcut实现。

NameMatchMethodPointcut

这是最简单的Pointcut实现,属于StaticMethodMatcherPointcut的子类,可以根据自身指定的一组方法名称与Joinpoint处的方法名称进行匹配,例如:

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("mathches");
// 或者传入多个方法名称
pointcut.setMappedNames(new String[] {"select", "update"});

NameMatchMethodPointcut无法对重载的方法名进行匹配,因为它仅对方法名进行匹配,不会考虑参数相关的信息,而且也没有提供可以指定参数匹配信息的途径。

NameMatchMethodPointcut除了可以指定方法名之外,还可以对指定的Joinpoint进行匹配,还可以使用"*"通配符,实现简单的模糊匹配,如下所示:

pointcut.setMappedNames(new string[] {"match", "sele*", "up*"});

它的底层使用ArrayList存储传入的要匹配的方法名称,然后调用matches方法对传入的Method方法进行类型匹配,判断Method的名称是否与我们设置的要匹配的方法名称一样。看看它的部分源码

public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {
	// 存储要匹配的方法名称
	private List<String> mappedNames = new ArrayList<>();
    // 设置要匹配的方法名称
    public void setMappedName(String mappedName) {
		setMappedNames(mappedName);
	}
    // 设置多个要匹配的方法的名称
    public void setMappedNames(String... mappedNames) {
		this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames));
	}
    // 添加要匹配的方法名称
    public NameMatchMethodPointcut addMethodName(String name) {
		this.mappedNames.add(name);
		return this;
	}
	// 匹配传入的方法和我们注册的方法名是否相同,相同则返回true,证明匹配成功,否则返回false
	@Override
	public boolean matches(Method method, Class<?> targetClass) {
		for (String mappedName : this.mappedNames) {
			if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
				return true;
			}
		}
		return false;
	}
    protected boolean isMatch(String methodName, String mappedName) {
		return PatternMatchUtils.simpleMatch(mappedName, methodName);
	}
}

JdkRegexpMethodPointcut

StaticMethodMatcherPointcut的子类有一个专门提供基于正则表达式的实现分支,其顶层实现是一个抽象类AbstractRegexpMethodPointcut,它声明了patterns和excludedPatterns属性,可以指定多个正则表达式的匹配模式(patterns),或者不参与代理的匹配模式。JdkRegexpMethodPointcut为其具体的实现,它基于JDK1.4之后引入的JDK标准正则表达式。

它的简单使用方法如下:

JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*match.*");
// 或者
pointcut.setPatterns(new String[] {".*matches", "*select*"});

当我们使用正则表达式来匹配连接点所处的方法时,必须写出方法的全限定类命加方法名,否则无法匹配到对应的方法。假如我们要匹配aop.AopTest.select()方法,那么使用".*select"则会匹配到select方法,但是如果使用"select. *"作为匹配的正则表达式,那就无法捕捉到select方法。

AnnotationMatchingPointcut

AnnotationMatchingPointcut根据目标对象中是否存在指定类型的注解来匹配Joinpoint,要使用该类型的Pointcut,首先需要声明相应的注解。

假设我们定义了两个注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassLevelAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodLevelAnnotation {
}

假设我们在某个类上加了注解:

@ClassLevelAnnotation
public class AopTest {
    @MethodLevelAnnotation
    public void test() {
        System.out.println("test!");
    }
}

针对AOPTest类,不同的AnnotationMatchingPointcut定义会产生不同的匹配行为。

  • AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(ClassLevelAnnotation.class):AnnotationMatchingPointcut仅指定类级别的注解,AOPTest中所有方法执行的时候,将全部匹配,不管该方法指定了注解还是没有指定。
  • AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(MethodLevelAnnotation.class):如果只指定方法级别的注解而忽略类级别的注解,则该pointcut只匹配标注了该注解的方法,而忽略其他方法
  • AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(ClassLevelAnnotation.class,MethodLevelAnnotation.class):如果同时限定类级别和方法级别的注解,那么只有同时标注了类级别和方法级别注解的方法才会被匹配。

ComposablePointcut

之前说过,Pointcut通常提供逻辑运算功能,而ComposablePointcut就是Spring aop提供的可以进行Pointcut逻辑运算的Pointcut实现,它可以进行Pointcut之间的并和交运算。例如:

ComposablePointcut pointcut1 = new ComposablePointcut(classFilter1,methodMatcher1);
ComposablePointcut pointcut2 = new ComposablePointcut(classFilter2,methodMatcher2);
ComposablePointcut union = pointcut1.union(pointcut2);
ComposablePointcut intersection = pointcut1.intersection(union);

Pointcut定义根据ClassFilter和MethodMatcher划分为两部分,一部分是为了重用这些定义,另一部分是为了可以互相结合。而通过ComposablePointcut,我们可以看出这两点目的:

ComposablePointcut pointcut3 = pointcut2.union(classFilter1).intersection(methodMatcher1);

我们在pointcut1和pointcut3中复用了classFilter1和methodMatcher1以及pointcut2的定义,同时,还进行了pointcut同classfilter和methodmatcher之间的逻辑组合运算。

ControlFlowPointcut

ControlFlowPointcut在理解和使用上面可能是比较难的,它可能不是很常用,但是某些场合可能会用到。ControlFlowPointcut主要用于匹配程序的调用流程,不是对某个方法执行所在的Joinpoint处的单一特征进行匹配。

假设我们所拦截的目标对象及调用类如下:

public class TargetObject {
    public void method() {}
}

public class TargetCaller {
    private TargetObject target;
    public void callMethod() {
        target.method();
    }
    public void setTarget(TargetObject target) {
        this.target = target;
    }
}

如果使用之前的任何Pointcut实现,我们只能指定在TargetObject的method方法每次执行的时候,都织入相应横切逻辑。也就是说,一旦通过pointcut指定method处为joinpoint,那么对该方法的执行进行拦截是必定的,不管method是被谁调用。

而通过ControlFlowPointcut,我们可以指定,只有当TargetObject的method方法在TargetCaller类所声明的方法中被调用的时候,才对方法method进行拦截,其他地方调用method不会拦截

那么具体如何实现呢?

ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class);
Advice advice = ...;

TargetObject target = new TargetObject();
TargetObject use = weaver.weave(advice).to(target).accordingto(pointcut);

// advice的逻辑在这里将不会被触发执行
use.method();
// advice的逻辑在这里会被触发执行
// 这里ControlFlowPointcut构造函数指定的类调用连接处的方法时才会执行横切逻辑
TargetCaller caller = new TargetCaller();
caller.setTarget(use);
caller.callMethod();

如果ControFlowPointcut的构造方法中单独指定class类型的参数,那么ControlFlowPointcut将尝试匹配指定的class中声明的所有方法,根目标对象的joinpoint处的方法流程组合。所以如果只想完成TargetCaller.callMethod调用TargetObject.method这样的匹配流程,而忽略TargetCaller中其他方法时,可以在构造函数中传入第二个参数,即调用方法的名称,例如:

ControlFlowPointcut pointcut = new ControlFlowPointcut(TargetCaller.class, 
"callMethod");

扩展Pointcut

如果有特殊的需求,spring aop提供的pointcut无法满足,那么我们可以自定义pointcut。

自定义pointcut,我们只需要继承spring aop已经提供的相应的抽象父类,然后实现或者覆写相应的方法逻辑即可。spring aop的pointcut类型可以划分为StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut两类,我们要实现自定义的pointcut,在这两个抽象类的基础上实现相应子类即可。

  • 自定义StaticMethodMatcherPointcut

    StaticMethodMatcherPointcut为子类提供了几个方面的默认实现。

    • 默认所有StaticMethodMatcherPointcut的子类的ClassFilter均为ClassFilter.TRUE,也就是忽略类的类型匹配,如果子类要做进一步限制,只需要通过setClassFilter方法设置相应的ClassFilter实现
    • StaticMethodMatcherPointcut的isRuntime方法返回false,同时三个参数的matches方法将抛出UnsupportedOperationException异常,以表示该方法不应该被调用到

    最终我们只需要实现两个参数的matches方法。

    如果我们想对数据访问层的数据访问对象中的查询方法所在的Joinpoint进行捕捉,那么可以这样做:

    public class QueryMethodPointcut extends StaticMethodMatcherPointcut {
        public boolean matches(Method method, Class clazz) {
            return method.getName().startWith("get")
                && clazz.getPackage().getName().startWith("..dao");
        }
    }
    

spring aop中的advice

spring aop加入了开源组织aop alliance,主要用于标准化aop的使用,促进各个aop的实现产品之间的交互性,鉴于此,spring中的各种advice类型实现与aop alliance中标准接口之间的关系如图:

image-20210510161326702

Advice实现了将被织入到Pointcut规定的joinpoint处的横切逻辑,在spring中,advice按照其自身的实例能否在目标对象类所在的所有实例中共享这一标准,可以划分为两大类,也就是:per-class类型的Advice和per-instance类型的Advice

per-class类型的Advice

per-class类型的Advice是指,该类型的Advice的实例可以在目标对象类的所有实例之间共享,这种类型的Advice通常只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性。除了Introduction类型的Advice不属于per-class类型的Advice之外,上图中的所有Advice均属此列。

1、Before Advice

Before Advice所实现的横切逻辑将在相应的Joinpoint之前执行,在Before Advice执行完成之后,程序执行流程将从Joinpoint处继续执行,所以Before Advice不会打断程序的执行流程。

在spring中需要实现MethodBeforeAdvice接口来实现Before Advice,该接口定义如下:

public interface MethodBeforeAdvice extends BeforeAdvice {

	/**
	 * 
	 */
	void before(Method method, Object[] args, @Nullable Object target) throws Throwable;

}

MethodBeforeAdvice继承自BeforeAdvice,BeforeAdvice与Advice一样,都是标志接口,没有定义任何方法。

我们可以使用Before Advice进行整个系统的某些资源初始化或者其他一些准备性的工作。当然还有其他很多场景。

2、ThrowsAdvice

spring中ThrowsAdvice接口对应aop概念中的AfterThrowingAdvice,虽然该接口没有定义任何方法,但是在实现相应的ThrowsAdvice时,我们的方法定义需要遵循如下规则:

void afterThrowing([Method, args, target], ThrowableSubclass);

其中[]中的三个参数可以省略,可以根据将要拦截的Throwable的不同类型,在同一个ThrowsAdvice中实现多个afterThrowing方法。

框架将会使用Java反射机制来调用这些方法。

例如,我们可以实现多个afterThrowing方法

public class ExceptionBarrierThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(Throwable t) {
        // 普通异常处理
    }
    public void afterThrowing(RuntimeException e) {
        // 运行时异常处理
    }
    public void afterThrowing(Method m, Object[] args, Object target, ApplicationException e) {
        // 处理应用程序生成的异常
    }
}

ThrowsAdvice通常用于对系统中特定的异常情况进行监控,以统一的方式对所发生的异常进行处理。当然也可以根据具体的应用场景来使用ThrowsAdvice。

我们可以对系统中的运行时异常进行监控,一旦捕捉到异常,需要马上以某种方式通知系统的监控人员或者运营人员。例如通过email的方式发送通知

例如:

public class ExceptionBarrierThrowsAdvice implements ThrowsAdvice {
    private JavaMailSender mailSender;
    private String[] receiptions;
    
    public void afterThrowing(Method m, Object[] args, Object target, RuntimeException e) {
        // 处理应用程序生成的异常
        final String exceptionMessage = ExceptionUtils.getFullStacktrace(e);
        getMailSender().send(new MimeMessagePreparator() {
           public void prepare(MimeMessage message) throws Exception {
               MimeMessageHelper helper = new MimeMessageHelper(message);
               helper.setSubject("...");
               helper.steTo(getReceiptions());
           	   helper.setText(exceptionMessage);
           } 
        });
    }
    
    // 省略getter和setter方法
}

3、AfterReturingAdvice

spring的AfterReturingAdvice定义如下:

public interface AfterReturningAdvice extends AfterAdvice {
	void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;

}

通过AfterReturningAdvice,我们可以访问当前连接点的方法返回值、方法、方法参数以及所在的目标对象。

只有方法正常返回的情况下,AfterReturningAdvice才会执行,所以用来处理资源清理制类的工作并不合适。不过如果有需要方法成功执行后进行的横切逻辑,使用AfterReturningAdvice倒比较合适。虽然AfterReturningAdvice可以访问方法的返回值,但是不可以更改返回值。我们可以通过Around Advice实现。

4、Around Advice

spring aop没有提供After Advice,使得我们没有一个合适的Advice类型来承载类似于系统资源清除之类的横切逻辑。

spring中没有直接定义对应around advice的实现接口,而是直接采用aop alliance的标准接口:org.aopalliance.intercept.MethodInterceptor,其定义如下:

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
   @Nullable
   Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;

}

MethodInterceptor作为Around Advice非常强大,前面提到的几种Advice它都可以完成,系统安全验证、性能检测、简单日志记录等场景都可以使用它。

以简单的检测系统某些方法执行性能为例,实现一个PerformanceInterceptor:

public class PerformanceInterceptor implements MethodInterceptor {
    private final Log Logger = LogFactory.getLog(this.getClass());
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch watch = new StopWatch();
        try {
            watch.start();
            return invocation.proceed();
        } finally {
            watch.stop();
            if (logger.isInfoEnables()) {
                logger.info(watch.toString());
            }
        }
    }
}

通过MethodInvocation的proceed方法可以让程序继续沿着调用链传播,如果没有调用proceed方法的话,那么程序在这里将会被中断,其他的MethodInterceptor逻辑和joinpoint处的方法逻辑将不会被执行。所以,不要忘记调用proceed()方法

per-instance类型的Advice

per-instance类型的Advice不会在目标类所有对象实例之间共享,而是会为不同的实例对象保存它们各自的状态以及相关逻辑。

Introduction是spring aop中唯一的一种per-instance型Advice。

Introduction可以在不改动目标类定义的情况下,为目标类添加新的属性以及行为。

在spring中,为目标对象添加新的属性和行为必须声明相应的接口以及相应的实现。然后通过特定的拦截器将新的接口定义以及实现类中的逻辑附加到目标对象上,之后目标对象也可以说是目标对象的代理对象就拥有了新的状态和行为。

这个特定的拦截器就是org.springframework.aop.IntroductionInterceptor,其定义如下:

public interface IntroductionInterceptor extends MethodInterceptor, DynamicIntroductionAdvice {}
public interface DynamicIntroductionAdvice extends Advice {
	boolean implementsInterface(Class<?> intf);
}

IntroductionInterceptor继承了DynamicIntroductionAdvice接口,界定当前的IntroductionInterceptor为那些接口类提供相应的拦截功能,通过MethodInterceptor可以处理新添加的接口上的方法调用了。对于IntroductionInterceptor来说,如果是新增加的接口上的方法调用,不必调用MethodInterceptor的proceed()方法,当前被拦截的方法实际上就是整个调用链中要最终执行的唯一方法。

如果把每个目标对象实例看作盒装牛奶生产线上的那一盒盒牛奶,那么生产合格证就是新的Introduction逻辑,而IntroductionInterceptor就是把这些生产合格证贴到一盒盒牛奶上的那个人。

我们来看看Introduction相关的类图结构:

image-20210510194107543

使用DynamicIntroductionAdvice可以到运行时再判定当前Introduction可应用到的目标接口类型,而不用预先就设定。而IntroductionInfo类型则完全相反,其定义如下:

public interface IntroductionInfo {

	/**
	 * 返回预定的目标接口类型
	 */
	Class<?>[] getInterfaces();

}

当对IntroductionInfo型的Introduction进行织入时,实际上就不需要指定目标接口类型了,因为它自身就带有这些必要的信息。

在大多数时候,直接使用spring提供的两个现成的实现类就可以对目标对象进行拦截并添加Introduction逻辑。

DelegatingIntroductionInterceptor不会自己实现将要添加到目标对象上的新的逻辑行为,而是委派给其他实现类。

假如我们要给程序员添加软件测试的职责时,可以这样编码:

public interface IDevelpoer {
    void developSoftware();
}
public class Developer implements IDevelpoer{
    @Override
    public void developSoftware() {
        System.out.println("我要开发一款风靡全国的软件");
    }
}
public interface ITest {

    boolean isBusyAsTester();

    void testSoftware();
}
public class Tester extends extends DelegatingIntroductionInterceptor implements ITest{
    private boolean busyAsTester;

    public void setBusyAsTester(boolean busyAsTester) {
        this.busyAsTester = busyAsTester;
    }

    @Override
    public boolean isBusyAsTester() {
        return false;
    }

    @Override
    public void testSoftware() {
        System.out.println("我要测试一款软件,保证它的质量!");
    }
}

通过Deleg

以上是关于一文带你掌握Spring AOP的底层实现的主要内容,如果未能解决你的问题,请参考以下文章

一文带你掌握Spring AOP的底层实现

一文带你掌握Spring AOP的底层实现

从源码入手,一文带你读懂Spring AOP面向切面编程

从源码入手,一文带你读懂Spring AOP面向切面编程

一文搞懂Spring AOP源码底层原理

面试必备:从源码入手,带你一文读懂Spring AOP面向切面编程