【Spring】深入理解spring-aop之使用篇

目录

spring-aop

aop的概念

spring aop支持的advice类型

spring aop支持的pointcut表达式

this()和target()的区别

基于注解配置aop

基于xml 配置aop

xml配置aop

         spring-aop特有的advisor

基于auto-proxy配置


spring-aop

    aop是经常使用的,不过之前没有完整的了解aop,促使笔者系统的学习aop是因为之前的遇到的问题。这个问题在《掉进sping-aop的陷阱》介绍过。因为sping-aop的底层实现是基于动态代理,所以有必要了解一下《动态代理》

    aop是Aspect Oriented Programming简写,即面向切面编程,是OOP(面向对象编程)的补充。切面不好解释,类似数学里面球面的切面(与球面只有一个交点),面向切面编程可以理解成在程序某个执行点切进去执行一段额外加的逻辑。aop是一个编程思想、方法论,sping-aop是一个实现aop思想的框架。当然还有其它框架,例如AspectJ。在spring的官方文档里有一段论述,表明了aop、spring-aop、aspectj的关系。

    Spring AOP’s approach to AOP differs from that of most other AOP frameworks. The aim is not to provide the most complete AOP implementation (although Spring AOP is quite capable). Rather, the aim is to provide a close integration between AOP implementation and Spring IoC, to help solve common problems in enterprise applications.
    Spring AOP never strives to compete with AspectJ to provide a comprehensive AOP solution. We believe that both proxy-based frameworks such as Spring AOP and full-blown frameworks such as AspectJ are valuable and that they are complementary, rather than in competition. 

大意如下:

    spring aop的实现跟其它aop框架不同。spring aop的目标不是提供一种完整的大而全的aop框架(尽管spring aop能力很强),它的目标仅仅是提供一种aop实现与spring的ioc紧密连接 
    sping aop不是与AspectJ竞争的,也不是提供一种完整的aop解决方案。基于代理的aop框架(例如spring aop)和完全成熟的aop框架(例如AspectJ)是互补的,而非竞争关系。

aop的概念

在使用aop之前,我们得了解aop的一些术语、概念。关于aop的术语,spring官方文档又有一段论述

These terms are not Spring-specific. Unfortunately, AOP terminology is not particularly intuitive. However, it would be even more confusing if Spring used its own terminology.

大致意思如下:

这些术语不是sping定义的。尽管这些术语不是很明确直观(理解起来费劲),但是如果spring自己定义另一套术语,可能更混乱(2套术语)。
  • Aspect:  切面-我们用aop所做的事情就叫aspect,例如日志管理(横切classes然后输出日志);
  • Join Point: 链接点-所有能够被切到的位置就是链接点,例如:method-execution、constructor-execution、staticinitialization、field-get 、field-set等等,spring-aop只关心method-execution这一类链接点(完整的aop框架如Aspectj支持所有的链接点);
  • Advice:通知-在切点(符合Pointcut条件的链接点)执行的额外逻辑,aop框架一般支持5种类型的advice,即before、after returning、after throwing、after finnally、around;
  • Pointcut:切点-符合条件的链接点,是Join Point子集;
  • Introduction:可以让目标对象增加新的接口(interface);
  • Target object:目标对象也叫advised object,被代理的对象;
  • AOP proxy:代理对象-由aop框架创建的对象,包装Target object,代理对象就是Target object的中间商,代理对象在执行原始方法前可执行Advice的逻辑;
  • Weaving:织入-aop框架将切面与Target object关联起来,并创建代理对象的过程(很抽象的概念),spring-aop是用jdk动态代理或者CGLIB动态代理达到这个目的。

上述概念有些抽象,记住一句话:spring-aop就是找到符合条件(Pointcut指定的条件)的对象(Target object)的方法(Joint Point),对其做动态代理,并在运行时执行额外逻辑(Advice)

spring aop支持的advice类型

  • Before advice: 在Join Point执行前执行的Advice,除非有异常,否则before advice不会终止流程即原Join point一定会执行;
  • After returning advice:join point正常返回后执行的Advice;
  • After throwing advice:join point非正常返回后执行的Advice(即join point有异常抛出);
  • After (finally) advice:join point 返回(无论正常还是抛异常)后执行的Advice;
  • Around advice:环绕在join point周围的一种Advice,是最通用、强大的一种Advice,aop框架把整个join point交给around advice,around advice可在前后加逻辑、可终止流程。。。 看一下around advice的方法体定义:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
   // do anything
   //如果不执行pjp.proceed就不会执行原join point
   Object retVal = pjp.proceed();
   // do anything
   return retVal;
}

