Spring AOP的理解

目录

1. AOP是什么

2. Spring AOP的实现原理

3. Spring AOP的使用-基于@AspectJ的配置

3.1 声明切面

3.2 声明切入点

3.3 声明通知

3.3.1 前置通知

3.3.2 后置通知

3.3.3 环绕通知

3.3.4 引入通知

4. 基于XML Schema的配置

5. 总结,直接背


1. AOP是什么

AOP是一种编程范式,它的目的是通过分离横切关注点来提升代码的模块化程度。所谓关注点就是一段特定的功能,有些关注点出现在多个模块中,就成为横切关注点。

AOP解决了两个问题:第一是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差;第二是代码分散,同一个功能的代码分散在多个模块中,不易维护。

在AOP中有几个重要的概念:

概念说明
切面(aspect)按关注点进行模块分解时,横切关注点就表示为一个切面
连接点(join point)程序执行的某一刻,在这个点上可以添加额外的动作
通知(advice)切面在特定连接点上执行的动作
切入点(pointcut)切入点是用来描述连接点的,它决定了当前代码与连接点是否匹配

通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来。

此外,声明式事务就是依靠AOP来实现的,Spring还为我们提供了简单的方式来使用AOP,可以选择基于注解或XML的方式来配置AOP相关的功能。

2. Spring AOP的实现原理

Spring AOP背后的核心技术是动态代理技术。代理模式是23中经典设计模式之一,我们可以为某个对象提供一个代理,控制对该对象的访问,代理可以在两个有调用关系的对象之间起到中介的作用——代理封装了目标对象,调用者调用了代理的方法,代理再去调用实际的目标对象。如图:

动态代理就是在运行时动态地为对象创建代理的技术。在Spring中,由AOP框架创建、用来实现切面的对象被称为AOP代理,一般采用JDK动态代理或者是CGLIB代理,两者在使用时的区别如下:

必须要实现接口支持拦截public方法支持拦截protected方法拦截默认作用域方法
JDK动态代理
CGLIB代理

Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,这就让我们感觉是在使用原始的Bean,其实不然。被切面拦截的对象称为目标对象或通知对象,因为Spring用了动态代理,所以目标对象就是要被代理的对象。

以JDK动态代理为例,假设我们希望在代码示例的方法执行前后增加两句日志,可以采用下面这套代码,

2-1 要被动态代理的Hello接口及其实现片段

public interface Hello {
    void say();
}

public class SpringHello implements Hello {
    @Override
    public void say() {
        System.out.println("Hello Spring!");
    }
}

随后设计一个InvocationHandler,于是对代理对象的调用都会转为调用invoke方法,传入的参数中就包含了所调用的方法和实际的参数。

2-2 在Hello.say()前后打印日志的InvocationHandler

public class LogHandler implements InvocationHandler {
    private Hello source;

    public LogHander(Hello source) {
        this.source = source;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Ready to say something.");
        try{
            return method.invoke(source, args);
        } finally {
            System.out.println("Already say something.");
        }
    }
}

最后,在通过Proxy.newProxyInstance()为Hello实现类的Bean实例创建使用LogHandler的代理,如2-3 创建JDK动态代理并调用方法

public class Application {
    public static void main(String[] args) {
        Hello original = new SpringHello();
        Hello target = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),
                original.getClass().getInterfaces(), new LogHandler(original));
        target.say();
    }
}

这段代码的运行结果如下:

Ready to say something.
Hello Spring!
Already say something.

Spring AOP的实现方式与上述例子大同小异,如果深究可以阅读proxyFactoryBean的源码,若是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;若是采用CGLIB代理,则是创建ObjenesisCglibAopProxy,前者的逻辑就和上述的例子差不多。


另外在Spring AOP中,为了能用到被AOP增强过的方法,我们应该始终与代理对象交互。如果存在一个类的内部方法调用,这个调用的对象不是代理,而是其本身,则无法享受AOP增强的效果。

比如下面这个类中的foo()方法调用了bar(),哪怕Spring AOP对bar()做了拦截,由于调用的不是代理对象,因而看不到任何效果。

public class Hello {
    public void foo() {
        bar();
    }

    public void bar() {...}
}

实际项目中这也可能会引起事务失效。

3. Spring AOP的使用-基于@AspectJ的配置

首先要引入org.springframework:spring-aspects依赖,然后通过@EnableAspectJAutoProxy注解或XML配置开启对@AspectJ的支持。


