简介
在日常的项目中,我们经常要对controller层(也就是API层)记录各式各样的日志。比如请求参数、响应参数、耗时等信息。这些相关的日志代码其实是重复的,且繁琐的。
那么我们到底有没有办法把它们统一起来,让我们的代码更整洁呢?答案是有的。这里我们可以利用Spring AOP来获取request、response并进行日志输出打印。
但仅仅是使用AOP的话,我们也需要每一个包每一个方法配置一个切面来满足我的需求。所以我们还需要自定义一个注解,然后让切面只切到这个注解上。然后在我们需要记录日志的方法中使用这个注解。这样就可以一劳永逸了。
改造
这里延续我之前一直做的脚手架为基础,如果有需要我可以在文末克隆代码。当然代码是独立,你们也可以类推到你们的代码种。
由于SpringBoot自身是不具有AOP依赖的。所以我们首先要在pom.xml里面引入AOP依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后创建一个叫annotation的包。这个包是专门用来存放我们自定义的注解类。
创建一个名为AutoLog的注解类。这里下面的参数只是暂定的,为了方便以后系统扩展而已。如果不需要的话可加可不加。
/*
* 日志配置类注解,下面参数为了以后扩展暂定,非必须
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {
/**
* 日志内容
*/
String value() default "";
/**
* 日志类型
*
* @return 0:操作日志;1:登录日志;2:定时任务;
*/
int logType() default 0;
/**
* 操作日志类型
*
* @return (1查询,2添加,3修改,4删除)
*/
int operateType() default 0;
}
然后创建一个aspect包,这个包是用于存放我们的aop类的。在创建一个LogAspect类。这个类里面就是我们的切面配置。
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(LogAspect.class);
private StopWatch stopWatch;
// 创建切点
@Pointcut("@annotation(com.turkeymz.baseboot.annotation.AutoLog)")
public void logPointCut() {
}
// 开始切面逻辑
@Before(value = "logPointCut()")
public void before(JoinPoint joinPoint) {
stopWatch = new StopWatch();
stopWatch.start();
// 获取方法相关
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取请求
HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
//获取请求参数
String requestBody = this.getParams(joinPoint,signature);
logger.info("请求信息:{}, 请求类型:{}, IP:{},请求方法:{}", request.getRequestURL(), request.getMethod(),
request.getRemoteAddr(), signature.getDeclaringTypeName());
logger.info("请求参数:{}", requestBody);
}
// 结束切面逻辑
@AfterReturning(value = "logPointCut()", returning = "apiResultBody")
public void after(APIResultBody apiResultBody) {
stopWatch.stop();
logger.info("响应参数:{}",apiResultBody.toString());
logger.info("耗时: {} ms",stopWatch.getTotalTimeMillis());
}
// 异常切面逻辑
@AfterThrowing(value = "logPointCut()", throwing = "exception")
public void doAfterThrowing(Exception exception) {
stopWatch.stop();
// 保存异常日志记录
logger.info("请求异常: {}", exception.getMessage());
logger.info("发生异常时间: {}", LocalDateTime.now());
logger.info("耗时: {} ms",stopWatch.getTotalTimeMillis());
}
private String getParams(JoinPoint joinPoint,MethodSignature signature){
StringBuffer requestBody = new StringBuffer();
// 获取请求参数
String[] paramNames = ((MethodSignature) signature).getParameterNames();
Object[] paramValues = joinPoint.getArgs();
int paramLength = null == paramNames ? 0 : paramNames.length;
if (paramLength == 0) {
requestBody.append("{}");
} else {
requestBody.append("[");
for (int i = 0; i < paramLength - 1; i++) {
requestBody.append(paramNames[i]).append("=").append(paramValues[i]).append(", ");
}
requestBody.append(paramNames[paramLength - 1]).append("=").append(paramValues[paramLength - 1]).append("]");
}
return requestBody.toString();
}
}
最后在我们的controller方法上面添加我们自定义的注解(@AutoLog)就可以实现自动log配置了。
@ApiOperation("测试普通API通信")
@ApiImplicitParams({
@ApiImplicitParam(paramType="path",name="name",dataType="String",required=true,value="名字",defaultValue="tom")
})
@GetMapping("/hello/{name}/{action}")
@AutoLog(value = "sys say")
public APIResultBody sayHello(@PathVariable("name") String name,@PathVariable("action") String action) throws Exception{
String result = "Hello " + name + ", here is base boot.Are you sure to "+ action;
if("error".equals(name)){
throw new SysException(HttpExceptionCode.SERVER_BUSY);
}
Thread.sleep(1000);
logger.info("方法在执行的过程中: {}",action);
return APIResultBody.success(result);
}
结果
正常 | 异常 |
---|---|
这里简单的列一下AOP的几个相关概念。因为上面我只想传播一种用AOP+注解的方式实现log记录。但这种方式其实有很多条路径可以选择,大家可以根据AOP的特性适当选择最适合自己项目的来改。
- Aspect(切面):声明类似于Java中的类声明,在Aspect中会包含一些Pointcut及相应的Advice。
- Joint point(连接点):表示在程序中明确定义的点。包括方法的调用、对类成员的访问等。
- Pointcut(切入点):表示一个组Joint point,如方法名、参数类型、返回类型等等。
- Advice(通知):Advice定义了在Pointcut里面定义的程序点具体要做的操作,它通过(before、around、after(return、throw)、finally来区别实在每个Joint point之前、之后还是执行 前后要调用的代码。
- Before:在执行方法前调用Advice,比如请求接口之前的登录验证。
- Around:在执行方法前后调用Advice,这是最常用的方法。
- After:在执行方法后调用Advice,after、return是方法正常返回后调用,after\throw是方法抛出异常后调用。
- Finally:方法调用后执行Advice,无论是否抛出异常还是正常返回。
- AOP proxy:AOP proxy也是Java对象,是由AOP框架创建,用来完成上述动作,AOP对象通常可以通过JDK dynamic proxy完成,或者使用CGLIb完成。
- Weaving:实现上述切面编程的代码织入,可以在编译时刻,也可以在运行时刻,Spring和其它大多数Java框架都是在运行时刻生成代理。
附录
课题目录:https://blog.csdn.net/turkeym4/article/details/106761043
项目地址:https://gitee.com/turkeymz/baseboot