spring AOP(代理模式、静态代理、动态代理)
AOP的底层机制就是动态代理
代理模式:
给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
简而言之,代理模式就是设置一个中间代理来控制访问原目标对象,已达到增强原对象的功能和简化访问的方式
在不修改原目标对象的前提下,提供格外的功能操作,扩展目标对象的功能
代理模式角色分为 3 种:
Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
RealSubject(真实主题角色):真正实现业务逻辑的类;
Proxy(代理主题角色):用来代理和封装真实主题;
-
静态代理
//抽象角色:增删改查业务 public interface UserService { void add(); void delete(); void update(); void query(); }
//真实对象,完成增删改查操作的人 public class UserServiceImpl implements UserService { public void add() { System.out.println("增加了一个用户"); } public void delete() { System.out.println("删除了一个用户"); } public void update() { System.out.println("更新了一个用户"); } public void query() { System.out.println("查询了一个用户"); } }
现在需要增加一个日志功能,怎么实现!
- 思路1 :在实现类上增加代码 【麻烦!】
- 思路2:使用代理来做,能够不改变原来的业务情况下,实现此功能就是最好的了
//代理角色,在这里面增加日志的实现 public class UserServiceProxy implements UserService { private UserServiceImpl userService; public void setUserService(UserServiceImpl userService) { this.userService = userService; } public void add() { log("add"); userService.add(); } public void delete() { log("delete"); userService.delete(); } public void update() { log("update"); userService.update(); } public void query() { log("query"); userService.query(); } public void log(String msg){ System.out.println("执行了"+msg+"方法"); } }
//测试 public class Client { public static void main(String[] args) { //真实业务 UserServiceImpl userService = new UserServiceImpl(); //代理类 UserServiceProxy proxy = new UserServiceProxy(); //使用代理类实现日志功能! proxy.setUserService(userService); proxy.add(); } }
输出结果
执行了add方法
增加了一个用户Process finished with exit code 0
-
抽象角色 : 一般使用接口或者抽象类来实现
-
真实角色 : 被代理的角色
-
代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作 .
-
客户 : 使用代理角色来进行一些操作 .
现实生活中的样子,你看不到房东,但是你依旧租到了房东的房子通过代理,这就是所谓的代理模式,程序源自于生活
静态代理的好处:
- 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
- 公共的业务由代理来完成 . 实现了业务的分工 ,
- 公共业务发生扩展时变得更加集中和方便 .
- 通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
缺点 :
- 类多了 , 多了代理类 , 工作量变大了 . 开发效率降低 .
- 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
想要静态代理的好处,又不想要静态代理的缺点,所以 , 就有了动态代理 !
思想:
我们在不改变原来的代码的情况下,实现了对原有功能的增强,这是AOP中最核心的思想
传统的开发:纵向开发 dao service servlet 前端
横向的编程:aop底层实现机制,增加一个代理类,横切进去
-
动态代理
-
动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
-
动态代理的角色和静态代理的一样 .
-
动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的
-
动态代理分为两类 : 一类是基于接口动态代理 , 一类是基于类的动态代理
-
- 基于接口的动态代理----JDK动态代理
- 基于类的动态代理–cglib
- 现在用的比较多的是 javasist 来生成动态代理 . 百度一下javasist
- 我们这里使用JDK的原生代码来实现,其余的道理都是一样的!
为什么类可以动态的生成?
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。
两种最常见的方式:
- 通过实现接口的方式 -> JDK动态代理
- 通过继承类的方式 -> CGLIB动态代理
注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦
JDK的动态代理需要了解两个类
java.lang.reflect.Proxy和
java.lang.reflect.InvocationHandler
核心 : InvocationHandler 和 Proxy
【InvocationHandler:调用处理程序】
Object invoke(Object proxy, 方法 method, Object[] args); //参数 //proxy - 调用该方法的代理实例 //method -所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。 //args -包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer或java.lang.Boolean 。
【Proxy : 代理】
Proxy提供了创建动态代理类和实例的静态方法。
测试:
编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke
方法中编写方法调用的逻辑处理
package com.hop.demo2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
/**
* @author HuangOuPeng
* @create 2020-12-22-14:05
*/
public class LogHandler implements InvocationHandler {
Object target;//被代理的对象,实际的方法执行者
public LogHandler() {
}
public LogHandler(Object target) {
this.target = target;
}
public void setLogHandler(Object target) {
this.target = target;
}
// proxy : 代理类
// method : 代理类的调用处理程序的方法对象.
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(method.getName());
Object result = method.invoke(target, args);//调用target的method方法
return result;
}
//调用invoke方法之前打印日志
private void before(String methodName){
System.out.println(String.format("log start time [%s] ", new Date()));
System.out.println("执行了"+methodName+"方法!!");
}
}
编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法,具体步骤可见代码和注释
package com.hop.demo2;
import com.hop.demo1.UserService;
import com.hop.demo1.UserServiceImpl;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
/**
* @author HuangOuPeng
* @create 2020-12-22-14:13
*/
public class Client2 {
public static void main(String[] args) {
//1.创建被代理的对象,UserService接口的实现类
UserServiceImpl userService = new UserServiceImpl();
//2.获取对应的ClassLoader
ClassLoader classLoader = userService.getClass().getClassLoader();
// 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
Class<?>[] interfaces = userService.getClass().getInterfaces();
// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
// 这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userService);
/*
5.根据上面提供的信息,创建代理对象 在这个过程中,
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
proxy.add();
}
}
结果
log start time [Tue Dec 22 14:35:19 CST 2020]
执行了add方法!!
增加了一个用户
在编写动态代理类中加入
//生成代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
这样在调用时候直接调用
public class Client3 {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
LogHandler logHandler = new LogHandler();
logHandler.setLogHandler(userService);
UserService proxy = (UserService) logHandler.getProxy();
proxy.add();
}
}
面试题
描述动态代理的几种实现方式?分别说出相应的优缺点
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object
- 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
- 缺点:不同的接口要有不同的代理类实现,会很冗余
JDK 动态代理:
-
为了解决静态代理中,生成大量的代理类造成的冗余;
-
JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,
-
jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象
-
jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口
-
优点:解决了静态代理中冗余的代理实现类问题。
-
缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。
CGLIB 代理:
-
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;
-
CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。
-
实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。
-
但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
-
同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
-
优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
-
缺点:技术实现相对难理解些。