Spring AOP
AOP概念
aop的本质就是动态代理, 在程序运行期间, 不修改源码对已有的方法进行增强
相关术语
-
连接点(Joinpoint):在程序中我们可以进行aop的点, 通常是指的方法, 在使用aop时并不需要定义。
-
切入点(Pointcut):被增强过的方法, (切入点一定是连接点, 但连接点并不一定是切入点), 切入点表示已经被增强过的, 连接点表示可以被增强的, 可以增强不表示被增强, 但是增强了的一定是可以被增强的。
-
通知(Advice):就是我们进行增强的过程, 使我们具体逻辑过程, 可以理解为我们写的增强方法。
-
切面(Aspect):切入点和通知组合就是一个切面, 就是我们通常用来进行切面的那个类, 可以使用注解 @Aspect 进行定义。
切入点设置
/**
* 权限 返回值 包.类.方法(参数。。。)
*/
@Pointcut(value = "execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void BrokerAspect(){
}
定义切入点我们使用Pointcut注解, 使用的是execution表达式, 其中 * 号表示任意, 两点 表示任意参数。
上述execution表示, 在com.zqh.demo.controller.Demo类下权限为public的任意方法```
execution(public * *(..)))
这个表示任意public方法
execution(* *To(..))
这个表示任意一To结尾的方法
通知
- 前置通知
/** * 定义切入点,切入点为 public 返回值 com.zqh.demo.controller.Demo.方法(参数) 中的所有函数 *通过@Pointcut注解声明频繁使用的切点表达式 */ @Pointcut(value = "execution(public * com.zqh.demo.controller.Demo.*(..)))") public void BrokerAspect(){ } /** * 前置通知,在方法运行前通知 */ @Before("BrokerAspect()") //@Before("execution(public * com.zqh.demo.controller.Demo.*(..)))") public void doBeforeGame(){ logger.info("前置通知"); }
前置通知使用@Before进行配置,里面值为切入点,可以手动写一个execution表达式也可以使用前面已经定义好的切入点, 这个方法将在切入点方法执行前执行
/** * 这是执行方法 */ @GetMapping("/aop1") public String aop1(String msg) { logger.info("运行了方法"); //int i = 1/0; return msg; }
运行前置通知后再运行方法 - 后置通知
/** * 后置通知, 方法运行后运行, 发生异常后不通知 */ @AfterReturning("BrokerAspect()") //@AfterReturning("execution(public * com.zqh.demo.controller.Demo.*(..)))") public void doAfterReturningGame(){ logger.info("后置通知"); }
- 异常通知
/** * 异常通知, 在方法发生异常时通知 */ @AfterThrowing("BrokerAspect()") //@AfterThrowing("execution(public * com.zqh.demo.controller.Demo.*(..)))") public void doAfterThrowingGame(){ logger.info("异常通知"); }
后置通知和异常通知只有一个会执行,当方法没有抛出异常时将执行后置通知,抛出异常后只会执行异常通知
在没有异常的情况下, 并没有执行异常通知
这时我们制造一个异常, 只执行了异常通知并没有执行后置通知
@GetMapping("/aop1")
public String aop1(String msg) {
logger.info("运行了方法");
int i = 1/0;
return msg;
}
- 最终通知
/** * 最终通知, 方法运行最后通知, 不管是否产生异常,都会通知 */ @After("BrokerAspect()") //@After("execution(public * com.zqh.demo.controller.Demo.*(..)))") public void doAfterGame(){ logger.info("最终通知"); }
-
环绕通知
这是功能最强的一个通知,我们可以使用这个通知完成其他通知的功能/** * 环绕通知 */ @Around("BrokerAspect()") public void doAroundGame(ProceedingJoinPoint pjp) { try { logger.info("环绕前置通知"); pjp.proceed(); logger.info("环绕后置通知"); } catch (Throwable throwable) { logger.info("环绕异常通知"); } finally { logger.info("环绕最终通知"); } }
ProceedingJoinPoint 表示切入点,可以通过它的proceed控制方法什么时候执行。并在方法执行前后进行通知。
现在我们将前面几个通知注释之后看看环绕通知效果
如果有异常的情况
和上面的效果是相似的,将环绕通知和其它通知同时存在是他的执行顺序又是如何呢?
可以看到环绕通知比其它通知更早开始, 更晚结束。其实这并不难理解。
前置通知,后置通知, 异常通知, 最终通知都是在方法执行时才会开始,在环绕通知时只有执行到pjp.proceed();方法时, 原来的方法才算是真正的开始执行,前置通知,后置通知, 异常通知, 最终通知相当于都在或者方法里面。
package com.zqh.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhuangqinghui
* @version 1.0
* @date 2020/11/19 10:10
*/
@RestController
@RequestMapping("/demo")
public class Demo {
public String a = "111";
private static final Logger logger = LoggerFactory.getLogger(Demo.class);
@GetMapping("/aop1")
public String aop1(String msg) {
logger.info("运行了方法");
//int i = 1/0;
return msg;
}
@GetMapping("/aop2")
public String aop2(String msg) {
logger.info("运行了方法2");
//int i = 1/0;
return msg;
}
@Override
public String toString() {
return "Demo{}";
}
public String aaa() {
logger.info("hjdfhdhfhdfhdh");
return "sss";
}
}
package com.zqh.demo.aop;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zqh.demo.controller.Demo;
import com.zqh.demo.pojo.User;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
/**
* @author zhuangqinghui
* @version 1.0
* @date 2020/11/19 10:16
*/
@Aspect
@Component
public class TestAspect {
private static final Logger logger = LoggerFactory.getLogger(TestAspect.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 定义切入点,切入点为 public 返回值 com.zqh.demo.controller.Demo.方法(参数) 中的所有函数
*通过@Pointcut注解声明频繁使用的切点表达式
*/
@Pointcut(value = "execution(public * com.zqh.demo.controller.Demo.aop1(..)))")
public void BrokerAspect(){
}
/**
* 前置通知,在方法运行前通知
*/
@Before("BrokerAspect()")
//@Before("execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void doBeforeGame() {
logger.info("前置通知");
}
/**
* 最终通知, 方法运行最后通知, 不管是否产生异常,都会通知
*/
@After("BrokerAspect()")
//@After("execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void doAfterGame(){
logger.info("最终通知");
}
/**
* 后置通知, 方法运行后运行, 发生异常后不通知
*/
@AfterReturning("BrokerAspect()")
//@AfterReturning("execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void doAfterReturningGame(){
logger.info("后置通知");
}
/**
* 异常通知, 在方法发生异常时通知
*/
@AfterThrowing("BrokerAspect()")
//@AfterThrowing("execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void doAfterThrowingGame(){
logger.info("异常通知");
}
/**
* 环绕通知
*/
@Around("BrokerAspect()")
public void doAroundGame(ProceedingJoinPoint pjp) {
try {
logger.info("环绕前置通知");
pjp.proceed();
logger.info("环绕后置通知");
} catch (Throwable throwable) {
logger.info("环绕异常通知");
} finally {
logger.info("环绕最终通知");
}
}
}
AOP之JoinPoint和ProceedingJoinPoint
在之前的案例中我们执行了各种通知, 但似乎这些通知和原来的方法没有任何联系, 我们也获取不到被代理方法的任何信息, 只是两个独立的方法分前后运行. 建立通知和被代理对象关系的对象正是JoinPoint
方法名 | 返回值 | 功能 |
---|---|---|
getSignature(); | Signature | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
getArgs(); | Object[] | 获取传入目标方法的参数对象 |
getTarget(); | Object | 获取未被代理的被代理对象 |
getThis(); | Object | 获取已经被代理的被代理对象 |
getSignature()方法
这个方法可以得到被代理对象和方法的一些信息, 常用的有:
- getName(); 获取被代理的方法名.
- getDeclaringType().getSimpleName(); 获取类名
- getDeclaringTypeName(); 带有包名的类名
/**
* 前置通知,在方法运行前通知
*/
@Before("BrokerAspect()")
//@Before("execution(public * com.zqh.demo.controller.Demo.*(..)))")
public void doBeforeGame(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
logger.info("被代理方法名:{}", signature.getName());
logger.info("类名:{}", signature.getDeclaringType().getSimpleName());
logger.info("带包名类名:{}", signature.getDeclaringTypeName());
logger.info("前置通知");
}
// 从切面植入点出通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
//也可以通过这样的方式获取方法的反射对象
getArgs();
这个方法可以获取被代理方法的所有参数以Object[]的形式返回.
我们多设一个int参数
@GetMapping("/aop1")
public String aop1(String msg, Integer i) {
logger.info("运行了方法: msg={}, i={}", msg, i);
return msg;
}
/**
* 前置通知,在方法运行前通知
*/
@Before("BrokerAspect()")
public void doBeforeGame(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
logger.info("参数类型为: {}, 值为: {}", arg.getClass().getName(), arg);
}
logger.info("前置通知");
}
getTarget与getThis
getTarget和getThis有些相似, 一个是获取未被代理的, 一个是获取已被代理的. 我们简单看一下例子
- 在Demo中新建一个方法
public void printMsg(String msg) {
logger.info("printMsg方法 msg:{}", msg);
}
- 设置通知
@Pointcut(value = "execution(public * com.zqh.demo.controller.Demo.aop1(..)))")
public void BrokerAspect(){
}
@Pointcut(value = "execution(public * com.zqh.demo.controller.Demo.printMsg(..)))")
public void printMsgAspect(){
}
/**
* 前置通知,在方法运行前通知
*/
@Before("printMsgAspect()")
public void doBeforePrintMsg() {
logger.info("PrintMsg前置通知");
}
- 使用getTarget
/**
* 前置通知,在方法运行前通知
*/
@Before("BrokerAspect()")
public void doBeforeGame(JoinPoint joinPoint) {
Demo target = (Demo)joinPoint.getTarget();
target.printMsg("使用 target");
logger.info("前置通知");
}
- 运行结果如下
- 使用getThis
/**
* 前置通知,在方法运行前通知
*/
@Before("BrokerAspect()")
public void doBeforeGame(JoinPoint joinPoint) {
Demo aThis = (Demo)joinPoint.getThis();
aThis.printMsg("使用 aThis");
logger.info("前置通知");
}
- 运行结果如下
我们发现在getThis中获取的对象有使用到通知,而getTarget获得的对象没有使用到通知.
ProceedingJoinPoint 特有
proceed(); 方法, 运行方法, 也可以传入新的参数以新参数运行方法