Spring - AOP

基本使用

在Spring中,AOP用于增强某些Bean中指定的方法。

  1. 使用@EnableAspectJAutoProxy开启Spring的AOP自动代理
  2. 使用@Aspect定义切面
  3. 使用@Pointcut定义切点
  4. 使用@Before@AfterReturning@Around等定义通知
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogInterceptor {

    @Pointcut("execution(* spring.service.UserService.*(..))")
    public void userServicePointcut() {
    }
    
    @Around("userServicePointcut()")
    public Object userServiceLog(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Log something ...");
        Object rev = joinPoint.proceed();
        System.out.println("Log something ...");
        return rev;
    }
    
}

切点表达式

  • execution表达式,支持通配符,通用形式为 execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
  • within表达式,用于表示被代理的方法在指定的包下

在Spring中,within能够表示的切点都能用execution表示,因此execution更为常用。其次是几个用作

  • args表达式,可用于匹配运行时传递的参数与指定类型匹配的执行,与execution指定参数类型不同 的是execution只会匹配方法的签名,args表达式还常用于参数的绑定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
        Hello a = applicationContext.getBean(Hello.class);
        a.sayHello("WORLD");
    }

    @Aspect
    @Component
    public static class Enhancer {
        // first for match runtime argument, and second for form binding
        @Before("args(java.io.Serializable) && args(arg)")
        public void enhance(Serializable arg) {
            System.out.println(arg);
        }
    }

    @Service
    public static class Hello {
        public void sayHello(Object str) {
            System.out.println("HELLO " + str);
        }
    }
}
// WORLD
// HELLO WORLD
  • target表达式,匹配被代理的对象实现了指定的接口的类中的方法,也用于绑定 被代理对象(即原始未被增强的对象)
  • this表达式,匹配代理对象实现了指定接口的类的方法,也用于绑定 代理对象本身(即JDK或CGLib生成的对象)

thistarget在Spring的AOP中用作匹配时并没有太大差别,但在AspectJ中则有所不同,this强调 被调用的方法,而target强调调用者,这也导致了将增强代码织入的位置有所不同。 参照thistarget的区别

  • @target表达式,匹配被指定注解修饰的类,同时也用于绑定修饰类的注解
  • @within表达式,匹配被指定注解修饰的类,同时也用于绑定修饰类的注解

上述两个表达式在Spring的AOP中也没有太大差别,但在AspectJ中则不同, 详细见@target@within的区别。 在使用Spring的AOP时,优先使用@target,以保证和AspectJ中的语义一致。

  • @args表达式,匹配参数中被指定注解修饰的方法,同时也用于参数注解的绑定
  • @annotation表达式,匹配被指定注解修饰的方法,同时也用于绑定修饰方法的注解
  • bean表达式,匹配指定Bean名称中的方法

通知类型

  • Before,前置通知用于在方法调用前对方法进行增强,一般而言可以用于对参数进行相关 的处理
  • AfterReturning,返回通知,能够通过returning参数绑定返回值
  • AfterThrowing,异常通知,能够通过throwing参数绑定异常
  • Around,环绕通知,通知方法可以接收ProceedingJointPoint类型的参数,用于执行原本的 逻辑等操作

底层关键实现

Bean增强机制

首先,我们思考如何通过@EnableAspectJAutoProxy开启Spring的AOP功能。

要通过@EnableAspectJAutoProxy注解开启相应的功能,最为简单和直接的方式就是通过反射机制解析类 是否存在该注解,然后通过if判断是否开启。但是,这种实现方式在需要新增功能时就需要修改源码并添 加相应的条件判断,这种方式并不利于维护和拓展。因此,Spring采用的方式是使用@Import 修饰@EnableAspectJAutoProxy, 并且通过这个Spring中更为基础的注解引入AOP所需的配置类,通过配置类为容器添加额外的功能。

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass() default false;
    boolean exposeProxy() default false;
}

上面的代码就是EnableAspectJAutoProxy的实现,我们从中可以得知配置类为AspectJAutoProxyRegistrar。 通过继续向下跟踪,我们最终可以得到配置类向Spring中注册了一个Bean,它的类为 AnnotationAwareAspectJAutoProxyCreator。而这个类是BeanPostProcessor的实现,即它能够作为生命 周期中的“钩子”,嵌入到所有Bean的创建过程中。

动态代理

代理模式

