SpringAOP

245 阅读10分钟

AOP概念

什么是AOP

Aop是面向切面编程。在程序中具有公共特性的某些类/某些方法上进行拦截, 在方法执行的前面/后面/执行结果返回后增加执行一些方法。偏重业务处理过程的某个步骤或阶段。强调对某个业务逻辑的补充。

AOP的编程思想就是把补充的逻辑和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。

AOP和OOP的关系

OOP是面向对象编程。编程思想是自上而下的进行的,注重逻辑单元业务的划分。OOP是纵向结构,AOP是横向结构,利用AOP可以对OOP的业务逻辑进行补充。主要应用为日志记录、事务处理等。

AOP、Aspectj和SpringAOP的关系

和IOC与spring IOC一样,AOP是思想,Aspectj和SpringAOP都是AOP的具体实现。Spring有自己的语法,但是过于复杂,于是SpringAOP借助了Aspectj的注解,但是,AOP运行时仍然是纯Spring AOP,并不依赖于AspectJ编译器。

SpringAOP提供两种编程风格

  • 基于Aspectj的注解
  • 基于schema-based AOP的支持

AOP术语

  • Aspect(切面):横切关注点。被模块化的特殊对象。跨越多个类。一定要给spring去管理
  • join point(连接点):目标对象中的方法。例如一个方法的执行或异常的处理。在Spring AOP,连接点总是代表一个方法执行。
  • Point cut(切点):表示连接点的集合。通过切点表达式定位到特殊的连接点。切点表达式决定 JoinPoint 的数量。
  • Advice(通知):切面必须要完成的动作。通知位置+逻辑
  • Target Object(目标对象):被通知的对象,原始对象。
  • AOP proxy(AOP代理):代理对象。向目标对象通知之后创建的对象。
  • Weaving(织入):把代理逻辑加入到目标对象上的过程

AOP代理

Spring默认使用JDK的动态代理,可以代理任何接口。SpringAOP也可以使用CGLIB代理,通过@EnableAspectJAutoProxy(proxyTargetClass = true)<aop:config proxy-target-class="true">开启CGLIB的动态代理。

JDK的动态代理

jdk中的动态代理只能为接口生成代理类,创建出来的代理都是java.lang.reflect.Proxy的子类。通过Proxy创建代理对象,当调用代理对象任意方法时候,会被InvocationHandler接口中的invoke方法进行处理,这个接口内容是关键

CGLIB的代理

本质上它是通过动态的生成一个子类去覆盖所要代理的类(非final修饰的类和方法)。实际上为通过继承来实现的

Cglib和java的动态代理区别

  • java动态代理只能对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,java不允许多重继承)。cglib能代理普通类。
  • java的动态代理使用java原生的反射Api进行操作,在生成类上比较高效。cglib使用asm框架直接对字节码进行操作。在类的执行过程中比较高效。

@Aspectj的支持

@AspectJ是一种将切面声明为带有注解的常规Java类的样式。

启用@Aspectj配置

可以通过xml或java config来开启@Aspectj的支持,首先确保项目中引入aspectjweaver.jar

java config

@Configuration
@ComponentScan("com.zjy.app")
@EnableAspectJAutoProxy
public class Appconfig {}

xml

<aop:aspectj-autoproxy/>

声明一个切面

启用@AspectJ支持后,Spring会自动检测到在应用程序上下文中使用@AspectJ方法的类定义的任何bean,并用于配置Spring AOP。
@Component
@Aspect
public class myAspectj {}

声明切入点

@Pointcut("execution (* com.zjy.app.dao.*.*(..))")
public void pointCutExecution(){};

声明通知

通知与切入点表达式关联,并且在切入点匹配的方法执行之前,之后或周围运行。切入点表达式可以引用命名切入点或在方法上声明一个切入点表达式。

引用命名切入点

@Before("pointCutExecution")
public void before() {    
    System.out.println("before");
}

声明一个切入点表达式

@Before("execution(* com.zjy.app.dao.*.*(..))")
public void before() {    
    System.out.println("before");
}

基于schema-based AOP的支持

