Spring面向切面编程(AOP)(Java学习笔记)

面向切面编程是什么?

在软件开发中,散布于应用中多处的功能被称为横切关注点(比如日志、安全和事务管理)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
面向切面编程的好处:
每个横切关注点都集中于一个地方,而不是分散到多处代码中。
服务模块更简洁,因为它们只包含业务逻辑的代码,而横切关注点的代码被转移到一个统一的地方了。

AOP的术语

  1. 横切关注点

跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志、安全、缓存、事务等等。

  1. 连接点

连接点是在应用执行中能够插入切面的一个点。即程序执行过程中能够应用通知的所有点。

  1. 切点

切点是真正需要插入切面的一个或多个连接点。即通知被应用的具体位置(在哪些连接点)。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点(比如Aspect切点表达式)。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

  1. 通知

切面的工作被称为通知。即包含了需要用于多个应用对象的横切行为。
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用5种类型的通知:

前置通知(Before):在目标方法被调用之前调用通知功能。
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
返回通知(After-returning):在目标方法成功执行之后调用通知。
异常通知(After-throwing)):在目标方法抛出异常后调用通知。
环绕通知(Around) :通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

  1. 切面

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

  1. 织入

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。Aspect的织入编译器就是以这种方式织入切面的。
类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader) ,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面。
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时, AOP容器会为目标对象动态地创建一个代理对象(动态代理)。Spring AOP就是以这种方式织入切面的。
7. 引入

引入指的是向现有的类添加新方法或属性。

Spring对AOP的支持

通常来说Spring对AOP有两种支持方式:

  1. Spring AOP —— 这种方式通过动态代理构建,因此只支持方法级别的连接点(或者说切点)。这种方式借鉴了AspectJ的切面,以提供注解驱动的AOP(也提供XML方式)。编程模型几乎与编写成熟的AspectJ注解切面完全一致。
  2. 注入式AspectJ切面 —— 提供构造器、字段级别的连接点。(更加细粒度)

AOP的实现分类

AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,看到这其实应该明白了,AOP 其实就是前面一篇文章讲的代理模式的典型应用。
按照 AOP 框架修改源代码的时机,可以将其分为两类:

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

AOP实例

//IBuy
package org.example;

public interface IBuy {
    String buy();
}

package org.example;

import org.springframework.stereotype.Component;

@Component
public class Boy implements IBuy{
    @Override
    public String buy() {
        System.out.println("男孩买一个游戏机!");
        return "游戏机";
    }
}

//Girl
package org.example;

import org.springframework.stereotype.Component;

@Component
public class Girl implements IBuy{
    @Override
    public String buy(){
        System.out.println("女孩买了一个洋娃娃!");
        return "洋娃娃";
    }
}

//AppConfig
package org.example;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = {org.example.IBuy.class})
public class AppConfig {

}

运行结果:

在这里插入图片描述

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

  1. 定义一个切面类,BuyAspectJ
  2. 这个类,我们使用了注解 @Component 表明它将作为一个Spring Bean 被装配,使用注解 @Aspect 表示它是一个切面。
    类中只有一个方法 haha 我们使用 @Before 这个注解,表示他将在方法执行之前执行。关于这个注解后文再作解释。
    参数(“execution(* org.example.IBuy.buy(…))”)声明了切点,表明在该切面的切点是org.example.IBuy这个接口中的buy方法。至于为什么这么写,下文再解释。
//BuyAspectJ
package org.example;

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

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

}


我们在配置文件类增加了@EnableAspectJAutoProxy注解,启用了 AOP 功能,参数proxyTargetClass的值设为了 true 。默认值是 false。

//AppConfig
package org.example;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackageClasses = {org.example.IBuy.class})
public class AppConfig {

}

在这里插入图片描述

通过注解配置Spring AOP

通过注解声明切点指示器

Spring AOP 所支持的 AspectJ 切点指示器

AspectJ指示器描述
args()限制连接点匹配参数类型的执行方法
@agrs()限制连接点匹配参数由指定注解标注的执行方法
excution()用于匹配连接点的执行方法
this()限制连接点匹配AOP代理的Bean引用为指定类型的类。
target()限制连接点匹配特定的执行对象为执行类型的类
@target()限制匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within()限制连接点匹配指定的数据
@within()限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类型
@annotation限制匹配带有指定注解连接点

在spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。

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

