文章目录
1、背景
最近在做 SpringBoot 的项目,里面需要记录 Controller 和 Feign 的请求响应日志到数据库。
AOP 用到的技术是动态代理,SpringBoot 的 AOP 主要是基于 aspectj 技术。
动态代理技术相关的知识可以参考:https://cloud.tencent.com/developer/article/1461796
SpringBoot AOP 处理类共有几个要素,分别是:
<!--引入AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
下面跟随我一起实践一下AOP的强大功能。
2、单切:记录Controller日志
代码:https://github.com/leexiangg/aop.git
2.1、切入点配置
对 controller 包下面所有类的所有方法做切入
private final String pointcut = "execution(* com.limouren.aop.controller..*(..))";
@Pointcut(value = pointcut)
public void log() {}
2.2、方法执行前
可以通过 JoinPoint 获取类和方法的信息,通过 Request 获取请求信息。
@Before(value = "log()")
public void before(JoinPoint joinPoint) throws Throwable {
msgLog = new MsgLog();
try {
// 请求时间
msgLog.setReqTime(new Date());
// 请求数据头
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 请求类型
msgLog.setReqType(request.getMethod());
// 请求URL
msgLog.setReqUrl(request.getRequestURI());
// ... 其他赋值部分省略
// 请求IP
msgLog.setReqIp(getIpAddress(request));
// 请求数据
String reqmsg = JSON.toJSON(joinPoint.getArgs()).toString();
msgLog.setReqMsg(reqmsg);
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClassName()).info(msgLog.getReqIp() + " " + msgLog.getReqType() + "请求 " + msgLog.getReqUrl() + ",请求信息:" + reqmsg);
} catch (Exception e) {
e.printStackTrace();
}
}
2.3、方法执行后
可以获取到方法执行成功的返回信息
@AfterReturning(returning = "result", pointcut = "log()")
public Object afterReturn(Object result) {
try {
if(msgLog != null) {
if(result != null && result instanceof ResponseEntity) {
if(HttpStatus.OK.equals(((ResponseEntity) result).getStatusCode())) {
msgLog.setRspStatus("成功");
} else {
msgLog.setRspStatus("失败");
}
}
// 耗时
if(msgLog.getReqTime() != null)
msgLog.setUseTime(msgLog.getRspTime().getTime() - msgLog.getReqTime().getTime());
}
// 保存到数据库
if(msgLog != null) {
try {
asyncInsertMsgLog.addMsgLog(msgLog);
} catch (Exception e) {}
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClassName()).info("交易耗时" + msgLog.getUseTime() + "毫秒,响应信息:" + rspmsg);}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
2.4、方法执行异常时
方法执行异常时,可以获取到异常信息,可以做全局异常处理
@AfterThrowing(pointcut = "log()",throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception){
try {
if(msgLog != null) {
// 响应时间
msgLog.setRspTime(new Date());
msgLog.setErrMsg(exception.getMessage());
msgLog.setRspStatus("失败");
// 耗时
if(msgLog.getReqTime() != null)
msgLog.setUseTime(msgLog.getRspTime().getTime() - msgLog.getReqTime().getTime());
}
// 保存到数据库
if(msgLog != null) {
try {
asyncInsertMsgLog.addMsgLog(msgLog);
} catch (Exception e) {}
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClassName()).info("处理异常,交易耗时" + msgLog.getUseTime() + "毫秒,异常信息:" + exception.getMessage());
}
} catch (Exception e) {}
}
3、双切:记录Feign日志
代码:https://github.com/leexiangg/aop.git
3.1、切入点配置
切点一:Feign中的ribbon.LoadBalancerFeignClient调用入口,为了获取请求Request
切点二:FeignClient接口,为了获取响应字符串
// 切点一:Feign中的ribbon.LoadBalancerFeignClient调用入口,为了获取请求Request
private final String pointcutRibbon = "execution(* org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(..))";
// 切点二:FeignClient接口,为了获取响应字符串
private final String pointcutFeign = "execution(* com.limouren.aop.feign..*(..))";
// Feign中的Ribbon切点
@Pointcut(value = pointcutRibbon)
public void logRibbon() {}
// Feign接口切点
@Pointcut(value = pointcutFeign)
public void logFeign() {}
3.2、在调用FeignClient接口前
在调用FeignClient接口前,获取类和方法信息
@Before(value = "logFeign()")
public void beforeFeign(JoinPoint joinPoint) {
try {
msgLog = new MsgLog();
// 请求时间
msgLog.setReqTime(new Date());
// 请求方 本系统
msgLog.setReqFrom(SystemEnum.SYSTEM_SELF.getCode());
// 被请求方 外系统
msgLog.setReqTo(SystemEnum.SYSTEM_OTHER.getCode());
// 调用类
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
msgLog.setClassName(methodSignature.getDeclaringTypeName());
// 调用方法
msgLog.setClassFunction(methodSignature.getMethod().getName());
// 请求数据
String reqmsg = JSON.toJSONString(joinPoint.getArgs());
msgLog.setReqMsg(reqmsg);
} catch (Exception e) {
e.printStackTrace();
}
}
3.3、在调用LoadBalancerFeignClient中的execute方法前
在调用LoadBalancerFeignClient中的execute方法前,获取请求信息
@Before(value = "logRibbon()")
public void beforeRibbon(JoinPoint joinPoint) {
try {
// 请求数据头
Request request = (Request) joinPoint.getArgs()[0];
// 请求类型
msgLog.setReqType(request.method());
// 请求URL
msgLog.setReqUrl(request.url());
// 请求模块
String[] split = request.url().split("/");
if(split.length > 2) {
// 请求模块
msgLog.setModel(split[split.length - 2]);
// 请求方法
String[] methods = split[split.length - 1].split("[?]");
msgLog.setFunction(methods[0]);
}
// 请求数据头信息
msgLog.setReqHeader(JSON.toJSONString(request.headers()));
// 请求IP
msgLog.setReqIp("0.0.0.0");
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClassName()).info("调用接口平台" + msgLog.getReqType() + "请求 " + msgLog.getReqUrl() + ",请求信息:" + msgLog.getReqMsg());
} catch (Exception e) {
e.printStackTrace();
}
}
3.4、在FeignClient接口响应完成后
在FeignClient接口响应完成后,获取响应内容
@AfterReturning(returning = "result", pointcut = "logFeign()")
public Object afterReturn(Object result) {
try {
String rspmsg = null;
if(msgLog != null) {
// 响应时间
msgLog.setRspTime(new Date());
// 响应内容
if(result != null) {
if(result instanceof String)
rspmsg = result.toString();
else
rspmsg = JSON.toJSONString(result);
msgLog.setRspMsg(rspmsg);
}
// 只要调用成功,就算成功,不管业务逻辑
msgLog.setRspStatus("成功");
// 耗时
if(msgLog.getReqTime() != null)
msgLog.setUseTime(msgLog.getRspTime().getTime() - msgLog.getReqTime().getTime());
// 保存到数据库
asyncInsertMsgLog.addMsgLog(msgLog);
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClass()).info("交易耗时" + msgLog.getUseTime() + "毫秒,响应信息:" + rspmsg);
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
3.5、在LoadBalancerFeignClient中的execute方法抛出异常
在LoadBalancerFeignClient中的execute方法中,如果报错,则记录调用失败
@AfterThrowing(pointcut = "logRibbon()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception){
try {
if(msgLog != null) {
// 响应时间
msgLog.setRspTime(new Date());
msgLog.setErrMsg(exception.getMessage());
msgLog.setRspStatus("失败");
// 耗时
if(msgLog.getReqTime() != null)
msgLog.setUseTime(msgLog.getRspTime().getTime() - msgLog.getReqTime().getTime());
// 记录到数据库
try {
asyncInsertMsgLog.addMsgLog(msgLog);
} catch (Exception e) {}
// 打印文件日志
LoggerFactory.getLogger(msgLog.getClassName()).info("处理异常,交易耗时" + msgLog.getUseTime() + "毫秒,异常信息:" + exception.getMessage());
}
} catch (Exception e) {}
}
4、异步记录数据库
使用生产者——消费者模式,防止多线程同时操作数据库,获取不到数据库锁。
@EnableAsync
public class AsyncInsertMsgLog {
private static boolean isRunnung = false;
private static Queue<MsgLogTimes> msgLogQueue = new ConcurrentLinkedQueue<>();
public void addMsgLog(MsgLog msgLog) {
MsgLogTimes msgLogTimes = new MsgLogTimes(msgLog, 0);
msgLogQueue.add(msgLogTimes);
if(!AsyncInsertMsgLog.isRunnung)
run();
}
@Async
public void run() {
AsyncInsertMsgLog.isRunnung = true;
while(msgLogQueue.size() > 0) {
MsgLogTimes msgLogTimes = msgLogQueue.poll();
try {
// 写数据库操作,省略
LoggerFactory.getLogger(msgLogTimes.getMsgLog().getClassName()).info(JSON.toJSON(msgLogTimes.getMsgLog()).toString());
} catch (Exception e) {
msgLogTimes.plusTimes();
if(msgLogTimes.getTimes() == 0) {
msgLogQueue.add(msgLogTimes);
try {
Thread.sleep(100);
} catch (InterruptedException interruptedException) {
}
}
}
}
AsyncInsertMsgLog.isRunnung = false;
}
class MsgLogTimes {
private MsgLog msgLog;
private int times;
public MsgLogTimes(MsgLog msgLog, int times) {
this.msgLog = msgLog;
this.times = times;
}
public MsgLog getMsgLog() {
return msgLog;
}
public void plusTimes() {
times++;
}
public int getTimes() {
return times;
}
}
}
5、业务失败不回滚
在 Service 层加上下面的注解,即使整体的业务失败,不会影响被注解的方法的提交。
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false, noRollbackFor = Exception.class)
public int insert(MsgLog record) {
return super.insert(record);
}
附录一:切入点配置
引用于:https://www.cnblogs.com/a591378955/p/8872114.html
例: execution(* com.sample.service….(…))
整个表达式可以分为五个部分:
- execution()::表达式主体。
- 第一个*号:表示返回类型, *号表示所有的类型。
- 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service包、子孙包下所有类的方法。
- 第二个*号:表示类名,*号表示所有的类。
- *(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
示例,引用于:https://blog.csdn.net/zhengzehongneil/article/details/7920230?locationNum=9&fps=1
// 任意公共方法的执行:
execution(public * (…))
// 任何一个以“set”开始的方法的执行:
execution( set*(…))
// AccountService 接口的任意方法的执行:
execution(* com.xyz.service.AccountService.(…))
// 定义在service包里的任意方法的执行:
execution( com.xyz.service..(…))
// 定义在service包或者子包里的任意方法的执行:
execution(* com.xyz.service….(…))
附录二:切入时间配置
参考资料:https://blog.csdn.net/zhanglf02/article/details/78132304?locationNum=6&fps=1
@Before:切入到方法执行前
@After:切入到方法执行后
@AfterReturning:切入到方法返回后
@AfterThrowing:切入到方法抛出异常
@Around:从方法执行前到方法执行后