Spring AOP原理

  • 概念
  • AOP(即Aspect Oriented Program)面向切面编程
  • 主要应用场景
  • AOP核心概念
  • AOP的两种代理方式
  • JDK动态接口代理
  • CGLib动态代理
  • 实际代码
  • 使用注解来开发Spring AOP
  • 第一步:选择连接点
  • 第二步:创建切面
  • 第三步:定义切点
  • 第四步:测试 AOP
  • 环绕通知
  • 使用XML配置开发Spring AOP

概念

AOP(即Aspect Oriented Program)面向切面编程

“横切”的技术,剖解开封装的对象内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面。所谓“切面”,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任(如日志管理、事务处理、权限认证等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,有利于未来的可操作性和可维护性。 使用“横切”技术,AOP把软件系统分为两个部分:

  • 核心关注点:业务处理的主要流程,比如登录、增加数据、删除数据等。
  • 横切关注点:与核心关注点无关的部分,如权限认证、日志、事务等。(特点:经常发生在核心关注点的多处,而各处基本相似。)AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

主要应用场景

  1. Authentication 权限
  2. Caching 缓存
  3. Context passing 内容传递
  4. Error handling 错误处理
  5. Lazy loading 懒加载
  6. Debugging 调试
  7. logging,tracing,profiling and monitoring 记录、跟踪、优化和校准
  8. Performance optimization 性能优化
  9. Persistence 持久化
  10. Resource pooling 资源池
  11. Synchronization 同步
  12. Transactions 事务

AOP核心概念

  • 切面(aspect):类是物体特征的抽象,切面就是横切关注点的抽象。一个关注点的模块化,这个关注点可能横切多个对象,例如事务管理。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。切面=切入点+通知,通俗点就是:在什么时机,什么地方,做什么增强!
  • 连接点(joinpoint):在程序执行过程中某个特定的点。在Spring AOP中,一个连接点总是标识一个方法的执行。实际上连接点还可以是字段或者构造器。
  • 切入点(pointcut):在哪些类、哪些方法上切入(where)。匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如 当执行某个特定名称的方法时)。切入点表达式如何跟连接点匹配是AOP的核心。
  • 通知(advice):所谓通知就是指拦截到连接点之后,要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。许多AOP框架(包括Spring)都是以拦截器作为通知模型,并维护一个以连接点为中心的拦截器链。在方法执行的什么时机(when:方法前/方法后/方法前后),做什么(what:增强功能)。
  • 引入(introduction):用来给一个类型声明额外的方法或属性(也被成为连接类型声明(inner-tyoe declaration))。Srping允许引入新的接口()以及一个对应的实现)到任何被代理的对象。例如,可以使用引入来使一个bean实现IsModified接口,以便建华缓存机制。
  • 织入(weaving):把切面加入到对象,并创建出代理对象的过程(由Spring来完成),这些可以在编译时、类加载时和运行时完成。
  • AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 目标对象(target object):被一个或多个切面所通知的对象,也称作被通知(advised)对象。既然Sprign AOP是通过运行时代理实现的,则这个对象永远是一个被代理(proxied)对象。

AOP的两种代理方式

JDK动态接口代理

主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。

  • InvocationHandler是一个接口,通过实现该接口,定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。
  • Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。

CGLib动态代理

CGLib全称Code Generation Library,是一个强大的高性能、高质量的代码生成类库,可在运行期扩展Java类和实现Java接口,还封装了 asm,可在运行期动态生成新的class。

为了更好的说明AOP的概念,举一个实际中的例子来说明:

spring AOP 切面环绕拿到方法参数类型 spring aop切面切点_spring

在上面的业务中,包租婆的核心业务就是签合同、收租金,而其他部分就是重复且边缘的事,交给中介就好了。这就是AOP的一个思想:让关注点代码与业务代码分离。

实际代码

1、在 Package【pojo】创建一个Landlord类

package pojo;

import org.springframework.stereotype.Component;

@Component("landlord")
public class Landlord {

    public void service() {
        // 仅仅只是实现了核心的业务功能
        System.out.println("签合同");
        System.out.println("收房租");
    }
}

2、在 Package【aspect】下创建中介Broker类

package aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
class Broker {

    @Before("execution(* pojo.Landlord.service())")
    public void before(){
        System.out.println("带租客看房");
        System.out.println("谈价格");
    }

    @After("execution(* pojo.Landlord.service())")
    public void after(){
        System.out.println("交钥匙");
    }
}

3、在 applicationContext.xml 中配置自动注入,并告诉 Spring IoC 容器去哪里扫描这两个 Bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="aspect" />
    <context:component-scan base-package="pojo" />

    <aop:aspectj-autoproxy/>
</beans>

4、在 Package【test】下编写测试代码:

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.Landlord;

public class TestSpring {

    public static void main(String[] args) {

        ApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class);
        landlord.service();

    }
}

5、执行结果:

我们在 Landlord 的 service() 方法中仅仅实现了核心的业务代码,其余的关注点功能是根据我们设置的切面自动补全的。

使用注解来开发Spring AOP

