Spring Boot使用AOP打印接口请求日志
Spring的两大特性 IOC(自动注入)和AOP(面向切面)本文讲的是使用AOP面向切面的特行,在用户访问系统时候,根据其访问的控制层方法 打印对应的请求日志,例如:客户端浏览器型号,电脑系统型号, 方法执行时间,请求参数等等。。。。。。。
1.理解AOP
(1)AOP
就是在某一个类或方法执行前后打个标记,声明在执行到这里之前要先执行什么,执行完这里之后要接着执行什么。
这就对应着AOP的一些专业术语以及使用场景:
Aspect
(切面): 声明类似于Java中的类声明,在Aspect中会包含一些Pointcut及相应的Advice。Joint point
(连接点): 表示在程序中明确定义的点。包括方法的调用、对类成员的访问等。Pointcut
(切入点): 表示一个组Joint point,如方法名、参数类型、返回类型等等。Advice
(通知): Advice定义了在Pointcut里面定义的程序点具体要做的操作,它通过(before、around、After
: 在执行方法后调用Advice,after、return是方法正常返回后调用,after\throw是方法抛出异常后调用。Before
: 在执行方法前调用Advice,比如请求接口之前的登录验证。Around
: 在执行方法前后调用Advice,这是最常用的方法。Finally
: 方法调用后执行Advice,无论是否抛出异常还是正常返回。
(2)AOP
再白话理解就是在项目原基础功能之上,使用AOP添加新功能 ,不会更改原来代码 例如充值 之前代码是没有充值成功发送短信这个功能的,现在有了这个发送短信的需求,在不改动代码,或者所有的充值接口统一处理,那么我们就可以在所有类似充值的控制访问层使用Aop,充值成功后发送一条短信。。。。。。
2.本文使用
本文使用的是注解式AOP
所需依赖
<!-- web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok代码简化插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Aop组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 分析客户端信息的工具类-->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.20</version>
</dependency>
基础使用演示
package com.leilei.web.controller.common;
import eu.bitwalker.useragentutils.UserAgent;
import java.time.LocalDateTime;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author leilei
* @version 1.0
* @date 2020/01/20 19:21
* @desc: 日志切面 打印请求日志
*/
@Slf4j //lombok中日志注解
@Aspect //表明是一个切面类
@Component //交给Spring管理
public class LogAop {
/** 进入方法时间戳 */
private Long startTime;
/** 方法结束时间戳(计时) */
private Long endTime;
public LogAop() {}
/** 自定义切点 */
private final String POINTCUT = "execution(* com.leilei.web.controller..*(..))";
/**
* 前置通知,方法之前执行
* @param joinPoint
*/
@Before(POINTCUT)
public void doBefore(JoinPoint joinPoint) {
// 获取当前的HttpServletRequest对象
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求头中的User-Agent
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
// 打印请求的内容
startTime = System.currentTimeMillis();
log.info("请求开始时间:{}", LocalDateTime.now());
log.info("请求Url : {}", request.getRequestURL().toString());
log.info("请求方式 : {}", request.getMethod());
log.info("请求ip : {}", request.getRemoteAddr());
log.info("请求内容类型 : {}", request.getContentType());
log.info("请求参数 : {}", Arrays.toString(joinPoint.getArgs()));
// 系统信息
log.info("浏览器 : {}", userAgent.getBrowser().toString());
log.info("浏览器版本 : {}", userAgent.getBrowserVersion());
log.info("操作系统: {}", userAgent.getOperatingSystem().toString());
}
@After(POINTCUT)
public void doAfter(JoinPoint joinPoint) {
// System.out.println("doAfter");
}
/**
* 返回通知 正常结束时进入此方法
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = POINTCUT)
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("请求结束时间 : {}", LocalDateTime.now());
log.info("请求耗时 : {}", (endTime - startTime));
// 处理完请求,返回内容
log.info("请求返回 : {}", ret);
}
/**
* 异常通知: 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
*
* @param throwable
*/
@AfterThrowing(pointcut = POINTCUT, throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
// 保存异常日志记录
log.error("发生异常时间 : {}", LocalDateTime.now());
log.error("抛出异常 : {}", throwable.getMessage());
}
}
com.leilei.web.controller
为我自己项目定义的切点路径 那么当用户访问此路径下所有Controller 以及之中的方法的时候,就是执行我LogAop 切点类之中的各个切点方法
@Before
和@AfterReturning
这部分的代码也可使用一个环绕通知@Around
来替换
@Around(POINTCUT)
public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取当前的HttpServletRequest对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//获取请求头中的User-Agent
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
//打印请求的内容
startTime = System.currentTimeMillis();
// 请求信息
// 获取请求类名和方法名称
Signature signature = joinPoint.getSignature();
// 获取真实的方法对象
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
log.info("请求开始时间:{}" , LocalDateTime.now());
log.info("请求Url : {}" , request.getRequestURL().toString());
log.info("请求方式 : {}" , request.getMethod());
log.info("请求ip : {}" , request.getRemoteAddr());
log.info("请求内容类型 : {}", request.getContentType());
log.info("请求参数 : {}" , Arrays.toString(joinPoint.getArgs()));
// 系统信息
log.info("浏览器 : {}", userAgent.getBrowser().toString());
log.info("浏览器版本 : {}", userAgent.getBrowserVersion());
log.info("操作系统: {}", userAgent.getOperatingSystem().toString());
// joinPoint.proceed():当我们执行完切面代码之后,还有继续处理业务相关的代码。proceed()方法会继续执行业务代码,并且其返回值,就是业务处理完成之后的返回值。
Object result = joinPoint.proceed();
log.info("请求结束时间:"+ LocalDateTime.now());
log.info("请求耗时:{}" , (System.currentTimeMillis() - startTime));
// 处理完请求,返回内容
log.info("请求返回 : " , result);
return result;
}
3.测试
我请求项目com.leilei.web.controller
路径下的一个方法,然后再IDEA中查看是否打印日志
那么,本文到这里就结束了。。。。。。