声明一个切面

<bean id="myAspect" class="com.zjy.schemabase.MyAspect"></bean>
<aop:config> 
    <aop:aspect id="aspect1" ref="myAspect">        
</aop:config>

声明一个切点

<aop:config>    
    <aop:pointcut id="printBefore"                  
        expression="execution(* com.zjy.schemabase.dao.*.*(..))" />    
</aop:config>

声明一个通知

<aop:config>   
    <aop:aspect id="aspect1" ref="myAspect">        
        <aop:before method="before"                    
            pointcut-ref="printBefore"></aop:before>    
    </aop:aspect>
</aop:config>

public class MyAspect {    
    public void before() {        
        System.out.println("before");    
    }
}

完整XML配置

<aop:config>    
    <aop:pointcut id="printBefore"                  
        expression="execution(* com.zjy.schemabase.dao.*.*(..))" />    
    <aop:aspect id="aspect1" ref="myAspect">        
    <aop:before method="before"                    
        pointcut-ref="printBefore"></aop:before>    
    </aop:aspect>
</aop:config>

execution切点表达式解析

表达式解析

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)
  • ?表示当前项可以有也可以没有
  • modifiers-pattern:方法的可见性,如public,protected;
  • ret-type-pattern:方法的返回值类型,如int,void等;
  • declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
  • name-pattern:方法名类型,如buisinessService();
  • param-pattern:方法的参数类型,如java.lang.String;
  • throws-pattern:方法抛出的异常类型,如java.lang.Exception;
除了`ret-type-pattern`,`name pattern`和`param pattern`以外的所有部分都是可选的。

ret-type-pattern模式确定该方法的返回类型必须是什么才能使连接点匹配。最常用作返回类型模式是*.它匹配任何返回类型。

`name-pattern`与方法名称匹配。您可以使用*通配符匹配所有方法。

parameters pattern()匹配不带参数的方法,而(..)匹配任意数量(零个或多个)的参数。()模式匹配一个任何类型的参数的方法。(,String)与两个参数的方法匹配。第一个可以是任何类型,而第二个必须是String

切点通配符

  • .. : 匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包

  • + :匹配给定类的任意子类

  • * :匹配任意数量的字符

常用的切入点表达式:

  1. 任何公共方法的执行

    execution(public * *(..))
    
  2. 名称以set开头的任意方法

    execution(* set*(..))
    
  3. service包中定义的任何方法

    execution(* com.xyz.service.*.*(..))
    
  4. service包或其子包之一中定义的任何方法的执行

    execution(* com.xyz.service..*.*(..))
    
  5. 执行方法带有@Transactional注释的任何连接点(仅在Spring AOP中为方法执行)
    @annotation(org.springframework.transaction.annotation.Transactional)
    

详细内容请点击连接跳转官网查看

切点指示符

spring AOP支持在切入点表达式中使用的AspectJ切入点指示符有9个。

execution

用于匹配方法执行的连接点。这是使用Spring AOP时要使用的主要切入点指示符。
execution(* com.chenss.dao.*.*(..))//匹配com.chenss.dao包下的任意接口和类的任意方法
execution(public * com.chenss.dao.*.*(..))//匹配com.chenss.dao包下的任意接口和类的public方法
execution(* com.chenss.dao.*.*(java.lang.String, ..))//匹配com.chenss.dao包下的任意接口和类的第一个参数为String类型的方法
execution(public * *(..))//匹配任意的public方法execution(* te*(..))//匹配任意的以te开头的方法
execution(* com.chenss.dao.IndexDao.*(..))//匹配com.chenss.dao.IndexDao接口中任意的方法
execution(* com.chenss.dao..*.*(..))//匹配com.chenss.dao包及其子包中任意的方法

由于spring的切面粒度最小达到方法级别,而execution表达式可以明确的指定方法的返回类型,类名,方法名,参数名等与方法有关的信息,因此execution表达式是使用最广泛的

within

匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法的执行)

表达式的最小粒度为类。 within与execution相比,粒度更大,仅能实现到包和接口、类级别。而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等

@Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法

this

