记录一下半年前做的一个电商的项目,每个业务方法上都有一个日志注解,是基于AOP注解实现的,然后有一天突然发现部分业务方法被调用的时候没有日志记录,后面调试的时候发现一个很奇怪的东西,如果业务方法是其他bean调用的话就能够正常记录,如果是bean调用自身函数时就失效了。因为之前的代码找不到了,那么接下来随便做一个示例跟大帅们探讨一下知识!!!

问题描述

自定义一个注解@Loggable,并使用AOP来实现日志记录

自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}
AOP切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("@annotation(com.example.demo.annotation.Loggable)")
    public void before(JoinPoint joinPoint) {
        System.out.println("方法前: " + joinPoint.getSignature().getName());
    }

    @After("@annotation(com.example.demo.annotation.Loggable)")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法后: " + joinPoint.getSignature().getName());
    }
}
服务类
import com.example.demo.annotation.Loggable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Loggable
    public void externalCall() {
        System.out.println("欢迎来到51CTO");
    }

    @Loggable
    public void internalCall() {
        this.externalCall(); // 使用this的方式调用
    }
}
控制器
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/external")
    public String externalCall() {
        userService.externalCall();
        return "外部调用已记录";
    }

    @GetMapping("/internal")
    public String internalCall() {
        userService.internalCall();
        return "内部调用已记录";
    }
}
测试功能

当访问/external接口时,日志能够正常记录:

方法前: externalCall
欢迎来到51CTO
方法后: externalCall

然而,当访问/internal接口时,日志无法记录:

方法前: internalCall
欢迎来到51CTO
方法后: internalCall
原因分析

AOP代理对象的工作原理是通过动态代理来增强目标对象的方法。Spring AOP默认使用JDK动态代理或CGLIB代理。当我们在同一个类中通过this调用另一个方法时,实际上是在调用原始对象的方法,而不是代理对象的方法。因此,AOP切面无法拦截到这个方法调用。

解决方案

为了使AOP切面能够拦截到内部方法调用,我们需要确保调用的是代理对象的方法。一种常见的解决方法是通过@Autowired注入自身,然后通过注入的对象调用方法。

修改后的服务类
import com.example.demo.annotation.Loggable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserService self;

    @Loggable
    public void externalCall() {
        System.out.println("欢迎来到51CTO");
    }

    @Loggable
    public void internalCall() {
        self.externalCall(); // 通过注入的self对象调用方法
    }
}
测试结果

修改后,再次访问/internal端点,日志能够正常记录:

方法前: internalCall
方法前: externalCall
欢迎来到51CTO
方法后: externalCall
方法后: internalCall
总结

当在同一个类中通过this调用方法时,AOP切面无法生效。为了避免这种情况,可以通过@Autowired注入自身,然后通过注入的对象调用方法。这样可以确保AOP切面能够正确地拦截到方法调用,从而实现预期的日志记录功能,如果各位大帅有其他的解决方法也欢迎在评论区留言讨论。

一个人写的烂软件将会给另一个人带来一份全职工作