Loading... SpringBoot 系列教程 - 源码地址:[https://github.com/laolunsi/spring-boot-examples](https://github.com/laolunsi/spring-boot-examples) --- 大家知道我现在还是一个 CRUD 崽,平时用 AOP 也是 CV 大法。最近痛定思痛,决定研究一下 Spring AOP 的原理。 这里写一篇文章总结一下。主要介绍 Java 中 AOP 的实现原理,最后以两个简单的示例来收尾。 --- ## 一、AOP 的基本概念 ### 1.1 什么是 AOP Aspect Oriented Programming,面向切面编程。 就跟我们说 OOP 是面向对象一样,AOP 是面向切面的。切面是分散在应用中的一个标准代码或功能。切面通常与实际的业务逻辑不同(例如,事务管理)。每个切面专注于一个特定的环切功能。 这里的切面呢,可以理解为横切。比如在所有的 DAO 层方法上加上一个同样的切面,功能是记录日志;又或者在某个接口上应用一个切面,作用是检查权限。 AOP 是基于`代理`来实现的。而代理又分为静态代理和动态代理。两者的区别在于代理类于何时生成。 下面我们讲讲代理是怎么回事? --- ### 1.2 代理与 Spring AOP 代理分为静态代理和动态代理: - 静态代理:代理类在编译阶段生成,程序运行前就存在。包括:AspectJ 静态代理、JDK 静态代理 - 动态代理:代理类在程序运行时创建。包括:JDK 动态代理、CGLib 动态代理 Spring AOP 原理: - JDK Proxy: interface based - CGLib Proxy: class based ![](http://zfh-public-blog.oss-cn-beijing.aliyuncs.com/image-1590916137957.png) Spring AOP 中默认使用 JDK 动态代理,通过反射获取被代理的类,这个类必须实现一个接口。如果目标类没有实现接口,就会默认使用 CGLIB Proxy 来动态生成代理目标类,后者是被代理类的子类。 可以通过获取代理对象并打印的方式来查看其类型(JDK Proxy 下是 com.sun.prxy, CGlib 下是子类. AspectJ: 用特定的编译器和语法,在编译时增强,实现了静态代理技术。 ### 1.3 Spring AOP 与 AspectJ 的区别 AspectJ 是一套完整的 AOP 解决方案,而 Spring AOP 并不是 —— 它只是在 Spring 框架下满足其使用要求的一个解决方法,比如 Spring AOP 仅支持对方法使用切面。 --- ## 二、静态代理 ### 2.1 AspectJ 静态代理 基于特殊的编译器和语法。这里不多介绍了。 IDEA 下编译 AspectJ 可以参考这篇:https://blog.csdn.net/gavin_john/article/details/80156963 --- ### 2.2 JDK 静态代理 实际上是利用实现一个具体的代理类来调用业务类。代理类持有了一个业务类的引用。 更概况地说,JDK 静态代理体现的是一种设计模式。 缺点很明显,代码冗余,难以维护。 这里以 `借书` 和 `还书` 这两个行为来作为一个示例: 编写一个 BookService 接口: ```java public interface BookService { boolean borrow(String id, String userName); boolean reBack(String id, String userName); } ``` 然后实现这个接口: ```java public class BookServiceImpl implements BookService { @Override public boolean borrow(String id, String userName) { System.out.println(userName + " 借书:" + id); return true; } @Override public boolean reBack(String id, String userName) { System.out.println(userName + " 还书:" + id); return true; } } ``` 下面我们来编写 `BookService` 的代理类: ```java public class BookProxy implements BookService { private BookServiceImpl bookService; public BookProxy(BookServiceImpl bookService) { this.bookService = bookService; } @Override public boolean borrow(String id, String userName) { boolean res = false; if (check()) { res = bookService.borrow(id, userName); } addLog(); return res; } @Override public boolean reBack(String id, String userName) { boolean res = false; if (check()) { res = bookService.reBack(id, userName); } addLog(); return res; } // private boolean check() { System.out.println("检查权限"); return true; } private void addLog() { System.out.println("操作完成"); } } ``` 编写一个测试类: ```java public class MainTest { public static void main(String[] args) { BookProxy proxy = new BookProxy(new BookServiceImpl()); proxy.borrow("123", "eknown"); proxy.reBack("234", "java"); } } ``` 这里我们可以看到,JDK 静态代理就是说在原来的实现类上套一层 `代理`。它好像是体现了代理模式,但实际上并没有带来太多的好处。代码相当冗余,也不利于维护。 真正体现代理模式好处的还是动态代理,下面我们来看看动态代理的原理。 ---- ## 三、动态代理 动态代理是程序运行时,由 JVM 根据反射等机制动态生成代理类的。 也就是说,程序运行前,我们仅仅定义了代理的规则,而不知道代理类具体长什么样,这不像上面的静态代理里,我们完整地定义了代理对象。 --- ### 3.1 JDK 动态代理 JDK 动态代理是基于接口的。 我们可以通过实现 `InvocationHandler` 接口来手动创建一个 JDK 代理类。 首先需要定义一个接口,让业务类和代理类都实现这个接口。 然后编写一个 InvocationHandler 接口的实现类: ```java public class BookProxy implements InvocationHandler { // 被该代理类处理的业务类 private BookService bookService; public BookProxy(BookService bookService) { this.bookService = bookService; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object res = null; if (check()) { // 调用实际的 method,参数是 接口 + 参数 res = method.invoke(bookService, args); } addLog(); return res; } private boolean check() { System.out.println("检查权限"); return true; } private void addLog() { System.out.println("操作完成"); } } ``` 测试: ```java public class MainTest { public static void main(String[] args) { // 创建被代理的实际业务类 BookServiceImpl bookServiceImpl = new BookServiceImpl(); ClassLoader classLoader = bookServiceImpl.getClass().getClassLoader(); // 获取所有的接口方法 Class[] interfaces = bookServiceImpl.getClass().getInterfaces(); // 构造 Handler InvocationHandler invocationHandler = new BookProxy(bookServiceImpl); // 创建代理 Object obj = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); BookService bookService = (BookService) obj; bookService.borrow("abc", "eknown"); bookService.reBack("c23", "py"); } } ``` --- ### 3.2 CGLIB 动态代理 CGLIB 代理的原理是:让代理类继承业务类(也就自动拥有了业务类的所有非 final 的 public 方法) 我们这里手动编写一个 CGLIB 的代理试试看。 首先我们有一个 BookServiceImpl 业务类,这个业务类可以实现接口,也可以就是单纯的一个业务类。 然后我们定义一个 BookCglibProxy 类: ```java public class BookCglibProxy implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { check(); // 调用实际的 method Object obj = methodProxy.invokeSuper(o, objects); addLog(); return obj; } private boolean check() { System.out.println("检查权限"); return true; } private void addLog() { System.out.println("操作完成"); } } ``` 测试类: ```java public class CglibTest { public static void main(String[] args) { BookServiceImpl bookServiceImpl = new BookServiceImpl(); BookCglibProxy proxy = new BookCglibProxy(); // cjlib 中的增强器,用于创建动态代理(被代理类的子类) Enhancer enhancer = new Enhancer(); // 设置要被代理的类 enhancer.setSuperclass(bookServiceImpl.getClass()); // 设置回调 enhancer.setCallback(proxy); // 强转成父类 BookServiceImpl proxyResult = (BookServiceImpl) enhancer.create(); proxyResult.borrow("12333", "ye"); proxyResult.reBack("123", "fe"); } } ``` 在第一节我们提到过 Spring AOP 是基于 JDK 动态代理和 CGLIB 动态代理的。下面我们来 Spring AOP 的一些基本案例。 --- ## 四、Spring AOP 实例 AOP 中一些概念词汇,通过这些词汇,我们可以对 AOP 有更高一层的抽象。 - Aspect - 切面,分散在应用中的一个标准代码或功能。切面通常与实际的业务逻辑不同(例如,事务管理)。每个切面专注于一个特定的环切功能。 - Joinpoint - 连接点,是程序执行过程中的特定点,比如方法执行、构造器调用、字段赋值 - Advice - 通知,切面在某个连接点采取的操作。Advice 有 5 种类型。 - Pointcut - 切入点,一个匹配连接点的正则表达式。每当连接点匹配了一个切入点时,一个特定的通知就会被执行。 - Weaving - 织入,指的是将切面和目标对象连接起来以创建代理对象的过程。 Spring AOP 有两种实现方式:基于 XML 或基于注解。更流行、更方便的是后者。(阿 sir,不会还有人用 XML 来做 Bean 的配置文件吧?) --- ### 4.1 基于 XML 的实例 首先定义一下接口和实现类(没有注解的!)。再编写一个代理类: 这里的代理类方法以 JoinPoint 为参数即可: ```java public class BookAspect { public void checkUser(JoinPoint point) { System.out.println("-----before-----"); Object[] args = point.getArgs(); for (Object arg : args) { System.out.println(arg); } System.out.println("检查用户权限..."); } public void saveLog(JoinPoint point) { System.out.println("-----after-----"); Object[] args = point.getArgs(); for (Object arg : args) { System.out.println(arg); } System.out.println("请求完毕,记录日志..."); } } ``` 然后编写 Spring 的配置文件: ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd"> <!-- 定义 bean --> <bean id="bookService" class="com.example.springaopdemo.basicxml.BookServiceImpl" /> <bean id="bookAspect" class="com.example.springaopdemo.basicxml.BookAspect" /> <aop:config> <!-- 这是定义一个切面,切面是切点和通知的集合--> <aop:aspect id="do" ref="bookAspect"> <!-- 定义切点 ,后面是 expression 语言,表示包括该接口中定义的所有方法都会被执行 --> <aop:pointcut id="point" expression="execution(* com.example.springaopdemo.basicxml.BookService.*(..))" /> <!-- 定义通知 --> <aop:before method="checkUser" pointcut-ref="point" /> <aop:after method="saveLog" pointcut-ref="point" /> </aop:aspect> </aop:config> </beans> ``` 运行测试: ```java public class AopXMLTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("SpringAop.xml"); BookService bookService = context.getBean("bookService", BookService.class); bookService.borrow("123", "eknown"); bookService.reback("123", "eknown"); } } ``` 基于 XML 配置的 Spring 现在已经很少使用了。下面我们来看看如何基于注解使用 Spring AOP --- ### 4.2 基于注解的实例 这里以一个使用 SpringBoot 框架的 Web 项目作为简单的实例。 首先创建一个 SpringBoot 项目,写好 Controller、Service、DAO 层的基本类。(示例源码中没有使用 Mybatis 等持久层框架,而是用 Map 来模拟数据的存取) 下面我们针对 UserService 接口类,添加切面。 ```java @Aspect @Component public class UserAspect { @Before(value = "execution(* com.example.springaopdemo.boot.UserService.*(..))") public void checkUser(JoinPoint point) { System.out.println("-----before-----"); Object[] args = point.getArgs(); for (Object arg : args) { System.out.println(arg); } System.out.println("检查..." + point); } @After(value = "execution(* com.example.springaopdemo.boot.UserService.*(..))") public void saveLog(JoinPoint point) { System.out.println("-----after-----"); Object[] args = point.getArgs(); for (Object arg : args) { System.out.println(arg); } // 这里可以使用 point.getTarget() 获取到切面对应的 bean //Object target = point.getTarget(); //UserService userService = (UserService) target; //List<User> userList = userService.findAll(); System.out.println("请求完毕,记录日志..." + point); } @Around(value = "execution(* com.example.springaopdemo.boot.UserService.save(..))") public Object saveAround(ProceedingJoinPoint point) { System.out.println("around-before"); Object obj = null; try { obj = point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println("around-after"); return obj; } } ``` 示例中使用了 @Before/@After/@Aroud 三个注解,value 中使用切点表达式,分别匹配了 UserService 接口的所有方法和单个 save 方法。 我们还可以通过切点表达式匹配自定义的注解,比如实现一个 UserMonitor 注解,然后定义其切点方法: ```java public @interface UserMonitor { String value() default ""; int roleLimit() default 0; } ``` 切点: ```java @Around("@annotation(com.example.springaopdemo.boot.UserMonitor)") public Object userRolePointCut(ProceedingJoinPoint point) { System.out.println("检查用户权限..."); // 获取参数 Object[] args = point.getArgs(); Class<?>[] argTypes = new Class[point.getArgs().length]; for (int i = 0; i < args.length; i++) { argTypes[i] = args[i].getClass(); } // 获取方法 Method method = null; try { method = point.getTarget().getClass() .getMethod(point.getSignature().getName(), argTypes); } catch (NoSuchMethodException | SecurityException e) { e.printStackTrace(); } // 获取方法上的该注解,之后可以根据注解中的值进行一些操作,比如判定是否具有权限 UserMonitor monitor = method.getAnnotation(UserMonitor.class); System.out.println(monitor); Object obj = null; try { obj = point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } return obj; } ``` --- 参考资料: - 什么是面向切面编程AOP?[https://www.zhihu.com/question/24863332](https://www.zhihu.com/question/24863332) - 一起来谈谈 Spring AOP!: [https://juejin.im/post/5aa7818af265da23844040c6](https://juejin.im/post/5aa7818af265da23844040c6) > 一个简单的 AOP 示例,从 XML 到注解。 - Spring AOP的实现原理: [https://juejin.im/post/5b67b9a86fb9a04fc71afeb4](https://juejin.im/post/5b67b9a86fb9a04fc71afeb4) > 也是一篇简单的使用示例文章。 - 从代理机制到Spring AOP: [https://juejin.im/post/5b90e648f265da0aea695672](https://juejin.im/post/5b90e648f265da0aea695672) > 这篇讲得非常好,深刻讲述了各种代理的原理和示例。强烈推荐看看这一篇! - 比较 Spring AOP 和AspectJ:[https://www.baeldung.com/spring-aop-vs-aspectj](https://www.baeldung.com/spring-aop-vs-aspectj) > 一篇有关 Spring AOP 和 AspectJ 的英文资料 - 三歪 - Spring【AOP模块】就是这么简单: [https://juejin.im/post/5aa8edf06fb9a028d0432584]( Last modification:June 1, 2020 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 0 请作者喝杯肥宅快乐水吧!