用于匹配当前AOP代理对象类型的执行方法。限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中bean引用(Spring AOP代理)是给定类型的实例。

JDK代理时,指向接口和代理类proxy,cglib代理时 指向接口和子类(不使用proxy)

//当前对象,也就是代理对象,代理对象时通过代理目标对象的方式获取新的对象,与原值并非一个
@Pointcut("this(com.zjy.app.dao.IndexDaoImpl)")
public void pointCutThis(){};

target

用于匹配当前目标对象类型的执行方法。在目标对象(代理的应用程序对象)是给定类型的实例的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)指向接口和子类。JDK代理的实现方式是基于接口实现,代理类继承Proxy,实现接口。而CGLIB继承被代理的类来实现。所以使用target会保证目标不变,关联对象不会受到这个设置的影响。但是使用this对象时,会根据该选项的设置,判断是否能找到对象。

// 目标对象,也就是被代理的对象。限制目标对象为com.zjy.app.dao.IndexDaoImpl类
@Pointcut("target(com.zjy.app.dao.IndexDaoImpl)")
public void pointCutTarget(){};

args

 args表达式的作用是匹配指定参数类型和指定参数数量的方法,与包名和类名无关

@Pointcut("args (java.lang.String)")
public void pointCutArgs(){};

@target

在执行对象的类具有给定类型的注解的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。

@args

限制匹配的连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。

@within

将匹配限制为具有给定注解的类型内的连接点(使用Spring AOP时,使用给定注解的类型中声明的方法的执行)。

@annotation

将匹配点限制在连接点的主题(在Spring AOP中运行的方法)具有给定注解的连接点。

@Pointcut("@annotation(com.zjy.app.anno.MyAnno)")
public void pointCutAnno(){};

组合切入点表达式

您可以使用`&&,` `||`和`!`组合切入点表达式。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} 

@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

AOP的通知方式

通知方式

  • Before(前置通知):在目标方法被调用之前调用通知方法。但是无法阻止连接点的正常执行,除非该段执行抛出异常
  • After (后置通知): 目标方法完成之后调用通知方法,此时不会关心目标方法是否执行成功或者抛出异常
  • After throwing(异常通知): 目标方法抛出异常后调用通知方法
  • After returning advice:无论连接点是正常退出还是异常退出,都会执行
  • Around (环绕通知):围绕连接点执行,例如方法调用。这是最有用的切面方式。around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。

通知参数

通过使用@Around注解来声明环绕通知,方法的第一个参数必须为ProceedingJoinPoint。调用proceed()来执行下一个通知或调用目标方法。

任何通知方法都可以将类型的参数声明为它的第一个参数org.aspectj.lang.JoinPoint(请注意,环绕通知必须在声明`ProceedingJoinPoint`为第一个参数,`ProceedingJoinPoint`是`JoinPoint`的子类。该JoinPoint接口提供了许多有用的方法:
  • getArgs(); 返回方法参数。
  • getThis(); 返回代理对象。
  • getTarget();返回目标对象。

Proceedingjoinpoint 和JoinPoint的区别

Proceedingjoinpoint 继承了JoinPoint,并扩充实现了proceed()方法,用于继续执行连接点。JoinPoint仅能获取相关参数,无法执行连接点。
**JoinPoint的方法**
  1. java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
  2. Signature getSignature() :获取连接点的方法签名对象;
  3. java.lang.Object getTarget() :获取连接点所在的目标对象;
  4. java.lang.Object getThis() :获取代理对象本身;
**Proceedingjoinpoint 对 proceed()有重载,有个带参数的方法,可以修改目标方法的的参数**
@Pointcut("within(com.zjy.app.dao.*)")
public void pointCutAround() {};

@Around("pointCutAround()")
public void around(ProceedingJoinPoint pjp) throws Throwable {    
    System.out.println("around-before");    
    Object[] args = pjp.getArgs();    
    if(args != null && args.length > 0) {        
        for (int i = 0; i < args.length; i++) {            
            args[i] += " world";        
        }    
    }    
    pjp.proceed(args);    
    System.out.println("around-after");
}