共计 23278 个字符,预计需要花费 59 分钟才能阅读完成。
前言
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 execute | 办法执行,即办法执行时这个办法就是一个连接点。 | 和办法调用连接点不同,办法执行连接点是聚焦于某个办法执行,此时这个办法是一个连接点,办法执行连接点也是 SpringAOP 次要的连接点。 |
AspectJ 反对但 SpringAOP 不反对。 |
||
SpringAOP 无奈反对切面作用于构造方法,这和 SpringAOP 是基于动静代理的实现无关。 |
||
AspectJ 反对但 SpringAOP 不反对 |
||
AspectJ 反对但 SpringAOP 不反对 |
除了上述表格列举的连接点外,还有其余的连接点诸如 handler(异样解决) 和static initialization(类初始化)等,然而这些连接点中,只有 method execute 是SpeingAOP
反对的,这和 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
语法。
SpringAOP
是 Spring
基于动静代理的 AOP
框架,纯 Java
语言实现,Spring
框架反对四种类型的AOP
,详见下表。
概述 | 阐明 | 分类 |
---|---|---|
SpringAOP |
Spring 的经典AOP ,轻便且简单。 |
SpringAOP |
SpringAOP |
||
AspectJ 注解驱动的切面 |
Spring 借鉴 AspectJ 切面提供的注解驱动的 AOP ,实质上还是Spring 基于动静代理的 AOP ,然而编程模型与AspectJ 注解驱动的切面齐全一样。 |
SpringAOP |
注入式 AspectJ 切面 |
应用 AspectJ 框架来实现 AOP 性能,在 AOP 需要的性能超过了简略的办法拦挡调用时,就须要应用 AspectJ 来编写切面。 |
AspectJ |
(上表中横线划掉的局部为个别不应用的 AOP
)只管SpringAOP
的编程模型和 AspectJ
的编程模型保持一致,然而底层实现上,SpringAOP
是基于动静代理,那么 SpringAOP
的作用范畴就局限在了办法的拦挡调用上,而 AspectJ
有专门的编译器,能够操作字节码,所以使得 AspectJ
可能作用的范畴更广,实现的性能更强。下表是一个 SpringAOP
与AspectJ
的直观比照。
比照项 | 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 World 的GET接口。而后编写一个日志打印切面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()
告诉在 HelloController
的sayHelloV1()
办法执行前执行了。
当初进行一点批改,新建一个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
接口,日志打印如下。
因为 HelloController
的sayHelloV2()
办法与 HelloService
的sayHello()
办法均在 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()
告诉在 HelloController
的sayHelloV3()
办法执行后执行了。
四. 示例演示 -@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()
切点准确匹配到 HelloService
的getHelloWorld()
办法,而后 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
接口,返回后果如下所示。
如上所示,只管在 HelloService
的getHelloWorld()
办法中没有为创立的 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
切面新增了一个准确匹配到 HelloService
的sayHelloButThrowException()
办法的切点,并且还新增了一个 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()
告诉在 HelloService
的sayHelloButThrowException()
办法抛出异样后执行了。
六. 示例演示 -@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
接口,日志打印如下。
如上所示,克制了 HelloService
的justSayHello()
办法的执行。
依据 ShieldAspect
切面的实现可知,shieldMethod()
盘绕告诉办法的第一个形参是 ProceedingJoinPoint
接口,其继承于JoinPoint
,类图如下所示。
ProceedingJoinPoint
接口的最重要的办法为proceed()
,该办法的官网阐明如下。
Proceed with the next advice or target method invocation.
即 ProceedingJoinPoint
的proceed()
办法调用后,如果有其它的告诉则执行其它的告诉的逻辑,如果没有则执行指标办法。因而,只有不调用 proceed()
办法,就能够做到克制指标办法的执行。相应的,盘绕告诉基于 ProceedingJoinPoint
的proceed()
办法,能够齐全决定在指标办法执行前后执行什么逻辑,以及齐全决定指标办法是否执行,在什么状况下执行。
再自定义一个注解,如下所示。
@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
接口,日志打印如下。
七. 统计接口拜访次数实战
沿用第六大节的工程,正文掉 ShieldAspect
和EnhanceAspect
切面。新增一个统计接口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
次要关注于办法,实现对办法的拦挡和调用。通过切点表达式定义进去的切点,能够准确或含糊的匹配到连接点即指标办法,再通过告诉与切点组合就能够确定在哪些办法的哪些阶段执行哪些逻辑。
最初,深究切面的各种概念不如理论搭建一个工程编写一个切面,应用过切面后,了解就会变得逐步粗浅。