Spring切面编程

一、什么是AOP

面向切面编程(AOP) 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

为什么需要AOP

想象下面的场景,开发中在多个模块间有某段重复的代码,我们通常是怎么处理的?在传统的面向过程编程中,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。然而需求总是变化的,有一天,新增了一个需求,需要再修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具有相同的修改的问题我们都可以通过 AOP 来解决。

AOP实现分类

AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码。

按照 AOP 框架修改源代码的时机,可以将其分为两类:

  • 静态 AOP 实现, AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
  • 动态 AOP 实现, AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。

下面给出常用 AOP 实现比较

AOP术语

AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

概念看起来总是有点懵,并且上述术语,不同的参考书籍上翻译还不一样,所以需要慢慢在应用中理解。

二、初步认识 Spring AOP

Spring AOP的特点

AOP 框架有很多种。 Spring 中的 AOP 是通过动态代理实现的。不同的 AOP 框架支持的连接点也有所区别,例如,AspectJ 和 JBoss,除了支持方法切点,它们还支持字段和构造器的连接点。而 Spring AOP 不能拦截对对象字段的修改,也不支持构造器连接点,我们无法在 Bean 创建时应用通知。

Spring AOP的简单例子

引入依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.16.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.6</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

首先创建一个接口 IBuy.java

public interface IBuy {
    String buy();
}

Boy 和 Gril 两个类分别实现了这个接口:

@Component
public class Boy implements IBuy {
    @Override
    public String buy() {
        System.out.println("男孩买了一个游戏机");
        return "游戏机";
    }
}
@Component
public class Girl implements IBuy {
    @Override
    public String buy() {
        System.out.println("女孩买了一件漂亮的衣服");
        return "衣服";
    }
}

测试类

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextHierarchy({
        @ContextConfiguration(classes = SpringConfig.class),
        @ContextConfiguration(classes = SpringMVCConfig.class)
})
public class AspectTest {


    @Autowired
    private Boy boy;

    @Autowired
    private Girl girl;

    @Test
    public void testAspect(){
        boy.buy();
        girl.buy();
    }
}

现在我们需要在男孩和女孩的 buy 方法之前,需要打印出“男孩女孩都买了自己喜欢的东西”。用 Spring AOP 来实现这个需求只需下面几个步骤:

1、定义一个切面类,BuyAspectJ.java

@Aspect
@Component
public class BuyAspectJ {
    @Before("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void buyLike() {
        System.out.println("男孩女孩都买自己喜欢的东西");
    }
}

注解 @Component 表明它将作为一个Spring Bean 被装配,使用注解 @Aspect 表示它是一个切面。

2、在配置类中开启aop支持

//true——使用CGLIB基于类创建代理;false——使用java接口创建代理
@EnableAspectJAutoProxy(proxyTargetClass = true)

三、通过注解配置 Spring AOP

通过注解声明切点指示器

 

上面这些,注意只有execution指示器是唯一的执行匹配,而其他的指示器都是用于限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器,在此基础上,我们使用其他指示器来限制所匹配的切点。

下图的切点表达式表示当Instrument的play方法执行时会触发通知。

 

我们使用execution指示器选择Instrument的play方法,方法表达式以 * 号开始,标识我们不关心方法的返回值类型。

然后我们指定了全限定类名和方法名。

对于方法参数列表,我们使用 .. 标识切点选择任意的play方法,无论该方法的入参是什么。

多个匹配之间我们可以使用链接符 &&||来表示 “且”、“或”、“非”的关系。

但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。

举例:

限定该切点仅匹配的包是 com.aop.modules.service,可以使用
execution(* com.aop.modules.service.IBuy.buy(..)) && within(com.aop.modules.service.*)
在切点中选择 bean,可以使用
execution(* com.aop.modules.service.IBuy.buy(..)) && bean(girl)

修改 BuyAspectJ.java:

@Aspect
@Component
public class BuyAspectJ {
    @Before("execution(* com.aop.modules.service.IBuy.buy(..)) && bean(girl)")
    public void buyLike() {
        System.out.println("男孩女孩都买自己喜欢的东西");
    }
}

此时,切面只会对 Girl.java 这个类生效。

