AOP技术介绍
AOP指的是面向切面编程(Aspect Oriented Programming),是一种无侵入式的编程范式,是一种设计思想,基于OCP(开闭原则),通过预编译方式和运行期动态代理方式,在不改变原来代码的基础上,添加其它的功能(AOP是基于代理模式来实现的),例如做日志功能等等。而Spring AOP是对面向切面编程的实现,具有强大的功能,属于Spring家族系列,开发者可以根据项目的需求合理的使用Spring AOP,方便快捷容易上手。
Spring AOP
了解Java的同僚都知晓面向对象编程OOP(Object Oriented Programming)的编程范式,而面向切面编程可能并不是那么好理解,或许经常用,但是每个概念具体表示的什么含义可能心中也没有底。利用面向切面编程可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。对于某些流程,就好比刚买新的手机玩一个游戏而言,那么一定包括这么几个粗略的流程:应用市场下载、账号登录、玩耍具体的某款游戏、退出登录。
可以明显看到,每次玩耍一款新游戏都会有相同的流程:应用市场下载、账号登陆、退出登录,每次都比较繁琐,不够灵活,而且耦合度极高,那么,把这些相同的流程剥离出来再进行切入(这里就可以看作是一个切面),就能很好的进行统一控制,对于开发而言,也能提高代码的优雅性和可观性。
上述例子虽然并不是很合理规范,但却很容易理解什么是面向切面编程,而且,OOP是纵向的,AOP是横向的
。
AOP优缺点
优点
- 提高代码的可重用性
- 降低代码的强耦合度
- 业务功能扩展更便捷
- 业务代码维护更高效
缺点
- 复杂性增强
- 额外的运行性能开销
- 流程难以理解
术语概念
1、连接点 JoinPoint
2、切入点 Pointcut
3、通知 Advice
4、切面 Aspect
5、织入 Weaving
6、目标对象 Target
连接点
可以被AOP控制的方法就是连接点。具体点就是程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。
切入点
简而言之就是找到连接点的方式,例如@Pointcut(value=“execution( ∗ * ∗ com.ssy.www. ∗ * ∗. ∗ * ∗( . . .. ..))”)、@Pointcut(value=“@annotation(com.ssy.www.anno.ReLoad)”),这种寻找连接点的切入方式就是切入点
通知
重复的逻辑,也就是共性功能(最终体现为一个方法)
切面
可以简单理解为所有的切点组成的一个面,在SpringBoot中,就相当于封装横切点的类信息(一般情况下基于注解的话具有@Aspectj注解的类都叫做切面类)
织入
把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象,这样的行为叫做织入。严谨来说就是织入就是一个生成动态代理对象并且将切面和目标对象方法编织成为约定流程的过程
目标对象
包含连接点的对象,也被称作被通知或被代理对象
通知类型
Spring AOP中的通知类型包括前置通知、后置通知、环绕通知、最终通知、异常通知。
通知类型概述
通知类型 | 处理情况 |
---|---|
前置通知 | 在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常,@Before |
后置通知 | 在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行,@AfterReturning |
环绕通知 | 环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行,@Around |
最终通知 | 在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容,@After |
异常通知 | 在连接点抛出异常后执行,@AfterThrowing |
注意:
@Aspect:严格来说,其不属于一种Advice,该注解主要用在类声明上,指明当前类是一个组织了切面逻辑的类,并且该注解中可以指定当前类是何种实例化方式,主要有三种:singleton、perthis和pertarget
@DeclareParents:其是一种Introduction类型的模型,在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现
平时一般说明的通知就是为前置、后置、最终、异常、环绕着五种通知(也可以称之为增强)
代码示例
此处采用Spring Boot集成的形式对通知类型进行逐个演示。演示之前需要引入必要的依赖,核心就是aop的依赖。
<?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>
<groupId>com.ssy.www</groupId>
<artifactId>aop-demo</artifactId>
<version>1.0</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--lombok工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger测试-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!--knife4j的测试界面ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!--swagger的测试界面ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<!--fastjson工具类-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<!--hutool工具集-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.2.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
application配置文件
server:
port: 9001
servlet:
context-path: /ssy
启动类
package com.ssy.www;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author ssy
* @className AopApplication
* @description AopApplication
* @date 2023-12-13 14:42:25
*/
@SpringBootApplication
@EnableSwagger2//启用swagger2测试
@EnableOpenApi//启用OpenApi注解,这个注解会配合swagger2进行测试
@EnableKnife4j//启用knife4j
public class AopApplication {
public static void main(String[] args) {
SpringApplication.run(AopApplication.class,args);
}
}
便于测试,直接使用swagger,需要进行写一个配置类交给Spring容器进行管理,便于运用
package com.ssy.www.config;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
/**
* @author ssy
* @className SwaggerConfig
* @description SwaggerConfig
* @date 2023-12-13 14:58:29
*/
@Configuration
public class SwaggerConfig {
@Bean
public Docket create(){
return new Docket(DocumentationType.OAS_30)
.host("127.0.0.1")
.groupName("AOP演示小组")
.enable(Boolean.TRUE)
.apiInfo(getApiInfo())
.select()
//便是匹配测试任意路径
.paths(PathSelectors.any())
//表示测试带有ApiOperation注解的方法或者类
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.build();
}
private ApiInfo getApiInfo(){
return new ApiInfoBuilder()
.title("面向切面编程")
.description("Spring AOP演示")
.version("1.0")
.contact(new Contact("ssy","http://127.0.0.1:9001/ssy/doc.html","ssy@qq.com"))
.build();
}
}
为了方便前后端数据的传输,封装一个工具类对参数进行传递,简易直观(小编这里使用了泛型,根据个人习惯,不太习惯泛型的伙伴可以使用Object定义data变量,去掉泛型即可)
package com.ssy.www.util;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author ssy
* @className R
* @description R
* @date 2023-12-13 14:47:33
*/
@Data
@ApiModel(value = "数据封装工具")
public class R<T> {
@ApiModelProperty(value = "返回编码", example = "", required = true)
private Integer code;
@ApiModelProperty(value = "数据信息", example = "", required = false)
private T data;
@ApiModelProperty(value = "备注信息", example = "", required = false)
private String msg;
private R(Integer code, T data, String msg){
this.code = code;
this.data = data;
this.msg = msg;
}
public static <E> R success(String msg){
return new R(200,null,msg);
}
public static <E> R success(){
return success("操作成功");
}
public static <E> R fail(String msg){
return new R(500,null,msg);
}
public static <E> R fail(){
return fail("操作失败");
}
}
创建一个演示使用的实体类传递参数信息
package com.ssy.www.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author ssy
* @className User
* @description User
* @date 2023-12-13 14:44:58
*/
@Data//lombok中的注解,自动生成getter、setter、equals、hashcode、toString等方法
@ApiModel(value = "用户信息")
public class User {
@ApiModelProperty(value = "编号", example = "", required = false)
private String id;
@ApiModelProperty(value = "姓名", example = "", required = false)
private String name;
@ApiModelProperty(value = "年龄", example = "", required = false)
private Integer age;
@ApiModelProperty(value = "性别", example = "", required = false)
private String sex;
}
控制层的接口代码
package com.ssy.www.controller;
import com.ssy.www.entity.User;
import com.ssy.www.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ssy
* @className UserController
* @description UserController
* @date 2023-12-13 14:57:38
*/
@RestController
@RequestMapping("/user")
@Api(tags = {"用户信息接口"})
public class UserController {
@PostMapping("/before")
@ApiOperation(value = "前置通知接口",httpMethod = "POST",response = R.class)
public R<User> before(@RequestBody User user){
System.out.println("UserController->before->" + user);
R success = R.success();
success.setData(user);
return success;
}
@PostMapping("/after")
@ApiOperation(value = "最终通知接口",httpMethod = "POST",response = R.class)
public R<User> after(@RequestBody User user){
System.out.println("UserController->after->" + user);
R success = R.success();
success.setData(user);
int i = 1/0;
return success;
}
@PostMapping("/around")
@ApiOperation(value = "环绕通知接口",httpMethod = "POST",response = R.class)
public R<User> around(@RequestBody User user){
System.out.println("UserController->around->" + user);
R success = R.success();
success.setData(user);
return success;
}
@PostMapping("/afterReturning")
@ApiOperation(value = "后置通知接口",httpMethod = "POST",response = R.class)
public R<User> afterReturning(@RequestBody User user){
System.out.println("UserController->afterReturning->" + user);
R success = R.success();
success.setData(user);
return success;
}
@PostMapping("/throwing")
@ApiOperation(value = "异常通知接口",httpMethod = "POST",response = R.class)
public R<User> afterThrowing(@RequestBody User user){
System.out.println("UserController->afterThrowing->" + user);
R success = R.success();
success.setData(user);
int i = 1/0;
return success;
}
}
切面类直接交给Spring容器进行管理
package com.ssy.www.aspect;
import com.ssy.www.entity.User;
import com.ssy.www.util.R;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author ssy
* @className AopAspect
* @description AopAspect
* @date 2023-12-13 15:23:52
*/
@Component
@Aspect//这个类就是切面类
public class AopAspect {
/**
* execution(* com.ssy.www.controller.UserController.before(..))这个就类似于切点
* 这个表达式代表所有权限下的所有返回类型com.ssy.www.controller.UserController.before方法
* ..表示参数可有可无
* <p/>
* 前置通知
*/
@Before(value = "execution(* com.ssy.www.controller.UserController.before(..))")
public void before(JoinPoint jp){//这个方法就是通知
System.out.println("前置通知开始了······");
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println(name);
}
/**
* 最终通知
*/
@After(value = "execution(* com.ssy.www.controller.UserController.after(..))")
public void after(JoinPoint jp){
System.out.println("最终通知开始了······");
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println(name);
}
/**
* 环绕通知
* ProceedingJoinPoint只能作用于环绕通知,其他通知不能使用,
* 这个类是JoinPoint的子类,接口上有明确说明:
* ProceedingJoinPoint exposes the proceed(..)
* method in order to support around advice in @AJ aspects
*/
@Around(value = "execution(* com.ssy.www.controller.UserController.around(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知开始了······");
Signature signature = pjp.getSignature();
String name = signature.getName();
System.out.println(name);
Object[] args = pjp.getArgs();
User user = null;
if(args[0] instanceof User) {
user = (User) args[0];
user.setName("李浩");
}
Object proceed = pjp.proceed(new Object[]{user == null ? args[0] : user});
if( proceed instanceof R){
R info = (R) proceed;
System.out.println(info);
user.setName("王八蛋");
info.setData(user);
System.out.println("环绕通知结束了······");
return info;
}else {
System.out.println("环绕通知结束了······");
return proceed;
}
}
/**
* 后置通知
*/
@AfterReturning(value = "execution(* com.ssy.www.controller.UserController.afterReturning(..))", returning = "info")
public Object afterReturning(JoinPoint jp, Object info){
System.out.println("后置通知开始了······");
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println(name);
System.out.println(info);
return info;
}
/**
*异常通知
*/
@AfterThrowing(value = "execution(* com.ssy.www.controller.UserController.afterThrowing(..))", throwing = "e")
public void afterThrowing(JoinPoint jp, Throwable e){
System.out.println("异常通知开始了······");
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println(name);
System.out.println(e.getMessage());
}
}
启动应用,浏览器输入http://127.0.0.1:9001/ssy/doc.html或者http://127.0.0.1:9001/ssy/swagger-ui/index.html通过swagger测试对应的通知接口
切点表达式
切点表达式包含类型匹配和注解匹配两种(小伙伴们可能还会看到有人说有切点表达式组合匹配的情况,其实就是各种表达式的组合表示形式,就类似于满足条件两个或以上的表示)
表达式 | 说明 |
---|---|
execution | 匹配方法切入点。根据表达式描述匹配方法,是最通用的表达式类型,可以匹配方法、类、包 |
within | 匹配指定类型。匹配指定类的任意方法,不能匹配接口 |
this | 匹配代理对象实例的类型,匹配在运行时对象的类型 |
target | 匹配目标对象实例的类型,匹配 AOP 被代理对象的类型 |
args | 匹配方法参数类型和数量,参数类型可以为指定类型及其子类 |
bean | 通过 bean 的 id 或名称匹配,支持 * 通配符 |
@within | 匹配指定类型是否含有注解。当定义类时使用了注解,该类的方法会被匹配,但在接口上使用注解不匹配 |
@annotation | 匹配方法是否含有注解。当方法上使用了注解,该方法会被匹配,在接口方法上使用注解不匹配 |
@args | 匹配方法参数类型是否含有注解。当方法的参数类型上使用了注解,该方法会被匹配 |
@target | 匹配目标对象实例的类型是否含有注解。当运行时对象实例的类型使用了注解,该类的方法会被匹配,在接口上使用注解不匹配 |
注意:this、target两种方式只有在目标对象实现接口,且 Spring 采用 JDK 动态代理时才有区别。由于 JDK 动态代理生成的代理对象与目标对象只是实现了相同的接口,而非相同类型的对象,因此不符合 this 语义
切点表达式组合,其实不是新鲜的东西,就是使用
&&
、||
和!
来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。对于某些复杂的场景或者为了提升匹配效率,可以用组合多种类型的表达式。平时一般使用的场景不算太多(当然这个得根据不同的项目和情况而定)
细节可以参考spring官网:👉https://spring.io
表达式类型
execution匹配方法
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
其中,❓部分可以省略
modifier:匹配修饰符,public, private、默认、protected,省略时匹配任意修饰符
ret-type:匹配返回类型(如:String、Object、Map、List等等),使用*
匹配任意类型
declaring-type:匹配目标类,省略时匹配任意类型
name-pattern:匹配方法名称,使用*
表示通配符
param-pattern:匹配参数类型和数量
()
匹配没有参数的方法
(..)
匹配有任意数量参数的方法
(*)
匹配有一个任意类型参数的方法
(*,String)
匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型throws-pattern:匹配抛出异常类型,省略时匹配任意类型
许多情况下会按照类型的匹配,类型匹配的语法如下:
- ∗ * ∗:匹配任何数量字符
- . . .. ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数(0个或者多个参数)
- + + +:匹配指定类型及其子类型;仅能作为后缀放在类型模式后边
在控制层UserController类中新增加测试接口方法
@PostMapping("/execution")
@ApiOperation(value = "execution表达式接口",httpMethod = "POST",response = R.class)
public R<User> execution(){
User user = new User();
user.setId("12306");
user.setName("王二小");
user.setSex("男");
user.setAge(12);
R out = R.success();
out.setData(user);
return out;
}
在切面类AopAspect中新增通知和切点
@Pointcut(value = "execution(* com.ssy.www.controller.UserController.execution())")
public void pointcut(){}
/**
* @Before(value = "execution(* com.ssy.www.controller.UserController.execution())")
* 也是和@Before(value = "pointcut()")等价的,
* 如果很多通知都用到同一个表达式,那么每次通知都写一大串会很臃肿,所以通过@Pointcut的形式抽取出来
*/
@Before(value = "pointcut()")
public void log(JoinPoint jp){
Signature signature = jp.getSignature();
System.out.println("方法名为" + signature.getName() + "开始执行了······");
}
within 匹配类
within 参数为全路径的类名(可使用通配符)
创建一个新的类DemoController
package com.ssy.www.controller;
import com.ssy.www.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ssy
* @className DemoController
* @description DemoController
* @date 2023-12-14 11:26:52
*/
@RestController
@RequestMapping("/demo")
@Api(tags = {"demo测试接口"})
public class DemoController {
@PostMapping("/withinOne")
@ApiOperation(value = "within接口",httpMethod = "POST",response = R.class)
public R<String> withinOne(){
R out = R.success();
out.setData("云服务TvT");
return out;
}
}
在切面类AopAspect中新增通知
/**
* 表示具体的类
* within(com.ssy.www.controller.DemoController)
* 表示这个包下的所有类
* within(com.ssy.www.controller.*)
* 表示这个包以及子包下的所有类
* within(com.ssy.www.controller..*)
*/
@Before(value = "within(com.ssy.www.controller.DemoController)")
public void monitor(JoinPoint jp){
System.out.println("within测试开始执行了······");
}
this匹配类
匹配调用当前切点表达式所指代对象方法的对象,使用的表达式必须是类型全限定名,this作用于代理对象
,注意:基于 JDK 动态代理实现的 AOP,this不能匹配接口的实现类,因为代理类和实现类并不是同一种类型
application指定为jdk代理
server:
port: 9001
servlet:
context-path: /ssy
# 此处是为了测试aop的this表达式,其他测试需要关闭这个配置
spring:
aop:
proxy-target-class: false
新增service层接口
package com.ssy.www.service;
import com.ssy.www.entity.User;
/**
* @author ssy
* @className UserService
* @description UserService
* @date 2023-12-14 14:02:54
*/
public interface UserService {
User get();
}
新增impl实现service层的接口
package com.ssy.www.service.impl;
import com.ssy.www.entity.User;
import com.ssy.www.service.UserService;
import org.springframework.stereotype.Service;
/**
* @author ssy
* @className UserServiceImpl
* @description UserServiceImpl
* @date 2023-12-14 14:03:43
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public User get() {
User user = new User();
user.setId("Tencent Games");
user.setName("王者荣耀");
user.setAge(8);
user.setSex("Unknown");
return user;
}
}
控制层在DemoController中新增测试接口
/**
* 对于this表达式,指定Spring采用JDK动态代理时才有区别
* 由于此处已经在配置文件中改为了JDK代理,那么必须遵守JDK代理原则
* UserService userService;
* 如果没有指定JDK代理,其实UserService userService或者UserServiceImpl userServiceImpl均可
*/
@Autowired
UserService userService;
@PostMapping("/thisOne")
@ApiOperation(value = "this接口",httpMethod = "POST",response = R.class)
public R<User> thisOne(){
R out = R.success();
out.setData(userService.get());
return out;
}
在切面类AopAspect中添加通知
/**
* this是作用于代理对象,指定Spring采用JDK动态代理时才有区别
* 不指定无论是this(com.ssy.www.service.UserService)
* 还是this(com.ssy.www.service.impl.UserServiceImpl)
* 控制层无论是UserService还是UserServiceImpl注入都是可以执行这个通知的
*/
@Before(value = "this(com.ssy.www.service.UserService)")
public void thisExec(JoinPoint jp){
System.out.println("this测试开始执行了······");
System.out.println("this被代理对象:" + jp.getTarget());
}
target匹配类
匹配切点表达式指定类型的对象,对前对象(代理对象)的目标对象(被代理对象)是指定的类或者其子类,target表示目标对象和指定的类型匹配会被拦截,匹配的是目标对象
举个例子:
接口:com.ssy.www.service.UserService
接口实现类:com.ssy.www.service.impl.UserServiceImpl
jdk生成的动态代理类:实现接口UserService,继承Proxy类
cglib生成的动态代理类:实现接口UserService,继承UserServiceImpl使用jdk动态代理
@Pointcut(“this(com.ssy.www.service.UserService)”):匹配切点
@Pointcut(“this(com.ssy.www.service.impl.UserServiceImpl)”):不匹配切点
@Pointcut(“target(com.ssy.www.service.UserService)”):匹配切点
@Pointcut(“target(com.ssy.www.service.impl.UserServiceImpl)”):匹配切点使用cglib动态代理
@Pointcut(“this(com.ssy.www.service.UserService)”):匹配切点
@Pointcut(“this(com.ssy.www.service.impl.UserServiceImpl)”):匹配切点
@Pointcut(“target(com.ssy.www.service.UserService)”):匹配切点
@Pointcut(“target(com.ssy.www.service.impl.UserServiceImpl)”):匹配切点
within、this、target这三种表达式的匹配范围比较:
范围 | 接口 | 实现接口的类 | 未实现接口的类 |
---|---|---|---|
within | ❌ | ✔ | ✔ |
this | ✔ | O (取决于代理类型) | ✔ |
target | ✔ | ✔ | ✔ |
args
匹配方法参数类型和数量,参数类型可以为指定类型及其子类
在控制层DemoController添加接口代码
@PostMapping("/argsOne")
@ApiOperation(value = "args接口",httpMethod = "POST",response = R.class)
public R argsOne(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestBody User user){
Map<String,Object> map = new HashMap(){
{
put("username",username);
};
{
put("password",password);
};
};
String jsonStr = JSON.toJSONString(user, SerializerFeature.DisableCircularReferenceDetect);
JSONObject jsonMap = JSONObject.parseObject(jsonStr, Feature.DisableCircularReferenceDetect);
map.putAll(jsonMap);
R out = R.success();
out.setData(map);
return out;
}
在切面类AopAspect中添加通知
/**
* 参数类型匹配:args(String,String,com.ssy.www.entity.User,..)
* 这种匹配方式不能在通知中添加相同类型的参数变量
* 反之
* args(username, password, user,..)可以匹配参数,
* 名字随便起不影响,然而通知的方法参数必须加上具体的类型和指定的args中变量,否则切点无效
* 如:
* public void argsExec(JoinPoint jp, String uasername, String password, User user){
*
* }
* 但是,args需要配套其他的表达式使用,太过于模糊会出问题
*/
@Before(value = "args(String,String,com.ssy.www.entity.User,..) && execution(* com.ssy.www.controller.DemoController.argsOne(..))")
public void argsExec(JoinPoint jp){
System.out.println("args测试开始执行了······");
}
注意,此处不加&& execution( ∗ * ∗ com.ssy.ww.controller.DemoController.argsOne( . . .. ..))会报错误:
Error creating bean with name ‘webMvcMetricsFilter’ defined in class path resource [org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.class]: Unsatisfied dependency expressed through method ‘webMvcMetricsFilter’ parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘simpleMeterRegistry’ defined in class path resource [org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfiguration.class]: Initialization of bean failed; nested exception is java.lang.NullPointerException原因是:
这个切点表达式出大问题了,凡是参数是一个String(其他类型包括自定义的对象类型也是同样的道理)的方法都会被去增强,想想整个项目中、包括整个spring框架中,不知道有多少这样的方法呢,凡是加载到容器中的类都会被匹配一遍,会有很多问题,所以对于args的表达式一定要降低模糊度。感兴趣的伙伴可以仔细研究源码,此处不做过多的赘述
bean
通过 bean 的 id 或名称匹配,支持 *
通配符
Spring默认不写bean的名字以类名(首字母小写,如:UserDemo类,那么bean默认为为userDemo)
service层创建DemoService接口
package com.ssy.www.service;
import java.util.Map;
/**
* @author ssy
* @className DemoService
* @description DemoService
* @date 2023-12-14 17:02:12
*/
public interface DemoService {
Map<String, Object> car();
Map<String, Object> animal();
}
impl实现层实现DemoService的实现类
package com.ssy.www.service.impl;
import com.ssy.www.service.DemoService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author ssy
* @className DemoServiceImpl
* @description DemoServiceImpl
* @date 2023-12-14 17:03:27
*/
@Service
public class DemoServiceImpl implements DemoService {
@Override
public Map<String, Object> car() {
Map<String, Object> map = new HashMap(){
{
put("JAF30","歼击机");
};
};
return map;
}
@Override
public Map<String, Object> animal() {
Map<String, Object> map = new HashMap(){
{
put("dog","边牧");
};
{
put("cat","狸猫");
};
};
return map;
}
}
控制层DemoController类中新增测试接口
@Autowired
DemoService demoService;
@PostMapping("/beanOne")
@ApiOperation(value = "beanOne接口",httpMethod = "POST",response = R.class)
public R beanOne(){
R out = R.success();
out.setData(demoService.car());
return out;
}
@PostMapping("/beanTwo")
@ApiOperation(value = "beanTwo接口",httpMethod = "POST",response = R.class)
public R beanTwo(){
R out = R.success();
out.setData(demoService.animal());
return out;
}
切面类AopAspect中添加通知
@Before(value = "bean(demoServiceImpl)")
public void beanExec(JoinPoint jp){
System.out.println("bean测试开始执行了······");
}
注解类型
@within注解匹配
该表达式的注解只有在类上才有效果,和within相比,within必须为类,类似,@within必须为类上的注解
自定义注解
package com.ssy.www.anno;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ReLoad {
String value() default "重新加载";
}
在控制层的DemoController类上加上自定义的注解,最终的DemoController层如下:
package com.ssy.www.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.ssy.www.anno.ReLoad;
import com.ssy.www.entity.User;
import com.ssy.www.service.DemoService;
import com.ssy.www.service.UserService;
import com.ssy.www.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* @author ssy
* @className DemoController
* @description DemoController
* @date 2023-12-14 11:26:52
*/
@ReLoad(value = "demoController接口加载中······")
@RestController
@RequestMapping("/demo")
@Api(tags = {"demo测试接口"})
public class DemoController {
@PostMapping("/withinOne")
@ApiOperation(value = "within接口",httpMethod = "POST",response = R.class)
public R<String> withinOne(){
R out = R.success();
out.setData("云服务TvT");
return out;
}
/**
* 对于this表达式,指定Spring采用JDK动态代理时才有区别
* 由于此处已经在配置文件中改为了JDK代理,那么必须遵守JDK代理原则
* UserService userService;
* 如果没有指定JDK代理,其实UserService userService或者UserServiceImpl userServiceImpl均可
*/
@Autowired
UserService userService;
@PostMapping("/thisOne")
@ApiOperation(value = "this接口",httpMethod = "POST",response = R.class)
public R<User> thisOne(){
R out = R.success();
out.setData(userService.get());
return out;
}
@PostMapping("/argsOne")
@ApiOperation(value = "args接口",httpMethod = "POST",response = R.class)
public R argsOne(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestBody User user){
Map<String,Object> map = new HashMap(){
{
put("username",username);
};
{
put("password",password);
};
};
String jsonStr = JSON.toJSONString(user, SerializerFeature.DisableCircularReferenceDetect);
JSONObject jsonMap = JSONObject.parseObject(jsonStr, Feature.DisableCircularReferenceDetect);
map.putAll(jsonMap);
R out = R.success();
out.setData(map);
return out;
}
@Autowired
DemoService demoService;
@PostMapping("/beanOne")
@ApiOperation(value = "beanOne接口",httpMethod = "POST",response = R.class)
public R beanOne(){
R out = R.success();
out.setData(demoService.car());
return out;
}
@PostMapping("/beanTwo")
@ApiOperation(value = "beanTwo接口",httpMethod = "POST",response = R.class)
public R beanTwo(){
R out = R.success();
out.setData(demoService.animal());
return out;
}
}
切面类AopAspect中添加通知
@Before(value = "@within(reLoad)")
public void annoWithinExec(JoinPoint jp, ReLoad reLoad){
System.out.println("@within测试开始执行了······");
System.out.println("ReLoad注解的属性value的值:" + reLoad.value());
//第二种获取注解的方式
ReLoad annotation = jp.getTarget().getClass().getAnnotation(ReLoad.class);
System.out.println("annotation:" + annotation.value());
}
@annotation注解匹配
只要匹配到方法注解就可以进行增强或通知
控制层UserController上新增测试的接口方法
@ReLoad
@PostMapping("/annotation")
@ApiOperation(value = "annotation表达式接口",httpMethod = "POST",response = R.class)
public R<User> annotation(){
User user = new User();
user.setId("12306");
user.setName("中国铁路");
user.setSex("男");
user.setAge(12);
R out = R.success();
out.setData(user);
return out;
}
切面类AopAspect中添加通知
/**
* 获取方法上注解的方式:获取到的签名就是方法的签名,强制转换为MethodSignature
* 注意是org.aspectj.lang.reflect.MethodSignature这个类
*/
@Before(value = "@annotation(com.ssy.www.anno.ReLoad)")
public void annotationExpression(JoinPoint jp){
System.out.println("@annotation测试开始执行了······");
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
ReLoad annotation = methodSignature.getMethod().getAnnotation(ReLoad.class);
System.out.println("annotation:" + annotation.value());
}
@args
方法的参数类型上有注解(参数上有注解和参数类型上有注解是两种情况,注意区别)
在DemoController类中有这么一个方法
@PostMapping("/argsOne")
@ApiOperation(value = "args接口",httpMethod = "POST",response = R.class)
public R argsOne(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password,
@RequestBody User user){
Map<String,Object> map = new HashMap(){
{
put("username",username);
};
{
put("password",password);
};
};
String jsonStr = JSON.toJSONString(user, SerializerFeature.DisableCircularReferenceDetect);
JSONObject jsonMap = JSONObject.parseObject(jsonStr, Feature.DisableCircularReferenceDetect);
map.putAll(jsonMap);
R out = R.success();
out.setData(map);
return out;
}
这个方法的第一第二个参数都是有@RequestParam注解,这是参数上的注解,参数类型上的注解指的是如第三个参数User类上有@Data、@AipModel注解,而且,对于@args和args一样,一定要配合其它较为明确的表达式使用,否则是会出现模糊度太高难以匹配导致空指针异常出错,
Initialization of bean failed; nested exception is java.lang.NullPointerException
在切面类AopAspect中添加通知
@Before(value = "@args(..,io.swagger.annotations.ApiModel) " +
"&& args(String,String,com.ssy.www.entity.User)" +
"&& @annotation(org.springframework.web.bind.annotation.PostMapping)" +
"&& @annotation(io.swagger.annotations.ApiOperation)")
public void annoArgsExpression(JoinPoint jp){
System.out.println("@args测试开始执行了······");
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();
StringBuilder sb = new StringBuilder();
for (Class<?> parameterType : parameterTypes) {
sb.append("- " + parameterType);
ApiModel annotation = parameterType.getAnnotation(ApiModel.class);
if(annotation!=null){
System.out.println("类型为" + parameterType.getSimpleName() + "的参数上具有注解为" + annotation.getClass().getSimpleName());
}
}
System.out.println("参数类型:" + sb.toString());
}
@target
代理的目标类上具有注解
接口实现类UserServiceImpl上添加注解@ReLoad
package com.ssy.www.service.impl;
import com.ssy.www.anno.ReLoad;
import com.ssy.www.entity.User;
import com.ssy.www.service.UserService;
import org.springframework.stereotype.Service;
/**
* @author ssy
* @className UserServiceImpl
* @description UserServiceImpl
* @date 2023-12-14 14:03:43
*/
@Service
@ReLoad
public class UserServiceImpl implements UserService {
@Override
public User get() {
User user = new User();
user.setId("Tencent Games");
user.setName("王者荣耀");
user.setAge(8);
user.setSex("Unknown");
return user;
}
}
控制层UserController中添加测试接口
@Autowired
UserService userService;
@PostMapping("/annoTarget")
@ApiOperation(value = "@target表达式接口",httpMethod = "POST",response = R.class)
public R<User> annoTarget(){
R out = R.success();
out.setData(userService.get());
return out;
}
在切面类AopAspect中添加通知
@Before(value = "within(com.ssy.www.service..*) " +
"&& @target(com.ssy.www.anno.ReLoad) " +
"&& @within(com.ssy.www.anno.ReLoad)")
public void annoTargetExpression(JoinPoint jp){
System.out.println("@target测试开始执行了······");
Annotation[] annotations = jp.getTarget().getClass().getAnnotations();
for (Annotation annotation : annotations) {
System.out.println("注解:" + annotation.getClass().getSimpleName());
}
System.out.println("-----------------");
}
JoinPoint接口
JoinPoint常用的方法解析
方法名 | 概述 |
---|---|
Object[] getArgs() | 获取传入目标方法的参数列表 |
Signature getSignature() | 返回被增强的方法的相关信息(修饰符+包名+类名+方法名),如果需要获取方法上的注解需要将签名转为为MethodSignature类型 |
Object getTarget() | 获取被代理的对象 |
Object getThis() | 获取代理对象 |
源码如下:
package org.aspectj.lang;
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
String toString();
String toShortString();
String toLongString();
/**
* 获取代理对象
*/
Object getThis();
/**
* 获取目标对象
*/
Object getTarget();
/**
* 获取目标方法的参数列表
*/
Object[] getArgs();
/**
* 返回被增强的方法的相关信息(修饰符+包名+类名+方法名)
* 如果需要获取方法上的注解需要将签名转为为MethodSignature类型
*/
Signature getSignature();
SourceLocation getSourceLocation();
/**
* 返回切入的类型
*/
String getKind();
public interface StaticPart {
Signature getSignature();
SourceLocation getSourceLocation();
String getKind();
int getId();
String toString();
String toShortString();
String toLongString();
}
public interface EnclosingStaticPart extends StaticPart {}
StaticPart getStaticPart();
}
ProceedingJoinPoint继承JoinPoint,是它的子接口,在joinPoint的基础上对其增强功能。其实主要是在JoinPoint的基础上暴露出 proceed
这个方法
环绕通知=前置 + 目标方法执行 + 后置通知,proceed方法就是用于启动目标方法执行的
ProceedingJoinPoint只能适用于环绕增强
package org.aspectj.lang;
import org.aspectj.runtime.internal.AroundClosure;
/**
* ProceedingJoinPoint exposes the proceed(..)
* method in order to support around advice in @AJ aspects
*
* 这里说明了只能用于环绕增强(around advice)
*/
public interface ProceedingJoinPoint extends JoinPoint {
void set$AroundClosure(AroundClosure arc);
default void stack$AroundClosure(AroundClosure arc) {
throw new UnsupportedOperationException();
}
/**
* 平时主要用的就是proceed方法,这个是aop代理链执行的方法
*/
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
@DeclareParents(可略过)
@DeclareParents也称为Introduction(引入),表示为指定的目标类引入新的属性和方法。其实使用的时候会感觉很奇怪,实际生活中很少使用。@DeclareParents有两个属性:
- value 属性指定了哪种类型的bean要引入新的接口行为
- defaultImpl属性指定了提供新的行为的
接口的实现类或具体的类(不能为接口)
新增测试业务Service和实现类逻辑代码
package com.ssy.www.service;
import com.ssy.www.entity.User;
/**
* @author ssy
* @className LoginService
* @description LoginService
* @date 2023-12-15 13:24:06
*/
public interface LoginService {
User login();
}
package com.ssy.www.service.impl;
import com.ssy.www.entity.User;
import com.ssy.www.service.LoginService;
import org.springframework.stereotype.Service;
/**
* @author ssy
* @className LoginServiceImpl
* @description LoginServiceImpl
* @date 2023-12-15 13:24:47
*/
@Service
public class LoginServiceImpl implements LoginService {
@Override
public User login() {
User user = new User();
user.setId("1008611");
user.setSex("男");
user.setName("中国移动");
user.setAge(21);
return user;
}
}
新增需要被业务代码引入的模板代码的接口和实现类
package com.ssy.www.service;
/**
* @author ssy
* @className OtherService
* @description OtherService
* @date 2023-12-15 13:27:45
*/
public interface OtherService {
void log();
}
package com.ssy.www.service.impl;
import com.ssy.www.service.OtherService;
/**
* @author ssy
* @className OtherServiceImpl
* @description OtherServiceImpl
* @date 2023-12-15 13:28:13
*/
//注意此处不交给Spring容器管理,不注册bean
//如果交给Spring容器管理,自动注入的时候需要指定具体的bean
//这个类只是提供新的方法或属性给其它需要增强的类织入或引用
//建议不交给Spring容器管理
public class OtherServiceImpl implements OtherService {
@Override
public void log() {
System.out.println("开始执行日志······");
}
}
在切面类AopAspect中织入(或引入)
代理目标类上新的行为
/**
* @DeclareParents也称为Introduction(引入)
* 表示为指定的目标类引入新的属性和方法
* value 属性指定了哪种类型的bean要引入新的接口行为,并在起后接着 "+",代表改类下的所有子类
* 也可以使用具体的实现类:value = "com.ssy.www.service.impl.LoginServiceImpl"
* defaultImpl属性指定了提供新的行为的接口的实现类
*
* 如果OtherServiceImpl交给了Spring容器管理,那么
* 自动注入的时候一定要指定具体的bean的名字
*/
@DeclareParents(value = "com.ssy.www.service.LoginService+",defaultImpl = OtherServiceImpl.class)
OtherService otherService;
新增控制层的接口测试
package com.ssy.www.controller;
import com.ssy.www.entity.User;
import com.ssy.www.service.LoginService;
import com.ssy.www.service.OtherService;
import com.ssy.www.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ssy
* @className LoginController
* @description LoginController
* @date 2023-12-15 13:39:01
*/
@RestController
@RequestMapping("/login")
@Api(tags = {"login测试接口"})
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping("/demo")
@ApiOperation(value = "DeclareParents引入新方法接口",httpMethod = "POST",response = R.class)
public R<User> declareParents(){
//这行代码就比较突兀,此处仅做对于@DeclareParents的演示
OtherService otherService = (OtherService) loginService;
otherService.log();
R out = R.success();
out.setData(loginService.login());
return out;
}
}
对于@DeclareParents小编并不推荐使用,用起来特别别扭,其实使用五大通知已经足够了,此处仅仅作为介绍引入,可以略过@DeclareParents
通知默认的顺序
- 没有异常的情况
环绕通知(前置)、前置通知、后置通知、最终通知、环绕通知(后置)- 存在异常的情况
- 1、环绕通知不处理,直接往外抛异常
环绕通知(前置)、前置通知、异常通知、最终通知- 2、环绕通知进行处理,try{} . . . . .... ....catch{}
- 环绕通知(前置)、前置通知、异常通知、最终通知、环绕通知(异常处理)、环绕通知(后置)
上述是默认情况下的执行顺序,也可以在通知上使用@Oreder(-1)注解的形式手动控制通知的执行顺序,数值越小,越优先执行
注意:对同一目标对象测试五种通知顺序需要把actuator的依赖去掉,否则的话会有冲突的情况发生
应用场景
日志记录
、事务管理
、安全控制
、权限管理
、性能监控
、缓存管理、动态数据源、异常处理、资源池、懒加载、同步······