前言
在SpringBoot中使用自定义注解、aop切面打印web请求日志。主要是想把controller的每个request请求日志收集起来,调用接口、执行时间、返回值这几个重要的信息存储到数据库里,然后可以使用火焰图统计接口调用时长,平均响应时长,以便于我们对接口的调用和执行情况及时掌握。添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.5</version> </dependency>
自定义注解
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface WebLogger { String value() default ""; }
AOP定义切面、切点
在实现切面之前先要了解几个aop的注解:
- @Aspect、
- @Pointcut 定义切点,后面跟随一个表达式,表达式可以是一个注解,也可以具体到方法级别。
- @Before
- @After
- @Around
实现切面,在before方法里统计request请求相关参数,在around方法里计算接口调用时间。这里统计请求的参数时有个bug,joinpoint.getArgs返回值是一个Object数组,但是数组里只有参数值,没有参数名。还没找到解决办法。
@Aspect @Component public class WebLoggerAspect { @Pointcut("@annotation(com.zhangfei.anno.WebLogger)" public void log() {} @Around("log()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { long startTime=System.currentTimeMillis(); Object result=joinPoint.proceed(); System.out.println("Response:"+new Gson().toJson(result)); System.out.println("耗时:"+(System.currentTimeMillis()-startTime)); return result; } @Before("log()") public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); System.out.println("==================Start================="); System.out.println("URL:" + request.getRequestURL().toString()); System.out.println("Description:" + getLogValue(joinPoint)); System.out.println("Method:" + request.getMethod().toString()); //打印controller全路径及method System.out.println("Class Method:" + joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName()); System.out.println("客户端IP:" + request.getRemoteAddr()); System.out.println("请求参数:" + new Gson().toJson(joinPoint.getArgs())); } private String getLogValue(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); WebLogger webLogger = method.getAnnotation(WebLogger.class); return webLogger.value(); } @After("log()") public void doAfter() { System.out.println("==================End================="); } }
怎么使用注解?
这里就直接在你的web请求方法上直接添加WebLogger注解
@Controller @RequestMapping("/student") public class StudentController { private final Logger logger= LoggerFactory.getLogger(StudentController.class); @GetMapping("/list") @WebLogger("学生列表") public @ResponseBody List<Student> list(){ ArrayList<Student> list=new ArrayList<>(); Student student0=new Student(1,"kobe",30); Student student1=new Student(2,"james",30); Student student2=new Student(3,"rose",30); list.add(student0); list.add(student1); list.add(student2); return list; } @WebLogger("学生实体") @RequestMapping("/detail") public @ResponseBody Student student(int id){ return new Student(1,"kobe",30); } }
切面日志输出效果
总结
从头条上一位朋友文章评论上看到有人提出,在分布式部署的环境中,出现高并发时日志打印会出现错乱的情况,这里还需要把线程id或者声明一个guid, 存入ThreadLoal中统计使用。当然更多的人简历还是使用ELK等分布式日志解决方法,好吧,这些还有待于自己部署环境去尝试。