下面列举其他一些指示器的使用方式:

  • 使用within表达式匹配包类型
//匹配ProductServiceImpl类里面的所有方法
@Pointcut("within(com.aop.service.impl.ProductServiceImpl)")
public void matchType() {}

//匹配com.aop.service包及其子包下所有类的方法
@Pointcut("within(com.aop.service..*)")
public void matchPackage() {}
  • 使用this、target、bean表达式匹配对象类型
//匹配AOP对象的目标对象为指定类型的方法,即ProductServiceImpl的aop代理对象的方法
@Pointcut("this(com.aop.service.impl.ProductServiceImpl)")
public void matchThis() {}

//匹配实现ProductService接口的目标对象
@Pointcut("target(com.aop.service.ProductService)")
public void matchTarget() {}

//匹配所有以Service结尾的bean里面的方法
@Pointcut("bean(*Service)")
public void matchBean() {}
  • 使用args表达式匹配参数
/匹配第一个参数为Long类型的方法
@Pointcut("args(Long, ..) ")
public void matchArgs() {}
  • 使用@annotation、@within、@target、@args匹配注解
//匹配标注有AdminOnly注解的方法
@Pointcut("@annotation(com.aop.annotation.AdminOnly)")
public void matchAnno() {}

//匹配标注有Beta的类底下的方法,要求annotation的Retention级别为CLASS
@Pointcut("@within(com.google.common.annotations.Beta)")
public void matchWithin() {}

//匹配标注有Repository的类底下的方法,要求annotation的Retention级别为RUNTIME
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void matchTarget() {}

//匹配传入的参数类标注有Repository注解的方法
@Pointcut("@args(org.springframework.stereotype.Repository)")
public void matchArgs() {}
  • 使用execution表达式

execution表达式是我们在开发过程中最常用的,它的语法如下:

modifier-pattern:用于匹配public、private等访问修饰符
ret-type-pattern:用于匹配返回值类型,不可省略
declaring-type-pattern:用于匹配包类型
modifier-pattern(param-pattern):用于匹配类中的方法,不可省略
throws-pattern:用于匹配抛出异常的方法

通过注解声明 5 种通知类型

@Before前置通知:在切入点运行前执行,不会影响切入点的逻辑。

@After后置通知:在切入点正常运行结束后执行,如果切入点抛出异常,则在抛出异常前执行。

@AfterThrowing异常通知:在切入点抛出异常前执行,如果切入点正常运行(未抛出异常),则不执行。

@AfterReturning返回通知:在切入点正常运行结束后执行,如果切入点抛出异常,则不执行。

@Around环绕通知:功能最强大的通知,可以在切入点执行前后自定义一些操作。环绕通知需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。

下面修改切面类:

@Aspect
@Component
public class BuyAspectJ {
    
    @Before("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void before() {
        System.out.println("before ...");
    }

    @After("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void after() {
        System.out.println("After ...");
    }

    @AfterReturning("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void afterReturning() {
        System.out.println("AfterReturning ...");
    }

