SpringBoot项目开发日志切面记录操作日志
springboot项目常常需要记录API的请求日志并记录到控制台或数据库,
常用的做法是使用AOP切面技术以及注解技术,实现API接口的请求日志记录以及详细的业务日志记录。
物料准备:
1.aspect切面类
2.自定义的业务逻辑枚举类
3.自定义的业务逻辑注解
请求日志切面类ReqLogAspect.java
@Aspect
@Order(1)
// 控制多个Aspect的执行顺序,越小越先执行
@Component
@Log4j2
public class ReqLogAspect {
@Autowired
private AsyncSaveOperationLogService asyncSaveOperationLogService;
@Autowired
private IdGenerator idGenerator;
/**
* 定义 切点
* 切点可以是 package路径 或 自定义注解路径
* package路径 如 execution(* cn.ath.knowwikibackend.rest..*.*(..))
* 自定义注解路径 如 @annoation(cn.ath.knowwikibackend.anno.MyAnnotation)
*/
@Pointcut("execution(* cn.ath.knowwikibackend.rest..*.*(..))")
public void myPointCut(){}
//环绕通知(可以获得请求前和响应后的数据)
// 切面为cn.ath.knowwikibackend.rest包下的类
// @Around(value = "execution(* cn.ath.knowwikibackend.rest..*.*(..))")
@Around(value = "myPointCut()") //可以指向切点方法 或者 直接指向切点的路径
public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("ReqAspect->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
long startTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
OperationLogIndex logEntity = new OperationLogIndex();
logEntity.setId(idGenerator.snowflakeId());
logEntity.setGmtCreate(DateTime.now().toString());
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String ip = Ip2regionUtil.getIp(request);
logEntity.setReqIp(ip);
String addr = Ip2regionUtil.getAddr(ip);
logEntity.setReqLocation(addr);
log.info("请求Port:{}", request.getRemotePort());
log.info("请求URI:{}", request.getRequestURI());
String token = request.getHeader("Authorization");
log.info("ReqHeader-Authorization:{}", token);
String account = "";
if (StrUtil.isNotBlank(token)) {
logEntity.setReqToken(token);
// account = (String) StpUtil.getLoginIdByToken(token);
// log.info("操作人:{}", account);
}
logEntity.setAccount(account);
logEntity.setReqUrl(request.getRequestURI());
}
Signature signature = proceedingJoinPoint.getSignature();
log.info("请求目标类:{}", signature);
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
//获取方法上加的自定义注解 @BusinessLogic 的属性
BusinessLogic businessLogic = methodSignature.getMethod().getAnnotation(BusinessLogic.class);
if (businessLogic != null) {
logEntity.setBusinessUnit(businessLogic.businessUnit().getBusinessDesc());
logEntity.setBusinessMethod(businessLogic.value());
log.info("业务操作:{}", businessLogic.value());
log.info("业务模块:{}", businessLogic.businessUnit().getBusinessDesc());
}
}
// 获取请求入参
Object[] args = proceedingJoinPoint.getArgs();
Arrays.stream(args).forEach(arg -> log.info("请求的 arg item is {}", arg));
// 获取响应
Object response = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
log.info("Around请求耗时:{}ms", (endTime - startTime));
//异步保存操作日志
asyncSaveOperationLogService.asyncSaveOperationLog(logEntity);
log.info("ReqAspect-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
return response;
}
@Before(value = "myPointCut()")
public void before() throws Throwable{
log.info("测试AOP 前置通知");
}
@After(value = "myPointCut()")
public void after() throws Throwable{
log.info("测试AOP 后置通知");
}
}
业务逻辑枚举类SysBusinessUnitEnum.java
package cn.ath.knowwikibackend.sys.log;
import lombok.Getter;
@Getter
public enum SysBusinessUnitEnum {
DEFAULT_UNIT("default","默认模块"),
LOGIN_UNIT("login","登录模块"),
USER_UNIT("user","用户模块"),
MENU_UNIT("menu","菜单模块"),
BUTTON_UNIT("button","按钮模块"),
LOG_UNIT("log","日志模块"),
ROLE_UNIT("role","角色模块");
private String businessCode;
private String businessDesc;
SysBusinessUnitEnum(String businessCode,String businessDesc){
this.businessCode=businessCode;
this.businessDesc=businessDesc;
}
}
业务逻辑注解BusinessLogic.java
package cn.ath.knowwikibackend.sys.log;
import java.lang.annotation.*;
/**
* 自定义注解 @BusinessLogic,用于标注业务逻辑
*
* 定义注解格式:
* public @interface 注解名 {定义体}
*
* 注解参数的可支持数据类型:
*
* 1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
* 2.String类型
* 3.Class类型
* 4.enum类型
* 5.Annotation类型
* 6.以上所有类型的数组
*
* Annotation类型里面的参数该怎么设定:
* 第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;
* 第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;
* 第三,如果只有一个参数成员,最好把参数名称设为"value",后加小括号.
*
*
* @Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,
* 因此可以被例如javadoc此类的工具文档化。
* Documented是一个标记注解,没有成员
*
* @Retention定义了该Annotation被保留的时间长短
* 作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
* 取值(RetentionPoicy)有:
* 1.SOURCE:在源文件中有效(即源文件保留)
* 2.CLASS:在class文件中有效(即class保留)
* 3.RUNTIME:在运行时有效(即运行时保留)
*
* @Target说明了Annotation所修饰的对象范围
* 作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
* 取值(ElementType)有:
* 1.CONSTRUCTOR:用于描述构造器
* 2.FIELD:用于描述域
* 3.LOCAL_VARIABLE:用于描述局部变量
* 4.METHOD:用于描述方法
* 5.PACKAGE:用于描述包
* 6.PARAMETER:用于描述参数
* 7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
*
*/
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BusinessLogic {
String value() default "";
SysBusinessUnitEnum businessUnit() default SysBusinessUnitEnum.DEFAULT_UNIT;
}
测试记录API接口的请求日志
package cn.ath.knowwikibackend.rest;
import cn.ath.knowwikibackend.rest.resp.ServerInfoResp;
import cn.ath.knowwikibackend.sys.log.BusinessLogic;
import cn.ath.knowwikibackend.sys.log.SysBusinessUnitEnum;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.system.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
@RestController
@Slf4j
@RequestMapping("/sys/log")
public class SysLogController{
@Value("${logging.file.path}")
private String logPath;
@Value("${spring.application.name}")
private String appName;
/**
* 这里写个api接口,供前端下载后台logback生成的log日志文件
*/
@GetMapping("/download")
@BusinessLogic(value = "下载系统诊断日志",businessUnit = SysBusinessUnitEnum.LOG_UNIT)
public void downLog(HttpServletResponse response) throws Exception {
//把logPath里的日志文件压缩成zip文件后输出到前端,供前端下载
File fileZip = ZipUtil.zip(logPath);
FileInputStream fis = new FileInputStream(fileZip);
String currentDate = DateTime.now().toString();
String fName = appName +"-诊断日志-"+currentDate+".zip";
log.info("导出文件:{}",fName);
response.setHeader("Content-Disposition","attachment;filename="+new String(fName.getBytes(), StandardCharsets.ISO_8859_1));
response.setContentType("application/x-zip-compressed;charset=utf-8");
ServletOutputStream ops = response.getOutputStream();
IoUtil.copy(fis,ops);
IoUtil.closeIfPosible(fis);
IoUtil.closeIfPosible(ops);
FileUtil.del(fileZip);
}
}
测试结果
2023-06-03 12:12:35,629 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 82] ReqAspect->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2023-06-03 12:12:35,630 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.u.Ip2regionUtil [Ip2regionUtil.java : 25] 请求ip:127.0.0.1
2023-06-03 12:12:35,645 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.u.Ip2regionUtil [Ip2regionUtil.java : 41] 请求addr:内网IP|内网IP
2023-06-03 12:12:35,646 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 100] 请求Port:7138
2023-06-03 12:12:35,646 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 101] 请求URI:/kwb/sys/log/download
2023-06-03 12:12:35,646 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 103] ReqHeader-Authorization:Basic YWRtaW46MTIzNDU2
2023-06-03 12:12:35,647 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 121] 请求目标类:void cn.ath.knowwikibackend.rest.SysLogController.downLog(HttpServletResponse)
2023-06-03 12:12:35,647 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 129] 业务操作:下载系统诊断日志
2023-06-03 12:12:35,647 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 130] 业务模块:日志模块
2023-06-03 12:12:35,647 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 136] 请求的 arg item is net.bull.javamelody.internal.web.CounterServletResponseWrapper@4aa94949
2023-06-03 12:12:35,648 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 158] 测试AOP 前置通知
2023-06-03 12:12:35,689 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.r.SysLogController [SysLogController.java : 42] 导出文件:know-wiki-backend-诊断日志-2023-06-03 12:12:35.zip
2023-06-03 12:12:35,713 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 163] 测试AOP 后置通知
2023-06-03 12:12:35,713 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 145] Around请求耗时:83ms
2023-06-03 12:12:35,715 INFO [http-nio-8080-exec-3] [4668fda6-320f-451f-8256-a8aaf06706c2] c.a.k.s.l.ReqLogAspect [ReqLogAspect.java : 151] ReqAspect-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<