第一步:选择连接点

Spring是方法级别的AOP框架,我们也是主要以某个类的某个方法作为连接点,另一种说法就是:选择哪一个类的哪一个方法用以增强功能。 这里选择Landlord类中的service()方法作为连接点。

....
    public void service() {
        // 仅仅只是实现了核心的业务功能
        System.out.println("签合同");
        System.out.println("收房租");
    }
    ....

第二步:创建切面

选择好连接点就可以创建切面了,我们可以把切面理解为一个拦截器,当程序运行到连接点的时候,被拦截下来,在开头加入了初始化的方法,在结尾也加入了销毁的方法而已。在Srping中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了:

package aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
class Broker {

    @Before("execution(* pojo.Landlord.service())")
    public void before(){
        System.out.println("带租客看房");
        System.out.println("谈价格");
    }

    @After("execution(* pojo.Landlord.service())")
    public void after(){
        System.out.println("交钥匙");
    }
}

注意: 被定义为切面的类仍然是一个 Bean ,需要 @Component 注解标注 代码部分中在方法上面的注解看名字也能猜出个大概,下面来列举一下 Spring 中的 AspectJ 注解:

注解

说明

@Before

前置通知,在连接点方法前调用

@Around

环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法,后面会讲

@After

后置通知,在连接点方法后调用

@AfterReturning

返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常

@AfterThrowing

异常通知,当连接点方法异常时调用

有了上表,我们就知道 before() 方法是连接点方法调用前调用的方法,而 after() 方法则相反,这些注解中间使用了定义切点的正则式,也就是告诉 Spring AOP 需要拦截什么对象的什么方法,下面讲到。

第三步:定义切点

在上面的注解中定义了 execution 的正则表达式,Spring 通过这个正则表达式判断具体要拦截的是哪一个类的哪一个方法:

execution(* pojo.Landlord.service())

依次对这个表达式作出分析:

  • execution:代表执行方法的时候触发
  • *:代表任意返回类型的方法
  • pojo.Landlord:代表类的全限定名
  • service():代表被拦截的方法名称

通过上面的表达式,Spring就会知道应该拦截pojo.Landlord类下的service()方法。上面的演示类还好,如果多处都需要写这样的表达式难免会有些复杂,我们可以通过使用 @Pointcut 注解来定义一个切点来避免这样的麻烦:

package aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
class Broker {

   @Pointcut("execution(* pojo.Landlord.service())")
    public void lService() {
    }

    @Before("lService()")
    public void before() {
        System.out.println("带租客看房");
        System.out.println("谈价格");
    }

    @After("lService()")
    public void after() {
        System.out.println("交钥匙");
    }
}

第四步:测试 AOP

环绕通知

环绕通知,是Spring AOP最强大的通知,因为它集成了前置通知和后置通知,它保留了连接点原有方法的功能,所以它强大又灵活。

package aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
class Broker {

//  注释掉之前的 @Before 和 @After 注解以及对应的方法
//  @Before("execution(* pojo.Landlord.service())")
//  public void before() {
//      System.out.println("带租客看房");
//      System.out.println("谈价格");
//  }
//
//  @After("execution(* pojo.Landlord.service())")
//  public void after() {
//      System.out.println("交钥匙");
//  }

    //  使用 @Around 注解来同时完成前置和后置通知
    @Around("execution(* pojo.Landlord.service())")
    public void around(ProceedingJoinPoint joinPoint) {
        System.out.println("带租客看房");
        System.out.println("谈价格");

        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        System.out.println("交钥匙");
    }
}

使用XML配置开发Spring AOP

注解是很强大的东西,但基于 XML 的开发我们仍然需要了解,我们先来了解一下 AOP 中可以配置的元素:

AOP 配置元素

用途

备注

aop:advisor

定义 AOP 的通知

一种很古老的方式,很少使用

aop:aspect

定义一个切面

——

aop:before

定义前置通知

——

aop:after

定义后置通知

——

aop:around

定义环绕通知

——

aop:after-returning

定义返回通知

——

aop:after-throwing

定义异常通知

——

aop:config

顶层的 AOP 配置元素

AOP 的配置是以它为开始的

aop:declare-parents

给通知引入新的额外接口,增强功能

——

aop:pointcut

定义切点

——

有了之前通过注解来编写的经验,并且有了上面的表,我们将上面的例子改写成 XML 配置很容易(去掉所有的注解):

<!-- 装配 Bean-->
<bean name="landlord" class="pojo.Landlord"/>
<bean id="broker" class="aspect.Broker"/>

<!-- 配置AOP -->
<aop:config>
    <!-- where:在哪些地方(包.类.方法)做增加 -->
    <aop:pointcut id="landlordPoint"
                  expression="execution(* pojo.Landlord.service())"/>
    <!-- what:做什么增强 -->
    <aop:aspect id="logAspect" ref="broker">
        <!-- when:在什么时机(方法前/后/前后) -->
        <aop:around pointcut-ref="landlordPoint" method="around"/>
    </aop:aspect>
</aop:config>

参考资料: 简书ID:@我没有三颗心脏