Spring AOP基本概念
- 是一种动态编译期增强性AOP的实现
- 与IOC进行整合,不是全面的切面框架
- 与动态代理相辅相成
- 有两种实现:基于jdk动态代理、cglib
Spring AOP与AspectJ区别
- Spring的AOP是基于动态代理的,动态增强目标对象,而AspectJ是静态编译时增强,需要使用自己的编译器来编译,还需要织入器
- 使用AspectJ编写的java代码无法直接使用javac编译,必须使用AspectJ增强的ajc增强编译器才可以通过编译,写法不符合原生Java的语法;而Spring AOP是符合Java语法的,也不需要指定编译器去编译,一切都由Spring 处理。
JDK动态代理与Cglib的区别
- jdk的动态代理需要实现接口 InvocationHandler
- cglib无需实现接口,使用字节码技术去修改class文件使继承
- spring默认使用jdk动态代理,如果没有实现接口会使用cglib
使用步骤
- 定义业务组件
- 定义切点(重点)
- 定义增强处理方法(切面方法)
依赖
jar包依赖,除此以外还有spring依赖
- aspectjweaver.jar
- aspectjrt.jar
- aspectj.jar
- aopalliance.jar
maven依赖
<dependencies> | |
<!-- 有此依赖会远程下载其它相关依赖 --> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-context</artifactId> | |
<version>4.2.9.RELEASE</version> | |
</dependency> | |
<!-- aspectJ AOP 织入器 --> | |
<dependency> | |
<groupId>org.aspectj</groupId> | |
<artifactId>aspectjweaver</artifactId> | |
<version>1.8.9</version> | |
</dependency> | |
</dependencies> |
注解方式开发
- 扫描Aspect增强的类
<context:component-scan base-package=""> | |
<context:include-filter type="annotation" | |
expression="org.aspectj.lang.annotation.Aspect"/> | |
</context:component-scan> |
- 开启@AspectJ支持
<aop:aspectj-autoproxy/>
- 使用@AspectJ注解来标记一个切面类(spring不会将切面注册为Bean也不会增强,但是需要扫描)
- 使用其它注解进行开发(如下)
常用注解的使用
- @Before:在切点方法前执行
- 在增强的方法上
@Before("execution(* 包名.*.*(..))")
- 上述表达式可使用pointcut或切入表达式,效果一致,之后不再赘述
- 切点方法没有形参与返回值
- 在增强的方法上
示例代码
@Aspect | |
public class AuthAspect { | |
//定义切点 | |
@Pointcut("execution(* com.cnblogs.hellxz.service.*.*(..))") | |
public void pointCut() {} | |
//前置处理 | |
@Before("pointCut()") | |
public void auth() { | |
System.out.println("模拟权限检查……"); | |
} | |
} | |
- @After:在切点方法后执行
- 用法同@Before
- @Around:在切点方法外环绕执行
- 在增强的方法上
@Around("execution(* 包名.*(..))")
或使用切点@Around("pointcut()")
- 接收参数类型为
ProceedingJoinPoint
,必须有这个参数在切面方法的入参第一位 - 返回值为Object
- 需要执行ProceedingJoinPoint对象的proceed方法,在这个方法前与后面做环绕处理,可以决定何时执行与完全阻止方法的执行
- 返回proceed方法的返回值
- @Around相当于@Before和@AfterReturning功能的总和
- 可以改变方法参数,在proceed方法执行的时候可以传入Object[]对象作为参数,作为目标方法的实参使用。
- 如果传入Object[]参数与方法入参数量不同或类型不同,会抛出异常
- 通过改变proceed()的返回值来修改目标方法的返回值
- 在增强的方法上
示例代码
@Aspect | |
public class TxAspect { | |
//环绕处理 | |
@Around("execution(* com.cnblogs.hellxz.service.*.*(..))") | |
Object auth(ProceedingJoinPoint point) { | |
Object object = null; | |
try { | |
System.out.println("事务开启……"); | |
//放行 | |
object = point.proceed(); | |
System.out.println("事务关闭……"); | |
} catch (Throwable e) { | |
e.printStackTrace(); | |
} | |
return object; | |
} | |
} |
- @AfterRetruning: 在方法返回之前,获取返回值并进行记录操作
- 和上边的方法不同的地方是该注解除了切点,还有一个返回值的对象名
- 不同的两个注解参数:returning与pointcut,其中pointcut参数可以为切面表达式,也可为切点
- returning定义的参数名作为切面方法的入参名,类型可以指定。如果切面方法入参类型指定Object则无限制,如果为其它类型,则当且仅当目标方法返回相同类型时才会进入切面方法,否则不会
- 还有一个默认的value参数,如果指定了pointcut则会覆盖value的值
- 与@After类似,但@AfterReturning只有方法成功完成才会被织入,而@After不管结果如何都会被织入
虽然可以拿到返回值,但无法改变返回值
示例代码
@Aspect | |
public class AfterReturningAspect { | |
@AfterReturning(returning="rvt", | |
pointcut = "execution(* com.cnblogs.hellxz.service.*.*(..))") | |
//声明rvt时指定的类型会限定目标方法的返回值类型,必须返回指定类型或者没有返回值 | |
//rvt类型为Object则是不对返回值做限制 | |
public void log(Object rvt) { | |
System.out.println("获取目标返回值:"+ rvt); | |
System.out.println("假装在记录日志……"); | |
} | |
/** | |
* 这个方法可以看出如果目标方法的返回值类型与切面入参的类型相同才会执行此切面方法 | |
* @param itr | |
*/ | |
@AfterReturning(returning="itr", | |
pointcut="execution(* com.cnblogs.hellxz.service.*.*(..))") | |
public void test(Integer itr) { | |
System.out.println("故意捣乱……:"+ itr); | |
} | |
} |
- @AfterThrowing: 在异常抛出前进行处理,比如记录错误日志
- 与
@AfterReturning
类似,同样有一个切点和一个定义参数名的参数——throwing - 同样可以通过切面方法的入参进行限制切面方法的执行,e.g. 只打印IOException类型的异常, 完全不限制可以使用Throwable类型
- pointcut使用同
@AfterReturning
- 还有一个默认的value参数,如果指定了pointcut则会覆盖value的值
- 如果目标方法中的异常被try catch块捕获,此时异常完全被catch块处理,如果没有另外抛出异常,那么还是会正常运行,不会进入AfterThrowing切面方法
- 与
示例代码
@Aspect | |
public class AfterThrowingAspect { | |
@Pointcut("execution(* com.cnblogs.hellxz.test.*.*(..))") | |
public void pointcut() {} | |
/** | |
* 如果抛出异常在切面中的几个异常类型都满足,那么这几个切面方法都会执行 | |
*/ | |
@AfterThrowing(throwing="ex1", | |
pointcut="pointcut()") | |
//无论异常还是错误都会记录 | |
//不捕捉错误可以使用Exception | |
public void throwing(Throwable ex1) { | |
System.out.println("出现异常:"+ex1); | |
} | |
@AfterThrowing(throwing="ex", | |
pointcut="pointcut()") | |
//只管IOException的抛出 | |
public void throwing2(IOException ex) { | |
System.out.println("出现IO异常: "+ex); | |
} | |
} |
pointcut定义的切点方法在@Before/@After/@Around需要写在双引号中,e.g. @Before("pointCut()")
JoinPoint的概念与方法说明
概念
- 顾名思义,连接点,织入增强处理的连接点
- 程序运行时的目标方法的信息都会封装到这个连接点对象中
- 此连接点只读
方法说明
Object[] getArgs()
:返回执行目标方法时的参数Signature getSignature()
:返回被增强方法的相关信息,e.g 方法名 etcObject getTarget()
:返回被织入增强处理的目标对象Object getThis()
:返回AOP框架目标对象生成的代理对象
使用
- 在@Before/@After/@AfterReturning/@AfterThrowing所修饰的切面方法的参数列表中加入JoinPoint对象,可以使用这个对象获得整个增强处理中的所有细节
- 此方法不适用于@Around, 其可用ProceedingJoinPoint作为连接点
ProceedingJoinPoint的概念与方法说明
概念
- 是JoinPoint的子类
- 与JoinPoint概念基本相同,区别在于是可修改的
- 使用@Around时,第一个入参必须为ProceedingJoinPoint类型
- 在@Around方法内时需要执行proceed()或proceed(Object[] args)方法使方法继续,否则会一直处于阻滞状态
方法说明
ProceedingJoinPoint是JoinPoint的子类,包含其所有方法外,还有两个公有方法
Object proceed()
:执行此方法才会执行目标方法Object proceed(Object[] args)
:执行此方法才会执行目标方法,而且会使用Object数组参数去代替实参,如果传入Object[]参数与方法入参数量不同或类型不同,会抛出异常
通过修改proceed方法的返回值来修改目标方法的返回值
编入的优先级
优先级最高的会最先被织入,在退出连接点的时候,具有最高的优先级的最后被织入
当不同切面中两个增强处理切入同一连接点的时候,Spring AOP 会使用随机织入的方式
如果想要指定优先级,那么有两种方案:
- 让切面类实现
org.springframework.core.Ordered
接口,实现getOrder方法,返回要指定的优先级 - 切面类使用
@Order
修饰,指定一个优先级的值,值越小,优先级越高
访问目标方法的形参
除了使用JoinPoint或ProceedingJoinPoint来获取目标方法的相关信息外(包括形参),如果只是简单访问形参,那么还有一种方法可以实现
- 在pointcut的execution表达式之后加入
&& args(arg0,arg1)
这种方式
@Aspect | |
public class AccessInputArgs { | |
@Before("execution(* com.cnblogs.hellxz.test.*.*(..)) && args(arg0, arg1)") | |
public void access(String arg0, String arg1){ | |
System.out.println("接收到的参数为arg0="+arg0+",arg1="+arg1); | |
} | |
} |
注意:通过这种方式会只匹配到方法只有指定形参数量的方法,并且,在切面方法中指定的类型会限制目标方法,不符合条件的不会进行织入增强
定义切入点
通过定义切入点,我们可以复用切点,减少重复定义切点表达式等
切入点定义包含两个部分:
- 切入点表达式
- 包含名字和任意参数的方法签名
使用@Pointcut注解进行标记一个无参无返回值的方法,加上切点表达式
@Pointcut("execution(* com.cnblogs.hellxz.test.*.*(..))") | |
public void pointcut(){} |
切入点指示符
Spring AOP 支持10种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation、bean下面做下简记(没有写@Pointcut(),请注意):
-
execution: 用来匹配执行方法的连接点的指示符。
用法相对复杂,格式如下:execution(权限访问符 返回值类型 方法所属的类名包路径.方法名(形参类型) 异常类型)
e.g. execution(public String com.cnblogs.hellxz.test.Test.access(String,String))
权限修饰符和异常类型可省略,返回类型支持通配符,类名、方法名支持*通配,方法形参支持..通配 -
within: 用来限定连接点属于某个确定类型的类
within(com.cnblogs.hellxz.test.Test)
within(com.cnblogs.hellxz.test.) //包下类
within(com.cnblogs.hellxz.test..) //包下及子包下 -
this和target: this用于没有实现接口的Cglib代理类型,target用于实现了接口的JDK代理目标类型
举例:this(com.cnblogs.hellxz.test.Foo) //Foo没有实现接口,使用Cglib代理,用this
实现了个接口public class Foo implements Bar{...}
target(com.cnblogs.hellxz.test.Test) //Foo实现了接口的情况 -
args: 对连接点的参数类型进行限制,要求参数类型是指定类型的实例。
args(Long) -
@target: 用于匹配类头有指定注解的连接点
@target(org.springframework.stereotype.Repository) -
@args: 用来匹配连接点的参数的,@args指出连接点在运行时传过来的参数的类必须要有指定的注解
@Pointcut("@args(org.springframework.web.bind.annotation.RequestBody)")
public void methodsAcceptingEntities() {}
-
@within: 指定匹配必须包括某个注解的的类里的所有连接点
@within(org.springframework.stereotype.Repository) -
@annotation: 匹配那些有指定注解的连接点
@annotation(org.springframework.stereotype.Repository) -
bean: 用于匹配指定Bean实例内的连接点,传入bean的id或name,支持使用*通配符
切点表达式组合
使用&&、||、!、三种运算符来组合切点表达式,表示与或非的关系execution(* com.cnblogs.hellxz.test.*.*(..)) && args(arg0, arg1)