一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
模式动机
在某些情况下,客户不想或不能直接引用某个对象,这个时候就需要一个中介(中间人)帮忙完成间接引用,该中介被称为 ”代理对象“。
举个具象化的例子:A 想向 C 借钱,但由于信用透支 / 彼此不认识 / ...,C 不会借钱给 A,这时 A 就可以让 B(代理对象)以他自己的名义去找 C 借钱,这样 A 不仅获得了钱💴,还让 C 以为是 B 借钱,并不知道 A 的存在。在这个案例中,B 充当了 A 的代理人。
🙄生活中代理对象比比皆是,比如 “相亲对象与红娘”、“明星与经纪人”、“当事人张三和律师”、“老板和秘书” 等等。
🚩我们试着从软件系统的角度举一个例子来说明为什么要控制对于某个对象的访问!
假设有一个消耗大量系统资源的巨型对象,你只是偶尔需要使用它,并非总是需要。
这种直接查询数据库的方式有可能会非常缓慢😡
我们可以实现延迟初始化(懒加载):在实际有需要时再创建该对象。但对象的所有客户端都要执行懒加载代码,这会带来很多重复代码。
在理想情况下,我们希望可以将这部分懒加载的代码放入对象的类中,但这并非总能实现:如果这个类是第三方库的一部分呢?
⭐所以我们要思考其他的出路,随着程序猿们经验的积攒与更深入的思考,它们想出了一种解决办法:新建一个与原服务对象接口相同的代理类,将客户端对原服务对象的请求转为客户端对代理对象的请求,一旦代理对象接收到客户端请求后就会创建实际的服务对象,并将所有工作委派给它。
代理将自己伪装成数据库对象,可在客户端或实际数据库对象不知情的情况下处理延迟加载和缓存查询结果的工作。
这就可以让代理对象在主要业务逻辑前后执行一些额外的工作,而且无需修改原对象接口。
OK,以上介绍的就是「代理模式」出现的动机以及「代理模式」的工作原理。
定义
代理模式是一种对象结构型模式。
代理模式能给某个对象提供一个代理,由代理对象控制着对原对象的访问,并允许在将请求提交给对象前后进行一些处理。
UML 类图
模式结构
代理模式包含如下角色:
ServiceInterface:服务接口声明了抽象的业务逻辑Service:服务类提供了具体实用的业务逻辑Proxy:代理类包含一个指向服务对象的引用成员变量,代理完成其他额外任务后会将请求传递给服务对象Client:能通过统一接口与服务对象/代理对象进行交互
更多实例
信用卡是银行账户🏦的代理,而银行账户是一大捆现金💴的代理。它们都实现了同样的接口,均可用于支付。
消费者会非常满意,因为不必再随身携带大量现金;商店老板也同样会十分高兴,因为交易收入能以电子化的方式存入老板的银行账户中,无须担心现金丢失或被抢劫的情况。
示例代码
ServiceInterface.java
public interface ServiceInterface {
void operation();
}
Service.java
public class Service implements ServiceInterface {
@Override
public void operation() {
// Real Service working...
}
}
Proxy.java
public class Proxy implements ServiceInterface {
private Service realService;
public Proxy(Service realService) {
this.realService = realService;
}
private boolean checkAccess() {
// check the condition to determine whether the realService needs to be manipulated...
return false;
}
private void preHandle() {
// TODO: do something before operation
}
private void postHandle() {
// TODO: do something after operation
}
@Override
public void operation() {
preHandle();
if (checkAccess()) {
realService.operation();
}
postHandle();
}
}
Client.java
public class Client {
public static void main(String[] args) {
Service realService = new Service();
Proxy proxy = new Proxy(realService);
proxy.operation();
}
}
优缺点
✔可以在客户端毫无察觉的情况下控制服务对象,同时分离客户与服务对象,在一定程度上降低了系统的耦合度。
✔即使服务对象还未准备好或不存在,代理也可以正常工作。
✔代理还可以对服务对象的整个生命周期进行管理。
✔你可以在不对服务/客户端做出修改的情况下创建新代理,符合”开闭原则“。
❌实现代理需要额外的工作,这可能会造成代码变得复杂。
❌由于在客户端和真实服务之间还存在着代理,所以可能造成服务响应有所延迟。
适用场景
在以下情况推荐使用代理模式:
下文 ”模式扩展“ 部分会详细讨论这部分内容
- 延迟加载。当你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。
- 本地执行远程服务。适用于服务对象位于远程服务器上的情形。
- 日志记录代理。代理可以在向服务传递请求前进行记录。
- 缓存代理。适用于需要缓存客户请求结果并对缓存生命周期进行管理时,特别是当返回结果的体积非常大时。
- 智能引用。可在没有客户端适用某个重量级对象时立刻销毁该对象。
「代理模式」落地
🚩代理模式的应用场景十分广阔,这里举一些我所学过的应用「代理模式」的知识。
前端 Vue 框架的数据响应式原理
「Vue 2.x」 使用 Object.defineProperty 来劫持各个属性的 setter/getter 进行数据监听
「Vue 3.x」 使用 Proxy 来监听数据
🚀我在之前 Vue 专栏的文章中有提到 Vue 响应式原理,它就是通过 Object.defineProperty 来实现的,感兴趣的可以看看这篇文章,帮助你更好地理解代理模式在这之中的应用:Vue 双向绑定的极简实现【Vue 响应式原理】
代理服务器
代理模式在计算机网络通信中也有一席之地,比如常见的正向代理服务器与反向代理服务器就是很好的例子。
🚀对这些概念还一知半解的建议看下这篇文章,里面对于正向代理 & 反向代理的概念及其应用讲解得很透彻:什么是「正向代理」与「反向代理」?
Spring AOP 动态代理技术
Spring 框架中的 AOP 技术也是代理模式的应用落地,它使用动态代理技术的 JDK 动态代理与 CGLIB 代理实现切面增强。
如果目标对象没有实现接口,则默认会采用 CGLIB 代理;
如果目标对象实现了接口,默认使用 JDK 动态代理,可以强制使用 CGLIB 实现代理。
⭐动态代理会在下文的「模式扩展」部分详细阐述,不要错过噢。
请求拦截器
我们使用 Axios 来进行 HTTP 请求时,可以提前对请求和响应做一些预处理,比如:
- 请求头的设置和
Cookie信息的设置 - 权限信息的预处理,如
Token验证 - 数据格式化
- 空字段格式化预处理,过滤
response通用报错处理
🚀关于 axios 拦截器的详细介绍可以翻看我之前的一篇文章:axios 拦截器
⭐下面给出一个比较经典的拦截案例,通过 Axios 搭配 ElementUI 实现加载效果:
// 通过axios拦截器增添Loading加载效果!
let loadingInstance = null
axios.interceptors.request.use((config) => {
// 发送请求后且返回数据前, 页面会一直处于加载状态
loadingInstance = Loading.service({ fullscreen: true, text: '加载数据中, 请稍等..' })
return config
}, (error) => {
return Promise.reject(error)
})
axios.interceptors.response.use((response) => {
// 关闭加载页面的效果
loadingInstance.close()
return response
}, (error) => {
return Promise.reject(error)
})
模式扩展
静态代理
前文所涉及的代理模式皆为静态代理模式,它在代理模式中较为常见,实现过程也比较简单和易于理解,因为静态代理的代理关系在编译期间就已经确定了。
动态代理
静态代理适合用于代理类比较少且确定的情况,试想如果 Service 的委托方法较多,Proxy 逐一进行代理也比较困难,所以就需要更高级的处理方法:动态代理。
动态代理就是一种较为高级的代理模式,其代理对象在程序运行时动态生成,典型应用 Spring AOP 技术。
JDK 动态代理
JDK 动态代理通过反射机制动态生成代理对象,被代理对象需要实现至少一个接口,主要涉及 java.lang.reflect 包下 Proxy 与 InvocationHandler.
⭐那如果被代理对象没有实现接口呢?那我们可以使用 CGLIB 动态代理,它是通过继承被代理类实现的。
👨💻接下来,我们使用 JDK 动态代理来真正实现一下缓存技术。
UML 类图
ServiceInterface.java
public interface ServiceInterface {
User getUserById(int id);
}
Service.java
被代理类
public class Service implements ServiceInterface{
@Override
public User getUserById(int id) {
System.out.println("Get user from database..");
// pretend to get from database
return new User(1);
}
}
JdkProxyHandler.java
public class JdkProxyHandler implements InvocationHandler {
// cache for users
private Map<Integer, User> userCache;
private Object targetObject;
public JdkProxyHandler(Object targetObject) {
this.targetObject = targetObject;
this.userCache = new ConcurrentHashMap<>();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (userCache.containsKey(args[0])) {
System.out.println("Get from cache..");
return userCache.get(args[0]);
}
// execute targetObject.getUserById()
Object cacheUser = method.invoke(targetObject, args);
// add to cache
userCache.put((Integer) args[0],(User) cacheUser);
return cacheUser;
}
}
JdkProxyFactory.java
通过 JDK 动态代理生成代理对象
public class JdkProxyFactory {
public static Object getDynamicProxy(Object targetObj) {
ClassLoader classLoader = JdkProxyFactory.class.getClassLoader();
Class<?>[] interfaces = targetObj.getClass().getInterfaces();
InvocationHandler handler = new JdkProxyHandler(targetObj);
// return dynamic_proxy_object
return Proxy.newProxyInstance(classLoader, interfaces, handler);
}
}
User.java
public class User {
private int id;
public User(int id) {
this.id = id;
}
}
Client.java
public class Client {
public static void main(String[] args) {
ServiceInterface realService = new Service();
ServiceInterface dynamicProxyObject = (ServiceInterface) JdkProxyFactory.getDynamicProxy(realService);
dynamicProxyObject.getUserById(1);
dynamicProxyObject.getUserById(1);
}
}
运行结果
CGLIB 动态代理
CGLIB 代理不同于 JDK 代理,CGLIB 底层是通过 ASM 字节码实现动态代理的,需要注意的是,CGLIB 不能代理 final 方法,因为 CGLIB 是动态生成代理类的子类(也就是继承实现)。
⭐捋一捋 JDK 代理与 CGLIB 代理:
- JDK 动态代理通过反射机制实现;CGLIB 动态代理通过 ASM 字节码实现。
- JDK 动态代理中的被代理类必须实现至少一个接口;CGLIB 动态代理需要继承被代理类,所以不能代理
final方法。
👨💻同样,我们也使用 CGLIB 动态代理来实现一下缓存技术,代码跟 JDK 动态代理相差不大,所以我对其进行更深一层的抽象,增强其复用性。
如果你要简单使用一下 CGLIB 动态代理,那十几行代码就可以搞定了:
- 首先你只需要创建一个类
implements MethodInterceptor,重写intercept();- 然后关注下文代码中的
CglibProxyFactory.java是如何生成代理对象就可以了。而下面这部分代码是实现一个针对缓存的小案例,让你对 CGLIB 动态代理有一个更深入的印象与了解。
UML 类图
Target.java
Target是被代理类,但却不需要像Service一样实现某个接口
public class Target {
public User getUserById(int id) {
User user = new User(id);
System.out.println("Get " + user + " from database...");
return user;
}
}
Before.java
前置增强接口
public interface Before {
Object before(Object[] args);
}
After.java
后置增强接口
public interface After {
void after(Object... args);
}
BeforeAdvice.java
前置增强实现类,从缓存中获取对象
public class BeforeAdvice implements Before {
private static Map<Integer, User> caching = new ConcurrentHashMap<>();
public static Map<Integer, User> getCaching() {
return caching;
}
@Override
public Object before(Object[] args) {
if (args == null)
return null;
// in default, the first arg is key
int keyId = (int) args[0];
// find the cache object
if (caching.containsKey(keyId)) {
User cachedUser = caching.get(keyId);
System.out.println("Get " + cachedUser + " from cache...");
return cachedUser;
}
// can't find the cache object
return null;
}
}
AfterAdvice.java
后置增强实现类,把从数据库获取的对象添加到缓存中,以便下次从缓存中获取
public class AfterAdvice implements After {
@Override
public void after(Object... args) {
if (args == null)
return;
BeforeAdvice.getCaching().put((Integer) args[0], (User) args[1]);
}
}
CglibProxy.java
public class CglibProxy implements MethodInterceptor {
private Object targetObject;
private Before beforeAdvice;
private After afterAdvice;
public CglibProxy(Object targetObject, Before before, After after) {
this.targetObject = targetObject;
this.beforeAdvice = before;
this.afterAdvice = after;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object obj = null;
if (beforeAdvice != null) {
obj = beforeAdvice.before(args);
// get 'obj' from cache
if (obj != null) {
return obj;
}
}
// get 'obj' from database
obj = method.invoke(targetObject, args);
if (afterAdvice != null) {
// storage the 'obj' in cache for next search
afterAdvice.after(args[0], obj);
}
// return the database_obj
return obj;
}
}
CglibProxyFactory.java
通过 CGLIB 动态代理生成代理对象
public class CglibProxyFactory {
public static Object getCglibProxy(Object target) {
Enhancer enhancer = new Enhancer();
// set Target as SuperClass
enhancer.setSuperclass(target.getClass());
// set callback function (which implements MethodInterceptor)
enhancer.setCallback(new CglibProxy(target, new BeforeAdvice(), new AfterAdvice()));
// create proxy object
return enhancer.create();
}
}
User.java
public class User {
private int id;
public User(int id) {
this.id = id;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
'}';
}
}
Client.java
public class Client {
public static void main(String[] args) {
Target proxyObject = (Target) CglibProxyFactory.getCglibProxy(new Target());
// The first time: get from database & storage it in cache
proxyObject.getUserById(1);
// The second time: get from cache
proxyObject.getUserById(1);
}
}
运行结果
区别:适配器模式 & 代理模式
适配器模式与代理模式有些许相同点可能会造成混淆,但只要记住以下区别就可以很好地区分它们:
- 适配器模式的封装对象
Adapter能提供不同于原服务对象Adaptee的接口 - 代理模式的代理
Proxy能提供与原服务对象Service相同的接口
更多:装饰模式能为对象提供加强的接口
浅聊几种常见的代理模式
远程代理
在日常开发中,有时会遇到本地服务完成不了的功能,那就需要访问远程服务中的对象(远程对象)。
⭐原理:远程代理可以作为另一个 JVM 上对象的本地代表,调用代理方法,会被代理利用网络转发到远程执行,其结果通过网络返回给代理对象,再由代理交付给客户。
- 从面向过程的角度:为了实现本地到远程的通信,我们需要实现网络通信,不过为了良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
- 从面向对象的角度:面向过程中我们关注本地服务如何获得远程服务的功能,而在面向对象中我们更关注本地服务如何获得远程对象,从而获得远程服务提供的功能。
Java 中可通过 Java RMI (Remote Method Invocation) 实现远程代理
虚拟代理
”模式动机“ 部分有具体相关的例子
如果需要创建一个资源消耗巨大的对象,首先会创建一个消耗相对较小的对象来表示,真实对象只在需要时才真正创建。
保护代理
当一个对象收到大量请求时,可以设置保护代理,通过一些判断条件过滤掉一些请求;甚至还可以给不同的访问用户提供不同级别的使用权限。
缓存代理
为某个目标操作的结果提供缓存空间,以便多个客户端可以共享这些结果,还能提高访问速度。
防火墙代理
保护目标不让恶意用户接近。
智能指引代理
主要用于调用目标对象时,代理附加一些额外处理功。比如:增加计算真实对象的引用次数的功能,当该对象没有被引用时,就可以自动释放它(JVM-GC 垃圾回收算法——引用计数法)。
同步化代理
让多个用户同时操作一个对象而不会发生冲突。
最后
❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!
❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!