当前位置: 首页 > article >正文

反射、动态代理、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> {
    /**
     * 处理请求
     *
     */
    @Override
    public 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优先级高于系统SPI
        Map<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.kler.cn/news/367659.html

相关文章:

  • 对角线遍历矩阵模板
  • 8. 数据结构—排序
  • 如何提高英语口语表达能力?
  • JSON.stringify用法
  • python爬虫——Selenium的基本使用
  • Docker无法拉取镜像解决办法
  • 如何系统学习销售?
  • 力扣33:搜索旋转排序数组
  • 2024-10-23 问AI: [AI面试题] 什么是卷积神经网络 (CNN)?
  • Vue 3 的响应式数据绑定(2)
  • 【LeetCode】每日一题 2024_10_21 最小差值 II(贪心)
  • redis 查找key使用正在表达式与java的区别
  • Linux的目录结构 常用基础命令(2)
  • Linux基础IO--重定向--缓冲区
  • 30. 串联所有单词的子串 C#实现
  • pip在ubuntu下换源
  • Android Studio超级详细讲解下载、安装配置教程(建议收藏)
  • 探索CSS动画下的按钮交互美学
  • MySQL 的元数据锁(Metadata Locks, MDL)原理详解
  • Python 协程详解----高性能爬虫
  • 适用在汽车诊断系统中的总线收发器芯片选型:CSM9241
  • Android AAR嵌套AAR打包出现问题解决方案
  • 自由学习记录(15)
  • 前端SSE-EventSource message事件执行异常问题
  • TypeScript(中)+算法(二)
  • 道可云人工智能元宇宙每日资讯|《嘉兴市推动人工智能高质量发展实施方案》发布