Spring入门(十):Spring AOP使用讲解

1. 什么是AOP?

AOP是Aspect Oriented Programming的缩写,意思是:面向切面编程,它是经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。java

能够认为AOP是对OOP(Object Oriented Programming 面向对象编程)的补充,主要使用在日志记录,性能统计,安全控制等场景,使用AOP可使得业务逻辑各部分之间的耦合度下降,只专一于各自的业务逻辑实现,从而提升程序的可读性及维护性。git

好比,咱们须要记录项目中全部对外接口的入参和出参,以便出现问题时定位缘由,在每个对外接口的代码中添加代码记录入参和出参固然也能够达到目的,可是这种硬编码的方式很是不友好,也不够灵活,并且记录日志自己和接口要实现的核心功能没有任何关系。github

此时,咱们能够将记录日志的功能定义到1个切面中,而后经过声明的方式定义要在什么时候何地使用这个切面,而不用修改任何1个外部接口。正则表达式

在讲解具体的实现方式以前,咱们先了解几个AOP中的术语。spring

1.1 通知(Advice)

在AOP术语中,切面要完成的工做被称为通知,通知定义了切面是什么以及什么时候使用。编程

Spring切面有5种类型的通知,分别是:安全

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

1.2 链接点(Join point)

链接点是在应用执行过程当中可以插入切面的一个点,这个点能够是调用方法时、抛出异常时、修改某个字段时。微信

1.3 切点(Pointcut)

切点是为了缩小切面所通知的链接点的范围,即切面在何处执行。咱们一般使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定切点。app

1.4 切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的所有内容:它是什么,在什么时候和何处完成其功能。ide

1.5 引入(Introduction)

引入容许咱们在不修改现有类的基础上,向现有类添加新方法或属性。

1.6 织入(Weaving)

织入是把切面应用到目标对象并建立新的代理对象的过程。

切面在指定的链接点被织入到目标对象中,在目标对象的生命周期里,有如下几个点能够进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式须要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式须要特殊的类加载器(ClassLoader),它能够在目标类被引入应用以前加强该目标类的字节码。
  • 运行期:切面在应用运行的某个时刻被织入。通常状况下,在织入切面时,AOP容器会为目标对象动态地建立一个代理对象。Spring AOP就是以这种方式织入切面的。

2. Spring 对AOP的支持

2.1 动态代理

Spring AOP构建在动态代理之上,也就是说,Spring运行时会为目标对象动态建立代理对象。

代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。

当代理类拦截到方法调用时,在调用目标bean方法以前,会执行切面逻辑。

2.2 织入切面时机

经过在代理类中包裹切面,Spring在运行期把切面织入到Spring 管理的bean中,也就是说,直到应用须要被代理的bean时,Spring才会建立代理对象。

由于Spring运行时才建立代理对象,因此咱们不须要特殊的编译器来织入Spring AOP切面。

2.3 链接点限制

Spring只支持方法级别的链接点,若是须要字段级别或者构造器级别的链接点,能够利用AspectJ来补充Spring AOP的功能。

3. Spring AOP使用

假设咱们有个现场表演的接口Performance和它的实现类SleepNoMore:

package chapter04.concert;

/** * 现场表演,如舞台剧,电影,音乐会 */
public interface Performance {
    void perform();
}
复制代码
package chapter04.concert;

import org.springframework.stereotype.Component;

/** * 戏剧:《不眠之夜Sleep No More》 */
@Component
public class SleepNoMore implements Performance {
    @Override
    public void perform() {
        System.out.println("戏剧《不眠之夜Sleep No More》");
    }
}
复制代码

既然是演出,就须要观众,假设咱们的需求是:在看演出以前,观众先入座并将手机调整至静音,在观看演出以后观众鼓掌,若是演出失败观众退票,咱们固然能够把这些逻辑写在上面的perform()方法中,但不推荐这么作,由于这些逻辑理论上和演出的核心无关,就算观众不将手机调整至静音或者看完演出不鼓掌,都不影响演出的进行。

针对这个需求,咱们可使用AOP来实现。

3.1 定义切面

首先,在pom.xml文件中添加以下依赖:

<!--spring aop支持-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>
<!--aspectj支持-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.5</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</version>
</dependency>
复制代码

而后,定义一个观众的切面以下:

package chapter04.concert;

import org.aspectj.lang.annotation.Aspect;

/** * 观众 * 使用@Aspect注解定义为切面 */
@Aspect
public class Audience {
}
复制代码

注意事项:@Aspect注解代表Audience类是一个切面。

3.2 定义前置通知

在Audience切面中定义前置通知以下所示:

/** * 表演以前,观众就座 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void takeSeats() {
    System.out.println("Taking seats");
}

/** * 表演以前,将手机调至静音 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
}
复制代码

这里的重点代码是@Before("execution(* chapter04.concert.Performance.perform(..))"),它定义了1个前置通知,其中execution(* chapter04.concert.Performance.perform(..))被称为AspectJ切点表达式,每一部分的讲解以下:

  • @Before:该注解用来定义前置通知,通知方法会在目标方法调用以前执行
  • execution:在方法执行时触发
  • *:代表咱们不关心方法返回值的类型,便可以是任意类型
  • chapter04.concert.Performance.perform:使用全限定类名和方法名指定要添加前置通知的方法
  • (..):方法的参数列表使用(..),代表咱们不关心方法的入参是什么,便可以是任意类型

3.3 定义后置通知

在Audience切面中定义后置通知以下所示:

/** * 表演结束,无论表演成功或者失败 */
@After("execution(* chapter04.concert.Performance.perform(..))")
public void finish() {
    System.out.println("perform finish");
}
复制代码

