反射、动态代理、SPI机制在RPC框架中应用

Java反射的理解

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
比如,获取一个Class对象。Class.forName(完整类名)。通过Class对象获取类的构造方法,class.getConstructor。根据Class对象获取类的方法,getMethod和getMethods。使用Class对象创建一个对象,class.newInstance等。
反射具有以下特性:

  1. 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
  2. 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
  3. 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现,允许你传入对象实例和参数值来执行方法。
  4. 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的get()和set()方法完成的。
    反射的优点就是增加灵活性,可以在运行时动态获取对象实例。缺点是反射的效率很低,而且会破坏封装,通过反射可以访问类的私有方法,不安全。

那我们为什么会用反射?什么时候会用到反射

在某种业务场景下,无法在编写源代码时就确定要用哪个类的对象,需要根据用户的行为做出动态地响应。这个时候就可以考虑用反射机制在运行阶段根据用户的输入来判断到底实列化哪个类的对象,并调用该对象的方法等操作。

例如:在美团点外卖后付款的界面,用户可以选择多种付款方式(微信、支付宝、银行卡等等)。假如每种支付方式都对应一个类,而在编写源代码的时候我们不能确定使用那种付款方式,为了代码的可扩展性,也不想用分支结构并为每个支付方式的类创建对象。那么,这种情况下就可以考虑用反射机制,用户点击哪个支付方式,程序就在运行阶段创建哪个支付方式类的对象完成支付。

我在项目中使用 Java 反射机制,实现了以下功能:

Java 反射机制让程序能在运行时访问某个类的信息或者操作某个类。比如在程序运行时检查类、获取类的属性和方法以及动态创建对象、调用方法、修改属性的值等。使用反射可以增强代码的动态性和灵活性。

