作者 | 張凱
審校 | 重樓
動態代理技術與RPC(Remote Procedure Call)架構是現代分布式系統中常用的兩項關鍵技術,二者結合可以極大地提高系統的靈活性和可擴展性。將動態代理技術與RPC架構結合使用,可以實現自動化的服務調用和增強功能。開發者可以專注于業務邏輯的實現,而不必擔心底層細節。這種結合不僅提高了代碼的復用性和可維護性,也增強了系統的監控和管理能力。本文將詳細解析這兩項技術的實現方式以及其背后的設計邏輯。
一、動態代理
1. 代理模式
代理模式為其他對象提供了一種代理以控制對這個對象的訪問,根據代理類的創建時機和創建方式的不同,可以將其分為靜態代理和動態代理兩種形式:在程序運行前就已經存在的編譯好的代理類稱為靜態代理,在程序運行期間根據需要動態創建代理類及其實例來完成具體的功能稱為動態代理。
代理模式的目的是為真實業務對象提供一個代理對象以控制對真實業務對象的訪問,代理對象的作用有:
- 代理對象存在的價值主要用于攔截對真實業務對象的訪問;
- 代理對象可以和目標對象(真實業務對象)實現共同的接口或繼承于同一個類;
- 代理對象是對目標對象的增強,以便對消息進行預處理和后處理。
2. 反射
動態代理的實現核心是反射,一切動態代理的代理操作都是反射實現的。所以要先對反射知識有一定的了解。
2.1 反射實現步驟
2.1.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();
2.1.2通過反射獲取的對象,獲取對象類中的方法,使用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. 動態代理原理
對代理模式而言,具體主題類與其代理類一般是一一對應的,這也是靜態代理的特點。但是,也存在這樣的情況:有N個主題類,但是代理類中的“預處理、后處理”都是相同的,僅僅是調用主題不同。那么,若采用靜態代理,那么必然需要手動創建N個代理類,這顯然讓人相當不爽。動態代理則可以簡單地為各個主題類分別生成代理類,共享“預處理,后處理”功能,這樣可以大大減小程序規模,這也是動態代理的一大亮點。(通俗上來說,動態代理不再是一個代理類代理一個委托類,而是像個大管家,指定那個委托對象,就代理誰的方法,只不過代理類中通用的邏輯會適用于每個委托類)
在動態代理中,代理類是在運行時期生成的。因此,相比靜態代理,動態代理可以很方便地對委托類的相關方法進行統一增強處理,如添加方法調用次數、添加日志功能等等。動態代理主要分為JDK動態代理和CGLIB動態代理兩大類。
下面以一個模擬動態代理案例來實現動態代理的實現思路:
設計兩個Service類:User和Order,使用ProxyUtils類動態代理這兩類的抽象接口。程序在調用過程中,通過接口直接到代理類中,由代理類實現接口實現類的功能以及代理類自身的一些增強功能和通用功能。
3.1 接口類
//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);
}
3.2 實現類
//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;
}
}
3.3動態代理類ProxyUtils
- 該類中的proxyService方法使用泛型可以讓proxyService方法代理多種不同類的Service接口;
- 返回一個Proxy通過newProxyInstance()提供的實例對象;
- newProxyInstance()需要三部分的參數。第一部分是創建方法形參對象obj的類加載器對象;第二部分是創建方法形參對象obj的類接口對象(都為固定方法);第三部分是創建一個InvocationHandler()方法,意味調用類對象方法的方法;
- 代理類中的增強功能就可以寫在InvocationHandler()方法中,本例是實現了一個方法運行時間的檢測功能。
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框架就顯得非常必要了。簡單的HTTP調用雖然也可以實現遠程通信,但效果不夠理想,原因有二:
- RPC遠程調用像本地調用一樣干凈簡潔,但其他方式對代碼的侵入性就比較強;
- 一般使用RPC框架實現遠程通信效率比其他方式效率要高一些。
2. RPC框架介紹
對于多體應用,由于各服務部署在不同機器,服務間的調用免不了網絡通信過程,服務消費方每調用一個服務都要寫較多網絡通信相關的代碼,不僅復雜而且極易出錯。若能實現一種機制,使得開發者能夠像調用本地服務一般便捷地調用遠程服務,同時對網絡通信等底層細節保持透明,那么這將極大地釋放程序員的開發負擔,顯著提升開發效率與生產力。比如,服務消費方在執行helloService.hi(“Panda”)時,實質上調用的是遠端的服務。這種方式其實就是RPC(Remote Procedure Call),在各大互聯網公司中被廣泛使用,如阿里巴巴的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框架實現的靈魂所在。
2.1.1服務端
服務端提供客戶端所期待的服務,一般包括三個部分:服務接口,服務實現以及服務的注冊暴露三部分。
//服務端接口
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);
}
}
2.1.2客戶端
客戶端消費服務端所提供的服務,一般包括兩個部分:服務引用和服務接口兩個部分。
//(服務引用)消費端通過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);
}
2.1.3 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;
}
2.2關于RPC框架的解析與說明
(1)RPC框架如何做到透明化遠程服務調用
在封裝通信細節以實現用戶能夠像本地調用服務一樣便捷地訪問遠程服務的方案中,Java的動態代理技術無疑是一個有效的解決方案。具體而言,Java支持兩種動態代理的實現方式:JDK動態代理和CGLIB動態代理。盡管基于字節碼生成的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序列化以及ProtoBuf序列化等。這些不同的序列化方式在傳輸效率上各有差異,同時也具備各自的特點和優勢,適用于不同場景的需求。因此,選擇適當的序列化方案對于提升RPC框架的性能和可用性至關重要。
三、總結
動態代理允許在運行時創建對象的代理,從而對方法調用進行攔截和處理。在Java等編程語言中,利用反射機制,可以動態創建代理類。這種方式使得開發人員可以在不修改已有代碼的情況下,增強或改變方法的行為。本文通過構建實例的方式詳細解釋了其工作原理。RPC(遠程過程調用)是一種允許程序在不同的地址空間(通常是不同機器上的進程)中調用子程序或服務的協議。通過RPC,開發者可以像調用本地方法一樣,方便地調用遠程服務,隱藏了網絡通信的復雜性。
通過動態代理技術與RPC架構的結合,可以實現以下優勢:
- 透明的遠程調用:使用動態代理,開發者在調用遠程服務時無需關心具體的網絡請求和響應處理,可以像調用本地方法一樣使用。
- 增強的功能:通過動態代理,能夠在服務調用時加入額外的處理邏輯,如日志記錄、方法監控、錯誤處理等。
- 解耦合:RPC客戶端和服務端的實現可通過接口進行解耦,更新和維護服務時不會影響到調用方的代碼。
- 優化和擴展:因為使用了動態代理,增加新的功能或改變業務邏輯時,只需更改代理的實現即可,而不需要修改每一個具體的服務調用。
動態代理技術和RPC架構的結合為分布式系統的開發提供了靈活和高效的解決方案。它們使得系統能夠在復雜的基礎設施中輕松處理遠程服務的調用,同時還增強了代碼的可重用性和可維護性。這種技術的廣泛應用,無疑對現代軟件架構的演變產生了深遠的影響。
作者介紹
張凱,中國農業銀行股份有限公司研發中心軟件研發工程師,擅長SpringBoot+Vue全棧式開發,數據挖掘與建模,熱愛編程和學習前沿技術信息了解內部的實現邏輯。