赞
踩
目录
4.2.1 JDK动态代理(Java Dynamic Proxy)
我们举个例子, 在一个 个人博客系统 中有 用户登录权限校验 , 那么这是一个 统一的功能 , 在所有需要这个功能的页面都需要具备, 但是要写多遍的话, 会使得代码冗余程度非常大, 代码比较臃肿, 并且耦合性是比较高的.
再比如, 没有AOP进行统一功能的处理时, 要写一个网页系统, 可能会在很多地方去写登录的判断:
(1) 在写文章添加的时候, 要判断用户有没有登录, 如果没有登录就不能让用户发文章, 所以添加文章的时候需要判断用户登录.
(2) 修改文章也要判断用户登录. 而且还要判断文章的归属人是不是用户本人, 不能是 张三 把 李四 的文章改了.
(3) 删除文章也要判断用户登录, 如果没有登录不能让用户删除文章, 并且张三不能删除李四的文章.
所以我们在进行文章管理功能的实现的时候, 针对的主表是文章表, 而用户是否登录又是对user用户表而言的, 可是在做文章表功能的时候是肯定和用户表没有任何关联的, 但是为了安全我们不得不做登录判断.
可见, 我们在写一个业务的时候往往会牵扯到另一个业务的发生, 这时耦合度较高, 在写一个通用的判断用户是否登录的方法, 如果这个时候这个方法里面的参数变了, 那么之后所有调用用户登录的地方都得去改, 导致工作的不必要增加. 代码会写得比较多, 不利于修改和维护, 而且可能还会有其他奇怪问题出现.
那么怎么样处理上述问题呢? 我们就是引入AOP面向切面编程.
AOP 全名为Aspect Oriented Programming 翻译过来中文意思为面向切面编程. 那么AOP是一种思想,它是对某一类事情(某一个通用功能)的集中处理。
我们之前是学过面向过程编程 (Procedural Programming, 即PP) 和面向对象编程 (Object-Oriented Programming, 即OOP)的, 在学完OOP之后我们的编程方式就从PP转变为OOP.
但是, 在这里的AOP并不是用来代替OOP的, 它反而是针对OOP做的一个补充和完善, 并不能去代替OOP.
类似于Spring和Spring Boot, Spring Boot是为了快速开发Spring, 是Spring的一个扩展插件.
所以引入AOP的好处在于 我们在去写一个功能的时候就只专注于做这个功能.
比如我们要添加一个文章, 那就把主要的精力放在文章上, 至于它有没有登录功能就不是我们要关注的问题, 这件事情统一交给AOP去处理, 我们只需要专注的添加文章的业务即可.
此时除了Article表之外的所有的东西就不需要我们关注了, 那么这个时候就需要在一个公共的地方做一个AOP的处理, 并且它不需要嵌入我们的代码.
这就是AOP的思想, 而 Spring AOP 是一个框架, 提供了一种对 AOP 思想的实现,它们的关系和loC 与 DI 类似。
在AOP思想中, "对某一类事情做集中处理"是做面向切面编程AOP, "某一类事情"就是一个"切面".
AOP的优点: 解耦, 能让代码只写一份, 在修改的时候能更加的方便.
除了统一的用户登录判断之外,AOP 还可以实现:
也就是说使用 AOP 可以扩充多个对象的某个能力,所以AOP 可以说是 OOP 的补充和完善.
上文提到, AOP可以实现的功能(作用)是非常多的, 不过每一个实现的功能之间是没有相互的关联关系的. 所以在一个系统里面可能会有多个AOP, 可能会有多个切面.
比如, 我们可能需要做一个统一的用户登录判断, 那么我们就得建一个用于用户的登录判断的AOP;
再比如, 我们需要做一个事务的开启和提交, 它也是AOP, 但是这个时候事务和用户登录之间就没有任何的联系了. 所以此时在我们程序就有两个AOP. 那么既然程序里面有多个AOP, 我们就得有一个定义.
像我们个人博客的库里面, 有多张表, 我们就得建多张表, 比如user, article表, 那么这个表就代表着一个业务, 文章表就代表着文章的增删改查的业务, 用户表就代表着用户的增删改查的业务.
那么我们AOP也是一样, 一个系统里面会有多个AOP, 那么一个切面就代表着一个AOP的类型.
切面指的就是某一方面的具体内容就是一个切面, 比如用户的登录判断就是一个"切面", 而日志的统计记录它又是一个"切面".
切面就类似于Java里面的类, 这个类可能有多个, 这个类就是用来做用户判断的, 那么用户判断的切面就会创建一个用户判断的切面类, 而日志统一记录也会创建一个统一记录的切面类.
那么我们"类"里面会有"方法", 所以第二个就是"切点"
切面是包含了: 通知、切点和切面的类,相当于AOP 实现的某个功能的集合。
在这个方法里面就会有具体的业务场景. 那么这个切点就是用来定义一组拦截规则.
以用户登录为例, 并不是所有的接口都要做用户登录, 比如用户登录本身, 本来要验证用户名和密码是佛正确, 结果验证之前要先验证是否登录了. 这就成了"让自己来证明是自己"的悖论. 再比如注册也是类似的, 都没有账号, 需要注册, 却先要验证是否登录.
这个时候就需要写规则了, 写 哪些接口要走用户的判断, 哪些接口不走, 这个就叫做"规则定义", 而这个"规则定义"是在"切点"中定义的.
所以一个切点就是一个方法, 这个方法上就定义了我们要拦截的规则.
切点就是定义(一个)拦截规则
除了切面和切点之外, 我们再想一下, 如果一个类里面, 要有类, 有方法, 之后就得有方法的具体实现, 也就是我们定义了切面, 也被我们拦截到了, 之后就得做 "通知" .
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。
"通知"也叫做Advice. 那么这个通知实际上就是要执行的具体的代码是什么.
它会执行AOP逻辑业务.
切点相当于要增强的方法
所有可能触发切点的点就叫做连接点。
比如个人博客项目的连接点就是那些所有的接口, 因为所有的接口都有可能要走我们的AOP, 走我们的拦截, 然后执行相应的方法. 那么这些所有的接口就是连接点.
这些连接点和切点有很大的区别, 连接点是指可能会触发的点, 而切点是具体的定义一个拦截规则的东西.
连接点相当于需要被增强的某个AOP功能的所有方法。
AOP 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:
接下来我们来实现一个目标: 实现拦截UserController中的所有方法, 在所有方法执行之前打印"执行了前置通知".
- <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-aop</artifactId>
- <version>2.7.14</version>
- </dependency>
切面是这个AOP是关于什么的, 那么就创建关于这个AOP的一个类, 以下代码就是一个切面.
- package com.example.demo.common;
-
- import org.aspectj.lang.annotation.Aspect;
- import org.springframework.stereotype.Component;
-
- @Aspect // 表明此类为一个切面
- @Component // 不能省略
- public class UserAOP {
- }
注意创建一个切面时首先要有一个类, 然后还要加两个注解. 一个是Aspect切面, 另外一个是Component, 我们想实现的目标是: 我们的切面随着框架的启动而启动, 这样我们可以完成:项目启动之后, 当我们去访问UserController, 那么我们就能拦截到. 如果没有Component, 就会导致我们写了一个切面, 但是切面没有随着项目的启动而启动, 那么这个切面就不能用, 写的这个切面就没有实际的作用了.
进行AOP拦截, 定义拦截规则. 面向切面编程, 就要筛选切面, 哪些是走切点, 哪些不走切点. 也就是定义要处理的某一类问题.
定义一个空方法, 它主要提供的是拦截规则, 而不是拦截实现. 在"通知"中才会有实现.
Spring AOP 切点的定义如下,在切点中我们要定义拦截的规则,具体实现如下:
- @Aspect // 表明此类为一个切面
- @Component // 不能省略
- public class UserAOP {
- // 切点 (配置拦截规则) 这里使用 Aspect 表达式语法
- @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
- public void pointcut(){ // 方法名可以任意
- }
- }
其中 pointcut 方法为空方法,它不需要有方法体,此方法名就是起到一个“标识”的作用,标识下面通知方法具体指的是哪个切点(因为切点可能有很多个) 。
AspectJ 支持三种通配符
* : 匹配任意字符,只匹配一个元素 (包,类,或方法,方法参数)
.. : 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
+ : 表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+表示继承该类的所有子类包括本身
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:
execution( <修饰符> <返回类型><包.类.方法(参数)> <异常> )
修饰符和异常可以省略,具体含义如下:
public | 公共方法 |
* | 任意 |
void | 返回没有值 |
String | 返回值字符串 |
* | 任意 |
com.example.demo | 固定包 |
om.example.demo.*.service | demo包下面子包任意 (例如: com.example.demo.staff.service) |
com.example.demo.. | demo包下面的所有子包 (含自己) |
com.example.demo.*.service.. | demo包下面任意子包,固定目录service,service目录任意包 |
UserServicelmpl | 指定类 |
*Impl | 以Impl结尾 |
User* | 以User开头 |
* | 任意 |
addUser | 固定方法 |
add* | 以add开头 |
*Do | 以Do结尾 |
* | 任意 |
() | 无参 |
(int) | 一个整型 |
(int, int) | 两个 |
(..) | 参数任意 |
表达式示例
通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务。Spring AOP 中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- package com.example.demo.common;
-
- @Aspect // 切面
- @Component // 不能省略
- public class UserAOP {
-
- @Pointcut("execution(public * com.example.demo.controller.UserController.*(..))")
- public void pointcut(){ // 方法名可以任意
- }
-
- // 前置通知
- @Before("pointcut()")
- public void doBefore(){
- System.out.println("执行了前置通知: " + LocalDateTime.now());
- }
- }
注: 这里的UserController就是一个普通的controller, 只是为了让我们演示实现里面的方法是怎么拦截的.
- package com.example.demo.controller;
-
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class UserController {
-
- @RequestMapping("/user/sayHi")
- public String sayHi() {
- System.out.println("执行了 sayHi 方法");
- return "hi, spring boot aop.";
- }
-
- @RequestMapping("/user/login")
- public String login() {
- System.out.println("执行了 login 方法");
- return "do user login";
- }
- }
作为对照组, 我们再建一个其他的controller, 比如ArticleController:
- package com.example.demo.controller;
-
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- @RestController
- public class ArticleController {
-
- @RequestMapping("/art/sayHi")
- public String sayHi() {
- System.out.println("执行了 Article sayHi 方法");
- return "article: hi, spring boot aop.";
- }
- }
启动项目来看一下UserController是否被拦截了, ArticleController是否没有拦截.
可以看到, 在执行sayHi方法之前, 执行了前置通知, 而且每一次执行都会执行一次前置通知, 然后再执行sayHi方法.
再执行一下login:
可以看到, 执行了login方法, 所以整个UserController中的代码都会正常执行.
作为对照组我们执行art/sayHi (访问后再刷新两次)
可以看到, 在执行ArticleController的sayHi方法的时候没有任何前置通知的执行. 这里因为我们的拦截规则配置了只拦截UserController, 其他是不拦截的.
以上我们就实现了UserController的拦截和处理.
还可以实现其他类型的通知.
- // 后置通知
- @After("pointcut()")
- public void doAfter(){
- System.out.println("执行了后置通知: " + LocalDateTime.now());
- }
可以看到, 执行了前置通知, 也执行了后置通知.
再刷新页面两次, 可以看到每次都执行了前置通知和后置通知.
访问/art/sayHi, 说明其他Controller不会受影响:
将事件本身交给方法, 也就是在方法中调用事件本身, 使用ProceedingJoinPoint的proceed()
其实如果分开去写就是前置通知+后置通知
- // 环绕通知
- @Around("pointcut()")
- public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
- System.out.println("开始执行环绕通知了");
- Object obj = joinPoint.proceed();
- System.out.println("结束环绕通知了");
- return obj;
- }
我们先注释掉前文的前置通知和后置通知, 启动项目, 来看一下环绕通知的效果.
当然在, 上图中, 我们是先访问了两次article再访问了两次user. 可以看到和代码预期结果一致.
那么环绕通知用于统计方法的执行时间是十分合适的, 在开始的时候记录一个开始时间, 在结束的时候记录一个结束时间, 那么就可以得到运行时间.
当我们取消注释掉前文中的前置通知和后置通知的代码后, 我们来看一下是先执行的是前置通知, 后置通知还是环绕通知.
可以看到, 先执行的是环绕通知, 那么这里的原因就是环绕通知中的代码joinPoint.proceed()所致, 这个代码里面才是前置通知和后置通知代码的执行. 在这个代码中会先执行前置通知, 再执行sayHi方法, 再执行后置通知, 然后才是出来进行环绕通知的结束打印.
Spring AOP 是构建在动态代理基础上,因此 Spring 对AOP 的支持局限于方法级别的拦截。
Spring AOP 支持JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。
Spring AOP 底层实现:
JDK 动态代理 : 速度快、反射实现动态代理
要求:被代理类一定要实现接口。
CGLIB: 通过实现代理类的子类来实现动态代理。它不能代理被 final 修饰类
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在 class 代码运行期,动态的织入字节码。
我们学习 Spring 框架中的AOP,主要基于两种方式: JDK 及CGLIB 的方式。这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类
注: 接下来以下动态代理所有有关内容摘自ChatGPT 3.5以及 www.javacn.site
在Spring框架中,AOP是一种编程范式,用于实现横切关注点(cross-cutting concerns)的分离,以提高代码的模块性和可维护性。在Spring AOP中,主要使用了两种动态代理来实现切面(Aspect)的功能:JDK动态代理和CGLIB动态代理。
JDK动态代理基于Java标准库提供的java.lang.reflect包,它只能代理接口(Interfaces)。
工作原理: 在运行时, 创建一个实现了被代理接口的匿名类,该类通过InvocationHandler接口来调用实际目标对象的方法。代理类和目标类之间的关系是通过接口实现来实现的。
JDK动态代理的使用步骤:
- // 定义接口
- public interface Calculator {
- int add(int a, int b);
- int subtract(int a, int b);
- }
-
- // 实际类实现接口
- public class CalculatorImpl implements Calculator {
- @Override
- public int add(int a, int b) {
- return a + b;
- }
-
- @Override
- public int subtract(int a, int b) {
- return a - b;
- }
- }
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
-
- // 实现InvocationHandler接口
- public class CalculatorInvocationHandler implements InvocationHandler {
- private Calculator target;
-
- public CalculatorInvocationHandler(Calculator target) {
- this.target = target;
- }
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println("Before method: " + method.getName());
- // 调用目标方法
- Object result = method.invoke(target, args);
- System.out.println("After method: " + method.getName());
- return result;
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- // 创建目标对象
- Calculator calculator = new CalculatorImpl();
- // 创建InvocationHandler实例
- CalculatorInvocationHandler invocationHandler
- = new CalculatorInvocationHandler(calculator);
- // 创建代理对象
- Calculator proxy = (Calculator) Proxy.newProxyInstance(
- calculator.getClass().getClassLoader(),
- calculator.getClass().getInterfaces(),
- invocationHandler
- );
- // 调用代理对象的方法
- int result = proxy.add(5, 3);
- System.out.println("Result: " + result);
- result = proxy.subtract(10, 4);
- System.out.println("Result: " + result);
- }
- }
在上述代码中,CalculatorInvocationHandler类实现了InvocationHandler接口, 并在invoke方法中添加了前置和后置的切面逻辑。Proxy.newProxyInstance方法通过反射创建了一个代理对象,将目标对象和切面逻辑绑定在一起。
运行上述代码会输出类似以下的结果:
Before method: add
After method: add
Result: 8
Before method: subtract
After method: subtract
Result: 6
这里的输出表明前置和后置的切面逻辑已经成功插入到了目标方法的执行过程中。
CGLIB(Code Generation Library)是一个基于字节码生成的动态代理机制,它能够代理类而不仅限于接口。
工作原理: CGLIB通过继承的方式创建目标对象的子类,并覆盖目标对象的方法,将切面逻辑插入其中。
CGLIB动态代理的使用步骤:
下面是一个使用CGLIB动态代理的简单示例:
首先,确保你在项目中引入了CGLIB库。通常,你需要添加以下依赖:
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>3.3.0</version>
- </dependency>
然后,实现一个代理类并使用CGLIB动态代理:
- import net.sf.cglib.proxy.Enhancer;
- import net.sf.cglib.proxy.MethodInterceptor;
- import net.sf.cglib.proxy.MethodProxy;
-
- import java.lang.reflect.Method;
-
- // 实现MethodInterceptor接口
- public class CalculatorMethodInterceptor implements MethodInterceptor {
- @Override
- public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
- System.out.println("Before method: " + method.getName());
- // 调用目标方法
- Object result = proxy.invokeSuper(obj, args);
- System.out.println("After method: " + method.getName());
- return result;
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- // 创建Enhancer实例
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(CalculatorImpl.class);
- // 设置拦截器
- enhancer.setCallback(new CalculatorMethodInterceptor());
- // 创建代理对象
- CalculatorImpl proxy = (CalculatorImpl) enhancer.create();
- // 调用代理对象的方法
- int result = proxy.add(5, 3);
- System.out.println("Result: " + result);
- result = proxy.subtract(10, 4);
- System.out.println("Result: " + result);
- }
- }
在上述代码中,我们首先实现了CalculatorMethodInterceptor类,它实现了CGLIB的MethodInterceptor接口。在intercept方法中,我们添加了前置和后置的切面逻辑。
然后,我们使用Enhancer类创建一个代理对象。我们设置目标类为CalculatorImpl.class,设置拦截器为刚刚实现的CalculatorMethodInterceptor。最后,我们调用enhancer.create()方法创建代理对象。
运行上述代码会输出类似以下的结果:
Before method: add
After method: add
Result: 8
Before method: subtract
After method: subtract
Result: 6
这里的输出同样表明前置和后置的切面逻辑已经成功插入到了目标方法的执行过程中。请注意,CGLIB动态代理创建的是目标类的子类,因此需要确保目标类不是final且具有默认的构造方法。
JDK动态代理适用于代理接口,使用起来相对简单,但对于非接口类无法直接代理。CGLIB动态代理适用于代理类,可以代理非接口类,但生成代理类的过程相对复杂,性能可能会受到影响。
在Spring AOP中,当目标类实现了接口时,会默认使用JDK动态代理,否则会使用CGLIB动态代理。开发者可以根据具体需求选择合适的动态代理方式来实现切面功能。
1. 代理对象类型:JDK动态代理只能代理实现了接口的类,即代理的对象需要实现至少一个接口。
2. 代理方式:JDK动态代理使用Java标准库中的java.lang.reflect.Proxy类和 java.lang.reflect.InvocationHandler 接口实现来创建代理对象。代理对象在运行时会生成一个实现了目标接口的匿名类。
3. 运行时特点:由于使用接口实现,代理对象与目标对象的关系在运行时通过接口来建立。调用代理对象的方法实际上会触发InvocationHandler中的invoke方法,从而实现切面逻辑的插入。
1. 代理对象类型:CGLIB动态代理可以代理任意类,包括没有实现接口的类。
2. 代理方式:CGLIB动态代理使用字节码生成技术,在运行时创建目标类的子类,并覆盖其中的方法以实现切面逻辑的插入。
3. 运行时特点:由于生成子类,代理对象与目标对象的关系在运行时通过继承来建立。调用代理对象的方法会被重定向到子类中的拦截器方法,从而实现切面逻辑的插入。
选择使用哪种代理方式取决于你的需求。如果目标类已经实现了接口,你可以考虑使用JDK动态代理;如果目标类没有实现接口或者你需要更高的灵活性,可以选择CGLIB动态代理。
关于两种动态代理的区别还可以参考 JDK动态代理和CGLIB有什么区别? | Javaᶜⁿ 面试突击
在企业实际开发中,JDK动态代理和CGLIB动态代理都有广泛的应用。它们都是Spring框架中AOP(Aspect-Oriented Programming)的基础,用于实现横切关注点的分离。以下是它们在实际开发中的应用情况:
JDK动态代理的应用场景:
1. 接口代理:当目标对象实现了接口时,可以使用JDK动态代理来实现切面逻辑。
2. AOP切面:企业应用中常见的事务管理、日志记录等切面逻辑可以通过JDK动态代理来实现。
3. 轻量级代理:JDK动态代理相对于CGLIB动态代理来说,生成的代理对象较轻量,适用于某些对性能要求不高的场景。
CGLIB动态代理的应用场景:
1. 类代理:当目标对象没有实现接口时,可以使用CGLIB动态代理来实现切面逻辑。
2. AOP切面:与JDK动态代理一样,可以用于事务管理、日志记录等切面逻辑。
3. 强大的代理能力:CGLIB动态代理可以代理任意普通的类,而不仅限于实现了接口的类。
在实际开发中,你可以根据项目需求来选择使用JDK动态代理还是CGLIB动态代理。通常情况下,如果目标对象实现了接口,首选JDK动态代理;如果目标对象没有实现接口或者性能要求较高,可以考虑使用CGLIB动态代理。Spring AOP会根据目标对象的特点自动选择使用合适的代理方式,因此在大部分情况下,开发者不需要过多地关注具体的代理方式。
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。