1. Spring AOP 系列1 —— 初识Spring AOP

发布于 2022年 03月 15日 16:26

腾讯服务器

88 / 年

  • 上海/北京/广州...
  • 2核 2G 4M
  • Linux/Windows
新年大优惠

腾讯服务器

425 / 年

  • 上海/北京/广州...
  • 4核 8G 10M
  • Linux/Windows
年度最便宜

腾讯服务器

1249 / 年

  • 上海/北京/广州...
  • 8核 16G 14M
  • Linux/Windows
点击查看

1. 什么是AOP

AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,提升开发效率。

AOP的拦截功能是由java中的动态代理来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执行。不同的切入时机对应不同的Interceptor的种类,如BeforeAdviseInterceptor,AfterAdviseInterceptor以及ThrowsAdviseInterceptor等)。那么动态代理是如何实现将切面逻辑(advise)织入到目标类方法中去的呢?下面我们就来详细介绍并实现AOP中用到的两种动态代理。AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

2. 应该场景

AOP是处理一些横切行问题。这些横切性问题不会影响到主逻辑的实现,但是会散落到代码的各个部分,难以维护。AOP就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

3. AOP与OOP的区别:

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

通过下面的图可以清晰的理解AOP与OOP的区别:

4. AOP中的概念

  1. AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
  2. Join point(连接点):连接点就是Advice在应用程序上执行的点或时机,表示在程序中明确定义的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
  3. Advice(通知):Advice 定义了在 Pointcut里面定义的程序点具体要做的操作,AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)。
    • Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即
    • AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
    • AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
    • After:在目标方法完成之后做增强,无论目标方法是否成功完成。@After可以指定一个切入点表达式
    • Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint
  4. Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
  5. Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  6. Target(目标对象):织入 Advice 的目标对象。
  7. Weave(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程。
  8. Target Object(目标对象): 包含连接点的对象。也被称作被通知或被代理对象。POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans。
  9. introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint

5. 实战

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

四种实现方式

  1. 经典的基于代理的AOP
  2. @AspectJ注解
  3. 纯POJO切面,通过aop:config标签配置
AOP配置元素 描述
aop:advisor 定义AOP通知器
aop:after 定义AOP后置通知(不管该方法是否执行成功)
aop:after-returning 在方法成功执行后调用通知
aop:after-throwing 在方法抛出异常后调用通知
aop:around 定义AOP环绕通知
aop:aspect 定义切面
aop:aspect-autoproxy 定义@AspectJ注解驱动的切面
aop:before 定义AOP前置通知
aop:config 顶层的AOP配置元素,大多数的aop:*包含在aop:config元素内
aop:declare-parent 为被通知的对象引入额外的接口,并透明的实现
aop:pointcut 定义切点
  1. 注入式AspectJ切面
AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的Bean引用为指定类型的类
target() 限制连接点匹配目标对象为执行类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型
@annotation() 限制匹配带有指定注解连接点

1. 原生spring实现

定义一个通用接口,所有实现此接口的类都有一个咸鱼方法和一个测试aop的方法

public interface HelloWorld {
    void saltedFish();

    void testPrintTime();
}

实现1

public class HelloWorldImpl1 implements HelloWorld{
    @Override
    public void saltedFish() {
        System.out.println("this is a salted fish =========== 1");
    }

    @Override
    public void testPrintTime() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("testPrintTime 1=============111111111");
    }
}

实现2

public class HelloWorldImpl2 implements HelloWorld{
    @Override
    public void saltedFish() {
        System.out.println("this is a salted fish =========== 2");
    }

    @Override
    public void testPrintTime() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("testPrintTime 2=============22222222");
    }
}

定义一个Advice,实现在连接点之前之后该干的事

public class TimeHandler implements MethodBeforeAdvice, AfterReturningAdvice {
    Long before = 0L;

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        Long after = System.currentTimeMillis();
        System.out.println("==========代理后time, " + after + " ======= 间隔: " + (after - before) + "==========");
    }

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        before = System.currentTimeMillis();
        System.out.println("==========代理前time:" + before + "===========");
    }
}

通过xml配置切面

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 定义 -->
    <bean id="h1" class="com.example.demo.aop.HelloWorldImpl1"></bean>
    <bean id="h2" class="com.example.demo.aop.HelloWorldImpl2"></bean>

    <!--  定义advice -->
    <bean id="timeHandler" class="com.example.demo.aop.TimeHandler"></bean>

    <!--  定义point cut  -->
    <bean id="timePointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*testPrintTime"></property>
    </bean>

    <!-- 切面 关联切入点与通知 -->
    <bean id="timeHandlerAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="timeHandler"></property>
        <property name="pointcut" ref="timePointCut"></property>
    </bean>

    <!-- 设置代理-->
    <bean id="proxy1" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--  代理的对象 -->
        <property name="target" ref="h1"></property>
        <!-- 使用的切面 -->
        <property name="interceptorNames" value="timeHandlerAdvisor"></property>
        <!-- 代理接口 -->
        <property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>
    </bean>

    <!-- 设置代理-->
    <bean id="proxy2" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--  代理的对象 -->
        <property name="target" ref="h2"></property>
        <!-- 使用的切面 -->
        <property name="interceptorNames" value="timeHandlerAdvisor"></property>
        <!-- 代理接口 -->
        <property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>
    </bean>
