乐趣区

关于spring:SpringSpringAOP的使用

前言

AOP(Aspect Oriented Programming) 面向切面编程 。定义一个通用性能,而后通过申明的形式来指定这个性能什么时候用,什么中央用。思考一个场景:给到一个我的项目,在接口层编写了大量的Controller,能够将这些Controller 进行如下示意。

此时须要对所有 Controller 中的接口的拜访次数进行统计,最间接的办法,就是在每个 Controller 中进行统计,示意如下。

因为统计的逻辑在每个 Controller 中都须要写一次,存在大量反复代码,并且后续批改起来很不不便,所以能够思考将统计的逻辑独自提取到一个类中,示意如下。

上述构造曾经能够达到以大量代码进行所有 Controller 中的接口的拜访次数的统计,然而如果 Controller 过多,在每个 Controller 中都须要引入实现统计性能的类,以及后续增加新的 Controller 时,也须要在新的 Controller 中引入统计性能的类,所以只管上述的构造能够实现性能,然而操作起来还是不便并且容易出错。如果将统计的性能放在切面中并将切面织入每个 Controller 中,那么能够以更少的代码并以更优雅的形式来实现接口拜访次数的统计,示意如下。

定义好的切面会织入以后曾经存在的 Controller 中以及后续增加的 Controller 中,极大缩小了反复代码,并且统计接口的拜访次数的性能和 Controller 高度解耦。通常,切面多用于 性能统计 日志记录 异样解决 平安解决 等。

本篇文章将联合读书时的一篇学习笔记,对 Spring 中的 AOP 的概念和应用进行学习和探讨,最初会应用切面来实现对所有 Controller 中的接口的拜访次数统计的实现。

Spring 版本:5.3.2
Springboot 版本:2.4.1

注释

一. AOP 的根底概念

