常见的小需求
在我们的后端项目中有很多要调用第三方接口的地方,而调用接口就免不了会因为传递给接口的参数有问题报错,或者对接口的返回值处理不全导致报错或后续的流程有问题。
对于调试接口通常的做法就是把入参用接口工具向接口地址提交,然后把获取到的返回值和项目中的返回值进行比对。这样来看,在代码中调用接口的入参和调用接口后的返回值对于排错来说就非常重要了。那这样的话,我们可以在每个调用接口地址的前后使用输出日志的方式来记录,就可以得到调用接口的入参和接口的返回值,从而有利于我们以后的调试了。
我们可以使用 SLF4J 或者 LogBack 等日志框架,在调用接口时来输出一下入参和返回值,大致方法是在调用接口前调用 logger.info 输出入参,然后调用接口后再次调用 logger.info 输出返回值。这样的方式虽然没有问题,但是在每个接口调用前后都要加这样的日志输出代码显得过于麻烦,且不优雅。
简单的解决方法
在 Spring 框架中为我们提供了 AOP,即面向切面编程。AOP 通过动态代理来管理切面环境,通过反射可以使我们在非侵入的方式下为我们增加前置、后置等方法用来贯穿整个代码层面,从而让我们更加关注业务本身的开发。在 Spring 中的事务就是通过 AOP 来进行管理的,我们这里通过 AOP 完成一个接口调用时打印入参和返回值的功能。
AOP 有一些名词需要理解,但是不理解这些名词好像又不影响我们实际 AOP 的使用。这些名词包括,切面、通知、引入、切点、连接点和织入。这里我们不讨论这些名词,直接上代码来进行演示。
代码演示
首先引入依赖,依赖如下:
代码语言:javascript复制<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
然后,我们来定义一个切面,所谓的切面不过是一个被 @Aspect 注解修饰的类,在类中可以定义一些通知,通知通常包含(前置通知、后置通知、返回通知、异常通知等) ,这里我们定义了前置通知和返回通知。通知是针对切点进行的,切点是用于针对的具体的方法,我们切面类如下:
代码语言:javascript复制@Aspect
@Component
@Slf4j
public class TestAspect {
@Pointcut("execution(public * io.coderup2u.xxx.util.yyy.*.*(..))")
public void controllerMethod() {}
@Before("controllerMethod()")
public void before(JoinPoint joinPoint) {
}
@AfterReturning(value = "controllerMethod()", returning = "methodResult")
public void afterReturning(JoinPoint joinPoint, Object methodResult) {
}
}
在上面的代码中,使用 @Before 和 @AfterReturning 定义了两个通知,分别是前置通知和返回通知。使用 @Pointcut 定义了一个切点,通过 execution 的正则表达式来确定一个连接点(所谓的连接点就是我们实际的业务类),这里 execution 正则表达式的意思是,当执行 io.coderup2u.xxx.util.yyy 下的所有方法时,都会执行切面中的前置通知和返回通知。前置方法是在业务方法执行前被执行,返回通知是在业务方法执行后且没有异常时执行。
在 before 和 afterReturning 方法中都有一个 JoinPoint 类型的参数,通过该参数可以得到被执行具体方法的名称以及参数,afterReturning 方法的 methodResult 参数可以得到方法执行后的返回值。具体代码如下:
代码语言:javascript复制@Pointcut("execution(public * io.coderup2u.xxx.util.yyy.*.*(..))")
public void controllerMethod() {}
@Before("controllerMethod()")
public void before(JoinPoint joinPoint)
{
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
long tid = Thread.currentThread().getId();
log.info("===> TID:{} => 准备调用 {} 方法", tid, method.getName());
if (joinPoint.getArgs().length == 0) {
return ;
}
log.info("===> TID:{} => 它的参数如下:", tid);
for (int i = 0; i < joinPoint.getArgs().length; i ) {
Object arg = joinPoint.getArgs()[i];
log.info("===> TID:{} => 第 {} 个参数是:{}", tid, i 1, arg.toString());
}
}
@AfterReturning(value = "controllerMethod()", returning = "methodResult")
public void afterReturning(JoinPoint joinPoint, Object methodResult)
{
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
long tid = Thread.currentThread().getId();
log.info("<=== TID:{} => 方法: {} 的返回值为: {}", tid, method.getName(), methodResult.toString());
}
最后调用一个接口,来看下 AOP 记录的日志,日志如下:
代码语言:javascript复制===> TID:40 => 准备调用 getAccessToken 方法
<=== TID:40 => 方法: getAccessToken 的返回值为: {"code":0,"data":{"accessToken":"xxxxxxxxxxxx","expiresIn":7200},"message":"成功"}
可以看到上面的输出,帮我们输出了线程的 ID,也输出了调用的方法名和方法的返回结果。