JDKProxy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class JdkProxyTest {
    @Test
    public void proxyTest() {
        // real subject
        IHello helloSubject = () -> System.out.println("HELLO");

        // proxy subject
        IHello helloProxy = (IHello) Proxy.newProxyInstance(
                getClass().getClassLoader(),
                new Class<?>[]{IHello.class},
                (proxy, method, args) -> {
                    System.out.println("BEFORE");
                    Object rev = method.invoke(helloSubject, args);
                    System.out.println("AFTER");
                    return rev;
                }
        );

        helloProxy.sayHello();
    }

    public interface IHello {
        void sayHello();
    }
}

// BEFORE
// HELLO
// AFTER

JDK动态代理的特点有2个:

  1. 只能够代理接口
  2. 需要被代理的对象执行原本逻辑

CGLib

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CglibProxyTest {
    @Test
    public void proxyTest() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Hello.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
            System.out.println("BEFORE");
            // call super method
            Object rev = methodProxy.invokeSuper(obj, args);
            System.out.println("AFTER");
            return rev;
        });

        Hello helloProxy = (Hello) enhancer.create();
        helloProxy.sayHello();
    }

    public static class Hello {
        public void sayHello() {
            System.out.println("HELLO");
        }
    }
}

// BEFORE
// HELLO
// AFTER

相较于JDK的代理模式,CGLib使用的是基于生成子类的实现方式。在上面的代码中,我们没有 创建被代理的对象,而是直接生成了代理类的实例,并通过调用该实例父类方法的方式 执行原本的逻辑。

当然,我们也可以与JDK一样在其中加入一个被代理的对象,并通过method参数和被代理对象 进行与JDK代理类似的原始方法调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CglibProxyTest {
    @Test
    public void proxyTest() {
        Hello helloSubject = new Hello();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Hello.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
            System.out.println("BEFORE");
            // call method
            Object rev = method.invoke(helloSubject, args);
            System.out.println("AFTER");
            return rev;
        });

        Hello helloProxy = (Hello) enhancer.create();
        helloProxy.sayHello();
    }

    public static class Hello {
        public void sayHello() {
            System.out.println("HELLO");
        }
    }
}

因此,CGLib的代理有以下特点:

  1. 能够代理类和接口,代理方式为生成其子类
  2. 代理类时可以不需要被代理的对象而依赖父类实现执行原本逻辑
  3. 由于需要生成子类,CGLib无法代理final修饰的类。

Singleton三级缓存

对Bean的生命周期有所了解的话就会知道,为了解决单例Bean依赖注入阶段产生的循环依赖问题,就必须暴露 没能完成创建的Bean的引用。

举个例子,假设A依赖B,且B依赖A,那么在A实例化后对A注入B时,发现B没创建, 那么接下来就会创建B,B实例化后需要对B注入A,那么此时需要暴露未完全创建完成的A才能使得这个循环终止。

那么就存在一个问题,如果上述的过程中,A被AOP增强了,那么上述的方法能够解决循环引用的问题吗?如果仅仅 从JDK和CGLib代理的角度思考,则有2种情况:

  1. 不使用代理对象,此时需要保证A是由enhancer.create()生成的子类对象,即Bean的实例化方式需要依赖 是否增强来决定
  2. 使用代理对象,此时需要保证向B暴露的半成本引用为代理对象的引用,而当返回A的创建时,继续完成Bean 生命周期的则是被代理对象

在Spring中,使用的是后者,因此在发现循环依赖产生时,Spring会生成用于其他Bean注入的半成品引用,而被 代理的对象(原始Bean)则会继续完成创建。

几个问题

切面的优先级

在某些引用场景下,一个Bean可能会被多个切面增强,而我们需要保证其中某些且面优先执行,此时 就需要使用@Order注解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
        Hello a = applicationContext.getBean(Hello.class);
        a.sayHello();
    }

    @Aspect
    @Order(2)
    @Component
    public static class Enhancer1 {
        @Around("execution(* spring.Main$Hello.sayHello(..))")
        public Object enhance1(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("BEFORE 1");
            Object rev = joinPoint.proceed();
            System.out.println("AFTER 1");
            return rev;
        }
    }

    @Aspect
    @Order(1)
    @Component
    public static class Enhancer2 {
        @Around("execution(* spring.Main$Hello.sayHello(..))")
        public Object enhance1(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("BEFORE 2");
            Object rev = joinPoint.proceed();
            System.out.println("AFTER 2");
            return rev;
        }
    }

    @Service
    public static class Hello {
        public void sayHello() {
            System.out.println("HELLO WORLD");
        }
    }
}
// BEFORE 2
// BEFORE 1
// HELLO WORLD
// AFTER 1
// AFTER 2

对于@Order注解而言,传入的值越小则切面的优先级越高,也就越早执行。

被代理对象方法自调用