另外Spring Boot的自动配置中的AopAutoConfiguration类已经帮助我们默认开启了对@AspectJ的支持。

Spring 5.x默认JDK动态代理;Spring Boot 2.x默认CGLIB代理。二者的主要区别是JDK动态代理只能代理接口,而后者没有这个限制。

JKD动态代理与CGLIB代理的区别:

必须要实现接口支持拦截public方法支持拦截protected方法拦截默认作用域方法
JDK动态代理
CGLIB代理否(比如支持将代理对象赋值给实现类)

虽然CGLIB支持拦截非public作用域的方法调用,但在不同对象之间交互时,建议还是以public方法调用为主。 

3.1 声明切面

只需要在类上面添加@Aspect注解即可。

注意:

  1. 添加@Aspect注解只是告诉Spring“这个类是切面”,但并没有把它声明为Bean,因此需要另外配置为Bean。
  2. Spring Framework会对带有@Aspect注解的类做特殊对待,因为其本身就是一个切面,所以不会被别的切面自动拦截。

3.2 声明切入点

注解方式的切入点声明由两部分组成——切入点表达式和切入点方法签名。前者用来描述要匹配的连接点,后者可以用来引用切入点,方便切入点的复用,一些简单的切入点声明如下所示:

package learning.spring.helloworld;

public class HelloPointcut {
    @Pointcut("target(learning.spring.helloworld.Hello)")
    public void helloType() {}    // 目标对象是learning.spring.helloworld.Hello类型

    @Pointcut("execution(public * say())")
    public void sayOperation() {}    // 执行public的say()方法

    @Pointcut("helloType() && sayOperation()")    // 复用其他切入点
    public void sayHello() {}    // 执行Hello类型中public的say()方法
}

@Pointcut注解中使用的就是AspectJ 5的表达式,其中一些常用的PCD(pointcut designator,切入点标识符)如下所示:

PCD说明
execution最常用的一个PCD,用来匹配特定方法的执行
within匹配特定范围内的类型,可以用通配符来匹配某个Java包内的所有类
this
target
args
bean

 另外还有针对注解的PCD:

PCD说明
@target执行的目标对象带有特定类型注解
@args传入的方法参数带有特定类型注解
@annotation拦截的方法上带有特定类型注解
@within比较特殊,下面详细说

因此我们可以通过配合自定义注解,灵活的实现定义切入点,比如先定义一个注解learning.spring.helloworld.Log,然后使用如下切入点:

public class LogAspect {
    @Pointcut("@within(learning.spring.helloworld.Log) ||
        @annotation(learning.spring.helloworld.Log)")
    public void logPoint() {}
}

表示拦截在类和方法上添加了@Log注解的所有方法,也可以看出切入点表达式支持&&、||、!。

关于@within:

Spring AOP虽然使用了AspectJ 5的切入点表达式,也共用了不少AspectJ的PCD,但其实两者还是有区别的。比如,Spring AOP 中仅支持有限的PCD,AspectJ中还有很多PCD是Spring AOP不支持的。

由于Spring AOP的实现基于动态代理,因而只能匹配普通方法的执行,像静态初始化、静态方法、构造方法、属性赋值等操作都是拦截不到的。所以说相比AspectJ而言,Spring AOP的功能弱很多,但在大部分场景下也基本够用。

在Spring AOP中,@target与 @within两者在使用上感受不到什么区别。前者要求运行时的目标对象带有注解,这个注解的@Retention是 RetentionPolicy.RUNTIME,即运行时的;后者要求被拦截的类上带有Retention是RetentionPolicy.CLASS 的注解。但Spring AOP只能拦截到非静态public方法的执行,两个PCD的效果一样,所以还是老老实实用@target 吧。

关于Spring AOP 中@target与 within的差异,StackOverflow上有一篇名为“Difference between @target and @witia(Spring AOP)”的文章说得比较清楚,感兴趣的同学可以参考。

(至于为什么上面示例我写的是@within,因为项目里就是用的这个。。。不过结合上面Spring和Spring Boot关于默认代理模式的不同,猜想也可能和这个有关系。)

3.3 声明通知

3.3.1 前置通知

@Before注解,注解中可以引用实现定义好的切入点,也可以直接传入一个切入点表达式(几个通知都一样),在被拦截到的方法开始执行前,会先执行通知中的代码:

@Aspect
public class BeforeAspect {
    @Before("learning.spring.helloworld.HelloPointcut.sayHello()")
    public void before() {
        System.out.println("Before Advice");
    }
}

前置通知的方法没有返回值,因为它在被拦截的方法前执行,就算有返回值也没地方使用,但是它可以对被拦截方法的参数进行加工,通过args这个PCD能明确参数,并将其绑定到前置通知方法的参数上。例如,要在sayHello(AtomicInteger)这个方法前对AtomicInteger类型的参数进行数值调整,就可以这样做:

@Before("learning.spring.helloworld.HelloPointcut.sayHello() && args(count)")
public void before(AtomicInteger count) {
    // 操作count
}

要是同时存在多个通知作用于同一处,可以让切面类实现Ordered接口,或者在上面添加@Order注解。指定的值越低,优先级则越高。(几个通知都一样)

3.3.2 后置通知

@AfterReturing注解用于拦截正常返回的调用,可以有返回值,使用returning属性获得,就暂时不举例了。

@AfterThrowing注解拦截抛出异常的调用,可以有返回值,使用throwing属性获得。

@After注解不关注执行是否成功,必须要能够处理正常与异常两种情况,但获取不到返回值,因此一般只被用来做一些资源清理的工作。

3.3.3 环绕通知

环绕通知的功能比较强大,不仅可以在方法执行前后加入自己的逻辑,甚至可以完全替换方法本身的逻辑,或者替换调用参数。我们可以添加@Around注解来声明环绕通知,这个方法的签名需要特别注意,它的第一个参数必须是ProceedingJoinPoint类型的,方法的返回类型是被拦截方法的返回类型,或者直接用Object类型。

例如,我们希望统计say()方法的执行时间,可以如下声明环绕通知:

@Aspect
public class TimerAspect {
    @Around("execution(public * say(...))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try{
            return pjp.proceed();
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("Total time: " + (end - start) + "ms");
        }
    }
}

