Skip to content

AOP

本质:代理模式 在Java平台上,对于AOP的织入,有3种方式: 1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入; 2. 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”; 3. 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。 最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。

实现

类似于:

public UserServiceAopProxy extends UserService {
    private UserService target;
    private LoggingAspect aspect;

    public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
        this.target = target;
        this.aspect = aspect;
    }

    public User login(String email, String password) {
        // 先执行Aspect的代码:
        aspect.doAccessCheck();
        // 再执行UserService的逻辑:
        return target.login(email, password);
    }

    public User register(String email, String password, String name) {
        aspect.doAccessCheck();
        return target.register(email, password, name);
    }

    ...
}

这些是 Spring 容器启动时为我们自动创建的注入了Aspect的子类,它取代了原始的UserService(原始的UserService实例作为内部变量隐藏在UserServiceAopProxy中)

拦截器

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

使用注解装配 AOP

@Aspect  
@Component  
public class TenantIgnoreAspect {  

    @Pointcut(value = "@annotation(com.byteox.core.aop.IgnoreTenant)")  
    private void pointcut() {}  


    @Around(value = "pointcut() && @annotation(ig)")  
    public Object around(ProceedingJoinPoint point, IgnoreTenant ig) throws Throwable {  
        boolean already = TenantHolder.isIgnore();  
        if (!already) {  
            // 设置为忽略TenantId  
            TenantHolder.setIgnore();  
        }
    }
}

经典问题

  • 如果原 class 上有初始化的成员变量,使用 AOP 并且通过实例去访问该成员变量时,很可能是 null.
    • 原因大概是 AOP 的实现原理是使用 CGLIB 生成 Proxy,这个类为了让调用方获得UserService的引用,它必须继承自UserService。然后,该代理类会覆写所有publicprotected方法,并在内部将调用委托给原始的UserService实例。但是,并没有初始化proxy的成员变量,因为proxy的目的是代理方法。

所以: 1. 访问被注入的Bean时,总是调用方法而非直接访问字段; 2. 编写Bean时,如果可能会被代理,就不要编写public final方法。