SpringAOP与@Transactional导致回滚失效以及,AOP失效

79 阅读3分钟

场景:我负责的功能模块需要对接一个记录用户操作的日志功能,我首先想到的是使用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对于子线程来说是获取不到