Spring AOP和AspectJ

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yzy199391/article/details/88171492

面向切面编程

  • 横切关注点(cross-cutting consern):散布于应用中多处,影响应用多处的功能。

概念上其应该与应用的业务逻辑分离,但多数情况横切关注点会嵌入到应用的业务逻辑之中,而面向切面编程(AOP)就是要把横切关注点与业务逻辑进行解耦

  • 切面(aspect):横切关注点集中在被模块化为特殊类的切面中;使业务代码更简洁。

AOP术语

通知(Advice)

切面必须完成的工作,并指定何时执行工作。

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

连接点(Join point)

应用执行过程中,能插入切面,应用通知的时机。如调用方法时、抛出异常时、修改一个字段时等等

切点(Pointcut)

定义切面在"何处"应用通知,有助于缩小切面所通知的连接点的范围。切点定义会匹配通知所要织入的一个或多个连接点。指定切点可以使用明确的类和方法名或用正则表达式定义所匹配的类和方法名来指定切点,或创建动态的切点。

切面(Aspect)

通知和切点组成

引入(Introduction)

向现有类添加新方法或属性,在无需修改现有类的情况下,让它们具有新的行为和状态。

织入(Weaving)

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

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

Spring对AOP的支持

  • 基于代理的经典Spring AOP
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面(适用于Spring各个版本)

前三种都属于Spring AOP实现,构建在动态代理的基础上,只支持方法拦截
Spring在运行初期把切面织入到Spring管理的bean中
在这里插入图片描述代理类封装目标类,并拦截被通知方法的调用,执行切面逻辑,对目标方法进行增强,再把调用转发给真正的目标。

如果AOP需求超过了简单的方法调用(如构造器拦截或属性拦截),需要考虑第四种方式,将值注入到AspectJ驱动的切面中。

通过切点选择连接点

Spring只支持AspectJ切点指示器(pointcut designator)的一个子集。下表列出了Spring AOP所支持的AspectJ切点指示器。

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

⚠️:在spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgumentException

上述指示器,只有execution是实际执行匹配的,而其它指示器都是用来限制匹配的。

编写切点

首先,定义一个接口Performance代表任何类型的现场表演,其中包含perform()方法:

package com.note.demo.aop.interf

public interface Performance {
    public void perform();
}
  • 我们以此方法作为连接点,我们可以如下编写一个perform()方法触发的通知:
    在这里插入图片描述
  • 在上述基础上,限定匹配的包名:
    在这里插入图片描述
    ⚠️:此处可以使用&&||!连接符;XML中可以使用and、or、not

在切点中选择bean

限制切点只匹配特定的bean
"execution(* concert.Performance.perform(..)) and bean('woodstock')"
此处限定的bean id为 woodstock
排除特定的bean
"execution(com.note.demo.aop.interf.Perform.perform(..)) and !bean('woodstock')"

使用注解创建切面

@Aspect
@Component
public class MyAspect{

@Before("execution(* com.note.demo.aop.interf.Performance.perform(..))")
public void before() throws Throwable{
        System.out.println("织入前处理");
    }
    
@After("execution(* com.note.demo.aop.interf.Performance.perform(..))")
public void before() throws Throwable{
        System.out.println("织入后处理");
    }
  }

简化上述代码,可以将公共的execution配置提取出来:

@Aspect
@Component
public class MyAspect{
    
    @Pointcut("execution(* com.note.demo.aop.interf.Performance.perform(..))")
    public void myPointcut(){}
    
    @Before("myPointcut()")
    public void before(){
        System.out.println("织入前处理");
    }
    
    @After("myPointcut()")
    public void after(){
        System.out.println("织入后处理");
    }
}

⚠️:声明的切面类也要注入到Spring中,否则不生效。

当然,若不使用@Component,还可以使用javaConfig的方式将其注入:

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    @Bean
    public MyAspect myAspect(){
        return new MyAspect();
    }
}

使用@EnableAspectJAutoProxy启用自动代理功能

Spring中定义通知的5个AspectJ注解

注解 通知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 返回后
@AfterThrowing 抛出异常后
@Around 通知方法会将目标方法封装起来
@Before 通知方法会在目标方法之前调用

其中,@Around格式比较特殊:

@Aspect
@Component
public class MyAspect{
    @Pointcut("execution(* com.note.demo.aop.interf.Performance.perform(..)))")
    public void myPointcut(){}
    
    @Around("myPointcut()")
    public void around(ProceedingJoinPoint point){
        System.out.println("环绕织入-前");
        point.proceed();
        System.out.println("环绕织入-后");
    }
}