    @Around("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void around(ProceedingJoinPoint pj) {
        try {
            System.out.println("Around aaa ...");
            pj.proceed();
            System.out.println("Around bbb ...");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

}

值得注意的是 @Around 修饰的环绕通知类型,是将整个目标方法封装起来了,在使用时,我们传入了 ProceedingJoinPoint 类型的参数,这个对象是必须要有的,而且是放在第一个参数,并且需要调用 ProceedingJoinPoint 的 proceed() 方法。如果不调用该对象的 proceed() 方法,表示原目标方法被阻塞调用,当然也有可能你的实际需求就是这样。

通过注解声明切点表达式

上面我们写的多个通知使用了相同的切点表达式,对于像这样频繁出现的相同的表达式,我们可以使用 @Pointcut注解声明切点表达式,然后使用表达式,修改代码如下:

@Aspect
@Component
public class BuyAspectJ {

    @Pointcut("execution(* com.aop.modules.service.IBuy.buy(..))")
    public void point() {
    }

    @Before("point()")
    public void before() {
        System.out.println("before ...");
    }

    @After("point()")
    public void after() {
        System.out.println("After ...");
    }

    @AfterReturning("point()")
    public void afterReturning() {
        System.out.println("AfterReturning ...");
    }

    @Around("point()")
    public void around(ProceedingJoinPoint pj) {
        try {
            System.out.println("Around aaa ...");
            pj.proceed();
            System.out.println("Around bbb ...");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

}
通过注解处理通知中的参数

上面的例子,我们要进行增强处理的目标方法没有参数,下面我们来说说有参数的情况,并且在增强处理中使用该参数。

IBuy.java

public interface IBuy {
    String buy(double price);
}

Boy.java

@Component
public class Boy implements IBuy {
    @Override
    public String buy(double price) {
        System.out.println(String.format("男孩买了一个%s元的游戏机", price));
        return "游戏机";
    }
}

Girl.java

@Component
public class Girl implements IBuy {
    @Override
    public String buy(double price) {
        System.out.println(String.format("女孩买了一件%s元的漂亮衣服", price));
        return "衣服";
    }
}

再看 BuyAspectJ 类,用一个环绕通知来实现这个功能:

@Aspect
@Component
public class BuyAspectJ {

    @Pointcut("execution(String com.aop.modules.service.IBuy.buy(double)) && args(price) && bean(girl)")
    public void gif(double price) {

    }

    @Around("gif(price)")
    public String around(ProceedingJoinPoint pj, double price) {
        try {
            pj.proceed();
            if (price > 68) {
                System.out.println("女孩买衣服超过了68元,赠送一双袜子");
                return "衣服和袜子";
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return "衣服";
    }
}

前文提到,当不关心方法返回值的时候,我们在编写切点指示器的时候使用了 * , 当不关心方法参数的时候,我们使用了 ..。现在如果我们需要传入参数,并且有返回值的时候,则需要使用对应的类型。在编写通知的时候,我们也需要声明对应的返回值类型和参数类型。

测试类

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextHierarchy({
        @ContextConfiguration(classes = SpringConfig.class),
        @ContextConfiguration(classes = SpringMVCConfig.class)
})
public class AspectTest {


    @Autowired
    private Boy boy;

    @Autowired
    private Girl girl;

    @Test
    public void testAspect(){
        boy.buy(60d);
        girl.buy(70d);
    }
}
通过注解配置织入的方式

在配置文件中,我们用注解 @EnableAspectJAutoProxy() 启用Spring AOP 的时候,我们给参数 proxyTargetClass 赋值为 true,如果我们不写参数,默认为 false。这个时候运行程序,程序会抛出异常。

这跟Spring AOP 动态代理的机制有关,这个 proxyTargetClass 参数决定了代理的机制。

当这个参数为 false 时,通过jdk的基于接口的方式进行织入,这时候代理生成的是一个接口对象,将这个接口对象强制转换为实现该接口的一个类。

proxyTargetClass 为 true,则会使用 cglib 的动态代理方式。这种方式的缺点是拓展类的方法被final修饰时,无法进行织入。

四、通过 XML 配置文件声明切面

下面先列出 XML 中声明 AOP 的常用元素:

XML 配置文件中切点指示器

在XML配置文件中,切点指示器表达式与通过注解配置的写法基本一致,区别前面有提到,即XML文件中需要使用 “and”、“or”、“not”来表示 “且”、“或”、“非”的关系。

XML 文件配置 AOP 实例

BuyAspectJ.java

public class BuyAspectJ {

    public void hehe() {
        System.out.println("before ...");
    }

    public void haha() {
        System.out.println("After ...");
    }

    public void xixi() {
        System.out.println("AfterReturning ...");
    }

    public void xxx(ProceedingJoinPoint pj) {
        try {
            System.out.println("Around aaa ...");
            pj.proceed();
            System.out.println("Around bbb ...");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

在 resources 目录下新建一个配置文件 aopdemo.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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="boy" class="com.aop.modules.service.impl.Boy"></bean>
    <bean id="girl" class="com.aop.modules.service.impl.Girl"></bean>
    <bean id="buyAspectJ" class="com.aop.modules.aspect.BuyAspectJ"></bean>

    <aop:config proxy-target-class="true">
        <aop:aspect id="qiemian" ref="buyAspectJ">
            <aop:before pointcut="execution(* com.aop.modules.service.IBuy.buy(..))" method="hehe"/>
            <aop:after pointcut="execution(* com.aop.modules.service.IBuy.buy(..))" method="haha"/>
            <aop:after-returning pointcut="execution(* com.aop.modules.service.IBuy.buy(..))" method="xixi"/>
            <aop:around pointcut="execution(* com.aop.modules.service.IBuy.buy(..))" method="xxx"/>
        </aop:aspect>
    </aop:config>
</beans>

这里分别定义了一个切面,里面包含四种类型的通知。
测试文件中,使用

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aopdemo.xml");

来获取 ApplicationContext,其它代码不变。

XML 文件配置声明切点

对于频繁重复使用的切点表达式,我们也可以声明成切点。
配置文件如下:aopdemo.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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

   <bean id="boy" class="com.aop.modules.service.impl.Boy"></bean>
    <bean id="girl" class="com.aop.modules.service.impl.Girl"></bean>
    <bean id="buyAspectJ" class="com.aop.modules.aspect.BuyAspectJ"></bean>

    <aop:config proxy-target-class="true">
        <aop:pointcut id="apoint" expression="execution(* com.aop.modules.service.IBuy.buy(..))"/>
        <aop:aspect id="qiemian" ref="buyAspectJ">
            <aop:before pointcut-ref="apoint" method="hehe"/>
            <aop:after pointcut-ref="apoint" method="haha"/>
            <aop:after-returning pointcut-ref="apoint" method="xixi"/>
            <aop:around pointcut-ref="apoint" method="xxx"/>
        </aop:aspect>
    </aop:config>
</beans>
XML文件配置为通知传递参数

BuyAspectJ.java

import org.aspectj.lang.ProceedingJoinPoint;

public class BuyAspectJ {
    public String hehe(ProceedingJoinPoint pj, double price){
        try {
            pj.proceed();
            if (price > 68) {
                System.out.println("女孩买衣服超过了68元,赠送一双袜子");
                return "衣服和袜子";
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return "衣服";
    }
}

aopdemo.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" 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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="boy" class="com.aop.modules.service.impl.Boy"></bean>
    <bean id="girl" class="com.aop.modules.service.impl.Girl"></bean>
    <bean id="buyAspectJ" class="com.aop.modules.aspect.BuyAspectJ"></bean>

    <aop:config proxy-target-class="true">
        <aop:pointcut id="apoint" expression="execution(String com.aop.modules.service.IBuy.buy(double)) and args(price) and bean(girl)"/>
        <aop:aspect id="qiemian" ref="buyAspectJ">
            <aop:around pointcut-ref="apoint" method="hehe"/>
        </aop:aspect>
    </aop:config>
</beans>
Xml 文件配置织入的方式

同注解配置类似,
CGlib 代理方式:

<aop:config proxy-target-class="true"> </aop:config>

JDK 代理方式:

<aop:config proxy-target-class="false"> </aop:config>

五、从@EnableAspectJAutoProxy分析Spring AOP原理

1、@EnableAspectJAutoProxy 开启AOP功能

2、@EnableAspectJAutoProxy 会给容器中注册一个组件 AnnotationAwareAspectJAutoProxyCreator

3、AnnotationAwareAspectJAutoProxyCreator是一个InstantiationAwareBeanPostProcessor类型后置处理器;

4、容器创建过程

  • registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator对象
  • inishBeanFactoryInitialization()初始化剩下的单实例bean
    • 创建业务逻辑组件和切面组件
    • AnnotationAwareAspectJAutoProxyCreator拦截组件的创建过程
    • 组件创建完之后,判断组件是否需要增强
      • 是:切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib);

5、执行目标方法:

  • 代理对象执行目标方法
  • CglibAopProxy.intercept();
    • 得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor)
    • 利用拦截器的链式机制,依次进入每一个拦截器进行执行;
    • 效果:
      • 正常执行:前置通知-》目标方法-》后置通知-》返回通知
      • 出现异常:前置通知-》目标方法-》后置通知-》异常通知

 

参考:

https://blog.csdn.net/zhoushimiao1990/article/details/89853368

https://www.cnblogs.com/joy99/p/10941543.html

 

posted @ 2020-07-14 21:59  codedot  阅读(543)  评论(0编辑  收藏  举报