前言

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 代理

应用场景

  1. Authentication 权限
  2. Caching 缓存
  3. Context passing 内容传递
  4. Error handing 错误处理
  5. Lazy loding 懒加载
  6. Debugging 调试
  7. logging,tracing,profiling and monitoring 记录 跟踪 优化 校准
  8. Performance optimzation 性能优化
  9. Persistence 持久化
  10. Resource pooling 资源池
  11. Synchronization 同步
  12. Transaction 事务

通知类型

  • Before Advice(前置通知):在某连接点(Join Point)之前执行的通知,但这个通知不能阻止连接点前的执行
  • After Advice(后置通知):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
  • After Return advice(正常返回通知):在某连接点正常完成后执行的通知,不包括抛出异常的情况。(如果连接点抛出异常,则不会执行。)
  • After throwing Advice(异常通知):在连接点抛出异常后执行
  • Around advice(环绕通知):环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。

AOP 的两种代理方式

Spring 提供了两种方式生成代理对象:JDKProxy 和 Cglib,具体使用哪种方式生成由 AopProxyFactory 根据 AdvisedSupport 对象的配置决定。默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。

  1. JDK 动态接口代理

    JDK 动态代理只要涉及到 Java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。InvocationHandler 是一个接口,通过实现接口定义横切逻辑,并通过反射机制调用目标代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象

  2. 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>

<!-- slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- mysql驱动 -->
<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;
/**
* 执行时间(ms 单位)
*/
private Integer time;
/**
* 操作方法
*/
private String method;
/**
* 参数
*/
private String params;
/**
* ip
*/
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();
}
// 执行时长(ms 单位)
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);
}
// 获取 request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置 IP 地址
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 {

/**
* 测试带有参数的方法
*
* @param name 名称
*/
@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

然后查看数据库

log

至此,使用 Spring AOP 实现对日志的操作就大功告成了!