记录:396
场景:实现自定义注解WriteLog,作用在类的方法上,每次执行方法就记录一条日志。使用AspectJ的注解@Aspect、@Pointcut、@Around、@Before、@AfterReturning、@AfterThrowing、@After拦截自定义注解WriteLog,从而完成记录日志功能。
AspectJ: The AspectJ weaver applies aspects to Java classes. It can be used as a Java agent in order to apply load-time weaving (LTW) during class-loading and also contains the AspectJ runtime classes.Eclipse AspectJ™ is a seamless aspect-oriented extension to the Java™ programming language.
版本:JDK 1.8,SpringBoot 2.6.3,aspectj-1.9.19
1.基础
1.1AspectJ依赖包
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
1.2AspectJ的注解执行顺序
AspectJ的注解在生效时,注解执行顺序。
(1)没有抛出异常时,执行顺序:依次@Aspect、@Pointcut、@Around、@Before、@AfterReturning、@After、@Around。
(2)抛出异常时,执行顺序:依次@Aspect、@Pointcut、@Around、@Before、@AfterThrowing、@After、@Around。
注意:@Around出现两次,是因为@Before、@AfterReturning、@AfterThrowing、@After的执行时机是在@Around注解开始执行和结束执行之间。
2.自定义注解
2.1自定义注解WriteLog
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WriteLog {
String businessName() default "";
String businessNo() default "";
}
2.2自定义注解作用范围
@WriteLog注解作用在方法上。
3.拦截自定义注解
在WriteLogAspect中拦截自定义注解。使用@Aspect和@Pointcut等注解拦截自定义注解实现记录日志。
@Component
@Aspect
@Slf4j
public class WriteLogAspect {
// 写日志
@Autowired
private WriteLogManager writeLogManager;
// 线程变量
private static final ThreadLocal<LogDto> threadLocal = new ThreadLocal<>();
public WriteLogAspect() {
}
// 日志操作切入点,指定使用注解
@Pointcut("@annotation(com.hub.example.aop.p20230407log.annotation.WriteLog)")
public void writeLogOpt() {
}
// 调用注解标记的目标方法前
@Before("writeLogOpt()")
public void doBefore(JoinPoint joinPoint) {
log.info("当前执行doBefore");
}
// 调用注解标记的目标方法
@Around("writeLogOpt()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
log.info("当前执行doAround");
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
// 获取方法上注解
WriteLog writeLog = method.getAnnotation(WriteLog.class);
String businessName = writeLog.businessName();
String businessNo = writeLog.businessNo();
// 获取方法上参数并转换为JSON字符串
Object[] argsObj = proceedingJoinPoint.getArgs();
String reqData = JSONUtil.toJsonStr(argsObj);
// 执行目标方法前记录日志
LogDto logDto = new LogDto();
logDto.setLogId(UUID.randomUUID().toString().replace("-", ""));
RequestAttributes reqAttr = RequestContextHolder.getRequestAttributes();
if (Objects.nonNull(reqAttr)) {
ServletRequestAttributes servletReq = (ServletRequestAttributes) reqAttr;
HttpServletRequest httpReq = servletReq.getRequest();
String token = httpReq.getHeader("token");
String sessionID = httpReq.getSession().getId();
logDto.setToken(token);
logDto.setSessionID(sessionID);
}
logDto.setReqStartTime(new Date());
logDto.setBusinessName(businessName);
logDto.setBusinessNo(businessNo);
logDto.setReqData(reqData);
threadLocal.remove();
threadLocal.set(logDto);
writeLogManager.insertLog(logDto);
Object obj = null;
// 执行目标方法
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 执行目标方法后记录日志
logDto.setReqEndTime(new Date());
logDto.setResData(JSONUtil.toJsonStr(obj));
writeLogManager.updateLog(logDto);
return obj;
}
@AfterReturning(pointcut = "writeLogOpt()",
returning = "rtnObj")
public void doAfterReturning(JoinPoint joinPoint, Object rtnObj) {
log.info("当前执行doAfterReturning");
LogDto logDto=threadLocal.get();
logDto.setSuccess(true);
logDto.setMessage("执行成功");
}
@AfterThrowing(pointcut = "writeLogOpt()",
throwing = "errObj")
public void doAfterThrowing(JoinPoint joinPoint, Throwable errObj) {
log.info("当前执行doAfterThrowing");
LogDto logDto=threadLocal.get();
logDto.setSuccess(false);
logDto.setMessage("执行失败: "+errObj.getMessage());
}
@After("writeLogOpt()")
public void doAfter(JoinPoint joinPoint) {
log.info("当前执行doAfter");
}
}
4.记录日志
示例中以打印控制台消息方式记录日志,生产中可以替换为写数据库、写Redis缓存、写Kafka等方式记录日志。
@Slf4j
@Service
public class WriteLogManager {
// 写日志
public void insertLog(LogDto restReqDto) {
log.info("日志管理器,写入一条日志到数据库.");
log.info("日志信息: " + restReqDto.toString());
}
// 更新日志
public void updateLog(LogDto restReqDto) {
log.info("日志管理器,更新一条日志到数据库.");
log.info("日志信息: " + restReqDto.toString());
}
}
5.使用自定义注解
在CityQueryController的方法上使用自定义注解。
@RestController()
@RequestMapping("/hub/example/city/aspect")
@Slf4j
public class CityQueryController {
@PostMapping("/queryCity")
@WriteLog(businessName = "查询城市服务",
businessNo = "queryCity")
public ResDto queryCity(@RequestBody ReqDto reqDto) {
log.info("当前执行CityQueryController的queryCity");
log.info("查询操作,接收数据:" + reqDto.toString());
try {
log.info("查询操作,正在处理业务...");
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
ResDto resDto = ResDto.getResDto(reqDto.getCityId()
, reqDto.getCityName(),
"这是一个美丽的城市", new Date());
log.info("查询操作,返回数据:" + resDto.toString());
return resDto;
}
}
6.支撑对象
6.1日志对象LogDto
记录一条日志需要的全部属性定义为一个实体类。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogDto {
private String logId;
private String token;
private String sessionID;
private Date reqStartTime;
private Date reqEndTime;
private String businessName;
private String businessNo;
private String reqData;
private String resData;
private Boolean success;
private String message;
}
6.2请求对象ReqDto
请求对象定义为一个实体类。
@Data
public class ReqDto {
private Long cityId;
private String cityName;
}
6.3响应对象ResDto
响应对象定义为一个实体类。
@Data
@Builder
public class ResDto {
private Long cityId;
private String cityName;
private String cityDescribe;
@JsonFormat(
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date updateTime;
public static ResDto getResDto(Long cityId, String cityName, String cityDescribe, Date updateTime) {
return builder().cityId(cityId).cityName(cityName).cityDescribe(cityDescribe).updateTime(updateTime).build();
}
}
7.使用Postman测试
测试URL:http://127.0.0.1:18200/hub-200-base/hub/example/city/aspect/queryCity
设置请求头:
token=T20230409223256
请求数据:
{
"cityId":"20230409001",
"cityName":"杭州"
}
返回数据:
{
"cityId": 20230409001,
"cityName": "杭州",
"cityDescribe": "这是一个美丽的城市",
"updateTime": "2023-04-09 16:17:15"
}
以上,感谢。
2023年4月9日