spring aop支持的pointcut表达式

标识符表达式语义备注
excution
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
匹配符合条件的method-excution链接点最常用的表达式
within
within(type-pattern)
符合条件的类包含的链接点对于spring-aop来说只关心method-excution
this
this(类全路径)
如果代理对象是指定类型,则该类型包含的链接点,同上
target
target(类全路径)
如果目标对象是指定类型,则该类型包含的链接点同上
args
args(类全路径)
如果参数是指定类型,则该链接点符合条件同上,也就是说方法参数是指定类型,则该方法将被代理
@target
@target(注解全路径)
如果目标对象上包含指定注解,则该对象包含的链接点符合条件spring-aop只关心method-excution
@args
@args(注解全路径)
如果传入链接点的实参类型被指定注解所注解同上,也就是说方法参数是指定类型,则该方法将被代理
@within@within(注解全路径)被指定注解所注解的类型包含的链接点spring-aop只关心method-excution
@annotation
@annotation(注解全路径)
被指定注解所注解的链接点同上,即有指定注解的方法

pointcut表达式中pattern支持的通配符:

* 表示任何数量的字符,除了(.)
.. 表示任何数量的字符包括任何数量的(.)
+ 描述指定类型的任何子类或者子接口

this()和target()的区别

   在pointcut表达式中最容易混淆的就是this和target,语义上this表达式是用代理对象的类型去跟this指定的类型的比较,target表达式是用目标对象去跟target指定的类型比较。大部分时候是一样,只有在动态代理方式采用jdk自带的动态代理功能时有些许差别。

    CGLIB是基于子类的方式实现动态(《动态代理》),代理类是目标类的子类,所以this和target几乎无区别

    而jdk动态代理是针对接口做代理,代理类和目标类没有直接关系,两者公共实现相同的接口,如果this表达式指定的类型是目标类而非接口,则这个链接点命中不了。举例:Printer接口、DemoPrinter实现Printer接口。

package com.focuse.aopdemo.printer;

public interface Printer {
    String print(String text);
}
package com.focuse.aopdemo.printer;

import org.springframework.stereotype.Component;

@Component
public class DemoPrinter implements Printer{
    private String name = "demoPrinter";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String print(String text) {
        return name + ":" + text;
    }
}

配置aop, pointcut表达式是this(com.focuse.aopdemo.printer.DemoPrinter),this指定的是具体的类而非Printer接口

package com.focuse.aopdemo.aop.xmlbase;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Component
public class DemoAspect{

    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before");
        Object retVal = pjp.proceed();
        System.out.println("around after");
        return retVal;
    }
}
<aop:config proxy-target-class="false">
        <aop:pointcut id="demoPointcut" expression="this(com.focuse.aopdemo.printer.DemoPrinter)"></aop:pointcut>
        <aop:aspect id="demoAspect" ref="demoAspect">
            <aop:around method="aroundAdvice" pointcut-ref="demoPointcut" />
        </aop:aspect>
    </aop:config>

这样之后运行是没有执行到aroundAdvice,因为代理对象的类型不是DemoPrinter子类。

    如果我们把表达式换成target表达式target(com.focuse.aopdemo.printer.DemoPrinter)之后运行,则能执行aroundAdvice,因为目标对象的类型是DemoPrinter,运行结果如下:

    实际上,上述例子中即便是配置成this表达式,spring-aop框架还是给demoPrinter这个bean创建类代理对象,因为此时还没有代理类,判断是否需要代理是根据目标对象的类型决定的。但是如果是this表达式,在真正执行Advice之前要判断代理对象是否符合this表达式。jdk动态代理有一个角色InvocationHandler,spring-aop定义了一个实现类JdkDynamicAopProxy,所有调代理的方法都会传递到org.springframework.aop.framework.JdkDynamicAopProxy#invoke。spring-aop把所有的advice都封装在JdkDynamicAopProxy,执行代理方法的时候判断advice符合执行条件。对this这段判断逻辑,读者可以跟踪调试代码,笔者就直接贴出源码最后判断的位置org.springframework.aop.aspectj.AspectJExpressionPointcut#matches(java.lang.reflect.Method, java.lang.Class<?>, java.lang.Object...)

