前言
AOP(Aspect Oriented Programming),被称为面向切面编程。AOP 基于 IoC 基础,是对 OOP 的有益补充,流行的 AOP 框架有 Sping AOP、AspectJ。
AOP 技术它利用一种称为横切的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为 Aspect,即切面。所谓切面,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
AOP 核心概念
切面(Aspect)
散落在系统各处的通用的业务逻辑代码,如上图中的日志模块,权限模块,事务模块等,切面用来装载 pointcut 和 advice
通知(Advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
连接点(Join Point)
被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
切入点(Pointcut)
拦截的方法,连接点拦截后变成切入点
目标对象(Target Object)
代理的目标对象,指要织入的对象模块
织入(Weaving)
通过切入点切入,将切面应用到目标对象并导致代理对象创建的过程
AOP 代理(AOP Proxy)
AOP 框架创建的对象,包含通知。在 Spring 中,AOP 代理可以是 JDK 动态代理或 CGLIB 代理
应用场景
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handing 错误处理
- Lazy loding 懒加载
- Debugging 调试
- logging,tracing,profiling and monitoring 记录 跟踪 优化 校准
- Performance optimzation 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transaction 事务
通知类型
- Before Advice(前置通知):在某连接点(Join Point)之前执行的通知,但这个通知不能阻止连接点前的执行
- After Advice(后置通知):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
- After Return advice(正常返回通知):在某连接点正常完成后执行的通知,不包括抛出异常的情况。(如果连接点抛出异常,则不会执行。)
- After throwing Advice(异常通知):在连接点抛出异常后执行
- Around advice(环绕通知):环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。
AOP 的两种代理方式
Spring 提供了两种方式生成代理对象:JDKProxy 和 Cglib,具体使用哪种方式生成由 AopProxyFactory 根据 AdvisedSupport 对象的配置决定。默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。
JDK 动态接口代理
JDK 动态代理只要涉及到 Java.lang.reflect
包中的两个类:Proxy 和 InvocationHandler。InvocationHandler 是一个接口,通过实现接口定义横切逻辑,并通过反射机制调用目标代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象
CGLIB 动态代理
CGLIB 全称为 Code Generation Libraay,是一个强大的高性能,高质量的代码生成类库,可以在运行期间扩展 Java 类与实现 Java 接口,CGLIB 封装了 asm
,可以在运行期间生成新的 Class。和 JDK 动态代理相比较:JDK 创建代理又一个限制,就是只能为创建对象代理实例,而没有通过接口定义业务方法的类,则通过 CGLIB 创建动态代理。
废话好像有点多了,那开始正文把…
正文
首先创建 Spring Boot 工程,Spring Boot 版本我用的是 2.4.4
添加依赖(pom.xml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <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.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency>
|
创建 SQL(我准备了 SQL 语句)
1 2 3 4 5 6 7 8 9 10 11
| CREATE TABLE `sys_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `operation` varchar(255) DEFAULT NULL, `time` int(11) DEFAULT NULL, `method` varchar(255) DEFAULT NULL, `params` varchar(255) DEFAULT NULL, `ip` varchar(255) DEFAULT NULL, `createTime` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
配置 yml 文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| spring: datasource: url: jdbc:mysql:///aop_demo?useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver username: xiaojia password: root
mybatis: type-aliases-package: cn.imzjw.entity mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
自定义一个注解(Log.java
)
1 2 3 4 5 6
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log {
String value() default ""; }
|
创建实体类(SysLog.java
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class SysLog implements Serializable {
private static final long serialVersionUID = -6309732882044872298L;
private Integer id;
private String username;
private String operation;
private Integer time;
private String method;
private String params;
private String ip;
private Date createTime; }
|
创建 Mapper 接口(SysLogMapper.java
)
1 2 3 4
| public interface SysLogMapper {
void saveSysLog(SysLog syslog); }
|
创建映射文件(src/main/resources/mapper/SysLogMapper.xml
)
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.imzjw.mapper.SysLogMapper"> <insert id="saveSysLog"> INSERT INTO sys_log(username, operation, time, method, params, ip, createTime) VALUES (#{username}, #{operation}, #{time}, #{method}, #{params}, #{ip}, #{createTime}) </insert> </mapper>
|
创建日志记录 AOP 实现(LogAspect.java
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Aspect @Component public class LogAspect {
@Autowired private SysLogMapper sysLogMapper;
@Pointcut("@annotation(cn.imzjw.anno.Log)") public void pointcut() { }
@Around("pointcut()") public void around(ProceedingJoinPoint point) { long beginTime = System.currentTimeMillis(); try { point.proceed(); } catch (Throwable e) { e.printStackTrace(); } long time = System.currentTimeMillis() - beginTime; saveLog(point, time); }
private void saveLog(ProceedingJoinPoint joinPoint, long time) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); SysLog sysLog = new SysLog(); Log logAnnotation = method.getAnnotation(Log.class); if (logAnnotation != null) { sysLog.setOperation(logAnnotation.value()); } String className = joinPoint.getTarget().getClass().getName(); String methodName = signature.getName(); sysLog.setMethod(className + "." + methodName + "()"); Object[] args = joinPoint.getArgs(); LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = u.getParameterNames(method); if (args != null && paramNames != null) { String params = ""; for (int i = 0; i < args.length; i++) { params += " " + paramNames[i] + ": " + args[i]; } sysLog.setParams(params); } HttpServletRequest request = HttpContextUtils.getHttpServletRequest(); sysLog.setIp(IPUtils.getIpAddr(request)); sysLog.setUsername("garvey"); sysLog.setTime((int) time); Date date = new Date(); sysLog.setCreateTime(date); sysLogMapper.saveSysLog(sysLog); } }
|
创建 Controller
测试
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController public class DemoController {
@Log("测试带有参数的方法") @GetMapping("/demo") public void demo(String name) { } }
|
最后别忘了在启动类上加上扫描 Mapper 接口的注解
1 2 3 4 5 6 7 8 9
| @SpringBootApplication @MapperScan("cn.imzjw.mapper") public class AopLogApplication {
public static void main(String[] args) { SpringApplication.run(AopLogApplication.class, args); }
}
|
打开浏览器访问 http://localhost:8080/demo?name=xiaojia
然后查看数据库
至此,使用 Spring AOP 实现对日志的操作就大功告成了!