</beans>

测试类

public class AOPTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("./application.xml");
        HelloWorld helloWorld1 = (HelloWorld) applicationContext.getBean("proxy1");
        HelloWorld helloWorld2 = (HelloWorld) applicationContext.getBean("proxy2");

        helloWorld1.saltedFish();
        System.out.println("---------------------");
        helloWorld1.testPrintTime();

        System.out.println("=======================");

        helloWorld2.saltedFish();
        System.out.println("---------------------");
        helloWorld2.testPrintTime();
    }
}

测试结果

this is a salted fish =========== 1
---------------------
==========代理前time:1582477901748===========
testPrintTime 1=============111111111
==========代理后time, 1582477902750 ======= 间隔: 1002==========
=======================
this is a salted fish =========== 2
---------------------
==========代理前time:1582477902750===========
testPrintTime 2=============22222222
==========代理后time, 1582477903250 ======= 间隔: 500==========
2. 基于AspectJ注解实现

对于将纯POJO申明成切面的方式中,如果不使用@AspectJ,那么就需要使用使用繁琐的XML配置,因此Spring借鉴了AspectJ的切面,以提供注解驱动的AOP,但是本质上依然是使用的SpringAop的动态代理的方式,只是变成模型几乎与AspectJ完全一样。

要在 Springboot中声明 AspectJ 切面, 需在 IOC 容器中将切面声明为 Bean 实例 即加入@Component 注解;当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

配置类

@Aspect
@Configuration      // 一定要加此注解,或者在入口引入
public class AopLogger {

    /**
     * 标识这个方法是个前置通知,  切点表达式表示执行任意类的任意方法.
     * 第一个 * 代表匹配任意修饰符及任意返回值,
     * 第二个 * 代表任意类的对象,
     * 第三个 * 代表任意方法,
     * 参数列表中的 ..  匹配任意数量的参数
     *
     * @param joinPoint
     */
    @Before("execution(* com.example.demo.aop..*.*(..))")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object result = Arrays.asList(joinPoint.getArgs());
        System.out.printf("method name is: %s, args is: %s%n", methodName, result);
    }

    @After("execution (* com.example.demo.aop..*.*(..))")
    public void after(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("after log method name is: " + methodName);
    }

    //    @AfterReturning(value = "execution(**.*(..))", returning = "result")
    @AfterReturning(value = "execution (* com.example.demo.aop..*.*(..))", returning = "result")
    public void afterReturn(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("method name is: %s, and the result is: %s%n", methodName, result);
    }

    //    @AfterThrowing(value = "execution(**.*(..))", throwing = "e")
    @AfterThrowing(value = "execution (* com.example.demo.aop..*.*(..))", throwing = "e")
    public void afterThrow(JoinPoint joinPoint, Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("method name is: %s, and the exception is: %s%n", methodName, e);
    }

    //    @Around("execution(**.*(..))")
    @Around("execution (* com.example.demo.aop..*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) {
        StopWatch stopWatch = new StopWatch();
        String name = joinPoint.getSignature().getName();
        System.out.println("===============↓↓↓↓↓↓↓↓ " + name + " ↓↓↓↓↓↓↓↓=================");

        stopWatch.start();
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            stopWatch.stop();
            System.out.println("===============↑↑↑↑↑↑↑↑ " + name + " ↑↑↑↑↑↑↑↑=================");
        }
        return null;
    }
}

测试方法

@Test
public void testAspectJ() {
    helloWorldImpl1.saltedFish();
    helloWorldImpl2.testPrintTime();
}

测试结果

===============↓↓↓↓↓↓↓↓ saltedFish ↓↓↓↓↓↓↓↓=================
method name is: saltedFish, args is: []
this is a salted fish =========== 1
===============↑↑↑↑↑↑↑↑ saltedFish ↑↑↑↑↑↑↑↑=================
after log method name is: saltedFish
method name is: saltedFish, and the result is: null
===============↓↓↓↓↓↓↓↓ testPrintTime ↓↓↓↓↓↓↓↓=================
method name is: testPrintTime, args is: []
testPrintTime 2=============22222222
===============↑↑↑↑↑↑↑↑ testPrintTime ↑↑↑↑↑↑↑↑=================
after log method name is: testPrintTime
method name is: testPrintTime, and the result is: null

推荐文章