public boolean matches(Method method, Class<?> targetClass, Object... args) {
		checkReadyToMatch();
		ShadowMatch shadowMatch = getShadowMatch(AopUtils.getMostSpecificMethod(method, targetClass), method);
		ShadowMatch originalShadowMatch = getShadowMatch(method, method);

		// Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target,
		// consistent with return of MethodInvocationProceedingJoinPoint
		ProxyMethodInvocation pmi = null;
		Object targetObject = null;
		Object thisObject = null;
		try {
			MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
			targetObject = mi.getThis();
			if (!(mi instanceof ProxyMethodInvocation)) {
				throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
			}
			pmi = (ProxyMethodInvocation) mi;
			thisObject = pmi.getProxy();
		}
		catch (IllegalStateException ex) {
			// No current invocation...
			if (logger.isDebugEnabled()) {
				logger.debug("Could not access current invocation - matching with limited context: " + ex);
			}
		}

		try {
			JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(thisObject, targetObject, args);

			/*
			 * Do a final check to see if any this(TYPE) kind of residue match. For
			 * this purpose, we use the original method's (proxy method's) shadow to
			 * ensure that 'this' is correctly checked against. Without this check,
			 * we get incorrect match on this(TYPE) where TYPE matches the target
			 * type but not 'this' (as would be the case of JDK dynamic proxies).
			 * <p>See SPR-2979 for the original bug.
			 */
			if (pmi != null) {  // there is a current invocation
				RuntimeTestWalker originalMethodResidueTest = getRuntimeTestWalker(originalShadowMatch);
                //
                //笔者注:如果是this表达式,这里会判断代理类是否是this表达式的子类
                //
				if (!originalMethodResidueTest.testThisInstanceOfResidue(thisObject.getClass())) {
					return false;
				}
				if (joinPointMatch.matches()) {
					bindParameters(pmi, joinPointMatch);
				}
			}

			return joinPointMatch.matches();
		}
		catch (Throwable ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Failed to evaluate join point for arguments " + Arrays.asList(args) +
						" - falling back to non-match", ex);
			}
			return false;
		}
	}

如上述源码,如果是this表达式,会判断代理对象的类型是否是this表达式的子类

 

基于注解配置aop

spring-aop支持aspectj的切面注解,spring容器开启对aspecj注解的支持有两种方式:a. 任何一个bean上添加注解@EnableAspectJAutoProxy; b. xml中配置<aop:aspectj-autoproxy>

配置一个aop需要有3个元素:aspect、pointcut、advice,然后将advice和pointcut关联起来。语义就是通过pointcut找到符合条件的链接点执行与该pointcut关联的advice

声明aspect  @Aspect

@Component
@EnableAspectJAutoProxy
@Aspect
public class AnnotationAspect {
}

声明Pointcut @Pointcut

    @Pointcut("target(com.focuse.aopdemo.printer.Printer)")
    public void targetPrinter(){

    }

声明Advice  @Around @Before @AfterReturning @AfterThrowing @After

@Around("targetPrinter()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("annotation around before");
        Object retVal = pjp.proceed();
        System.out.println("annotation around after");
        return retVal;
    }

声明Advice的这些注解里面需要填入Pointcut表达式,这样就相当于把Advice和Pointcut关联起来了。完整的aop demo如下

package com.focuse.aopdemo.aop.annotations;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

@Component
@EnableAspectJAutoProxy
@Aspect
public class AnnotationAspect {

    @Pointcut("target(com.focuse.aopdemo.printer.Printer)")
    public void targetPrinter(){

    }

    @Around("targetPrinter()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("annotation around before");
        Object retVal = pjp.proceed();
        System.out.println("annotation around after");
        return retVal;
    }
}

也可以将pointcut表达式直接写在声明Advice的注解中,这样不用单独声明Pointcut 

    @Around("target(com.focuse.aopdemo.printer.Printer")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("annotation around before");
        Object retVal = pjp.proceed();
        System.out.println("annotation around after");
        return retVal;
    }

基于xml 配置aop

xml配置aop

基于xml配置aop需要引入aop的namespace

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

配置各种bean....

<aop:config>
配置aop
</aop:config>

