Spring读源码系列之AOP--01---aop基本概念扫盲---上

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring读源码系列之AOP--01---aop基本概念扫盲---上相关的知识,希望对你有一定的参考价值。

Spring读源码系列之AOP--01---aop基本概念扫盲--上


AOP相关概念

  • 切面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。切面用spring的Advisor或拦截器实现。
  • 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
  • 通知(Advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice:BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
  • 切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解,MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上
  • 引入(Introduction): 添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现IsModified接口,来简化缓存。Spring中要使用Introduction,可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口(使用较少)
  • 目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
  • AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 织入(Weaving):组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

这里有一个非常形象的比喻:

  • Aspect就是找茬(pointcut)揍你(target)一顿(advice),一群欠揍的的人(join point)

Pointcut

notice !!!

首先需要说明一点:Pointcut接口有两个。

  • 一个是:org.aspectj.lang.reflect.Pointcut,它是aspectj内部使用的。它只有一个实现类PointcutImpl。是它内部的抽象
  • 另一个是:org.springframework.aop.Pointcut,这是Spring AOP体系中对切点的顶层抽象,贯穿整个AOP过程,非常重要。

Pointcut继承体系


ExpressionPointcut,它是用于解析String类型的切点表达式的接口,这个很重要,一会重点分析。


Pointcut分析

public interface Pointcut 

	/**
	 类过滤器
	 */
	ClassFilter getClassFilter();

	/**
   方法级别的过滤
	 */
	MethodMatcher getMethodMatcher();


	/**
	 始终匹配的规范切入点实例
	 */
	Pointcut TRUE = TruePointcut.INSTANCE;


主要负责对系统的相应的Joinpoint进行捕捉,对系统中所有的对象进行Joinpoint所定义的规则进行匹配。

提供了一个TruePointcut实例,当Pointcut为TruePointcut类型时,则会忽略所有的匹配条件,永远返回true。

ClassFilter与MethodMatcher分别用于在不同的级别上限定Joinpoint的匹配范围,满足不同粒度的匹配

ClassFilter限定在类级别上,MethodMatcher限定在方法级别上


ClassFilter—类过滤器

@FunctionalInterface
public interface ClassFilter 

	/**
	  pointcut是否应该应用于给定的接口或目标类?
	 */
	boolean matches(Class<?> clazz);


	/**
	 所有情况下都返回true
	 */
	ClassFilter TRUE = TrueClassFilter.INSTANCE;


我们可以来看一下它的继承体系:


RootClassFilter
public class RootClassFilter implements ClassFilter, Serializable 

	private final Class<?> clazz;


	public RootClassFilter(Class<?> clazz) 
		Assert.notNull(clazz, "Class must not be null");
		this.clazz = clazz;
	
    
    //过滤条件: 当前类是clazz的子类或者接受clazz类,那么返回true
	@Override
	public boolean matches(Class<?> candidate) 
		return this.clazz.isAssignableFrom(candidate);
	

...省略toString等方法


AnnotationClassFilter
public class AnnotationClassFilter implements ClassFilter 

	private final Class<? extends Annotation> annotationType;

	private final boolean checkInherited;

// 默认情况下checkInherited给的false:不去看它继承过来的注解
	public AnnotationClassFilter(Class<? extends Annotation> annotationType) 
		this(annotationType, false);
	

// checkInherited true:表示继承过来得注解也算
	public AnnotationClassFilter(Class<? extends Annotation> annotationType, boolean checkInherited) 
		Assert.notNull(annotationType, "Annotation type must not be null");
		this.annotationType = annotationType;
		this.checkInherited = checkInherited;
	


	@Override
	public boolean matches(Class<?> clazz) 
		return (this.checkInherited ? 
//继承的注解也会找出来
AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
//只会看自己本类的注解
				clazz.isAnnotationPresent(this.annotationType));
	
	
...省略toString等方法


MethodMatcher—方法匹配器

public interface MethodMatcher 
	
	// 这个称为静态匹配:在匹配条件不是太严格时使用,可以满足大部分场景的使用
	boolean matches(Method method, @Nullable Class<?> targetClass);
	// 这个称为动态匹配(运行时匹配): 它是严格的匹配。在运行时动态的对参数的类型进行匹配
	boolean matches(Method method, @Nullable Class<?> targetClass, Object... args);
	
	//两个方法的分界线就是boolean isRuntime()方法,步骤如下
	// 1、先调用静态匹配,若返回true。此时就会继续去检查isRuntime()的返回值
	// 2、若isRuntime()还返回true,那就继续调用动态匹配
	// (若静态匹配都匹配上,动态匹配那铁定更匹配不上得~~~~)

	// 是否需要执行动态匹配
	boolean isRuntime();
	MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;




它有两个非常重要的抽象实现:StaticMethodMatcher和DynamicMethodMatcher


StaticMethodMatcher—静态匹配
public abstract class StaticMethodMatcher implements MethodMatcher 
	// 永远返回false表示只会去静态匹配
	@Override
	public final boolean isRuntime() 
		return false;
	
	// 三参数matches抛出异常,使其不被调用
	@Override
	public final boolean matches(Method method, @Nullable Class<?> targetClass, Object... args) 
		// should never be invoked because isRuntime() returns false
		throw new UnsupportedOperationException("Illegal MethodMatcher usage");
	



作用:它表示不会考虑具体 方法参数。因为不用每次都检查参数,那么对于同样的类型的方法匹配结果,就可以在框架内部缓存以提高性能。比如常用的实现类:AnnotationMethodMatcher


DynamicMethodMatcher 动态匹配
public abstract class DynamicMethodMatcher implements MethodMatcher 
	
	// 永远返回true
	@Override
	public final boolean isRuntime() 
		return true;
	
	// 永远返回true,去匹配动态匹配的方法即可
	@Override
	public boolean matches(Method method, @Nullable Class<?> targetClass) 
		return true;
	


说明:因为每次都要对方法参数进行检查,无法对匹配结果进行缓存,所以,匹配效率相对 StatisMethodMatcher 来说要差,但匹配度更高。(实际使用得其实较少)


JdkRegexpMethodPointcut----基于正则过滤


它提供了最重要的4个属性(patterns和excludedPatterns来自于父类AbstractRegexpMethodPointcut):

这里两个属性来自于父类,相对来说就是比较简单的匹配signatureString(方法的全路径名称)

  • String[] patterns:匹配的正则表达式。如find.*表示所有方法名以find开始的方法
  • String[] excludedPatterns:排除的正则表达式们

下面两个是子类,也就是JdkRegexpMethodPointcut自己提供的属性

  • Pattern[] compiledPatterns:相当于把正则字符串,Pattern.compile()成正则对象
  • Pattern[] compiledExclusionPatterns:同上

都是数组,正则表达式都可以多个哟~~


我们来看一下match匹配过程的核心逻辑:

match方法在抽象父类AbstractRegexpMethodPointcut中实现:

@Override
	public boolean matches(Method method, Class<?> targetClass) 
		//先搞到当前方法的名字,然后传入matchesPattern方法中进行匹配,看是否符合切入的正则表达式
		return (matchesPattern(ClassUtils.getQualifiedMethodName(method, targetClass)) ||
				(targetClass != method.getDeclaringClass() &&
						matchesPattern(ClassUtils.getQualifiedMethodName(method, method.getDeclaringClass()))));
	

方法名必须满足正则数组中所有的正则表达式并且当前方法名不满足排除某个方法的正则数组里面所有正则表达式的匹配,才会返回true

	protected boolean matchesPattern(String signatureString) 
		for (int i = 0; i < this.patterns.length; i++) 
		//match方法由子类实现
			boolean matched = matches(signatureString, i);
			if (matched) 
				for (int j = 0; j < this.excludedPatterns.length; j++) 
					//matchesExclusion方法也由子类实现
					boolean excluded = matchesExclusion(signatureString, j);
					if (excluded) 
						return false;
					
				
				return true;
			
		
		return false;
	

JdkRegexpMethodPointcut中对match和matchesExclusion方法的实现

	@Override
	protected boolean matches(String pattern, int patternIndex) 
		Matcher matcher = this.compiledPatterns[patternIndex].matcher(pattern);
		return matcher.matches();
	
	
	@Override
	protected boolean matchesExclusion(String candidate, int patternIndex) 
		Matcher matcher = this.compiledExclusionPatterns[patternIndex].matcher(candidate);
		return matcher.matches();
	


使用演示
<!-- 自己书写的日志切面 -->
<bean id="logBeforeAdvice" class="com.fsx.aop.LogBeforeAdvice" />

<!-- 使用JDK的正则切点~~~~~~ -->
<bean id="regexPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
         <list>
             <value>find.*</value><!-- 拦截所有方法名以find开始的方法 -->
         </list>
    </property>
</bean>

<!-- 切面+切点  组合成一个增强器即可~~~~~~ -->
<aop:config>
     <aop:advisor advice-ref="logBeforeAdvice" pointcut-ref="regexPointcut"/>
 </aop:config>

其实Spring为我们提供了一个简便的Advisor定义,可以方便的让我们同时指定一个JdkRegexpMethodPointcut和其需要对应的Advice,它就是RegexpMethodPointcutAdvisor,这样配置起来非常的方便

	<bean id="logBeforeAdvice" class="com.fsx.aop.LogBeforeAdvice" />
    <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="logBeforeAdvice"/>
        <property name="pattern" value="find.*"/>
    </bean>


上面是xml的实现方式,下面采用java代码来实现一遍:

    public static void main(String[] args) 

        ProxyFactory factory = new ProxyFactory(new Person());

        //声明一个aspectj切点,一张切面
        JdkRegexpMethodPointcut cut = new JdkRegexpMethodPointcut();
        //cut.setPattern("com.fsx.maintest.Person.run"); //它会拦截Person类下所有run的方法(无法精确到方法签名)
        //cut.setPattern(".*run.*");//.号匹配除"\\r\\n"之外的任何单个字符。*号代表零次或多次匹配前面的字符或子表达式  所以它拦截任意包下任意类的run方法
        cut.setPatterns(new String[]".*run.*", ".*say.*"); //可以配置多个正则表达  式...  sayHi方法也会被拦截 

        // 声明一个通知(此处使用环绕通知 MethodInterceptor )
        Advice advice = (MethodInterceptor) invocation -> 
            System.out.println("============>放行前拦截...");
            Object obj = invocation.proceed();
            System.out.println("============>放行后拦截...");
            return obj;
        ;

        //切面=切点+通知
        // 它还有个构造函数:DefaultPointcutAdvisor(Advice advice); 用的切面就是Pointcut.TRUE,所以如果你要指定切面,请使用自己指定的构造函数
        // Pointcut.TRUE:表示啥都返回true,也就是说这个切面作用于所有的方法上/所有的方法
        // addAdvice();方法最终内部都是被包装成一个 `DefaultPointcutAdvisor`,且使用的是Pointcut.TRUE切面,因此需要注意这些区别  相当于new DefaultPointcutAdvisor(Pointcut.TRUE,advice);
        Advisor advisor = new DefaultPointcutAdvisor(cut, advice);
        factory.addAdvisor(advisor);
        Person p = (Person) factory.getProxy();

        // 执行方法
        p.run();
        p.run(10);
        p.say();
        p.sayHi("Jack");
        p.say("Tom", 666);

    


class Person 

    public int run() 
        System.out.println("我在run...");
        return 0;
    

    public void run(int i) 
        System.out.println("我在run...<" + i + ">");
    

    public void say() 
        System.out.println("我在say...");
    

    public void sayHi(String name) 
        System.out.println("Hi," + name + ",你好");
    

    public int say(String name, int i) 
        System.out.println(name + "----" + i);
        return 0;
    


输出:
============>放行前拦截...
我在run...
============>放行后拦截...
============>放行前拦截...
我在run...<10>
============>放行后拦截...
============>放行前拦截...
我在say...
============>放行后拦截...
============>放行前拦截...
Hi,Jack,你好
============>放行后拦截...
============>放行前拦截...
Tom----666
============>放行后拦截...

最后需要注意的是:RegexpMethodPointcutAdvisor没有提供不匹配的正则表达式注入方法,即没有excludedPatterns注入,如果需要该功能请还是使用JdkRegexpMethodPointcut。


AspectJExpressionPointcut—基于切点表达式过滤

切点表达式依赖于AspectJ的jar包去解析的~~~~ Spring在使用@Aspect注解时,会大量的用到它

用AspectJExpressionPointcut实现的切点比JdkRegexpMethodPointcut实现切点的好处就是,在设置切点的时候可以用切点语言来更加精确的表示拦截哪个方法。(可以精确到返回参数,参数类型,方法名,当然,也可以模糊匹配)

    public static void main(String[] args) 
        以上是关于Spring读源码系列之AOP--01---aop基本概念扫盲---上的主要内容,如果未能解决你的问题,请参考以下文章

Spring读源码系列之AOP--02---aop基本概念扫盲---下

Spring读源码系列之AOP--03---aop底层基础类学习

Spring读源码系列之AOP--08--aop执行完整源码流程之自动代理创建器导入的两种方式

Spring读源码系列之AOP--05---aop常用工具类学习

Spring读源码系列之AOP--07---aop自动代理创建器(拿下AOP的最后一击)

Spring读源码系列之AOP--06---AopProxy===>spring使用jdk和cglib生成代理对象的终极奥义