我们使用execution指示器选择Instrument的play方法,方法表达式以 * 号开始,标识我们不关心方法的返回值类型。然后我们指定了全限定类名和方法名。对于方法参数列表,我们使用 … 标识切点选择任意的play方法,无论该方法的入参是什么。
多个匹配之间我们可以使用链接符 &&、||、!来表示 “且”、“或”、“非”的关系。但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。
限制指定该切点仅匹配的包是:

execution(* com.example.IBuy.buy(..)&& within(com.example.*)

在切点中选择bean,可以使用:

execution(* com.example.IBuy.buy(..) && within(com.example.*) && bean(girl))

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

注解通知
@Before通知方法会在目标方法调用之前执行
@After通知方法会在目标方法返回或异常后调用
@AfterReturing通知会在目标方法返回后调用
@AfterThrowing通知方法会在目标方法抛出异常后调用
@Around通知方法会将目标方法封装起来
package org.example;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class BuyAspectJ {
    @Before("execution(* org.example.IBuy.buy(..))")
    public void beforeTest01(){
        System.out.println("beforeTest01");
    }
    @After("execution(* org.example.IBuy.buy(..))")
    public void afterTest01(){
        System.out.println("AfterTest01");
    }
    @AfterReturning("execution(* org.example.IBuy.buy(..))")
    public void afterReturningTest01(){
        System.out.println("AfterReturningTest01...");
    }
//    @Around("execution(* org.example.IBuy.buy(..))")
//    public void aroundTest01(ProceedingJoinPoint pj){
//        try {
//            System.out.println("Around Before...");
//            pj.proceed();
//            System.out.println("Around After...");
//        }catch (Throwable throwable){
//            throwable.printStackTrace();
//        }
//    }

}


运行结果
在这里插入图片描述
结果显而易见。指的注意的是 @Around 修饰的环绕通知类型,是将整个目标方法封装起来了,在使用时,我们传入了 ProceedingJoinPoint 类型的参数,这个对象是必须要有的,并且需要调用 ProceedingJoinPoint 的 proceed() 方法。 如果没有调用 该方法,执行结果的原目标方法就被阻塞,当然也有可能你的实际需求就是这样。

通过注解声明切点表达式

可以使用 @Pointcut注解声明切点表达式,然后使用表达式,修改代码如下:

package org.example;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class BuyAspectJ2 {
    @Pointcut("execution(* org.example.IBuy.buy(..))")
    public void point(){};

    @Before("point()")
    public void beforeTest01(){
        System.out.println("beforeTest01...");
    }
    @After("point()")
    public void afterTest01(){
        System.out.println("afterTest01...");
    }
    @AfterReturning("point()")
    public void afterReturningTest01(){
        System.out.println("afterReturnTest01...");
    }
//    @Around("point()")
//    public void around(ProceedingJoinPoint pj){
//        try {
//            System.out.println("Around Before....");
//            pj.proceed();
//            System.out.println("Around After...");
//        }catch (Throwable throwable){
//            throwable.printStackTrace();
//        }
//    }

    @Pointcut("execution(String org.example.IBuy.buy(double) )&& args(price) && bean(girl))")
    public void gif(Double price){
    }
    @Around("gif(price)")
    public String gifTest01(ProceedingJoinPoint pj,double price){
        try {
            pj.proceed();
            if (price>68) {
                System.out.println("女孩买衣服超过了68元,赠送一双袜子");
                return "衣服和袜子";
            }
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }
        return "衣服";
    }
}

声明了一个切点表达式,该方法 point 的内容并不重要,方法名也不重要,实际上它只是作为一个标识,供通知使用。

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

//IBuy接口
package org.example;

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

//Boy
package org.example;

import org.springframework.stereotype.Component;

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

//Girl
package org.example;

import org.springframework.stereotype.Component;

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

//AppConfig
package org.example;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackageClasses = {org.example.IBuy.class})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}

//BuyAspectJ
package org.example;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class BuyAspectJ {
//    @Pointcut("execution(* org.example.IBuy.buy(..))")
//    public void point(){};
//
//    @Before("point()")
//    public void beforeTest01(){
//        System.out.println("beforeTest01...");
//    }
//
//    @After("point()")
//    public void afterTest01(){
//        System.out.println("afterTest01...");
//    }
//    @AfterReturning("point()")
//    public void afterReturningTest01(){
//        System.out.println("afterReturningTest01");
//    }
//
//    @Around("point()")
//    public void aroundTest01(ProceedingJoinPoint proceedingJoinPoint){
//        try {
//            System.out.println("around before ....");
//            proceedingJoinPoint.proceed();
//            System.out.println("around after...");
//        }catch (Throwable throwable){
//            throwable.printStackTrace();
//        }
//
//    }
    @Pointcut("execution(String org.example.IBuy.buy(double)) && args(price) && bean(girl)")
    public void gif(double price) {
    }

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

}

