写点什么

Java 核心基础——动态代理、静态代理

用户头像
老农小江
关注
发布于: 2020 年 11 月 21 日

一、前言

代理,相信大多数人对这个名词并不陌生,从本质上说,代理并不是 Java 语言层面特有的特性,它更多的是一种思想,或者说一种行为模式,但笔者认为这也是 Java 核心基础 。因为代理的思想在 Java 的世界里非常重要,充分理解代理的思想,对于我们理解 Java 整个技术体系非常重要。

二、日常生活中的代理

在生活当中,代理的场景可以说是随处可见,各种中介公司、代理加盟品牌、便利店等等,从本质上来说,都算是一种代理。

以便利店为例,它并不直接生产各种商品,而是将各个工厂生产的商品聚集在一起,以方便消费者选择自己需要的商品。在这个过程中,工厂、便利店、消费者这三者之间的关系简化如下所示:

在这三者关系中,便利店就充当了工厂、消费者之间的代理这个角色,而也正是有了中间这个代理,工厂不需要将生产的大量商品逐个寻找需要的消费者销售,而当消费者需要购买多种物品时,也不再需要自己去寻找多家工厂一一购买,通过便利店这一中间代理角色的引入,极大地解放了工厂、消费者之间的供需关系。

通过便利店的例子可以发现,代理在某些场景下是可以给我们带来极大地便利的。虽然说消费者也可以直接找工厂购买所需商品,工厂也可以直接将生产的商品卖给消费者,但是这样的话,不管是对消费者还是工厂来说,都是一件效率低下且麻烦的事情,而有了便利店这一层代理,就充分解决了这些问题。日常生活中如此,在代码的世界里同样如此,毕竟,从某种意义上来说,程序代码是对生活的高度抽象。

Tips:

为了解释代理这种模式,笔者简化了示例,实际生活当中的工厂、便利店、消费者之间的关系远比笔者描述的要更加复杂。比方说,各工厂其实可能同时代理生产多个品牌的多种商品,而商品品牌方同时可能找多家工厂代理生产,便利店也有总店、分店、加盟店等相关层级代理。类似这样的,多个代理方,多层代理的场景,在代码的世界里,同样也是允许存在的,当我们面对这种情况的时候,不必过于慌张,只要逐步理清其间的关系 ,问题也就迎刃而解了。

三、静态代理

我们从一个例子说起。假如说,我们有一个照相机的对象,其有拍照片、美化照片、删除照片共 3 个方法,那么,我们设计接口和实现类时,通常如下所示:

照相机接口 ICamera.java

package com.xiaojiang.demo.proxy;
/** * @Description : 照相机接口 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 22:07 */public interface ICamera { /** * 拍照片 */ void takePhoto();
/** * 美化照片 */ void modifyPhoto();
/** * 删除照片 */ void deletePhoto();}
复制代码

照相机接口实现类 CameraImpl.java

package com.xiaojiang.demo.proxy;
/** * @Description : 照相机接口实现类 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 22:08 */public class CameraImpl implements ICamera {
/** * 拍照片 */ @Override public void takePhoto() { System.out.println("--拍照片--"); }
/** * 美化照片 */ @Override public void modifyPhoto() { System.out.println("--美化照片--"); }
/** * 删除照片 */ @Override public void deletePhoto() { System.out.println("--删除照片--"); }}
复制代码

而当我们需要调用照相机ICamera这个接口相关方法时,示例如下:

package com.xiaojiang.demo.proxy;
/** * @Description : 主函数 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 21:56 */public class App { public static void main(String[] args) { // 直接创建实现类 调用 ICamera iCamera = new CameraImpl(); iCamera.takePhoto(); iCamera.modifyPhoto(); iCamera.deletePhoto(); }}
复制代码

这样看起来挺好,面向接口设计,业务逻辑也清晰,没毛病。但是呢,实际需求通常是不断变化的,如果说有一天,我们需要在这几个业务方法中添加额外的业务逻辑,比如说,添加操作日志记录一下相关操作,那我们该怎么办呢?

简单的做法,当然是直接修改原有的实现方法,在原有代码中添加新的业务逻辑,修改后,示例如下:

/**  * 拍照片  */@Overridepublic void takePhoto() {    System.out.println("某年某月末日,拍照片,记录一下");    System.out.println("--拍照片--");}
/** * 美化照片 */@Overridepublic void modifyPhoto() { System.out.println("某年某月末日,美化照片,记录一下"); System.out.println("--美化照片--");}
/** * 删除照片 */@Overridepublic void deletePhoto() { System.out.println("某年某月末日,删除照片,记录一下"); System.out.println("--删除照片--");}
复制代码

这样做的话,一般情况下当然是可以的,但是,思考一下,这样做有没有什么不足呢?

笔者此处简单列举几点:

  1. 需要修改原有业务逻辑代码;(修改原有业务逻辑易出错、风险高,且不一定允许直接修改,如项目底层 framework 包、第三方服务包等);

  2. 如果是通用操作的,通常修改量非常大,如日志、接口参数校验、权限校验等;

  3. 不便于后续修改扩展,如后续相关通用逻辑更改了,就又需要大量修改代码。

那么,有没有一种更好的方式,或者说更优雅的方式来实现类似场景的需求呢?

当然是有的,那就是代理。通过引入一个中间的代理角色,让代理角色去代理实现最终的业务逻辑,而我们就可以在代理角色中添加我们所需的业务逻辑。

还是以照相机这个场景为例,当我们需要添加操作日志时,我们不直接在原有代码逻辑中修改,而是先创建一个照相机实现类的代理对象,然后在代理对象中添加操作日志的逻辑,代理类示例如下:

照相机代理实现类ProxyCameraImpl.java

package com.xiaojiang.demo.proxy;
/** * @Description : 照相机接口 代理实现类 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 22:18 */public class ProxyCameraImpl implements ICamera {
/** * 在代理类内部定义 被代理实现的接口 */ private ICamera iCamera;
/** * 通过构造函数,在代理类内中传入实际被代理的对象 * @param iCamera */ public ProxyCameraImpl(ICamera iCamera) { this.iCamera = iCamera; }
/** * 拍照片 */ @Override public void takePhoto() { System.out.println("某年某月末日,拍照片,记录一下"); iCamera.takePhoto(); }
/** * 美化照片 */ @Override public void modifyPhoto() { System.out.println("某年某月末日,美化照片,记录一下"); iCamera.modifyPhoto(); }
/** * 删除照片 */ @Override public void deletePhoto() { System.out.println("某年某月末日,删除照片,记录一下"); iCamera.deletePhoto(); }}
复制代码

创建代理对象后,我们在使用照相机接口时,示例如下:

package com.xiaojiang.demo.proxy;
/** * @Description : 主函数 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 21:56 */public class App { public static void main(String[] args) { // 静态代理调用 ICamera iCamera = new ProxyCameraImpl(new CameraImpl()); iCamera.takePhoto(); iCamera.modifyPhoto(); iCamera.deletePhoto(); }}
复制代码

通过示例代码可以看出,通过引入代理类ProxyCameraImpl ,我们在并没有修改原有实现类CameraImpl的情况下,成功地添加了我们所需的操作日志业务代码。在使用照相机接口ICamera的时候,我们不再直接实例化原CameraImpl对象,而是实例化代理对象ProxyCameraImpl,然后将原CameraImpl对象实例传入代理对象ProxyCameraImpl中,最终通过接口调用方法时,和原来并无任何差别。(有点绕,多看几遍代码)

像这种不修改原有代码逻辑,而是提供一个新的对象实现扩展原有对象的接口,从而实现对原有对象的间接控制与扩展,就是代理模式。需要注意的是,代理对象与被代理对象需要实现同一个接口,或继承同一个类。而之所以叫静态代理,是因为我们的代理对象ProxyCameraImpl是我们自己手动编写的代码,而不是系统自动创建的。(注:该描述非官方严格定义,仅供参考理解)

四、动态代理

我们继续思考一下,在以上的例子中,我们手动为原实现类创建了一个对应代理类,从而实现对原实现类的代理。假如说原实现类不止三个方法,而是有十几个方法,那我们是否就要修改代理类中的十几个方法?再假如说,我们有多个类需要添加通用业务逻辑,比如说需要对多个类的多个方法进行耗时统计,那我们是不是就需要手动创建多个代理类,然后依次修改各个代理类的方法呢?

如果真的是这样的话,那实在是一件麻烦的事情。好在前辈大佬们已经想到了一种更好的方式,那就是动态代理,也就是在程序运行过程中,动态地创建代理对象。

JDK 为实现动态代理的机制,提供了java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类这两个核心基础,通过InvocationHandler接口、Proxy类的组合使用,我们就可以构建一个动态代理类。

我们还是以照相机场景为例,假如我们需要统计拍照片、美化照片、删除照片这 3 个接口调用的耗时,那么通过动态代理的方式,示例如下:

照相机动态代理实现类ProxyCameraImplHandler.java

package com.xiaojiang.demo.proxy;
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;
/** * @Description : 照相机接口 动态代理实现类 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 12:12 */public class ProxyCameraImplHandler implements InvocationHandler { /** * 被代理的对象 */ private Object targetObject;
/** * 构造函数 ( 通过构造函数将被代理的对象传入动态代理类中 ) * @param targetObject 被代理的对象 */ public ProxyCameraImplHandler(Object targetObject) { this.targetObject = targetObject; }
/** * 执行调用被代理对象的方法 * @param proxy 被代理对象的真实代理对象,即动态生成的代理对象; * 执行invoke方法后,我们通常返回 '调用被代理对象方法的返回结果'【笔者示例就是】; * 但是,我们也可直接返回真实代理对象,也就是proxy, * 通过这个proxy对象,我们就可以直接调用真实对象的各个方法,从而灵活地满足具体需求。 * @param method 被代理的方法 * @param args 被代理的方法参数 * @return Object * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //TODO 这里就是我们可以添加自己所需业务逻辑的地方,如添加操作日志、调用方法执行耗时等 long start = System.currentTimeMillis();
Object result = method.invoke(targetObject,args);
long end = System.currentTimeMillis(); System.out.println("执行方法" + method.getName() + "调用耗时:" + (end - start) + "ms"); return result; }}
复制代码

创建动态代理对象后,我们在使用照相机接口时,示例如下:

package com.xiaojiang.demo.proxy;
/** * @Description : 主函数 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 21:56 */public class App { public static void main(String[] args) { // 动态代理调用 InvocationHandler invocationHandler = new ProxyCameraImplHandler(new CameraImpl()); ICamera iCamera = (ICamera) Proxy.newProxyInstance(CameraImpl.class.getClassLoader(),CameraImpl.class.getInterfaces(),invocationHandler); iCamera.takePhoto(); iCamera.modifyPhoto(); iCamera.deletePhoto(); }}
复制代码

通过示例我们可以发现,笔者并没有手动地直接创建原CameraImpl类的代理对象,而是借助于 InvocationHandler 接口、Proxy类创建了动态代理对象ProxyCameraImplHandler,通过动态代理创建代理对象实现对 CameraImpl 类的代理。

为帮助理解,笔者大致说一下 JDK 提供的 InvocationHandler 接口、Proxy类。(此处描述仅笔者为帮助理解的描述,非官方定义,如有兴趣,读者可自行查看 JDK 源码中的javadoc定义描述)

1、java.lang.reflect.InvocationHandler接口:

InvocationHandler接口内只有一个invoke方法,我们创建的动态代理类需要实现该接口,然后在invoke方法中添加我们需要的业务逻辑,当用Proxy类动态创建代理对象时,传入我们创建的动态代理类对象,从而当最终调用被代理的类的方法时,就会调用该invoke方法。

2、java.lang.reflect.Proxy类:

Proxy类就是用来动态创建代理对象的类,其提供多个方法,比较常用的是newProxyInstance方法,其接口参数说明如下:

/** * @param loader 加载被代理类的classloader对象 * @param interfaces 被代理类的接口对象数组 * @param h InvocationHandler对象,即实现该接口的动态代理对象,最终将调用该对象中的invoke方法 * @return Object * @throws IllegalArgumentException */public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,                                      InvocationHandler h) throws IllegalArgumentException
复制代码

以上就是一个基本的 JDK 动态代理示例,通过示例我们可以发现:通过动态代理的方式,代理对象的代码就相对固定了,在同一业务场景下,不需要随着相关业务类、方法的增加而创建相应的代理类;同时,我们也不再需要事先实例化代理对象,而是程序运行时动态判断被代理的类,从而动态创建代理对象,大大增加了灵活性,降低了代码耦合度。另外,通过动态代理,也就可以相对轻松地实现 AOP 编程。

当然,以上的动态代理示例,我们可以进一步优化代码结构,让接口调用时更加方便,优化后代码如下:

package com.xiaojiang.demo.proxy;
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;
/** * @Description : 照相机接口 动态代理实现类(优化版) * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-21 22:27 */public class ProxyCameraImplHandler implements InvocationHandler { /** * 被代理的对象 */ private Object targetObject;
/** * 执行调用被代理对象的方法 * @param proxy 被代理对象的真实代理对象,即动态生成的代理对象; * 执行invoke方法后,我们通常返回 '调用被代理对象方法的返回结果'【笔者示例就是】; * 但是,我们也可直接返回真实代理对象,也就是proxy, * 通过这个proxy对象,我们就可以直接调用真实对象的各个方法,从而灵活地满足具体需求。 * @param method 被代理的方法 * @param args 被代理的方法参数 * @return Object * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //TODO 这里就是我们可以添加自己所需业务逻辑的地方,如添加操作日志、调用方法执行耗时等 long start = System.currentTimeMillis();
Object result = method.invoke(targetObject,args);
long end = System.currentTimeMillis(); System.out.println("执行方法" + method.getName() + "调用耗时:" + (end - start) + "ms"); return result; }
/** * 将被代理的对象传入,从而获得它的类加载器和实现接口,然后作为Proxy.newProxyInstance方法的参数。 * this参数即表示当前ProxyCameraImplHandler对象的实例 */ public Object newProxyInstance(Object targetObject){ this.targetObject = targetObject; return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this); }
}
复制代码

优化后,调用示例如下:

package com.xiaojiang.demo.proxy;
/** * @Description : 主函数 * @Version : 0.0.1 * @Author : xiaojiang * @Date : Created in 2020-11-13 21:56 */public class App { public static void main(String[] args) { // 动态代理调用 ,进一步封装改进 ICamera iCamera = (ICamera) new ProxyCameraImplHandler().newProxyInstance(new CameraImpl()); iCamera.takePhoto(); iCamera.modifyPhoto(); iCamera.deletePhoto(); }}
复制代码

通过优化后的代码可以发现,使用 JDK 的动态代理还是较为简单的,且功能强大。那么,思考一下,JDK 是如何实现动态代理的呢,它是如何通过动态判断实现类,从而动态创建对应代理对象的呢?

通过示例代码可知,动态代理的操作核心是Proxy.newProxyInstance(...)方法,此处笔者贴一下其源代码(JDK1.8):

public static Object newProxyInstance(ClassLoader loader,                                          Class<?>[] interfaces,                                          InvocationHandler h)        throws IllegalArgumentException    {        Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); }
/* * Look up or generate the designated proxy class. */ Class<?> cl = getProxyClass0(loader, intfs);
/* * Invoke its constructor with the designated invocation handler. */ try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); }
final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } }
复制代码

通过源代码初步分析可以发现,newProxyInstance方法内是通过 Java 的反射机制来动态创建代理对象的。当然,newProxyInstance方法内还有许多分支方法调用,笔者此处就不再贴过多源代码了,有兴趣的同学可以去看一看。

不过,此处笔者需要特别指出的是,通过反射获取代理对象实例时,有一个优先从缓存中获取的操作,这也就意味着,JDK1.8 后的动态代理性能没有那么差。该操作源代码片段如下:

/** * Generate a proxy class.  Must call the checkProxyAccess method * to perform permission checks before calling this. */private static Class<?> getProxyClass0(ClassLoader loader,                                       Class<?>... interfaces) {  if (interfaces.length > 65535) {    throw new IllegalArgumentException("interface limit exceeded");  }
// If the proxy class defined by the given loader implementing // the given interfaces exists, this will simply return the cached copy; // otherwise, it will create the proxy class via the ProxyClassFactory return proxyClassCache.get(loader, interfaces);}
复制代码

至此,动态代理也就说完了。不过,为帮助理解 JDK 动态代理基本原理,尤其是调用InvocationHandler接口的invoke方法这个地方,笔者建议去了解一下反射相关的知识。

五、小结

1、Java 中的代理分为静态代理、动态代理;不管是静态代理还是动态代理,代理对象和被代理对象都需要实现同一个接口;

2、理解代理这种思维方式很重要,日常工作也许直接写动态代理代码的场景不多,但是,很多基础框架都是基于动态代理来实现相关功能的,如 Spring、Mybatis 等,理解代理有利于我们深入理解框架的实现原理;

4、通过动态代理,可以方便地扩展原有代码功能,也可以实现 AOP 编程;(Spring 的 IOC 与 AOP 两大特性的核心基础都是动态代理,不过 Spring 的动态代理和 JDK 的动态代理有所不同,在有些场景下它是利用 Cglib 来实现的)


发布于: 2020 年 11 月 21 日阅读数: 73
用户头像

老农小江

关注

好好学习,天天向上 2020.03.26 加入

还未添加个人简介

评论 (2 条评论)

发布
用户头像
讲的很详细,学习了!!
2020 年 11 月 26 日 10:12
回复
🐂
2020 年 11 月 28 日 15:36
回复
没有更多了
Java核心基础——动态代理、静态代理