场景:我负责的功能模块需要对接一个记录用户操作的日志功能,我首先想到的是使用JDK动态代理来对方法前后进行日志记录,但是我们公司使用的是JPA,业务层不需要去实现接口,而JDK动态代理又需要必须实现接口,所以就放弃采用了SpringAOP的方式来进行日志记录,在使用过程中发现,业务层方法加了@Transactional注解,如果业务方法报错了,那么回滚时会把我在环绕通知(@Around)中finally块的日志记录逻辑也回滚。
/**
* @Author: henry
* @Date: 2023/11/18 09:12
* @Description: 记录操作日志切面
*/
@Aspect
@Component
public class OperateLogAspect {
private final Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass());
@Autowired
private OperateLogService operateLogService;
@Autowired
private HttpServletRequest request;
@Pointcut("@annotation(com.cesc.ewater.biz.resourceAllocation.tunnel.annotation.OperateLog)")
public void loggableMethods() {
}
@Around("loggableMethods()")
public Object aroundMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
final String ipHost = ServletUtil.getClientIP(request); //获取ip地址
String requestMethod = request.getMethod(); //获取请求方式
boolean isFail = true;
final Date startDate = new Date();
final String token = TokenUtil.getToken();
LoginUser loginUser = TokenUtil.getLoginUser();
if (Objects.isNull(loginUser)) {
throw new RuntimeException("请先登录");
}
Method method = getMethod(joinPoint);
OperateLog annotation = method.getDeclaredAnnotation(OperateLog.class);
// 记录日志参数
Map<String, Object> paramMap = getMethodParameters(method, joinPoint.getArgs());
Object id = paramMap.get("id");
String content = annotation.content();
Object result = null;
try {
result = joinPoint.proceed();
isFail = false;
} finally {
// 保存操作日志
OperateLogDTO dto = new OperateLogDTO();
dto.setContent(content);
dto.setUserName(loginUser.getName());
dto.setIpAddress(ipHost);
dto.setState(isFail ? "失败" : "成功");
dto.setOperateTime(startDate);
dto.setVersion("1.0");
dto.setMethod(requestMethod);
dto.setReturns(isFail ? "操作失败" : JsonConvertUtil.toJsonString(result));
dto.setParameters(JsonConvertUtil.toJsonString(paramMap));
String logResult = operateLogService.addOperateLog(dto);
// 使用 logger.info 输出日志
logger.info(logResult);
}
return result;
}
private Method getMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getParameterTypes());
}
/**
* 获取方法参数转为map
* @param method
* @param args
* @return
*/
private Map<String, Object> getMethodParameters(Method method, Object[] args) {
Map<String, Object> paramMap = new HashMap<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
Object arg = args[i];
//如果arg是自定义的对象,需要获取对象的属性
if (arg != null && !arg.getClass().isPrimitive() && !arg.getClass().getName().startsWith("java.lang")) {
Field[] fields = arg.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
paramMap.put(field.getName(), field.get(arg));
} catch (IllegalAccessException e) {
logger.error("获取参数失败", e);
}
}
} else {
paramMap.put(parameter.getName(), arg);
}
}
return paramMap;
}
/**
* 自定义注解 用于记录操作日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
String module() default ""; // 模块名称
String content() default ""; // 操作内容
}
/**
* 业务层逻辑
*/
@OperateLog(module = "隧道管理", content = "新增或修改隧道")
@Transactional
public Object saveOrUpdate(TunnelBo tunnelBo) {
if (Objects.isNull(tunnelBo)) {
return "参数不能为空";
}
long id = tunnelBo.getId();
Tunnel entity = null;
if (id > 0) {
entity = tunnelRepository.findById(id).orElse(null);
}
if (entity == null) {
entity = new Tunnel();
entity.setCuuid(UUID.randomUUID().toString());
}
entity.setName(tunnelBo.getName());
entity.setResponsibler(tunnelBo.getResponsibler());
entity.setType(tunnelBo.getType());
entity.setWkt(tunnelBo.getWkt());
Tunnel save = tunnelRepository.save(entity);
//模拟错误
int i = 1/0;
return save;
}
- 事务失效:查阅网上资料,发现回滚失效是因为,AOP的异常处理是在事务管理器异常处理之前的,如果AOP先catch处理了异常,那么就会导致事务管理器捕获不到异常信息,而异常信息是决定是否回滚的决定因素。
- AOP失效:按理来说,我如果想要切面finally块中的代码不受事务的管理,我只需要把finally块脱离事务就行了,所以采用了子线程来执行finally块,发现还是不对,最后发现,是因为我在子线程中直接将注入的request传入了,导致子线程报错所以不生效,改成在进入子线程之前就获取到请求方式传入,一切正常。
分析:为什么直接传入request就不行呢?
原因是ThreadLocal对于子线程来说是获取不到