</beans>

aop的配置需要放在<aop:config>的标签里面

配置切面并定义切面bean <aop:aspect>

<aop:config>   
    <aop:aspect id="demoAspect" ref="demoAspect">
    </aop:aspect>
</aop:config>
package com.focuse.aopdemo.aop.xmlbase;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Component
public class DemoAspect{

    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before");
        Object retVal = pjp.proceed();
        System.out.println("around after");
        return retVal;
    }
}

配置pointcut <aop:pointcut>

<aop:pointcut id="demoPointcut" expression="target(com.focuse.aopdemo.printer.DemoPrinter)"></aop:pointcut>

配置Advice并关联pointcut

Advice需要配置在标签<aop:aspect>里,属性method指定aspect类里面的一个方法,pointcut-ref关联一个pointcut。我们也可以直接把pointcut表达式配置在advice中,就不需要单独配置<aop:pointcut>

 

<aop:around method="aroundAdvice" pointcut-ref="demoPointcut" />

完整的配置如下

    <aop:config >
        <!-- 配置一个pointcut -->
        <aop:pointcut id="demoPointcut" expression="target(com.focuse.aopdemo.printer.DemoPrinter)"></aop:pointcut>
        <!-- 配置一个切面,切面的bean是demoAspect -->
        <aop:aspect id="demoAspect" ref="demoAspect">
            <!-- 配置advice,指定advice执行的方法demoAspect里面的aroundAdvice,同时关联pointcut -->
            <aop:around method="aroundAdvice" pointcut-ref="demoPointcut" />
        </aop:aspect>
        
    </aop:config>

spring-aop特有的advisor

advisor不是aop规范里定义的概念,而是spring-aop实现aop框架的底层组件,spring-aop也把advisor的配置暴漏出来了,我们可以这么理解advisor就是只包含一个advice的aspect,前面<aop:aspect>标签里可以配多个advice。advisor配置很简单

配置pointcut(跟前面一样) <aop:pointcut>

<aop:pointcut id="demoPointcut" expression="target(com.focuse.aopdemo.printer.DemoPrinter)"></aop:pointcut>

创建advice的bean

默认情况下advisor的advice必须是org.aopalliance.intercept.MethodInterceptor、org.springframework.aop.AfterReturningAdvice、org.springframework.aop.MethodBeforeAdvice、org.springframework.aop.ThrowsAdvice这四种,当然spring也提供了扩展功能可以注册其它Advice的适配从而支持其它Advice,具体请看org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry#registerAdvisorAdapter

package com.focuse.aopdemo.aop.advisor;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.stereotype.Component;

@Component
public class DemoAdvisor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("advisor method interceptor");
        return invocation.proceed();
    }
}

配置advisor并关联advice 和 pointcut

<aop:advisor advice-ref="demoAdvisor" pointcut-ref="demoPointcut"></aop:advisor>

完整配置如下

<aop:config>
        <aop:pointcut id="demoPointcut" expression="target(com.focuse.aopdemo.printer.DemoPrinter)"></aop:pointcut>
        <!-- 配置advisor关联advice和pointcut-->
        <aop:advisor advice-ref="demoAdvisor" pointcut-ref="demoPointcut"></aop:advisor>
    </aop:config>

基于auto-proxy配置

spring-aop还提供了一个简单的配置代理的方式BeanNameAutoProxyCreator

    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
        <property name="beanNames">
            <list>
                <value>demoPrinter</value>
            </list>
        </property>
        <property name="interceptorNames">
            <list>
                <value>beanNameAdvice</value>
            </list>
        </property>
    </bean>

 

package com.focuse.aopdemo.aop.beanname;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class BeanNameAdvice implements MethodBeforeAdvice {
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("bean name autoproxy before");
    }
}

beanNames设置需要代理的bean的name(支持正则);interceptorNames设置advice的bean的name,可以是Advice或者Advisor,如果是advice,默认也只能是org.aopalliance.intercept.MethodInterceptor、org.springframework.aop.AfterReturningAdvice、org.springframework.aop.MethodBeforeAdvice、org.springframework.aop.ThrowsAdvice这四种。如果是Advice,将会对目标对象的类型的所有方法做代理。如果想指定方法,需要配合Advisor,interceptorNames配置Advisor的bean,例如NameMatchMethodPointcutAdvisor。

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值