AOP 中,存在着如下的概念。

  • 告诉(Advice
  • 切点(Pointcut
  • 连接点(JoinPoint
  • 切面(Aspect
  • 织入(Weaving

上面将别离对上述概念进行解释。

1. 连接点

连接点 最艰深的含意就是切面能够作用的中央,再说艰深一点,就是应用程序提供给切面插入的中央,上面的表格上给出了一些常见的连接点,联合表格能够加深对连接点的了解。留神:横线划掉的局部是 AspectJ 反对但 SpringAOP 不反对的连接点。

连接点 阐明 补充解释
method call 办法调用,即办法调用的中央就是一个连接点。 某行代码调用了某个办法,那么这行代码能够称为一个连接点。
method execute 办法执行,即办法执行时这个办法就是一个连接点。 和办法调用连接点不同,办法执行连接点是聚焦于某个办法执行,此时这个办法是一个连接点,办法执行连接点也是 SpringAOP 次要的连接点。
constructor call 构造方法调用,即构造方法调用的中央就是一个连接点 和办法调用连接点一样,AspectJ反对但 SpringAOP 不反对。
constructor execute 构造方法执行,即构造方法执行时这个构造方法就是一个连接点。 试验了一下,SpringAOP无奈反对切面作用于构造方法,这和 SpringAOP 是基于动静代理的实现无关。
field get 获取变量 AspectJ反对但 SpringAOP 不反对
field set 设置变量 AspectJ反对但 SpringAOP 不反对

除了上述表格列举的连接点外,还有其余的连接点诸如 handler(异样解决)static initialization(类初始化)等,然而这些连接点中,只有 method executeSpeingAOP反对的,这和 SpringAOP 的底层实现无关,所以当初就忘掉上述的被横线划掉的连接点吧,专一于method execute

2. 切点

切点 是连接点的汇合,申明一个切点就确定了一堆连接点,即确定了应用程序中切面能够插入的中央的汇合。在 SpringAOP 中,是应用的 AspectJ 的切点表达式来定义的切点,上面是一个示例。

@Pointcut("execution(* com.spring.aop..*.*(..))")
private void allMethodExecutePointcut() {}

如果素来没有接触过AOP,那么上述的切点定义可能第一眼看过来会感觉比较复杂,上面将对切点的定义进行逐帧的解读。

首先看下图。

@Pointcut注解示意在申明切点,能够先看一下 @Pointcut 注解的定义,如下所示。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {

    // 切点表达式
    String value() default "";
    
    // 参数名字相干
    String argNames() default "";}

临时不探讨 argNames(),所以应用@Pointcut 注解申明切点时须要提供切点表达式。在 SpringAOP 中,切点表达式次要是基于 execution@annotation编写,必要时还会搭配 within,上述示例是应用的execution,所以先阐明execution 的语法,首先明确一个概念:SpringAOP的切点表达式就是为了匹配办法,即最终的落脚点是办法 execution 的语法能够由下图进行阐明。

那么上述切点表达式就是匹配 com.spring.aop 包下的所有子包里所有类的所有办法,将这个切点表达式设置给 @Pointcut 注解申明进去的切点示意切面能够插入 com.spring.aop 包下的所有子包里所有类的所有办法。

上面再给出 @annotation 的语法,如下所示。

上述切点表达式就是匹配由 @GetMapping 注解润饰的所有办法。在 SpringAOP 中用得较多的还有应用 within 的切点表达式,within中能够指定类,指定的类中的所有办法都是匹配指标,上面用一个示例进行阐明。

上述切点表达式就是匹配 com.spring.aop 包下的除开 com.spring.aop.controller 包及其子包的所有子包里所有类的所有办法。

3. 告诉

告诉就是定义切面须要做什么,什么时候做。告诉的分类能够由下表进行阐明(基于SpringAOP)。

告诉 含意
前置告诉(@Before 在指标办法执行前执行特定逻辑。
后置告诉(@After 在指标办法执行后执行特定逻辑。
返回告诉(@AfterReturning 在指标办法执行后,能够捕捉指标办法的返回值并执行特定逻辑,比方对返回值进行批改。
异样告诉(@AfterThrowing 在指标办法执行并抛出异样时执行特定逻辑。
盘绕告诉(@Around) 可能获取到指标办法,并能够决定指标办法是否执行,同时能够在指标办法执行前和执行后执行特定逻辑。

告诉的概念就先介绍到这里,上面介绍完切面的概念并联合例子,告诉是啥也就高深莫测。

4. 切面

切面就是告诉 + 切点,即告诉和切点独特定义了切面的全部内容:须要做什么,什么时候做,作用在哪里 。上面以一个示例对SpringAOP 的切面进行阐明。

@Aspect
@Component
public class ExampleAspect {@Pointcut("execution(* com.spring.aop..*.*(..))")    // 作用在哪里
    private void allMethodExecutePointcut() {}

    @Before("allMethodExecutePointcut()")    // 什么时候做
    public void doBeforeMethodExecute(JoinPoint joinPoint) {
        ......
        // 须要做什么
    }

}

注解 @Aspect 申明 ExampleAspect 为切面,注解 @Component 表明 ExampleAspect 须要由 Spring 容器治理。在 ExampleAspect 切面中,定义了一个切点并由 allMethodExecutePointcut() 作为标识符,而后由 @Before 注解润饰的 doBeforeMethodExecute() 办法就是一个前置告诉,该前置告诉作用的中央由 allMethodExecutePointcut() 切点确定。

在切面里的告诉中,能够获取到 JoinPoint 即以后切面作用的连接点,通过连接点能够获取到连接点即指标办法的信息,JoinPoint类图如下所示。

通过 JoinPoint 能够获取到办法名,办法参数以及办法所在类的信息。

5. 织入

织入示意把切面利用到连接点的过程。在指标的生命周期里,有多个工夫点能够进行织入,详见下表。

工夫点 阐明 适用范围
编译期 切面在指标类编译时就织入。 AspectJ
类加载期 切面在指标类被加载到 JVM 时织入。 AspectJ
运行期 切面在利用程序运行的某个时刻织入,织入切面时,会为指标对象创立动静代理对象。 SpringAOP

6. SpringAOP 与 AspectJ 的分割

AspectJ是一个面向切面的框架,AspectJ定义了 AOP 语法。

SpringAOPSpring 基于动静代理的 AOP 框架,纯 Java 语言实现,Spring框架反对四种类型的AOP,详见下表。

概述 阐明 分类
基于动静代理的经典SpringAOP Spring的经典AOP,轻便且简单。 SpringAOP
POJO 切面 须要 XML 配置。 SpringAOP
AspectJ注解驱动的切面 Spring借鉴 AspectJ 切面提供的注解驱动的 AOP,实质上还是Spring 基于动静代理的 AOP,然而编程模型与AspectJ 注解驱动的切面齐全一样。 SpringAOP
注入式 AspectJ 切面 应用 AspectJ 框架来实现 AOP 性能,在 AOP 需要的性能超过了简略的办法拦挡调用时,就须要应用 AspectJ 来编写切面。 AspectJ

(上表中横线划掉的局部为个别不应用的 AOP)只管SpringAOP 的编程模型和 AspectJ 的编程模型保持一致,然而底层实现上,SpringAOP是基于动静代理,那么 SpringAOP 的作用范畴就局限在了办法的拦挡调用上,而 AspectJ 有专门的编译器,能够操作字节码,所以使得 AspectJ 可能作用的范畴更广,实现的性能更强。下表是一个 SpringAOPAspectJ的直观比照。

比照项 SpringAOP AspectJ
实现 Java语言实现 Java语言的扩大实现
编译器 无需要 须要 AspectJ 编译器
功能性 不强,对 AOP 的反对仅局限于办法的拦挡和调用
作用对象 只能作用在 Spring 容器治理的 bean 所有对象上
速度

二. 示例演示 -@Before

最好的了解办法就是入手试验,从本大节开始,将以 Springboot 搭建的工程为根底,以最简略的例子进行 SpringAOP 的最直观的演示。新建 Maven 我的项目,POM文件配置如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath/>
    </parent>

    <groupId>com.spring.aop</groupId>
    <artifactId>spring-aop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>

先编写一个HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

}

HelloController中提供了一个获取 Hello WorldGET接口。而后编写一个日志打印切面LogAspect,如下所示。

@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* com.spring.aop..*.*(..))")
    private void allMethodExecutePointcut() {}

    @Before("allMethodExecutePointcut()")
    public void logBeforeMethodExecute(JoinPoint joinPoint) {Signature signature = joinPoint.getSignature();
        String declaringTypeName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        log.info(declaringTypeName + "." + methodName + "execute.");
    }

}

LogAspect切面做的事件就是在 com.spring.aop 包下所有子包的所有类中的办法执行前,打印本次执行的办法的办法名和办法所在类的全限定名。

整体工程的目录构造如下所示。

当初启动 Springboot,并应用rest 工具调用 /aop/v1/sayhello 接口,日志打印如下。

可见 LogAspect 中的 logBeforeMethodExecute() 告诉在 HelloControllersayHelloV1()办法执行前执行了。

当初进行一点批改,新建一个HelloService,如下所示。

@Slf4j
@Service
public class HelloService {public String sayHello() {log.info("method com.spring.aop.service.HelloService.sayHello execute.");
        return HelloController.HELLO_WORLD;
    }

}

批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

}

整体工程的目录构造变更如下。

当初启动 Springboot,并应用rest 工具调用 /aop/v2/sayhello 接口,日志打印如下。

因为 HelloControllersayHelloV2()办法与 HelloServicesayHello()办法均在 LogAspect 切面中的 logBeforeMethodExecute() 告诉的作用范畴内,所以两个办法执行前,logBeforeMethodExecute()告诉均执行了。当初对 LogAspect 进行批改,以使得 logBeforeMethodExecute() 告诉的作用范畴不蕴含 com.spring.aop.service 包下所有子包的所有类中的办法,如下所示。

@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* com.spring.aop..*.*(..)) && !within(com.spring.aop.service..*)")
    private void allMethodExecutePointcut() {}

    @Before("allMethodExecutePointcut()")
    public void logBeforeMethodExecute(JoinPoint joinPoint) {Signature signature = joinPoint.getSignature();
        String declaringTypeName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        log.info(declaringTypeName + "." + methodName + "execute.");
    }

}

当初启动 Springboot,并应用rest 工具调用 /aop/v2/sayhello 接口,日志打印如下。

如上所示,执行后果合乎预期,LogAspect切面中的 logBeforeMethodExecute() 告诉不再作用于 HelloService 的办法。

三. 示例演示 -@After

沿用第二大节中的工程持续演示。批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v3/sayhello")
    public ResponseEntity<String> sayHelloV3(HttpServletRequest request) {log.info("interface /aop/v3/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

}

即增加了一个由 @GetMapping 注解润饰的接口。批改LogAspect,如下所示。

@Slf4j
@Aspect
@Component
public class LogAspect {@Pointcut("execution(* com.spring.aop..*.*(..)) && !within(com.spring.aop.service..*)")
    private void allMethodExecutePointcut() {}

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    private void allGetMappingPointcut() {}

    @Before("allMethodExecutePointcut()")
    public void logBeforeMethodExecute(JoinPoint joinPoint) {Signature signature = joinPoint.getSignature();
        String declaringTypeName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        log.info(declaringTypeName + "." + methodName + "execute.");
    }

    @After("allGetMappingPointcut()")
    public void logAfterGetMappingExecute(JoinPoint joinPoint) {Signature signature = joinPoint.getSignature();
        String declaringTypeName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        log.info(declaringTypeName + "." + methodName + "execute.");
    }

}

上述 LogAspect 中,减少了 allGetMappingPointcut() 切点,其作用范畴是全副由 @GetMapping 注解润饰的办法,而后减少了 logAfterGetMappingExecute() 告诉,其逻辑是在指标办法执行后打印信息。

当初启动 Springboot,并应用rest 工具调用 /aop/v3/sayhello 接口,日志打印如下。

可见 LogAspect 中的 logAfterGetMappingExecute() 告诉在 HelloControllersayHelloV3()办法执行后执行了。

四. 示例演示 -@AfterReturning

沿用第三大节中的工程,然而将 LogAspect 切面正文掉。新建一个类HelloWorld,如下所示。

@Data
public class HelloWorld {private LocalDateTime dateTime;}

批改HelloService,如下所示。

@Slf4j
@Service
public class HelloService {public String sayHello() {log.info("method com.spring.aop.service.HelloService.sayHello execute.");
        return HelloController.HELLO_WORLD;
    }

    public HelloWorld getHelloWorld() {log.info("method com.spring.aop.service.HelloService.getHelloWorld execute.");
        return new HelloWorld();}

}

新增一个切面HelloWorldAspect,如下所示。

@Slf4j
@Aspect
@Component
public class HelloWorldAspect {@Pointcut("execution(* com.spring.aop.service.HelloService.getHelloWorld(..))")
    private void getHelloWorldPointcut() {}

    @AfterReturning(pointcut = "getHelloWorldPointcut()", returning = "result")
    public void assembleHelloWorld(JoinPoint joinPoint, Object result) {HelloWorld helloWorld = (HelloWorld) result;
        helloWorld.setDateTime(LocalDateTime.now());
    }

}

HelloWorldAspect切面中的 getHelloWorldPointcut() 切点准确匹配到 HelloServicegetHelloWorld()办法,而后 assembleHelloWorld() 告诉会在 getHelloWorld() 办法执行完返回 HelloWorld 后捕捉到这个返回值对象,并丰盛其 dateTime 字段。

批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v3/sayhello")
    public ResponseEntity<String> sayHelloV3(HttpServletRequest request) {log.info("interface /aop/v3/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v4/sayhello")
    public ResponseEntity<HelloWorld> sayHelloV4(HttpServletRequest request) {log.info("interface /aop/v4/sayhello execute.");
        return new ResponseEntity<>(helloService.getHelloWorld(), HttpStatus.CREATED);
    }

}

整体工程的目录构造变更如下。

当初启动 Springboot,并应用rest 工具调用 /aop/v4/sayhello 接口,返回后果如下所示。

如上所示,只管在 HelloServicegetHelloWorld()办法中没有为创立的 HelloWorld 设置 dateTime 字段,然而 HelloWorldAspect 切面的 assembleHelloWorld() 告诉捕捉到了 getHelloWorld() 办法的返回值并丰盛了 dateTime 字段。

五. 示例演示 -@AfterThrowing

沿用第四大节中的工程。首先在 HelloService 中增加一个只有调用就会抛出运行时异样的办法,如下所示。

@Slf4j
@Service
public class HelloService {public String sayHello() {log.info("method com.spring.aop.service.HelloService.sayHello execute.");
        return HelloController.HELLO_WORLD;
    }

    public HelloWorld getHelloWorld() {log.info("method com.spring.aop.service.HelloService.getHelloWorld execute.");
        return new HelloWorld();}

    public String sayHelloButThrowException() {log.info("method com.spring.aop.service.HelloService.sayHelloButThrowException execute.");
        throw new RuntimeException("Exception was thrown.");
    }

}

批改 HelloWorldAspect 切面,如下所示。

@Slf4j
@Aspect
@Component
public class HelloWorldAspect {@Pointcut("execution(* com.spring.aop.service.HelloService.getHelloWorld(..))")
    private void getHelloWorldPointcut() {}

    @Pointcut("execution(* com.spring.aop.service.HelloService.sayHelloButThrowException(..))")
    private void throwExceptionPointcut() {}

    @AfterReturning(pointcut = "getHelloWorldPointcut()", returning = "result")
    public void assembleHelloWorld(JoinPoint joinPoint, Object result) {HelloWorld helloWorld = (HelloWorld) result;
        helloWorld.setDateTime(LocalDateTime.now());
    }

    @AfterThrowing(pointcut = "throwExceptionPointcut()", throwing = "e")
    public void logExceptionInfo(JoinPoint joinPoint, Throwable e) {log.info(e.getMessage());
    }

}

HelloWorldAspect切面新增了一个准确匹配到 HelloServicesayHelloButThrowException()办法的切点,并且还新增了一个 logExceptionInfo() 告诉,其会在 sayHelloButThrowException() 办法抛出异样时执行,执行逻辑是打印异样信息。

批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v3/sayhello")
    public ResponseEntity<String> sayHelloV3(HttpServletRequest request) {log.info("interface /aop/v3/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v4/sayhello")
    public ResponseEntity<HelloWorld> sayHelloV4(HttpServletRequest request) {log.info("interface /aop/v4/sayhello execute.");
        return new ResponseEntity<>(helloService.getHelloWorld(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v5/sayhello")
    public ResponseEntity<String> sayHelloV5(HttpServletRequest request) {log.info("interface /aop/v5/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHelloButThrowException(), HttpStatus.CREATED);
    }

}

当初启动 Springboot,并应用rest 工具调用 /aop/v5/sayhello 接口,日志打印如下。

如上所示,HelloWorldAspect切面的 logExceptionInfo() 告诉在 HelloServicesayHelloButThrowException()办法抛出异样后执行了。

六. 示例演示 -@Around

沿用第五大节中的工程,正文掉 HelloWorldAspect 切面。首先自定义一个注解,如下所示。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ShieldExecute {}

顾名思义,冀望所有由 @ShieldExecute 注解润饰的办法会被屏蔽掉即不执行。

而后批改HelloService,如下所示。

@Slf4j
@Service
public class HelloService {public String sayHello() {log.info("method com.spring.aop.service.HelloService.sayHello execute.");
        return HelloController.HELLO_WORLD;
    }

    public HelloWorld getHelloWorld() {log.info("method com.spring.aop.service.HelloService.getHelloWorld execute.");
        return new HelloWorld();}

    public String sayHelloButThrowException() {log.info("method com.spring.aop.service.HelloService.sayHelloButThrowException execute.");
        throw new RuntimeException("Exception was thrown.");
    }

    @ShieldExecute
    public void justSayHello() {log.info(HelloController.HELLO_WORLD);
    }

}

HelloService 中增加了一个由 @ShieldExecute 注解润饰的 justSayHello() 办法,justSayHello()办法本来的逻辑就是仅仅打印一句Hello World

增加一个切面ShieldAspect,如下所示。

@Slf4j
@Aspect
@Component
public class ShieldAspect {@Pointcut("@annotation(com.spring.aop.annotation.ShieldExecute)")
    private void shieldMethodPointcut() {}

    @Around("shieldMethodPointcut()")
    public Object shieldMethod(ProceedingJoinPoint joinPoint) throws Throwable {return null;}

}

ShieldAspect 切面中增加了一个匹配所有由 @ShieldExecute 注解润饰的办法的切点 shieldMethodPointcut(),而后定义了一个告诉shieldMethod(),其会克制shieldMethodPointcut() 切点匹配到的办法的执行,如果办法有返回值,则固定返回null

批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v3/sayhello")
    public ResponseEntity<String> sayHelloV3(HttpServletRequest request) {log.info("interface /aop/v3/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v4/sayhello")
    public ResponseEntity<HelloWorld> sayHelloV4(HttpServletRequest request) {log.info("interface /aop/v4/sayhello execute.");
        return new ResponseEntity<>(helloService.getHelloWorld(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v5/sayhello")
    public ResponseEntity<String> sayHelloV5(HttpServletRequest request) {log.info("interface /aop/v5/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHelloButThrowException(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v6/sayhello")
    public ResponseEntity<String> sayHelloV6(HttpServletRequest request) {log.info("interface /aop/v6/sayhello execute.");
        helloService.justSayHello();
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

}

整体工程的目录构造变更如下。

当初启动 Springboot,并应用rest 工具调用 /aop/v6/sayhello 接口,日志打印如下。

如上所示,克制了 HelloServicejustSayHello()办法的执行。

依据 ShieldAspect 切面的实现可知,shieldMethod()盘绕告诉办法的第一个形参是 ProceedingJoinPoint 接口,其继承于JoinPoint,类图如下所示。

ProceedingJoinPoint接口的最重要的办法为proceed(),该办法的官网阐明如下。

Proceed with the next advice or target method invocation.

ProceedingJoinPointproceed()办法调用后,如果有其它的告诉则执行其它的告诉的逻辑,如果没有则执行指标办法。因而,只有不调用 proceed() 办法,就能够做到克制指标办法的执行。相应的,盘绕告诉基于 ProceedingJoinPointproceed()办法,能够齐全决定在指标办法执行前后执行什么逻辑,以及齐全决定指标办法是否执行,在什么状况下执行。

再自定义一个注解,如下所示。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnhanceExecute {}

即冀望所有由 @EnhanceExecute 注解润饰的办法在执行时能够实现一些加强的性能。当初编写一个 EnhanceAspect 切面,如下所示。

@Slf4j
@Aspect
@Component
public class EnhanceAspect {@Pointcut("@annotation(com.spring.aop.annotation.EnhanceExecute)")
    private void enhanceMethodPointcut() {}

    @Around("enhanceMethodPointcut()")
    public Object enhanceMethod(ProceedingJoinPoint joinPoint) throws Throwable {log.info("enhance before method execute.");
        Object proceed = joinPoint.proceed();
        log.info("enhance after method execute.");
        return proceed;
    }

}

EnhanceAspect切面的 enhanceMethod() 告诉会在所有由 @EnhanceExecute 注解润饰的办法执行前和执行后打印一些日志。当初为 HelloService 增加一个办法并由 @EnhanceExecute 注解润饰,如下所示。

@Slf4j
@Service
public class HelloService {public String sayHello() {log.info("method com.spring.aop.service.HelloService.sayHello execute.");
        return HelloController.HELLO_WORLD;
    }

    public HelloWorld getHelloWorld() {log.info("method com.spring.aop.service.HelloService.getHelloWorld execute.");
        return new HelloWorld();}

    public String sayHelloButThrowException() {log.info("method com.spring.aop.service.HelloService.sayHelloButThrowException execute.");
        throw new RuntimeException("Exception was thrown.");
    }

    @ShieldExecute
    public void justSayHello() {log.info(HelloController.HELLO_WORLD);
    }

    @EnhanceExecute
    public void continueSayHello() {log.info(HelloController.HELLO_WORLD);
    }

}

批改HelloController,如下所示。

@Slf4j
@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    public static final String HELLO_WORLD = "Hello World";

    @RequestMapping(value = "/aop/v1/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV1(HttpServletRequest request) {log.info("interface /aop/v1/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/aop/v2/sayhello", method = RequestMethod.GET)
    public ResponseEntity<String> sayHelloV2(HttpServletRequest request) {log.info("interface /aop/v2/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHello(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v3/sayhello")
    public ResponseEntity<String> sayHelloV3(HttpServletRequest request) {log.info("interface /aop/v3/sayhello execute.");
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v4/sayhello")
    public ResponseEntity<HelloWorld> sayHelloV4(HttpServletRequest request) {log.info("interface /aop/v4/sayhello execute.");
        return new ResponseEntity<>(helloService.getHelloWorld(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v5/sayhello")
    public ResponseEntity<String> sayHelloV5(HttpServletRequest request) {log.info("interface /aop/v5/sayhello execute.");
        return new ResponseEntity<>(helloService.sayHelloButThrowException(), HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v6/sayhello")
    public ResponseEntity<String> sayHelloV6(HttpServletRequest request) {log.info("interface /aop/v6/sayhello execute.");
        helloService.justSayHello();
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

    @GetMapping(value = "/aop/v7/sayhello")
    public ResponseEntity<String> sayHelloV7(HttpServletRequest request) {log.info("interface /aop/v7/sayhello execute.");
        helloService.continueSayHello();
        return new ResponseEntity<>(HELLO_WORLD, HttpStatus.CREATED);
    }

}

整体工程的目录构造变更如下。

当初启动 Springboot,并应用rest 工具调用 /aop/v7/sayhello 接口,日志打印如下。

七. 统计接口拜访次数实战

沿用第六大节的工程,正文掉 ShieldAspectEnhanceAspect切面。新增一个统计接口CountApi,如下所示。

public interface CountApi<T> {void count(T t);

}

编写 VisitCountService 实现 CountApi 接口,如下所示。

@Slf4j
@Service
public class VisitCountService implements CountApi<HttpServletRequest> {private final Lock lock = new ReentrantLock();

    private final Map<String, Integer> countMap
            = new ConcurrentHashMap<>();

    @Override
    public void count(HttpServletRequest request) {lock.lock();
        try {String requestURI = request.getRequestURI();
            countMap.merge(requestURI, 1, Integer::sum);
            log.info(requestURI + "count is =" + countMap.get(requestURI));
        } finally {lock.unlock();
        }
    }

}

当初编写一个切面VisitCountAspect,如下所示。

@Slf4j
@Aspect
@Component
public class VisitCountAspect {

    @Autowired
    private CountApi<HttpServletRequest> visitCountService;

    @Pointcut("execution(* com.spring.aop.controller..*.*(..))")
    private void allControllerMethodPointcut() {}

    @Before("allControllerMethodPointcut()")
    public void countVisit() {RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes != null) {HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest();
            visitCountService.count(request);
        } else {log.warn("No request found.");
        }
    }

}

整体工程的目录构造变更如下。

当初启动 Springboot,并应用rest 工具别离调用 /aop/v1/sayhello 接口和 /aop/v3/sayhello 接口三次,日志打印如下。

如上所示,能够胜利统计每个接口的拜访次数。

总结

SpringAOP次要关注于办法,实现对办法的拦挡和调用。通过切点表达式定义进去的切点,能够准确或含糊的匹配到连接点即指标办法,再通过告诉与切点组合就能够确定在哪些办法的哪些阶段执行哪些逻辑。
最初,深究切面的各种概念不如理论搭建一个工程编写一个切面,应用过切面后,了解就会变得逐步粗浅。

退出移动版