一、概述
实现AOP的切面主要有以下几个要素:
使用@Aspect注解将一个java类定义为切面类
使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。
根据需要在切入点不同位置的切入内容
参考:
@Pointcut()的execution、@annotation等参数说明
https://blog.csdn.net/java_green_hand0909/article/details/90238242
@pointcut execution表达式_@Pointcut 的 12 种用法,你知道几种?
https://blog.csdn.net/weixin_39681171/article/details/113039439
二、简单案例
1、定义切入点
@Configuration
@Aspect
public class UserInfoAspect {
//在事件通知类型中申明returning即可获取返回值
@AfterReturning(returning = "data", pointcut = "execution(* com.scy.controller.*.*(..))")
public Object afterReturning(JoinPoint joinPoint, Object data) {
log.info("返回数据 - {}", data);
return data;
}
}
2、将方法定义为切入点
@Configuration
@Aspect
public class UserInfoAspect {
@Pointcut("execution(* com.*.test(*))")
public void test() {}
//使用JoinPoint 对象可以接收到切入点方法的参数
@AfterReturning(value = "test()")
public void logMethodCall(JoinPoint jp) throws Throwable {
System.out.println("进入后置增强了!");
String name = jp.getSignature().getName();
System.out.println(name);
Object[] args = jp.getArgs();
for (Object arg : args) {
System.out.println("参数:" + arg);
}
}
}
3、多个切入点
如果你想同时拥有多个切入点的话,可以使用逻辑操作符 “&&
”,“||
”等,如下所示:
@Pointcut("execution(* com.*.(*))")
public void addUser() {}
@Pointcut("execution(* com.*.(*))")
public void updateUser() {}
@After(value = "addUser() || updateUser()", returning="returnValue")
public void pushAccountInfo(JoinPoint jp, Object returnValue){
//这里写切面逻辑:
}
三、切入点执行顺序
使用@Before在切入点开始处切入内容
使用@After在切入点结尾处切入内容
使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
try{
try{
doBefore();//对应@Before注解的方法切面逻辑
method.invoke();
}finally{
doAfter();//对应@After注解的方法切面逻辑
}
doAfterReturning();//对应@AfterReturning注解的方法切面逻辑
}catch(Exception e){
doAfterThrowing();//对应@AfterThrowing注解的方法切面逻辑
}
四、SpringBoot日志案例
1、环境准备
需要引入aop依赖以及测试环境(主要是为了方便测试)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2、方法一(环绕增强)
这里通过为方法加上注解的形式来实现日志记录开关。当然,通过切面表达式直接匹配方法实现也可以。
(1)注解设计
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogInfo {
String value();
}
注解只包含一个基本的元素,用于记录方法的用途。
这里需要注意的是注解的保留策略,由于我们需要在运行时获取注解的内容,所以注解的保留策略需要为RetentionPolicy.RUNTIME
(2)环绕实现
@Component
@Aspect
public class LogAspect {
@Pointcut("@annotation(cn.xuhuanfeng.annnotation.LogInfo)")
public void logPointCut(){}
@Around(value = "logPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) {
// 获取执行方法签名,这里强转为MethodSignature
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
// 通过反射来获取注解内容
LogInfo loginfo = signature.getMethod().getAnnotation(LogInfo.class);
String description = loginfo.value();
System.out.println("****** "+ description + " Before: " + signature + " args " + Arrays.toString(args));
Object result = null;
long start = System.currentTimeMillis();
try {
// 调用原来的方法
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("****** " + description +" After: " + signature + " " + result + "total time: " + (end - start) + " ms");
return result;
}
}
(3)操作类
@Service
public class BlogService {
@LogInfo("获取博客")
public String getBlog() {
return "Blog info";
}
@LogInfo("增加博客")
public boolean addBlog(String blog) {
System.out.println("adding blog");
return true;
}
}
(4)测试类
@SpringBootTest
@RunWith(SpringRunner.class)
public class BlogServiceTest {
@Autowired
private BlogService blogService;
@Test
public void getBlog() {
blogService.getBlog();
}
@Test
public void addBlog() {
blogService.addBlog("blog");
}
}
(5)运行结果
****** 增加博客 Before: boolean cn.xuhuanfeng.blog.BlogService.addBlog(String) args [blog]
adding blog
****** 增加博客 After: boolean cn.xuhuanfeng.blog.BlogService.addBlog(String) truetotal time: 4 ms
****** 获取博客 Before: String cn.xuhuanfeng.blog.BlogService.getBlog() args []
****** 获取博客 After: String cn.xuhuanfeng.blog.BlogService.getBlog() Blog infototal time: 0 ms
可以看到,我们所需要的功能已经成功通过AOP中的环绕实现了,既保持了代码的整洁性,以及模块性,又基本实现了功能
3、方法二(前置后置)
除了上面环绕
实现外,还可以通过前置以及后置增强
来实现。测试类以及执行结果同上。
@Component
@Aspect
public class LogAspect {
@Pointcut("@annotation(cn.xuhuanfeng.annnotation.LogInfo)")
public void logPointCut(){}
// 通过ThreadLocal来记录进入方法的时间
private ThreadLocal<Long> logRecorder = new ThreadLocal<>();
private String getDescription(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
return methodSignature.getMethod().getAnnotation(LogInfo.class).value();
}
private long calculateExecutionTime() {
long end = System.currentTimeMillis();
long duration = end - logRecorder.get();
logRecorder.remove();
return duration;
}
@Before("logPointCut()")
public void beforeMethod(JoinPoint joinPoint) {
logRecorder.set(System.currentTimeMillis());
String description = getDescription(joinPoint);
System.out.println("****** "+ description +
" Before: " + joinPoint.getSignature() +
" args " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(value = "logPointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String description = getDescription(joinPoint);
System.out.println("****** " + description +
" After: " + joinPoint.getSignature() +
" " + result +
" total time: " + calculateExecutionTime() + " ms");
}
/**
* @description 在连接点执行之后执行的通知(异常通知)
*/
@AfterThrowing(pointcut = "logAspect()", throwing = "ex")
public void doAfterThrowing(JoinPoint point, Exception ex) {
//异常处理
}
}
注意:
使用方法二,当方法抛出异常的时候,@AfterReturing
是不会执行的。
原因在于,@AfterReturing
是在方法调用结束,返回之前进行织入的,所以一旦抛出异常,就无法处理了。这时候有两种解决方案。
- 使用
@After
,@After
是在方法调用结束之后织入的,所以可以正常记录。 - 使用**
@AfterThrowing
**在异常抛出的时候进行处理。
4、参考
AOP实战小案例
https://blog.csdn.net/xuhuanfeng232/article/details/81298198AOP基于注解环绕通知
https://blog.csdn.net/jiemaio/article/details/90048136Spring Boot 使用AOP(环绕通知)完成对用户操作的日志记录
https://blog.csdn.net/qq_33576276/article/details/88786090
五、常用方法
1、JoinPoint
提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
package org.aspectj.lang;
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
Object getThis(); //返回AOP代理对象
Object getTarget(); //返回目标对象
Object[] getArgs(); //返回被通知方法参数列表
Signature getSignature(); //返回当前连接点签名
SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
String getKind(); //连接点类型
StaticPart getStaticPart(); //返回连接点静态部分
}
2、proceed()
用于环绕通知,使用proceed()方法来执行目标方法
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
3、StaticPart
- 访问连接点的静态部分,如被通知方法签名、连接点类型等
public interface StaticPart {
Signature getSignature(); //返回当前连接点签名
String getKind(); //连接点类型
int getId(); //唯一标识
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
}
特别说明:JoinPoint 必须是第一个参数