Spring框架之AOP面向切面编程

AOP面向切面

概念

1、什么是面向切面编程?

切面能帮助我们模块化关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。

项目开发过程中有些功能需要在多个模块中使用,比如:安全,事务等,那么就可以将安全和事务作为一个模块贯穿所有用到他们的模块中,类似一个切面,那么不需要每个模块自己单独实现。

横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。

好处:

首先现在每个关注点都集中于一个地方,而不是分散在多处代码中;

其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

2、AOP术语

通知(advice):在AOP术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。

​ Spring切面可以应用5中类型的通知:

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

连接点(Join Point):连接点是应用执行工程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,修改字段时。切面可以利用这些点插入到应用的正常流程中,添加新的行为。

切点(Pointcot):个人理解切点就是连接点在切面上的位置。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

切面(Aspect):切面是通知和切点的结合

引入(Introduction):引入允许我们想现有的类添加新方法或者属性

织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。

​ 在目标对象的生命周期里有多个点可以进行织入:

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

Spring对AOP的支持

Spring提供了4中类型的AOP支持:

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

前面三种都是SpringAOP实现的变体,SpringAOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截。

通过切点来选择连接点

Spring仅支持AspectJ切点指示器的一个子集,如下:

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

我们发现只有execution指示器是实际执行匹配的,其他指示器都是用于限制匹配的,这说明execution指示器是我们在编写切点定义时最主要使用的指示器。

首先创建一个主题,比如表演,可以有舞台剧表演,戏剧表演,歌舞表演等

public interface Performance {
     
     void perform();
}

execution的使用:

在这里插入图片描述

其中类名是完全限定名。

execution也可以与其他限制指示器结合使用
在这里插入图片描述

连接方式可以是&&(and )或者 || (or ) 或者 not(!)

使用注解创建切面

@AspectJ 支持三种通配符:

* :匹配任意字符,但它只能匹配上下文中的一个元素。

… :匹配任意字符,可以匹配上下文中的多个元素,参数的代表。

+ :匹配指定类的所有类,必须跟在类后面,如:com.tz.aspectj.IUserService+代表继承和扩展指定类的所有类,同时包括指定类本身。

Spring使用AspectJ注解来声明通知方法

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

那么创建一个观众类

//定义一个切面
@Aspect
public class Audience {
    //执行perform方法之前,即表演之前观众就坐
    @Before("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void silenceCellPhones(){
        System.out.println("表演前观众就坐");
    }
    //执行perform方法之前,即表演之前观众手机调静音
    @Before("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void takeSeats(){
        System.out.println("表演前观众手机调成静音");
    }
    //执行perform方法调用后,即表演成功观众喝彩
    @AfterReturning("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void applause(){
        System.out.println("表演精彩观众喝彩");
    }
    //执行perform方法调用异常时,即表演失败观众要求退票
    @AfterThrowing("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void demandRefund(){
        System.out.println("表演出状况观众要求退票");
    }
}

观众这些反应是基于同一个切点,所以这些切点可以整合,那么可以使用@Pointcut注解,如下:

@Aspect
public class Audience {

    /**
     *定义切点
     */
    @Pointcut("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void performance(){}

    @Before("performance()")
    public void silenceCellPhones(){
        System.out.println("表演前观众就坐");
    }

    @Before("performance()")
    public void takeSeats(){
        System.out.println("表演前观众手机调成静音");
    }

    @AfterReturning("performance()")
    public void applause(){
        System.out.println("表演精彩观众喝彩");
    }

    @AfterThrowing("performance()")
    public void demandRefund(){
        System.out.println("表演出状况观众要求退票");
    }
}

创建一个Performance的实现类

public class SingPerformanceImpl implements Performance {
    public void perform() {
        System.out.println("歌唱表演");
    }
}

若使用JavaConfig的话,通过配置类装配bean,并且使用 @EnableAspectJAutoProxy 注解启用自动代理功能。

@Configuration
@EnableAspectJAutoProxy
public class JavaConfig {

    @Bean("singPerformanceImpl")
    public SingPerformanceImpl getSingPerformance(){
        return new SingPerformanceImpl();
    }

    @Bean("audience")
    public Audience audience(){
        return new Audience();
    }
}

测试类如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProxyConfig.class)
public class AopTest {
    /**此处必须注入的是接口,若注入具体实现类会报错,类似Caused by:      org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'singPerformanceImpl' is expected to be of type 'com.cxd.springaop.service.impl.SingPerformanceImpl' but was actually of type 'com.sun.proxy.$Proxy26'
*/
    @Autowired
    private Performance singPerformanceImpl;

    @Test
    public void test(){
        singPerformanceImpl.perform();
    }
结果:
表演前观众就坐
表演前观众手机调成静音
歌唱表演
表演精彩观众喝彩

创建环绕通知

环绕通知是最为强大的通知类型。它能够让你所编写的逻辑被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

比如上面关于表演的例子,若使用环绕通知如下:

@Aspect
public class AroundAudience {

    /**
     *定义切点
     */
    @Pointcut("execution(* com.cxd.springaop.service.Performance.perform(..))")
    public void performance(){}
    
    @Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("表演前观众就坐");
            System.out.println("表演前观众手机调成静音");
            joinPoint.proceed();
            System.out.println("表演精彩观众喝彩");
        }catch (Throwable e){
            System.out.println("表演出状况观众要求退票");
        }
    }
}

这里使用到了 ProceedingJoinPoint 作为参数,这个对象必须要有,因为要在通知中用它来调用被通知的方法。若没有调用proceed()这个方法,那么就会阻塞对被通知方法的调用,当然如果场景需要可以这么干,比如调用之前出现状况了,例如表演之前下暴雨了没法表演了,那么就不再调用被通知的方法。

处理通知带参数

上面我们举例被通知的方法没有参数,那么我们看看如果有参数该怎么处理。

在这里插入图片描述

@Aspect
public class AudienceNum {

    @Pointcut("execution(* com.cxd.springaop.service.Performance.audienceNumber(int)) && args(audienceNum)")
    public void performance(int audienceNum){}

    @Around("performance(audienceNum)")
    public void watchPerformance(ProceedingJoinPoint joinPoint,int audienceNum){
        try {
            System.out.println("表演前观众就坐");
            System.out.println("表演前观众手机调成静音");
            System.out.println("清点观众人数");
            if (audienceNum >= 100){
                System.out.println("增加座位");
            }
            joinPoint.proceed();
            System.out.println("表演精彩观众喝彩");
        }catch (Throwable e){
            System.out.println("表演出状况观众要求退票");
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值