1.完成服务调用:服务提供端在从本地服务注册器中找到服务实现类后,通过反射创建实例,并通过 method.invoke调用实例的指定方法。
public class TcpServerHandler implements Handler<NetSocket> {/*** 处理请求**/@Overridepublic void handle(NetSocket netSocket) {TcpBufferHandlerWrapper bufferHandlerWrapper = new TcpBufferHandlerWrapper(buffer -> {// 处理请求代码// 接受请求,解码ProtocolMessage<RpcRequest> protocolMessage;try {protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer);} catch (IOException e) {throw new RuntimeException("协议消息解码错误");}RpcRequest rpcRequest = protocolMessage.getBody();ProtocolMessage.Header header = protocolMessage.getHeader();// 处理请求// 构造响应结果对象RpcResponse rpcResponse = new RpcResponse();try {// 获取要调用的服务实现类,通过反射调用Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());// 封装返回结果rpcResponse.setData(result);rpcResponse.setDataType(method.getReturnType());rpcResponse.setMessage("ok");} catch (Exception e) {e.printStackTrace();rpcResponse.setMessage(e.getMessage());rpcResponse.setException(e);}// 发送响应,编码header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey());header.setStatus((byte) ProtocolMessageStatusEnum.OK.getValue());ProtocolMessage<RpcResponse> responseProtocolMessage = new ProtocolMessage<>(header, rpcResponse);try {Buffer encode = ProtocolMessageEncoder.encode(responseProtocolMessage);netSocket.write(encode);} catch (IOException e) {throw new RuntimeException("协议消息编码错误");}});netSocket.handler(bufferHandlerWrapper);}
}

该代码的作用是服务端接收来着客户端的请求,经过解码后拿到服务名,并在LocalRegistry中获取服务实现类,通过请求中的服务名称、参数拿到Method方法进行反射调用。

这里为什么要用反射实现呢?
(1)动态方法调用:

服务的实现在不同的机器上,使用反射在运行时动态调用方法,不需要再编译时确定所有的实现。这种动态性使得RPC能够支持不同的服务。不需要重启。

(2)解耦和灵活

反射使客户端与服务端之间调用保持松耦合。客户端只需要知道接口和方法名,不需要知道具体的实现细节。

(3) 参数与返回值处理

反射允许在运行时获取方法的参数类型和返回值类型,从而可以动态地构造方法调用。例如,RPC 框架可以根据请求的序列化数据解析出方法所需的参数,并通过反射将这些参数传递给目标方法。

性能考虑

虽然反射带来了灵活性和动态性,但也有性能上的开销。反射调用比直接方法调用要慢,因此在性能敏感的场景中,RPC 框架可能会进行优化,比如使用缓存来存储方法的元数据,减少反射调用的频率。

2.实现动态代理:服务消费端使用了基于JDK的动态代理,通过反射获取到调用的服务名称、方法、参数列表等,从而构造请求。

3.实现 SPI 机制:扫描自定义 SPI 的配置文件后,通过反射机制获取自定义 SPI 类的 Class 类型和创建实例。
@Slf4j
public class SpiLoader {//存储已加载的类:接口名=>(key =>实现类)private static final Map<String, Map<String,Class<?>>> loaderMap=new ConcurrentHashMap<>();//对象实例缓存(避免重复new),类路径 =>对象实例,单例模式private static final Map<String,Object> instanceCache=new ConcurrentHashMap<>();//系统SPI目录private static final String RPC_SYSTEM_SPI_DIR="META-INF/rpc/system/";//用户自定义SPI目录private static final String RPC_CUSTOM_SPI_DIR="META-INF/rpc/custom/";//扫描路径private static final String[] SCAN_DIRS=new String[]{RPC_SYSTEM_SPI_DIR,RPC_CUSTOM_SPI_DIR};/*** 动态加载的类列表*/private static final List<Class<?>> LOAD_CLASS_LIST = Collections.singletonList(Serializer.class);//加载所有类型public static void loadAll(){log.info("加载所有SPI");for (Class<?> aClass : LOAD_CLASS_LIST) {load(aClass);}}//获取某个接口的实例public static <T> T getInstance(Class<?> tClass,String key){String tClassName=tClass.getName();Map<String ,Class<?>> keyClassMap=loaderMap.get(tClassName);if (keyClassMap==null){throw new RuntimeException(String.format("SpiLoader 未加载%s类型",tClassName));}if(!keyClassMap.containsKey(key)){throw new RuntimeException(String.format("SpiLoader的%s不存在key=%s的类型",tClassName,key));}//获取到要加载的实现类型Class<?> implClass=keyClassMap.get(key);//从实例缓存中加载指定类型的实例String implClassName=implClass.getName();if(!instanceCache.containsKey(implClassName)){try {instanceCache.put(implClassName,implClass.newInstance());}catch (InstantiationException|IllegalAccessException e){String errorMsg=String.format("%s 类实例化失败",implClassName);throw new RuntimeException(errorMsg,e);}}return (T) instanceCache.get(implClassName);}//加载某个类型public static Map<String,Class<?>> load(Class<?> loadClass){log.info("加载类型为{}的SPI",loadClass.getName());//扫描路径,用户自定义的SPI优先级高于系统SPIMap<String,Class<?>> keyClassMap=new HashMap<>();for (String scanDir : SCAN_DIRS) {List<URL> resources= ResourceUtil.getResources(scanDir+loadClass.getName());//读取每个资源文件for (URL resource : resources) {try {InputStreamReader inputStreamReader=new InputStreamReader(resource.openStream());BufferedReader bufferedReader=new BufferedReader(inputStreamReader);String line;while ((line= bufferedReader.readLine())!=null){String [] strArray=line.split("=");if(strArray.length>1){String key=strArray[0];String className=strArray[1];keyClassMap.put(key,Class.forName(className));//jdk   类名jdkSevie}}}catch (Exception e){log.error("spi resource load error",e);}}}loaderMap.put(loadClass.getName(),keyClassMap);return keyClassMap;}
}
1. 接口与实现的映射

SpiLoader 类中通过 loaderMap 存储了接口名与其实现类的映射关系。具体来说,Map<String, Map<String, Class<?>>> loaderMap 用于存储接口的实现类,键为接口的全限定名,值为一个包含多个实现类的映射表,其中键为自定义的标识(key),值为对应的实现类(Class)。

2. 动态加载实现类

loadAll() 方法通过调用 load(Class<?> loadClass) 方法,遍历预定义的 LOAD_CLASS_LIST,动态加载每个类(在本例中是 Serializer.class)。这个方法会扫描指定的资源目录,读取包含键值对的配置文件,以便找到对应的实现类。

3. 从配置文件中读取实现

在 load(Class<?> loadClass) 方法中,使用 ResourceUtil.getResources(scanDir + loadClass.getName()) 读取资源文件。这些资源文件通常位于 META-INF/rpc/system/ 和 META-INF/rpc/custom/ 目录中,文件内容遵循 key=ClassName 的格式。通过解析这些文件,加载并存储实现类。

4. 获取实例

getInstance(Class<?> tClass, String key) 方法用于根据接口类型和键获取对应的实例。如果该实例尚未被创建,使用反射创建它。这样可以避免重复创建相同的实例,确保在整个应用程序中使用同一个实例(单例模式)。

5. 异常处理

在方法中使用了异常处理机制,确保在 SPI 加载过程中出现问题时能够捕获异常并给出清晰的错误信息。

总结

整体上,SpiLoader 类利用 SPI 机制实现了接口的动态加载和管理,通过配置文件来注册和加载不同的实现类。这种设计模式使得框架可以在运行时根据需要动态选择不同的实现,提高了系统的灵活性和扩展性。**

在项目中是如何实现消费方调用的?为什么选用JDK动态代理和工厂式?

调用流程:

1.消费方通过代理工厂,根据指定服务类型获取代理对象。

2.通过InvocationHandler接口实现JDK动态代理,在invoke方法中根据method等参数构造请求对象

3.通过注册中学进行服务发现,获取提供该服务的所有服务提供者信息

4.使用负载均衡选择一个服务提供者

5.发起RPC请求并得到相应结果

为什么使用 JDK 动态代理?
1. 解耦客户端与服务实现

动态代理允许客户端代码与服务的具体实现解耦。客户端只需要知道服务的接口,而不需要了解具体实现的细节。这种设计使得在更换服务实现时,无需修改客户端代码,提高了系统的灵活性和可维护性。

2. 统一调用逻辑

动态代理可以统一处理远程调用的逻辑,例如序列化、网络传输、异常处理等。在客户端通过代理调用方法时,所有的调用逻辑都可以集中管理,避免了在每个客户端实现中重复代码。

3. 简化代码

通过动态代理,开发者可以简化代码,避免手动实现所有接口的方法。只需定义接口,代理类会自动处理方法调用,减少了样板代码。

4. 增强功能

**动态代理可以在方法调用前后添加增强功能,例如:

  • 日志记录:记录每次方法调用的时间、参数和返回值。**
  • 性能监控:监控方法执行时间,进行性能分析。
  • 权限检查:在方法调用前进行权限验证。
  • 重试机制:在调用失败时进行自动重试。
5. 实现透明的远程调用

使用动态代理,客户端调用服务的方法看起来就像调用本地方法,用户不需要关心底层的网络通信细节。代理会负责将方法调用转换为网络请求,并处理响应,使得远程调用过程更加透明。

6. 支持多种协议

动态代理可以使得 RPC 框架支持不同的协议(如 HTTP、TCP 等)和数据格式(如 JSON、Protocol Buffers 等)。只需修改代理的实现,就可以适应不同的底层通信需求。

7. 灵活性与扩展性

在使用动态代理时,框架可以在运行时动态决定使用哪个具体的实现。这使得系统在扩展时更加灵活,可以方便地加入新的服务或修改现有服务的实现。

总结:JDK动态代理是 Java 提供的一种原生代理机制,允许开发者在运行时动态地创建代理类,这种机制主要用于接口的代理,符合我项目的需求。

选用它的原因:

1.JDK 动态代理作为 Java 标准库的一部分,无需引入任何第三方依赖即可使用。

2.相比于 CGLIB 等动态生成字节码的动态代理实现,JDK 动态代理的性能更高。

3.JDK 动态代理能够在不同的 Java 平台上运行,具有良好的跨平台性。

为什么选用工厂模式?

1.更灵活:通过工厂模式提供的“根据类型获取代理类"的方法,我能够根据需要动态地选择合适的代理对象类型进2.解耦合:将创建代理对象的过程进行封装,****调用方不需要了解具体的对象创建细节,使代码更利于维护。

此外,我还可以在工厂类中结合双检锁单例模式,实现代理对象的延迟初始化。

可以有效地提高性能,并避免不必要的对象创建。

  • 延迟初始化的好处

    性能优化:在多线程环境下,延迟初始化可以避免在每次请求时都创建对象,只有在第一次真正需要时才会创建实例,从而减少资源消耗。

    节约内存:如果某个对象并不总是需要,那么延迟初始化可以避免不必要的内存占用。

  • 双检锁单例模式

双检锁单例模式使用两个检查来确保线程安全,同时又不在每次访问实例时都加锁,从而提升性能。

什么是 Java 的 SPI机制?你是如何利用 SPI机制实现模块动态扩展的?

SPl(Service Provider Interface)服务提供接口是Java 的重要机制,主要用于实现模块化开发和插件化扩展。

SPI机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。

一个典型的 SPI! 应用场景是JDBC(ava数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。

此外,我们使用的主流 Java 开发框架中,几乎都使用到了 SP1 机制,比如 Servlet 容器、日志框架、ORM 框架、Spring框架。

虽然 Java 内置了 Serviceloader 来实现 SPI,但是如果想定制多个不同的接口实现类,就没办法在框架中指定使用哪-个了,也就无法实现像“通过配置快速指定序列化器”这样的需求。

所以我自己定义了 SPI机制的实现,能够给每个自行扩展的类指定键名。

比如读取如下配置文件,能够得到一个 序列化器名称 =>序列化器实现类对象 的映射,之后就可以根据用户配置的序列化器名称动态加载指定实现类对象了。

jdk=com.hujx.hjxrpc.serializer.JdkSerializer
hessian=com.hujx.hjxrpc.serializer.HessianSerializer
json=com.hujx.hjxrpc.serializer.JsonSerializer
kryo=com.hujx.hjxrpc.serializer.KryoSerializer

具体实现方法如下:

1)指定 SPI的配置目录,并且将配置再分为系统内置 SP1和用户自定义 SPI,便于区分优先级和维护。

2)编写 SpiLoader 加载器,实现读取配置、加载实现类的方法。

1.用 Map 来存储已加载的配置信息 键名 =>实现类 。

2.通过 Hutool工具库提供的 Resourceuti1.getResources 扫描指定路径,读取每个配置文件,获取到 键名 =>实现类 信息并存储在 Map 中。

3.定义获取实例方法,根据用户传入的接口和键名,从 Map 中找到对应的实现类,然后通过反射获取到实现类对象。可以维护一个对象实例缓存,创建过一次的对象从缓存中读取即可。

使用静态代码块调用 SP! 的加载方法,在工厂首次加载时,就会调用 Spiloader 的load 方法加载序列化器接口的所有实现类,之后就可以通过调用 getnstance 方法获取指定的实现类对象了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/56871.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Cesium基础-(Viewer)

1. Viewer 构造参数介绍 Cesium中的Viewer是用于显示和控制3D场景的核心组件。它提供了创建和管理3D地球模型、加载图像覆盖物、设置相机位置和方向以及处理用户输入等功能。Viewer可以看作是一个带有多种功能的可交互的三维数字地球容器&#xff0c;是任何Cesium应用程序的基…

利用Arcgis进行沟道形态分析

在做项目的时候需要学习到水文分析和沟道形态分析的学习&#xff0c;所以自己摸索着做了下面的工作和内容。如有不对请多指正&#xff01;&#xff01; 一、沟道形态分析概述 沟道形态分析是水文分析的一个重要方面&#xff0c;用于研究河流的形态和特征。沟道形态分析可以帮助…

C# 企业微信机器人推送消息 windows服务应用程序的使用

C# 企业微信机器人推送消息 先添加一个机器人! 然后查看机器人就可以得到一个 webhook 特别特别要注意&#xff1a;一定要保护好机器人的webhook地址&#xff0c;避免泄漏&#xff01; 然后开始写代码 &#xff0c;只需要httpPost 调用一下这个地址就可以发送消息了。 首先我…

Z-BlogPHP显示错误Undefined array key 0 (set_error_handler)的解决办法

今天打开博客的时候&#xff0c;意外发现页面&#xff0c;打开均显示错误&#xff1a;Undefined array key 0 (set_error_handler)。 博客程序采用的是Z-BlogPHP。百度了一圈没有找到解决办法&#xff0c;在官方论坛里也没找到解决办法。 于是开始自己排查原因。我服务器采用…

【vue3|第29期】Vue3中的插槽:实现灵活的组件内容分发

日期&#xff1a;2024年10月24日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉在这里插入代码片得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不…

【分立元件】低阻值电阻器的趋势(Face down type)

低阻值电阻器不仅可正确显示电阻器的阻值,还是小型、大功率产品或散热性优良的产品所必不可少的。 为了应对大功率或提高散热性,一般使用较大贴片尺寸的产品或长边电极型产品。 但是,如果贴片尺寸变大,就需要一定的贴装空间,还会减弱温度循环试验强度。 长边电极型…

利用Docker搭建一套Mycat2+MySQL8一主一从、读写分离的最简单集群(保姆教程)

文章目录 1、Mycat介绍1.1、mycat简介1.2、mycat重要概念1.3、Mycat1.x与Mycat2功能对比1.2、主从复制原理 2、前提准备3、集群规划4、安装和配置mysql主从复制4.1、master节点安装mysql8容器4.2、slave节点安装mysql8容器4.2、配置主从复制4.3、测试主从复制配置 5、安装mycat…

yolov11的onnx模型C++ 调用

yolov11的onnx模型C调用 效果图一、python调用二、onnx模型导出三、python的onnx调用调用检测模型调用分割模型 四、C的onnx模型调用五 、视频流的检测后续 效果图 一、python调用 本文只记录生成的yolov11模型如何调用&#xff0c;其他可参考各种yolov11博客 模型下载&#x…

万年历制作

#include<stdio.h> int main() { int year0, month0, day0, y0, m0&#xff1b; scanf_s("%d %d", &year,&month); //判断闰年 for(y1900;y<year;y) { if ((y % 4 0 && y % 100 ! 0) || y % 400 0) …

C语言[求x的y次方]

C语言——求x的y次方 这段 C 代码的目的是从用户输入获取两个整数 x 和 y &#xff0c;然后计算 x 的 y 次幂&#xff08;不过这里有个小错误&#xff0c;实际计算的是 x 的 (y - 1) 次幂&#xff0c;后面会详细说&#xff09;&#xff0c;最后输出结果。 代码如下: #include…

Apache Paimon Catalog

Paimon Catalog可以持久化元数据&#xff0c;当前支持两种类型的metastore&#xff1a; 文件系统&#xff08;默认&#xff09;&#xff1a;将元数据和表文件存储在文件系统中。hive&#xff1a;在 hive metastore中存储元数据。用户可以直接从 Hive 访问表。 2.2.1 文件系统…

centeros7 编译ffmpeg

使用yum安装的路似乎已经堵住了&#xff0c;请求的镜像全是404或503 1.打开终端并使用yum安装EPEL存储库(Extra Packages for Enterprise Linux)&#xff1a;sudo yum install epel-release2.接下来&#xff0c;使用以下命令来安装FFmpeg&#xff1a;sudo yum install ffmpeg …

remote: HTTP Basic: Access denied

解决方法 输入&#xff1a; git config --system --unset credential.helper 再次进行 Git 操作&#xff0c;输入正确的用户名&#xff0c;密码即可。

static、 静态导入、成员变量的初始化、单例模式、final 常量(Content)、嵌套类、局部类、抽象类、接口、Lambda、方法引用

static static 常用来修饰类的成员&#xff1a;成员变量、方法、嵌套类 成员变量 被static修饰&#xff1a;类变量、成员变量、静态字段 在程序中只占用一段固定的内存&#xff08;存储在方法区&#xff09;&#xff0c;所有对象共享可以通过实例、类访问 (一般用类名访问和修…

OpenHarmony(1)开发环境搭建

一&#xff1a;开源项目 OpenHarmony是由开放原子开源基金会&#xff08;OpenAtom Foundation&#xff09;孵化及运营的开源项目&#xff0c;目标是面向全场景、全连接、全智能时代&#xff0c;基于开源的方式&#xff0c;搭建一个智能终端设备操作系统的框架和平台&#xff0…

使用SQL在PostGIS中创建各种空间数据

#1024程序员节&#xff5c;征文# 一、目录 1. 概述 2. 几何&#xff08;Geometry&#xff09;类型 创建点 创建线 创建面 3. 地理&#xff08;Geography&#xff09;类型 地理点&#xff08;GEOGRAPHY POINT&#xff09; 地理线串&#xff08;GEOGRAPHY LINESTRING&#xff…

Redis 单机、主从、哨兵和集群架构详解和搭建

目录 前言 单机部署 检查安装 gcc 环境 下载安装 Redis 启动 Redis 关闭 Redis 配置Redis 主从部署 整体架构图 主从复制配置 重启 Redis 验证 主从复制的作⽤ 主从复制缺点 哨兵部署&#xff08;Sentinel&#xff09; 整体架构图 哨兵模式配置 启动哨兵 验证…

MySQL-32.索引-操作语法

一.语法 二.代码实现 指定某个字段为主键&#xff0c;其实就是建立一个主键索引。而指定某个字段唯一&#xff0c;就是建立一个唯一索引。 -- 索引 -- 创建&#xff1a;为tb_emp表的name字段建立一个索引 create index idx_emp_name on tb_emp(name);-- 查询&#xff1a;查…

【智能大数据分析 | 实验四】Spark实验:Spark Streaming

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈智能大数据分析 ⌋ ⌋ ⌋ 智能大数据分析是指利用先进的技术和算法对大规模数据进行深入分析和挖掘&#xff0c;以提取有价值的信息和洞察。它结合了大数据技术、人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&a…

基于java的山区环境监督管理系统(源码+定制+开发)环境数据可视化、环境数据监测、 环境保护管理 、污染防治监测系统 大数据分析

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…