//Main
package org.example;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Boy boy = context.getBean("boy",Boy.class);
        Girl girl = (Girl) context.getBean("girl");
        String boyBought = boy.buy(35);
        String girlBought = girl.buy(99.8);

        System.out.println("男孩买到了:" + boyBought);
        System.out.println("女孩买到了:" + girlBought);
    }
}

运行结果
在这里插入图片描述
当不关心方法返回值的时候,我们在编写切点指示器的时候使用了 * , 当不关心方法参数的时候,我们使用了 …。现在如果我们需要传入参数,并且有返回值的时候,则需要使用对应的类型。在编写通知的时候,我们也需要声明对应的返回值类型和参数类型。

通过注解织入的方式

前面还有一个遗留问题,在配置文件中,我们用注解 @EnableAspectJAutoProxy() 启用Spring AOP 的时候,我们给参数 proxyTargetClass 赋值为 true,如果我们不写参数,默认为 false。这个时候运行程序,程序抛出异常。
这是一个强制类型转换异常。为什么会抛出这个异常呢?或许已经能够想到,这跟Spring AOP 动态代理的机制有关,这个 proxyTargetClass 参数决定了代理的机制。当这个参数为 false 时,
通过jdk的基于接口的方式进行织入,这时候代理生成的是一个接口对象,将这个接口对象强制转换为实现该接口的一个类,自然就抛出了上述类型转换异常。
反之,proxyTargetClass 为 true,则会使用 cglib 的动态代理方式。这种方式的缺点是拓展类的方法被final修饰时,无法进行织入。

通过XMl配置文件声明切面

XML声明AOP的常用标签:

AOP元素用途
aop:advisor定义AOP通知器
aop:after定义一个后置通知(不管目方法是否执行成功)
aop:after-retuening定义AOP异常通知
aop:after-throwing定义AOP异常通知
aop:around定义环绕通知
aop:aspectj-autoproxy启动@AspectJ注解驱动切面
aop:before定义一个AOP前置通知
aop:config顶层AOP配置元素,大多素的aop元素都必须包含在Aop:config元素内
aop:declare-parents以透明的方式为被通知对象引入额外的接口
aop:pointcut定义一个切点

XML文件配置AOP切点指示器

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

//AppCofig
package org.example;

import org.aspectj.lang.ProceedingJoinPoint;

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();
        }
    }
}

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd

         http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="boy" class="org.example.Boy"/>
    <bean id="girl" class="org.example.Girl"/>
    <bean id="buyAspectJ" class="org.example.BuyAspectJ"/>
    <aop:config proxy-target-class="true">
        <aop:aspect id="aspectJ" ref="buyAspectJ">
            <aop:before pointcut="execution(* org.example.IBuy.buy(..))" method="hehe"/>
            <aop:after pointcut="execution(* org.example.IBuy.buy(..))" method="haha"/>
            <aop:after-returning pointcut="execution(* org.example.IBuy.buy(..))" method="xixi"/>
<!--            <aop:around pointcut="execution(* org.example.IBuy.buy(..))" method="xxx"/>-->
        </aop:aspect>
    </aop:config>
</beans>

运行结果:
在这里插入图片描述

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="org.example.Boy"/>
    <bean id="girl" class="org.example.Girl"/>
    <bean id="buyAspectJ" class="org.example.BuyAspectJ"/>

    <aop:config proxy-target-class="true">
        <aop:pointcut id="apoint" expression="execution(* org.example.IBuy.buy(..))"/>
        <aop:aspect id="acpectJ" 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文件配置为通知传递参数

<?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="org.example.Boy"/>
    <bean id="girl" class="org.example.Girl"/>
    <bean id="buyAspectJ" class="org.example.BuyAspectJ"/>

    <aop:config proxy-target-class="true">
        <aop:pointcut id="apoint" expression="execution(String org.example.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>

//MainTest
package org.example;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("demo4.xml");
        Boy boy = context.getBean("boy",Boy.class);
        Girl girl =(Girl) context.getBean("girl");
        String boyBought =boy.buy(35);
        String girlBought = girl.buy(99.8);

        System.out.println("男孩买到了:"+boyBought);
        System.out.println("女孩买到了:"+girlBought);
    }
}

运行结果:
在这里插入图片描述

XML文件配置织入方式

CGlib代理方式

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

JDK代理方式:

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

学习资料

学习知识

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值