其中ProceedingJoinPoint参数是必须的,通知方法要将控制权交给被通知方法时,需要调用ProceedingJoinPoint的proceed()方法。
⚠️:你可以不调用proceed()方法,从而阻塞被通知方法的访问;也可以在通知中多次调用它,例如在实现重试逻辑时,可以这样使用,在被通知方法失败后,进行重复尝试。

另外,还可以增强指定注解标注的bean

//连接点所在类
package com.note.demo.aop.interf

public interface Performance {
    public void perform();
}

@Component
public class PerformanceImpl implements Performance {
    @Override
    @AopLog
    public String perform() {
        System.out.println("连接点1");
        return "连接点1";
    }
}

//注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AopLog {
}

//切面类
@Aspect
@Component
public class MyAspect {
  @Pointcut("@annotation(com.note.demo.aop.AopLog)")
  public void logPointCut(){}
  
  @Before("logPointCut()")
    public void before(){
        System.out.println("织入前处理");
    }
}

⚠️:指定注解必须加在实现类的方法上,加在接口上不生效。

处理通知中的参数

//被通知方法所在类
public interface Performance {
    void count(Integer count);
}
@Component
public class PerformanceImpl implements Performance {
    @Override
    public void count(Integer count){
        System.out.println("逻辑方法传入数值为:" + count);
    }
}

//切面类
@Aspect
@Component
public class MyAspect {
    @Pointcut("execution(* com.note.demo.aop.interf.Performance.count(Integer))" + " && args(count)")
    public void countPointCut(Integer count){}
    
    @Around("countPointCut(count)")
    public void aroundWithParam(ProceedingJoinPoint point, Integer count) throws Throwable {
        System.out.println("带参数-织入前处理" + "切面处理参数为 " + count);
        point.proceed();
        System.out.println("带参数-织入后处理 " + "切面处理参数为 " + count);
    }
}

上述代码中,通过args(count)限制了传入通知的参数,其名字和countPointCut(Integer count)方法中参数名一致。

在这里插入图片描述

通过注解引入新功能

利用被称为引入的AOP概念,切面可以为Spring bean添加新方法。当Spring发现一个bean使用了@Aspect注解时,Spring会创建一个代理,然后将调用委托给被代理的bean或被引入的实现。

在这里插入图片描述
现有一个接口Performance,其有多个实现类;还有另一个接口Encoreable,现在需要将此接口应用到Performance的多个实现类中,该如何做呢?
当然,我们可以修改所有的实现类,让它们都实现Encoreable接口。但是,这样做并非最佳方案:

  • 并非所有实现类都具有Encoreable特性
  • 若实现类来自第三方,我们并不能获取其源码,则无法通过此方法来实现需求。

下面,看看AOP的引入功能如何实现上述需求:

@Aspect
public class EncoreableIntroducer{
    @DeclareParents(value="concert.Performance+", defaultImpl=DefaultWncoreable.class)
public static Encoreable encoreable;
}

@DeclareParents注解是此处的关键,其由三部分组成:

  • value属性:指定哪种类型的bean要引入该接口,+号表示是Performance的所有子类型而不是其本身;
  • defaultImpl属性:指定了为引入功能提供实现的类;
  • 被标注的静态属性:指明了要引入的接口。

在XML中声明切面

若需要声明切面,但又不能为通知类添加注解时,可以使用XML配置。

Spring aop命名空间提供了多个元素用于在XML中声明切面:

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

此时,以下面类为例:

package com.note.demo;

public class Audience {
    private Map<Integer, Integer> counterMap = new HashMap<>();
    
    public void silenceCellPhone() {
        System.out.println("Silence cell phone");
    }

    public void takeSeats() {
        System.out.println("Taking seats");
    }

    public void applause() {
        System.out.println("CLAP, CLAP, CLAP!!!");
    }

    public void demandRefund() {
        System.out.println("Demanding a refund");
    }

    public void watchPerformance(ProceedingJoinPoint point){
        try{
            System.out.println("Silencing cell phone");
            System.out.println("Taking seats");
            point.proceed();
            System.out.println("CLAP, CLAP, CLAP!!!");
        }catch(Throwable e){
            System.out.println("Demanding a refund");
        }
    }
    
    //统计各个节目表演的次数
    public void counter(ProceedingJoinPoint point, Integer id) throws Throwable {
        int currentCount = getProgramCount(id);
        counterMap.put(id, currentCount + 1);
    }
    