假设存在以下场景,类A中存在方法m()n(),并且m()的实现为直接调用n()。现在在Spring的容器 中A的实例为a且仅有n()被增强,那么a.m()a.n()的结果有什么不同?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
        A a = applicationContext.getBean(A.class);
        a.m();
        System.out.println("------------------------");
        a.n();
    }

    @Aspect
    @Component
    public static class Enhancer {

        @Around("execution(* spring.Main$A.n(..))")
        public Object aroundN(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("BEFORE");
            Object rev = joinPoint.proceed();
            System.out.println("AFTER");
            return rev;
        }

    }

    @Service
    public static class A {
        public void m() {
            n();
        }

        public void n() {
            System.out.println("HELLO WORLD");
        }
    }
}

// HELLO WORLD
// ------------------------
// BEFORE
// HELLO WORLD
// AFTER

测试代码见上述代码,结果是只有a.n()会得到增强,这是由于Spring的动态代理无论使用JDK或 者CGLib都只会通过代理对象 的方式调用原本的逻辑。而如果通过CGLib直接生成子类实例的方式,就会得到a.m()a.n()都被增强的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CglibProxyTest {
    @Test
    public void proxyTest() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(A.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
            if ("n".equals(method.getName())) {
                System.out.println("BEFORE");
                Object rev = methodProxy.invokeSuper(obj, args);
                System.out.println("AFTER");
                return rev;
            }
            return methodProxy.invokeSuper(obj, args);
        });

        A helloProxy = (A) enhancer.create();
        helloProxy.m();
        System.out.println("--------------------");
        helloProxy.n();
    }

    public static class A {
        public void m() {
            n();
        }

        public void n() {
            System.out.println("HELLO");
        }
    }
}
// BEFORE
// HELLO
// AFTER
// --------------------
// BEFORE
// HELLO
// AFTER

通过明确调用方法的对象,我们就能够理解2种代理方式所带来的不同。

  1. proxy.m() => subject.m() => subject.n(),由于两个对象引用不同,被代理对象无法调用 代理对象的方法
  2. proxy.m() => proxy.super.m() => proxy.n(),由于多态的存在,在父类方法中调用n()会 调用子类的实现

而在Spring中,无论代理方式是JDK还是CGLib,结果都仅仅只有a.m()不会被增强一种。这也表明了代理的方式 一定是通过代理对象而非子类调用父类方法完成的。

统一使用代理对象的方式带来的好处是:

  1. 由于代理对象和被代理对象的分离,能够在未完成创建之前就得到完成之后的Bean的引用
  2. 能够统一JDK动态代理和CGLib动态代理的行为,程序员无需关心实际使用的底层代理类型
  3. 2种行为在实际开发中都可能需要,但子类调用父类方法的实现产生的结果却无法简单转为代理对象产生的结果

针对上述的问题,解决的方式有2个:

  • Bean自我注入,通过被注入的对象调用自己的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
        A a = applicationContext.getBean(A.class);
        a.m();
        System.out.println("------------------------");
        a.n();
    }

    @Aspect
    @Component
    public static class Enhancer {

        @Around("execution(* spring.Main$A.n(..))")
        public Object aroundN(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("BEFORE");
            Object rev = joinPoint.proceed();
            System.out.println("AFTER");
            return rev;
        }

    }

    @Service
    public static class A {

        // self injection
        @Autowired
        private A self;

        public void m() {
            // Do not call n() directly
            self.n();
        }

        public void n() {
            System.out.println("HELLO WORLD");
        }
    }
}
// BEFORE
// HELLO WORLD
// AFTER
// ------------------------
// BEFORE
// HELLO WORLD
// AFTER
  • @EnableAspectJAutoProxy(exposeProxy = true)并使用AopContext.currentProxy()调用方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Configuration
// expose the proxy reference
@EnableAspectJAutoProxy(exposeProxy = true)
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class);
        A a = applicationContext.getBean(A.class);
        a.m();
        System.out.println("------------------------");
        a.n();
    }

    @Aspect
    @Component
    public static class Enhancer {

        @Around("execution(* spring.Main$A.n(..))")
        public Object aroundN(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("BEFORE");
            Object rev = joinPoint.proceed();
            System.out.println("AFTER");
            return rev;
        }

    }

    @Service
    public static class A {

        public void m() {
            // Do not call n() directly
            ((A) AopContext.currentProxy()).n();
        }

        public void n() {
            System.out.println("HELLO WORLD");
        }
    }
}
// BEFORE
// HELLO WORLD
// AFTER
// ------------------------
// BEFORE
// HELLO WORLD
// AFTER
Built with Hugo
主题 StackJimmy 设计