SpringBoot如何实现AOP?
AOP面向切面编程,是Spring的一大特性,那么它的作用是什么?使用场景有哪些?我们在SpringBoot中如何使用呢?
下面我们详细的解释下上述问题,并根据实际使用场景,以SpringBoot项目为例,给大家演示下。
通常来说,AOP的作用是解耦代码中的非核心业务代码,这样说可能有点抽象,我们继续举实际例子来说明一下。
比如日志监控,这个功能虽然非核心业务代码,但是几乎我们所有的业务代码中都需要去处理。拿接口举例,我们一般会记录接口入参,回参的日志。因为如果你没有这个日志记录,后面出现接口异常了,对一些没有明确错误信息的,你就是两眼一抹黑,甚至不知道接口到底有没有调用成功,问题是在网络层还是你的代码层。
那么通常我们需要在每个接口里,加上日志记录。比如下面这样:
log.info(StrUtil.format("XXX接口入参数据:{}", dataList.stream().map(
item -> item.getA() + item.getB() + item.getC()).collect(Collectors.joining(","))
));
当然这样不是不可以,但是不够优雅,因为我们每个接口都需要写这些重复的代码,繁琐且麻烦。这时AOP就派上用场了,面向切面编程,我们写一个切面,来切入所有需要的方法中,进行统一的日志记录。从这个例子,我们可以初步认识到AOP的作用了,就是可以让我们轻松的解耦这些非核心业务代码,减少重复代码等。
那么除了日志监控,还有其它的使用场景吗?显而易见,类似日志监控这样的非核心业务代码,都可以通过AOP来优化,比如我们项目中最常使用的性能监控,权限认证等,这些方法中需要重复编写的非核心业务代码,都可以从方法中抽离出来,面向切面编程。
简单解答了上述AOP的作用和使用场景后,下面我们进入实际使用环节,具体讲讲我们在SpringBoot项目中如何使用AOP。
这里还是以最常用的日志监控为例,假如我们需要在controller层的公共接口方法中,记录请求日志和返回日志信息,如果没有切面,我们需要在入口出,进行记录。在返回结果处进行记录,比如下面这样。
log.info(StrUtil.format("XXX接口入参数据:{}", dataList.stream().map(
item -> item.getA() + item.getB() + item.getC()).collect(Collectors.joining(","))
));
//请求信息记录日志表
requestLog.insert(xx);
xxxx业务代码;
log.info(StrUtil.format("XXX接口返回数据:{}", response)
));
//返回信息记录日志表
requestLog.update(xx);
一个接口这样写还可以接受,但是我们的日志记录往往所有的public接口都需要记录,所以这一块的代码就会冗余重复。这个时候,就可以使用Aop写一个日志切面,优雅的完成日志记录这个操作。
这里,我们采用注解+表达式的方式来切入,第一步,先写一个注解@Log,代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String value() default "";
}
这个注解的作用主要是条件限制,如果需要全部切入的话,就不需要注解。再写一个核心的日志切面,代码如下:
@Aspect
@Slf4j
@Component
@Order(-1)
public class LogAop {
@Pointcut("execution(public * com.aaa.bbb.*Controller.*(..))")
private void controllerPoint() {
}
@Around("controllerPoint() && @annotation(com.aaa.bbb.annotation.Log)")
public Object logRecord(ProceedingJoinPoint pjp) throws Throwable {
RequestLog requestLog = new RequestLog();
try {
//请求日志记录
requestLog = aaa(pjp);
} catch (Exception ex) {
log.error("请求日志保存失败:" + ex.getMessage());
}
//调用目标方法执行
Object response = pjp.proceed();
try {
//响应日志记录
bbb(requestLog.getId(), JSON.toJSONString(response));
} catch (Exception ex) {
log.error("响应日志保存失败:" + ex.getMessage());
}
return response;
}
}
我们大体看一下思路,这里切面方法上用@Around注解实现环绕切入,也可以根据需要使用@Before等注解实现。注解包括两个条件,一是所有controller下的public修饰的公共方法,二是方法上有注解@Log修饰的。只要满足上述条件,那么就会先执行我们的切面方法logRecord。
logRecord方法中,我们先记录请求的日志信息,比如请求地址,请求参数,有用的header,请求ip等信息,存入日志记录表。然后调用目标方法执行。最后记录业务方法的返回结果。这样一个记录日志的切面方法框架就算完成了。当然这里面具体细节很多,一不小心就踩坑了。我们这里挑几个应该注意的重点讲讲。
首先,一个大原则就是我们的日志切面,不应该影响到正常业务方法的执行。比如我们第一步,请求日志记录时,如果发生异常,不能影响到pjp.proceed()调用业务方法执行,不然因为我们的日志记录失败,导致业务方法没有执行,就离谱了。所以我们在第一个aaa(pjp)记录日志方法上,用了try-catch来捕获异常处理,即使日志没记录成功,那也没事,会继续执行业务方法。
同理,返回结果记录也是,不能因为返回结果日志记录失败,导致业务接口无法正常返回。所以我们在 bbb(requestLog.getId(), JSON.toJSONString(response))上也用了try-catch单独处理异常,即使发生日志结果记录异常,还是可以正常return response。
那为什么要分开两段try-catch处理呢?不能简单点,直接放在同一个try-catch里吗?答案当然是不行,这里也是有坑的,因为如果我们都放在一个try-catch中,就会连业务方法的异常进行捕获。比如pjp.proceed()时,业务方法报错,我们是不应该处理的,要将业务异常继续抛出,交由对应的异常处理方法进行处理。
所以,一定要注意,不能因为切面方法的执行,影响到原来的业务方法执行。
另外,详细讲讲具体的请求日志记录情况。在请求日志记录时,一个最主要的内容就是入参记录。通常我们想到的就是通过request.getInputStream()来获取body流,最后转为string存储。这里有个坑要注意,因为是流,只能读取一次。我们在切面中读取了,后面的其它方法就无法读取到了。
这里也是推荐两种处理方法,一种是自己写一个类继承HttpServletRequestWrapper,在里面重写getInputStream等方法,读取后再将body放进去返回。这种方式麻烦些,但是可操作性强,比如我们在处理一些加密传输接口时,进行解密操作后,返回的body也直接放解密后的数据,这样下游方法在使用时,就不用重复解密了。
另外一种方式更简单明了,也足以满足普通接口记录参数日志的需求。就是通过pjp.getArgs(),直接获取参数数组。
总结一下,这里容易踩坑的地方。就是获取参数时,如果直接通过request请求获取输入流,只能读取一次。要么自己重写方法处理,要么使用getArgs()方法获取。
另外,还有一些小技巧,大家根据自己需求和业务场景来具体扩展切面方法。比如有时请求的日志需要加一个追踪标记,可以在第一步存储时通过MDC来放进去一个uuid等。还可以加一下接口重复请求校验等,都可以通过Aop切面方法来实现。
综上,Aop的核心思想大家应该已经初步了解了,它的使用场景还有很多,比如项目需要鉴权或者加解密或者做统一的入参校验等操作,都可以通过Aop切面来处理。具体的使用,就需要大家在项目中慢慢熟悉了。
感谢大家看到这,我是创作者卡兰,热衷用简单易懂的小文章,分享总结自己的技术经验,欢迎大家关注、点赞和收藏。
转载自:https://juejin.cn/post/7209839961440354360