注意事项:@After注解用来定义后置通知,通知方法会在目标方法返回或者抛出异常后调用

3.4 定义返回通知

在Audience切面中定义返回通知以下所示:

/** * 表演以后,鼓掌 */
@AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
}
复制代码

注意事项:@AfterReturning注解用来定义返回通知,通知方法会在目标方法返回后调用

3.5 定义异常通知

在Audience切面中定义异常通知以下所示:

/** * 表演失败以后,观众要求退款 */
@AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
public void demandRefund() {
    System.out.println("Demanding a refund");
}
复制代码

注意事项:@AfterThrowing注解用来定义异常通知,通知方法会在目标方法抛出异常后调用

3.6 定义可复用的切点表达式

细心的你可能会发现,咱们上面定义的5个切点中,切点表达式都是同样的,这显然是很差的,好在咱们可使用@Pointcut注解来定义可重复使用的切点表达式:

/** * 可复用的切点 */
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
复制代码

而后以前定义的5个切点均可以引用这个切点表达式:

/** * 表演以前,观众就座 */
@Before("perform()")
public void takeSeats() {
    System.out.println("Taking seats");
}

/** * 表演以前,将手机调至静音 */
@Before("perform()")
public void silenceCellPhones() {
    System.out.println("Silencing cell phones");
}

/** * 表演结束,无论表演成功或者失败 */
@After("perform()")
public void finish() {
    System.out.println("perform finish");
}

/** * 表演以后,鼓掌 */
@AfterReturning("perform()")
public void applause() {
    System.out.println("CLAP CLAP CLAP!!!");
}

/** * 表演失败以后,观众要求退款 */
@AfterThrowing("perform()")
public void demandRefund() {
    System.out.println("Demanding a refund");
}
复制代码

3.7 单元测试

新建配置类ConcertConfig以下所示:

package chapter04.concert;

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

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
    @Bean
    public Audience audience() {
        return new Audience();
    }
}
复制代码

注意事项:和以往不一样的是,咱们使用了@EnableAspectJAutoProxy注解,该注解用来启用自动代理功能。

新建Main类,在其main()方法中添加以下测试代码:

package chapter04.concert;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);

        Performance performance = context.getBean(Performance.class);
        performance.perform();

        context.close();
    }
}
复制代码

运行代码,输出结果以下所示:

Silencing cell phones

Taking seats

戏剧《不眠之夜Sleep No More》

perform finish

CLAP CLAP CLAP!!!

稍微修改下SleepNoMore类的perform()方法,让它抛出一个异常:

@Override
public void perform() {
    int number = 3 / 0;
    System.out.println("戏剧《不眠之夜Sleep No More》");
}
复制代码

再次运行代码,输出结果以下所示:

Silencing cell phones

Taking seats

perform finish

Demanding a refund

Exception in thread "main" java.lang.ArithmeticException: / by zero

由此也能够说明,无论目标方法是否执行成功,@After注解都会执行,但@AfterReturning注解只会在目标方法执行成功时执行。

值得注意的是,使用@Aspect注解的切面类必须是一个bean(无论以何种方式声明),不然切面不会生效,由于AspectJ自动代理只会为使用@Aspect注解的bean建立代理类。

也就是说,若是咱们将ConcertConfig配置类中的如下代码删除或者注释掉:

@Bean
public Audience audience() {
    return new Audience();
}
复制代码

运行结果将变为:

戏剧《不眠之夜Sleep No More》

3.8 建立环绕通知

咱们可使用@Around注解建立环绕通知,该注解可以让你在调用目标方法先后,自定义本身的逻辑。

所以,咱们以前定义的5个切点,如今能够定义在一个切点中,为不影响以前的切面,咱们新建切面AroundAudience,以下所示:

package chapter04.concert;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AroundAudience {
    /** * 可重用的切点 */
    @Pointcut("execution(* chapter04.concert.Performance.perform(..))")
    public void perform() {
    }

    @Around("perform()")
    public void watchPerform(ProceedingJoinPoint joinPoint) {
        try {
            System.out.println("Taking seats");
            System.out.println("Silencing cell phones");

            joinPoint.proceed();

            System.out.println("CLAP CLAP CLAP!!!");
        } catch (Throwable throwable) {
            System.out.println("Demanding a refund");
        } finally {
            System.out.println("perform finish");
        }
    }
}
复制代码

这里要注意的是,该方法有个ProceedingJoinPoint类型的参数,在方法中能够经过调用它的proceed()方法来调用目标方法。

而后修改下ConcertConfig类的代码:

package chapter04.concert;

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

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
    /*@Bean public Audience audience() { return new Audience(); }*/

    @Bean
    public AroundAudience aroundAudience() {
        return new AroundAudience();
    }
}
复制代码

运行结果以下所示:

Taking seats

Silencing cell phones

戏剧《不眠之夜Sleep No More》

CLAP CLAP CLAP!!!

perform finish

4. 源码及参考

源码地址:github.com/zwwhnly/spr…,欢迎下载。

Craig Walls 《Spring实战(第4版)》

AOP(面向切面编程)_百度百科

最后,欢迎关注个人微信公众号:「申城异乡人」,全部博客会同步更新。

相关文章
相关标签/搜索