文章目录
动态代理与RPC原理实现详解
一.动态代理
1. 什么是代理模式?
代理模式为其他对象提供了一种代理以控制对这个对象的访问,根据代理类的创建时机和创建方式的不同,可以将其分为静态代理和动态代理两种形式:在程序运行前就已经存在的编译好的代理类是为静态代理,在程序运行期间根据需要动态创建代理类及其实例来完成具体的功能是为动态代理。
代理模式的目的就是为真实业务对象提供一个代理对象以控制对真实业务对象的访问,代理对象的作用有:
- 代理对象存在的价值主要用于拦截对真实业务对象的访问;
- 代理对象具有和目标对象(真实业务对象)实现共同的接口或继承于同一个类;
- 代理对象是对目标对象的增强,以便对消息进行预处理和后处理。
2. 反射
动态代理的实现核心是反射,一切动态代理的代理操作都是反射实现的。所以要先对反射知识有一定的介绍。
2.1 反射实现步骤
-
通过反射获取对象的.class文件,也就是class对象(有三种方式)
//方式一:使用Class.forName方法进行包名+类名的定位 Class c1=Class.forName("zk.reflect.Person"); //方式二:采用类名.class方法获取反射 Class c2=Person.class; //方式三:采用对象名.class方法获取反射(运行过程中) Person ps=new Person(); Class c3 = ps.getClass();
-
通过获取的对象,对对象类进行构造方法,成员变量的调用
Class c =Class.forName(“com.zk.User”); //获取类名 c.getName(); //获取的是包名+类名(返回--->com.zk.User) c.getSimpleName(); //类名(返回--->User) //获取类的属性 Fields[] fi=c.getFields(); //只能获取public属性 Fields[] fi2=c.getDeclaredFields(); //可以获取类的全部属性(包括私有属性) Fields name= c.getDeclaredField(“name”); //获取指定属性值, //获取类的构造器 Constructor[] co=c.getConstructors(); //仅获得public构造器 Constructor[] co2=c.getDeclaredConstructor();//获取全部构造器 //获取指定构造器,必须参数 Constructor con=c.getConstructor(String.class,int.class,int.class);
-
通过反射获取的对象,获取对象类中的方法,使用invoke([实例化对象],[方法对应的参数])方法,进行方法使用。
//获取类的方法 Method[] m =c.getMethods(); //获得本类和其父类的全部public方法 Method[] m2=c.getDeclaredMethods(); //仅获取本类的全部方法(包括私有方法) //获取指定方法,并使用 //通过实例对象获取反射 //Person p =new Person(); //获取Person类对象 //Class c=p.getClass(); Class c=Person.class; Person p= (Person) c.newInstance(); //获取该类实例对象中的具体方法--第一个参数要写Person类的方法名称才能匹配上;后面参数是方法参数类型 Method m=c.getDeclaredMethod("eat",String.class); //使用invoke方法对反射获取的方法进行激活---第一个参数是实例化对象,后面参数是方法对应参数值 String s= (String) m.invoke(p,"zhangkai"); System.out.println(s); //获取指定方法,必须加入参数,没有加null;因为存在重载,只有方法名和参数个数两个才能精确定位方法 Method getid=c.getMethod(“getid”,null); Method setid=c.setMethod(“setid”,int.class);
3. 静态代理原理
静态代理是代理模式的实现方式之一,是相对于动态代理而言的。所谓静态代理是指,在程序运行前,由程序员创建或特定工具自动生成源代码并对其编译生成.class文件。静态代理的实现只需要三步:首先,定义业务接口;其次,实现业务接口;然后,定义代理类并实现业务接口;最后便可通过客户端进行调用。
真正的业务功能还是由委托类来实现,但是在实现业务功能前后可以增加一些公共逻辑,用于增强业务功能。就是说,核心功能还是由委托类中的方法进行实现,代理类可以在此基础上进行额外信息的包装。例如,在项目前期开发中我们没有加入缓存、日志等这些功能,后期若想加入,我们就可以使用代理来实现,而且不必对原有代码进行改动。因此,代理模式是对开闭原则的典型实践,也是AOP理念的实现基础。
4.动态代理原理
对代理模式而言,一般来说,具体主题类与其代理类是一一对应的,这也是静态代理的特点。但是,也存在这样的情况:有N个主题类,但是代理类中的“预处理、后处理”都是相同的,仅仅是调用主题不同。那么,若采用静态代理,那么必然需要手动创建N个代理类,这显然让人相当不爽。动态代理则可以简单地为各个主题类分别生成代理类,共享“预处理,后处理”功能,这样可以大大减小程序规模,这也是动态代理的一大亮点。(通俗上来说,动态代理不再是一个代理类代理一个委托类,而是像个大管家,指定那个委托对象,就代理谁的方法,只不过代理类中通用的逻辑会适用于每个委托类)
在动态代理中,代理类是在运行时期生成的。因此,相比静态代理,动态代理可以很方便地对委托类的相关方法进行统一增强处理,如添加方法调用次数、添加日志功能等等。动态代理主要分为JDK动态代理和CGLIB动态代理两大类。
下面以一个模拟动态代理案例来实现动态代理的实现思路:
有两个Service类,分别是User和Order,使用ProxyUtils类动态代理这两类的抽象接口,程序在调用过程中,通过接口直接到代理类中,由代理类实现接口实现类的功能以及代理类自身的一些增强功能和通用功能。
-
接口类
//User的一些方法 public interface UserService { String login(String s,String p) throws InterruptedException; void selectById(int id); void delect(); } //Order的一些方法 public interface OrderService { String zhuyi(); void select(); String delectById(int id); }
-
实现类
//UserServiceImpl实现类 public class UserServiceImpl implements UserService { @Override public String login(String name,String password) { String resword="用户名或密码错误!"; if(name.equals("admin") && password.equals("123456")){ resword="登录成功"; } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return resword; } @Override public void selectById(int id) { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(id+"的信息如下:----"); } @Override public void delect() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("正在删除中------"); } } //OrderServiceImpl实现类 public class OrderServiceImpl implements OrderService{ @Override public String zhuyi() { return "订单功能区在这里————————"; } @Override public void select() { System.out.println("全部订单如下所示--------"); } @Override public String delectById(int id) { String s=id+"的信息正在删除中。。。。。"; return s; } }
-
动态代理类ProxyUtils
- 该类中的proxyService方法使用泛型====可以使proxyService方法代理多种不同类的Service接口;
- return 一个Proxy通过newProxyInstance()提供的实例对象
- newProxyInstance()需要三部分的参数。第一部分是创建方法形参对象obj的类加载器对象;第二部分是创建方法形参对象obj的类接口对象(都为固定方法);第三部分是创建==一个InvocationHandler()==方法,意味调用类对象方法的方法;
- 代理类中的增强功能就可以写在InvocationHandler()方法中,本例是实现了一个方法运行时间的检测功能;
/** * @Date:2022/10/15-10-15-21:40 * @Description: 动态代理类 */ public class ProxyUtils { //这里使用泛型思想,可以使proxyService方法代理多种不同类的Service接口 public static <T> T proxyService(T obj){ return (T)Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new InvocationHandler() { //利用反射对被代理对象的方法进行实现,使用反射中的invoke方法进行obj对象,和args参数的传入 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //代理类中增强功能在此内方法中实现,通用所有代理类方法 //增加一个方法运行效率检测的功能 long startTime = System.currentTimeMillis(); //调用代理实现类obj对象的方法,使用invoke激活,args考虑方法可能存在参数传入,args可以为null Object o = method.invoke(obj, args); long endTime = System.currentTimeMillis(); System.out.println(method.getName()+"方法的运行时间为:"+(endTime-startTime)/1000.0+"s"); //返回调用方法的执行结果 return o; } }); } }
-
测试类(应用程序类)
/** * @Date:2022/10/15-10-15-21:58 * @Description: java_workspace */ public class ProxyTest { public static void main(String[] args) throws Exception { //创建一个User类的代理对象,代理对象中也传入了一个新建的User实现类对象 UserService userService=ProxyUtils.proxyService(new UserServiceImpl()); String rs = userService.login("admin", "123456"); System.out.println(rs); userService.selectById(2); userService.delect(); //创建一个Order类的代理对象 OrderService orderService=ProxyUtils.proxyService(new OrderServiceImpl()); String ss = orderService.delectById(9); System.out.println(ss); } }
以上就是动态代理案例的全部实现,核心的调用逻辑就是前面流程图中的调用逻辑,使用动态代理类不仅可以大幅提高代码的复用程度,而且还可以在被代理类的基础上实现一些公共的增强功能,这其实就是Spring中的AOP的核心实现原理。例如本例就实现了所有被代理Service的方法运行时间检测的功能,对于多个Service都实现检测功能,其实就是一种横向编程的思路。
二.RPC框架
1.RPC的出现原因
将不同的业务拆分到多个应用中,让不同的应用分别承担不同的功能是解决这些问题的必杀技。将不同业务分拆到不同的应用后,不但可以大幅度提升系统的稳定性还有助于丰富技术选型,进一步保证系统的性能。总的来说,从单体应用到分布式多体应用是系统升级必经之路。
当一个单体应用演化成多体应用后,远程调用就粉墨登场了。在一个应用时,相互通信直接通过本地调用就可完成,而变为多体应用时,相互通信就得依赖远程调用了,这时一个高效稳定的RPC框架就显得非常必要了。可能有的同学会觉得,没必要非得用RPC框架啊,简单的HTTP调用不是也可以实现远程通信吗?确实,简单的HTTP调用确实也可以实现远程通信,但是它不是那么的合适,原因有二:
-
RPC远程调用像本地调用一样干净简洁,但其他方式对代码的侵入性就比较强;
-
一般使用RPC框架实现远程通信效率比其他方式效率要高一些。
2. RPC框架介绍
对于多体应用,由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。如果有一种方式能让我们像调用本地服务一样调用远程服务,而让调用者对网络通信这些细节透明,那么将大大解放程序员的双手,大幅度提高生产力。比如,服务消费方在执行helloService.hi(“Panda”)时,实质上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call Protocol),在各大互联网公司中被广泛使用,如阿里巴巴的HSF、Dubbo(开源)、Facebook的Thrift(开源)、Google GRPC(开源)、Twitter的Finagle(开源)等。
RPC的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,下面是一个RPC的经典调用的流程,并且反映了所涉及到的一些通信细节:
(1). 服务消费方(client)以本地调用方式调用服务;
(2). client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
(3). client stub找到服务地址,并将消息发送到服务端;
(4). server stub收到消息后进行解码;
(5). server stub根据解码结果 反射调用 本地的服务;
(6). 本地服务执行并将结果返回给server stub;
(7). server stub将返回结果打包成消息并发送至消费方;
(8). client stub接收到消息,并进行解码;
(9). 服务消费方得到最终结果。
RPC框架就是要将2~8这些步骤封装起来,让用户对这些细节透明,使得远程方法调用看起来像调用本地方法一样。
下面给出一个实现简易RPC框架的代码
从该RPC框架的简易实现来看,RPC客户端逻辑是:1.首先创建Socket客户端并与服务端建立链接,2.然后使用Java原生的序列化/反序列化机制将调用请求发送给客户端,包括所调用方法的名称、参数列表将服务端的响应返回给用户即可。至此,一次简单PRC调用的客户端流程执行完毕。特别地,从代码实现来看,实现透明的PRC调用的关键就是动态代理,这是RPC框架实现的灵魂所在。
-
服务端
服务端提供客户端所期待的服务,一般包括三个部分:服务接口,服务实现以及服务的注册暴露三部分
//服务端接口 public interface HelloService { String hello(String name); String hi(String msg); } //服务实现类 public class HelloServiceImpl implements HelloService{ @Override public String hello(String name) { return "Hello " + name; } @Override public String hi(String msg) { return "Hi, " + msg; } } //(重要)服务暴露 public class RpcProvider { public static void main(String[] args) throws Exception { HelloService service = new HelloServiceImpl(); // 自实现的RpcFramework,RPC框架使用export()方法将服务暴露出来,供客户端消费 RpcFramework.export(service, 1234); } }
-
客户端
客户端消费服务端所提供的服务,一般包括两个部分:服务引用和服务接口两个部分
//(服务引用)消费端通过RPC框架进行远程调用,这也是RPC框架功能之一 public class RpcConsumer { public static void main(String[] args) throws Exception { // 由RpcFramework类中refer()方法生成的HelloService接口的代理 HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234); //使用代理对象进行实现类中hello()方法的调用 String hello = service.hello("World"); System.out.println("客户端收到远程调用的结果 : " + hello); } }
//与服务端共享同一个服务接口 public interface HelloService { String hello(String name); String hi(String msg); }
-
RPC框架类RpcFramework
public class RpcFramework { /** * 暴露服务 * @param service 服务实现 * @param port 服务端口 * @throws Exception */ public static void export(final Object service, int port) throws Exception { //如果服务和端口存在问题,抛出异常 if (service == null) { throw new IllegalArgumentException("service instance == null"); } if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port); } System.out.println("Export service " + service.getClass().getName() + " on port " + port); // 建立Socket服务端 ServerSocket server = new ServerSocket(port); for (; ; ) { try { // 监听Socket请求 final Socket socket = server.accept(); new Thread(new Runnable() { @Override public void run() { try { try { /* 获取请求流,Server解析并获取请求*/ // 构建对象输入流,从源中读取对象到程序中 ObjectInputStream input = new ObjectInputStream( socket.getInputStream()); try { System.out.println("\nServer解析请求 : "); String methodName = input.readUTF(); System.out.println("methodName : " + methodName); // 泛型与数组是不兼容的,除了通配符作泛型参数以外 Class<?>[] parameterTypes = (Class<?>[])input.readObject(); System.out.println( "parameterTypes : " + Arrays.toString(parameterTypes)); Object[] arguments = (Object[])input.readObject(); System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 处理请求,进行响应*/ ObjectOutputStream output = new ObjectOutputStream( socket.getOutputStream()); try { // service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求 // 反射调用,处理请求 Method method = service.getClass().getMethod(methodName, parameterTypes); //invoke方法调用对应方法,得到处理返回值保存在result中 Object result = method.invoke(service, arguments); System.out.println("\nServer 处理并生成响应 :"); System.out.println("result : " + result); //将请求处理结果写回socket信息 output.writeObject(result); } catch (Throwable t) { output.writeObject(t); } finally { output.close(); } } finally { input.close(); } } finally { socket.close(); } } catch (Exception e) { e.printStackTrace(); } } }).start(); } catch (Exception e) { e.printStackTrace(); } } } /** * 引用服务 * * @param <T> 接口泛型 * @param interfaceClass 接口类型 * @param host 服务器主机名 * @param port 服务器端口 * @return 远程服务,返回代理对象 * @throws Exception */ @SuppressWarnings("unchecked") public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception { if (interfaceClass == null) { throw new IllegalArgumentException("Interface class == null"); } // JDK 动态代理的约束,只能实现对接口的代理 if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( "The " + interfaceClass.getName() + " must be interface class!"); } if (host == null || host.length() == 0) { throw new IllegalArgumentException("Host == null!"); } if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port); } System.out.println( "Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port); // JDK 动态代理,使用泛型实现广泛代理 T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] { interfaceClass}, new InvocationHandler() { // invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应 @Override public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable { // 创建Socket客户端,并与服务端建立链接 Socket socket = new Socket(host, port); try { /* 客户端向服务端进行请求,并将请求参数写入流中*/ // 将对象写入到对象输出流,并将其发送到Socket流中去 ObjectOutputStream output = new ObjectOutputStream( socket.getOutputStream()); try { // 发送请求 System.out.println("\nClient发送请求 : "); output.writeUTF(method.getName()); System.out.println("methodName : " + method.getName()); output.writeObject(method.getParameterTypes()); System.out.println("parameterTypes : " + Arrays.toString(method .getParameterTypes())); output.writeObject(arguments); System.out.println("arguments : " + Arrays.toString(arguments)); /* 客户端读取并返回服务端的响应*/ ObjectInputStream input = new ObjectInputStream( socket.getInputStream()); try { Object result = input.readObject(); //如果result是一个异常说明服务端返回没成功,客户端只能抛出异常 if (result instanceof Throwable) { throw (Throwable)result; } System.out.println("\nClient收到响应 : "); System.out.println("result : " + result); return result; } finally { input.close(); } } finally { output.close(); } } finally { socket.close(); } } }); //给消费端返回代理对象,供使用 return proxy; } }
3.关于RPC框架的若干问题说明
(1).RPC框架如何做到透明化远程服务调用?
如何封装通信细节才能让用户像以本地调用方式调用远程服务呢?就Java而言,动态代理恰是解决之道。Java动态代理有JDK动态代理和CGLIB动态代理两种方式。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,因此RPC框架的大部分实现还是选择JDK动态代理的方式。在上面的例子中,RPCFramework实现中的invoke方法封装了与远端服务通信的细节,消费方首先从RPCFramework获得服务提供方的接口,当执行helloService.hi(“Panda”)方法时就会调用invoke方法。
(2).如何发布自己的服务?
如何让别人使用我们的服务呢?难道就像我们上面的代码一样直接写死服务的IP以及端口就可以了吗?事实上,在实际生产实现中,使用人肉告知的方式是不现实的,因为实际生产中服务机器上/下线太频繁了。如果你发现一台机器提供服务不够,要再添加一台,这个时候就要告诉调用者我现在有两个IP了,你们要轮询调用来实现负载均衡;调用者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip。这必然是相当痛苦的!
有没有一种方法能实现自动告知,即机器的上线/下线对调用方透明,调用者不再需要写死服务提供方地址?当然可以,生产中的RPC框架都采用的是自动告知的方式,比如,阿里内部使用的RPC框架HSF是通过ConfigServer来完成这项任务的。此外,Zookeeper也被广泛用于实现服务自动注册与发现功能。不管具体采用何种技术,他们大都采用的都是发布/订阅模式。
(3).序列化与反序列化
我们知道,Java对象是无法直接在网络中进行传输的。那么,我们的RPC请求如何发给服务端,客户端又如何接收来自服务端的响应呢?答案是,在传输Java对象时,首先对其进行序列化,然后在相应的终端进行反序列化还原对象以便进行处理。事实上,序列化/反序列化技术也有很多种,比如Java的原生序列化方式、JSON、阿里的Hessian和ProtoBuff序列化等,它们在效率上存在差异,但又有各自的特点。
除上面提到的三个问题外,生产中使用的RPC框架要考虑的东西还有很多,在此就不作探讨了。本文的目的就是为了让各位看官对RPC框架有一个感性的、较为深入的了解,如果达到了这一目的,笔者的目的基本就算达到了。
4.致谢
以上关于RPC框架内容摘自下方链接博主,仅供学习思考,非常感谢大佬提供的简洁有通透的实现思路,RPC实现还要各方各面的细节,还需要不断学习完善!
原文链接:https://blog.csdn.net/justloveyou_/article/details/79407248