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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理