    private int getProgramCount(int id){
        if(counterMap.containsKey(id)){
            return counterMap.get(id);
        }else{
            return 0;
        }
    }
}

声明前置和后置通知

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:aspect ref="audience">
            <aop:before method="silenceCellPhone" pointcut="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
            <aop:before method="takeSeats" pointcut="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
            <aop:after-returning method="applause" pointcut="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
            <aop:after-throwing method="demandRefund" pointcut="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
        </aop:aspect>
    </aop:config>

    <bean id="audience" class="com.note.demo.Audience"/>
</beans>

当然,我们也可以将上述中重复的pointcut部分抽象成公共的模块:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:aspect ref="audience">
            <aop:pointcut id="performance" expression="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
            <aop:before method="silenceCellPhone" pointcut-ref="performance"/>
            <aop:before method="takeSeats" pointcut-ref="performance"/>
            <aop:after-returning method="applause" pointcut-ref="performance"/>
            <aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
        </aop:aspect>
    </aop:config>

    <bean id="audience" class="com.note.demo.Audience"/>
</beans>

声明环绕通知

在上面声明的aspect中,加入如下标签

<aop:around method="watchPerformance" pointcut-ref="performance"/>

就可以将watchPerformance(ProceedingJoinPoint point)声明为环绕通知

为通知传递参数

按照上面声明aspect的方式,将其中pointcut和通知的内容改为如下:

<aop:pointcut id="counter" expression="execution(* com.note.demo.aop.interf.Performance.countPrograms(int)) and args(id)"/>
            <aop:before method="counter" pointcut-ref="counter"/>

就可以将countPrograms(int id)方法的参数传递到通知中,从而使用切面来统计各个节目的表演次数。

⚠️:在一个aop:aspect标签中,不能声明多个aop:pointcut,可以抽象出一个aop:pointcut,其它的不能进行抽象,可以使用pointcut属性进行声明

eg:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:aspect ref="audience">
            <aop:pointcut id="performance" expression="execution(* com.note.demo.aop.interf.Performance.perform(..))"/>
            <aop:before method="silenceCellPhone" pointcut-ref="performance"/>
            <aop:before method="takeSeats" pointcut-ref="performance"/>
            <aop:after-returning method="applause" pointcut-ref="performance"/>
            <aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
            <aop:around method="watchPerformance" pointcut-ref="performance"/>
            <aop:before method="counter" pointcut="execution(* com.note.demo.aop.interf.Performance.countPrograms(int)) and args(id)"/>
        </aop:aspect>
    </aop:config>

    <bean id="audience" class="com.note.demo.Audience"/>
</beans>

通过切面引入新的功能

和注解方式类似,为接口的实现类引入新功能,可以在上述aop:aspect注解方式内如下声明:

<aop:declare-parents types-matching="com.note.demo.aop.interf.Performance+"
                                 implement-interface="com.note.demo.aop.interf.Encoreable"
                                 default-impl="com.note.demo.aop.impl.DefaultEncoreable"/>
  • types-matching:声明了此切面所通知的bean;
  • implement-interface:切面通知的bean,其父类结构中需要添加的内容;
  • default-impl:全限定类名显示指定Encoreable的实现
    或者,我们可以使用
  • delegate-ref:引用一个指定了实现类的bean

注入AspectJ切面

Spring aop是基于动态代理实现的,无法把通知应用于对象的创建过程。

AspectJ与之独立,可以织入到任何java应用中,包括Spring应用。

使用AspectJ声明切面如下:

public aspect AspectJAspect {
    private Performance performance;

    public void setPerformance(Performance performance) {
        this.performance = performance;
    }

    pointcut performance() : execution(* perform(..));
}

可以借助Spring的IOC特性将AspectJ依赖的其它类注入到其中。若想这样做,需要将切面注入为Spring bean。

<bean id="performance" class="com.note.demo.aop.impl.PerformanceImpl"/>

<bean class="com.note.demo.aop.AspectJAspect" factory-method="aspectOf">
        <property name="performance" ref="performance" />
    </bean>

将AspectJ切面声明为bean,需要使用factory-method属性:
Spring bean由Spring容器初始化;AspectJ切面由AspectJ在运行期创建。由于Sping不负责创建AspectJ切面,在切面需要被注入时,其可能已经被实例化了。

所有的AspectJ切面都提供了一个静态方法aspectOf(),该方法返回切面的一个单例,我们通过factory-method来调用aspectOf()获取切面实例。

猜你喜欢

转载自blog.csdn.net/yzy199391/article/details/88171492