实际项目中做个性化日志处理一般也是用环绕通知,可以通过ProceedingJoinPoint对象得到被拦截的类名、方法名等信息。

3.3.4 引入通知

不太常用,暂时先略过

4. 基于XML Schema的配置

需要配置XML、实现不同的接口,现在一般不用这种方式,就略过吧。

5. 总结,直接背

AOP是一种编程范式,它的目的是通过分离横切关注点来提升代码的模块化程度。所谓关注点就是一段特定的功能,有些关注点出现在多个模块中,就成为横切关注点。

AOP解决了两个问题:第一是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差;第二是代码分散,同一个功能的代码分散在多个模块中,不易维护。

在AOP中有几个重要的概念:切面、连接点、 通知、切入点。

它们之间的联系是:通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来。

此外,声明式事务就是依靠AOP来实现的,Spring还为我们提供了简单的方式来使用AOP,可以选择基于注解或XML的方式来配置AOP相关的功能。


Spring AOP背后的核心技术是动态代理技术,动态代理就是在运行时动态地为对象创建代理的技术。Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,被切面拦截的对象称为目标对象,因为Spring用了动态代理,所以目标对象就是要被代理的对象。

代理对象底层是通过AopProxyFactory生成的,如果是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;如果是CGLIB代理,则是创建ObjenesisCglibAopProxy。

JDK动态代理织入AOP切面的原理是InvocationHandler接口,里面的invoke方法的入参中就包含了所调用的方法和实际的参数。


在项目中AOP用到最多的就是日志,可以通过先声明切面,然后在切面类里面声明切入点和通知,切入点也可以先定义自定义注解,然后拦截所有用这个日志注解标记的类和方法。


零散点:

  • 由于AOP的原理是动态代理,所以如果存在一个类的内部方法调用,这个调用的对象不是代理,而是其本身,则无法享受AOP增强的效果。
  • Spring 5.x默认JDK动态代理;Spring Boot 2.x默认CGLIB代理。二者的主要区别是JDK动态代理只能代理接口,而后者没有这个限制。
  • 环绕通知的功能比较强大,不仅可以在方法执行前后加入自己的逻辑,甚至可以完全替换方法本身的逻辑,或者替换调用参数。它的第一个参数必须是ProceedingJoinPoint类型的,方法的返回类型是被拦截方法的返回类型,或者直接用Object类型。可以通过ProceedingJoinPoint对象得到被拦截的类名、方法名等信息。
  • 事务失效的场景:数据库不支持事务;事务没有被Spring管理;是通过内部调用;JDK动态代理时访问权限不是public、CGLIB代理时访问权限不是public或protect;异常然后外层被try包住。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值