引言
AOP,作为Spring的两大特性之一,想必所有人都会背这一段概念:面向切面编程,通过预编译方 式和运行期动态代理实现程序功能的统一维护的一种技术。何为切面,所谓切面,其实就可以理解为接口或者方法的整体,我们想对某个方法进行逻辑上的补充,但又不想改动整个方法体,于是就可以利用这种切面的思想,在方法体的上下环境中进行逻辑填充,这是设计模式当中代理模式的思想,同时也是AOP的底层机制。本文就来详细介绍AOP到底做了什么,以及我们该怎么使用这一神器。
AOP原理
讲到AOP的底层机制,不得不提到代理模式,我们常接触到的代理模式主要有三种:静态代理、动态代理以及Cglib代理。笔者以静态代理为例,详细阐述代理模式的特点。
首先,创建一个目标接口:
/**
* 目标接口
*/
public interface IUserDao {
void save();
}
然后,要创建一个目标对象,来实现目标接口的方法:
public class UserDao implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
如果现在需要对 save() 方法进行增强,比方说,在原方法体上方打印日志提示已经开始处理数据,在原方法体下方打印日志提示已经处理数据完毕。一般而言,可以直接修改save方法体,达到效果,但如果一旦有了新的需求,又需要修改原始代码,这样无疑增加了开发的不可控性。代理模式的解决方案是再创建一个代理对象,然后将目标对象作为参数传入代理对象,在代理对象中的代理方法中实现逻辑增强。
public class UserDaoProxy implements IUserDao {
public IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开始保存数据!");
target.save(); // 目标对象方法
System.out.println("保存数据完毕!");
}
}
以上就是代理模式的一个简单实现,就如上述所言,代理模式最大的优势就是解耦。但静态代理有一个问题,我们需要手动创建代理类并编写增强代码。动态代理便解决了这个问题,利用java.lang.reflect.Proxy 下的 Proxy.newProxyInstance() 方法在运行时创建代理对象。
以上就是AOP的底层机制——代理模式的应用,Spring默认使用JDK的动态代理。
AOP应用
在介绍 AOP 应用之前,还需要普及一些基本概念(参考细说Spring——AOP详解(AOP概览)):
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
- Target(目标对象):织入 Advice 的目标对象.。
- Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
下面本文开始正式介绍如何应用 AOP :
- 首先,引入web和aop依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.4.4</version> </dependency> <!-- aop 依赖, aspectj --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>注意,后续还会用到lombok的注解,看个人喜好,可以在这里加上lombok的依赖
- 然后,为了简单起见,直接创建Service层和Controller层
@Service @Slf4j public class UserService { public void save() { log.info("saving data!"); } }@Controller @RequestMapping("demo/") public class DemoController { final UserService userService; public DemoController(UserService userService) { this.userService = userService; } /** * 模拟 AOP */ @RequestMapping("/aop") @ResponseBody public void aopTest() { userService.save(); } } - 构建切面类
@Aspect @Component @Slf4j public class UserServiceAspect { /** * 环绕切面 */ @Around("execution(* com.computer.fundamentals.networkprogram.service.UserService.*(..))") public void saveHandle(ProceedingJoinPoint proceedingJoinPoint) { log.info("start"); Object result; try { result = proceedingJoinPoint.proceed(); } catch (Throwable throwable) { log.info("server error"); } log.info("end"); } }
经过测试,日志界面会打印出如下结果:
2021-11-01 10:07:11.887 INFO 12052 --- [nio-8080-exec-1] c.c.f.n.service.aop.UserServiceAspect : start
2021-11-01 10:07:11.894 INFO 12052 --- [nio-8080-exec-1] c.c.f.n.service.UserService : saving data!
2021-11-01 10:07:11.894 INFO 12052 --- [nio-8080-exec-1] c.c.f.n.service.aop.UserServiceAspect : end
这里采用的是环绕切面,在方法体的上下文补充逻辑,事实上还有许多种切面,有需要的同学可以自行查阅了解,这里仅仅是抛砖引玉。
结语
看到这里,各位同学是不是觉得 AOP 确实是一大神器,它可以在不影响原方法体的情况下添加许多有用的东西,如日志记录、运行时间测算等等,而且它实际上就是代理模式的一种具体应用,熟悉了这一设计模式,那么理解 AOP 也就不那么难了。