前言
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@Componentpublic 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@RestControllerpublic 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@Componentpublic 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@Servicepublic class HelloService { public String sayHello() { log.info("method com.spring.aop.service.HelloService.sayHello execute."); return HelloController.HELLO_WORLD; }}
批改HelloController
,如下所示。
@Slf4j@RestControllerpublic 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@Componentpublic 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@RestControllerpublic 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@Componentpublic 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
,如下所示。
@Datapublic class HelloWorld { private LocalDateTime dateTime;}
批改HelloService
,如下所示。
@Slf4j@Servicepublic 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@Componentpublic 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@RestControllerpublic 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@Servicepublic 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@Componentpublic 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@RestControllerpublic 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)@Documentedpublic @interface ShieldExecute {}
顾名思义,冀望所有由@ShieldExecute
注解润饰的办法会被屏蔽掉即不执行。
而后批改HelloService
,如下所示。
@Slf4j@Servicepublic 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@Componentpublic 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@RestControllerpublic 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)@Documentedpublic @interface EnhanceExecute {}
即冀望所有由@EnhanceExecute
注解润饰的办法在执行时能够实现一些加强的性能。当初编写一个EnhanceAspect
切面,如下所示。
@Slf4j@Aspect@Componentpublic 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@Servicepublic 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@RestControllerpublic 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@Servicepublic 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@Componentpublic 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
次要关注于办法,实现对办法的拦挡和调用。通过切点表达式定义进去的切点,能够准确或含糊的匹配到连接点即指标办法,再通过告诉与切点组合就能够确定在哪些办法的哪些阶段执行哪些逻辑。
最初,深究切面的各种概念不如理论搭建一个工程编写一个切面,应用过切面后,了解就会变得逐步粗浅。