AOP与SpringBoot使用AOP实例
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。
动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
使用场景:
记录操作日志、权限控制、事务管理......
优势:
代码无入侵、减少重复代码、提高开发效率、维护简单
AOP的核心概念
JoinPoint(连接点):被拦截的点。Spring中指可以被动态代理拦截目标类的方法。
PointCat(切入点):指要对哪些JointPoint进行拦截,既被拦截的连接点。
Advice(通知):拦截JoinPoint后要做的事。既对切入点增强的内容。
Target(目标): 指代理的目标对象。
Weavìng(植入):指把增强代码应用到目标上,生成代理对象的过程。
Proxy (代理):生成的代理对象
Aspect(切面):切入点和通知的结合。
AOP 的通知类型
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行 @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值
AOP的通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
1、不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
2、用 @Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
切入点表达式
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
1、execution(……):根据方法的签名来匹配
2、@annotation(注解名) :根据注解匹配
切入点表达式-execultion
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,
语法为:execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带 ? 的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头。
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包。
切入点表达式-@annotation
AOP案例
将增、删、改 相关接口的操作日志记录到数据库表中。
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
需要对所有业务类中的增、删、改 方法添加统一功能,使用 AOP 技术最为方便
切入点表达式:利用exclution或@annotation都可,这里我用@annotation
通知类型:用@Around环绕通知就可
数据库设计
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for sys_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (`id` int NOT NULL AUTO_INCREMENT,`create_user` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,`create_time` datetime NOT NULL,`class_name` varchar(256) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`method_name` varchar(256) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`method_params` varchar(256) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`return_value` varchar(256) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`cost_time` mediumtext CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci COMMENT = '日志信息类' ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperatorLog {private Integer id; //IDprivate String createUser; //操作人IDprivate LocalDateTime createTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private String returnValue; //操作方法返回值private Long costTime; //操作耗时public OperatorLog(String createUser, LocalDateTime createTime, String className, String methodName, String methodParams, String returnValue, Long costTime) {this.createUser = createUser;this.createTime = createTime;this.className = className;this.methodName = methodName;this.methodParams = methodParams;this.returnValue = returnValue;this.costTime = costTime;}
}
Mapper
@Mapper
public interface LogMapper {@Insert("insert into sys_log (create_user,create_time,class_name,method_name,method_params,return_value,cost_time)" +"values (#{createUser},#{createTime},#{className},#{methodName},#{methodParams},#{returnValue},#{costTime});")void addLog(OperatorLog log);
}
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperatorLog {
}
导入AOP的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
自定义AOP
@Slf4j
@Configuration
@Aspect
public class CustomizationAop {@AutowiredHttpServletRequest request;@AutowiredEmpMapper empMapper;@AutowiredLogMapper logMapper;@Around("@annotation(com.huang.anno.OperatorLog)")public Object around(ProceedingJoinPoint p) throws Throwable{//获取tokenString token = request.getHeader("token");Claims claims = JwtUtils.parseJwt(token);//获取操作人名字Integer id = (Integer) claims.get("id");String operatorName = empMapper.getOneById(id).getName();//获取现在时间LocalDateTime now = LocalDateTime.now();//获取类名String className = p.getTarget().getClass().getName();//获取方法名String methodName = p.getSignature().getName();//获取参数名Object[] args = p.getArgs();String params = Arrays.toString(args);//方法执行前时间long begin = System.currentTimeMillis();//方法执行Object result = p.proceed();//方法执行后时间long end = System.currentTimeMillis();//获取执行时间long costTime = end - begin;//方法返回值String returnValue = JSONObject.toJSONString(result);if (returnValue.length()>255) {returnValue = returnValue.substring(0, 255);}OperatorLog operatorLog = new OperatorLog(operatorName, now, className, methodName, params, returnValue, costTime);logMapper.addLog(operatorLog);log.info("日志记录:{}",operatorLog);return result;}
}
实现
@GetMapping@OperatorLogpublic Result page(@RequestParam(required = false) String name,@RequestParam(required = false) Short gender,@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize){PageBean pageBean = empService.page(name,gender,begin,end,page,pageSize);return Result.success(pageBean);}
测试