Marco's Java【Dubbo 之手寫Dubbo框架實現遠程調用】

前言

關於Dubbo入門的網上教程也特別多,因此我沒有專門出關於Dubbo的系列博文(主要呢… 也是在忙些工作上的事兒),用Dubbo特別簡單,但是想要把Dubbo學好,學精還得花費不少時間的,特別是Dubbo的源碼是非常值得研究的!有時間會再出一套關於Dubbo的源碼解析,那麼本篇博文呢,咱也不講別的,主要是帶各位看看Dubbo最核心的部分,也就是遠程調用是如何實現的,如果這個給弄明白了,那麼你對Dubbo的認知會更上一層樓!
如果是初學Dubbo的朋友建議不要往下看啦!以下代碼純本人手寫,可能會有寫的不嚴謹的地方,還請見諒啦!也歡迎各位指出不足~

Dubbo原理分析

好啦,廢話咱也不多說,大家也知道Dubbo本質上就是使用JAVA開發的高性能RPC(Remote Procedure Call)框架,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議,當然這麼說很籠統,很官方,也不好理解,那如何去理解這個RPC框架是幹啥的呢?
還是舉個栗子吧!也比方說以前呢,你一個單身狗(我不是針對在場的各位… 哈哈哈)在家,會自己給自己做飯吃,自給自足,做好了飯,自己就可以吃上了,這就是本地調用。
那有一天,你談了個女朋友,女朋友在外面和閨蜜們逛街,肚子餓了,想回家就吃上飯,就call你,“晚上我回家吃飯哦”,那你不得屁顛屁顛的去買菜給準備準備,那麼這種情況對於你女朋友來說就是遠程調用!

懂了什麼是遠程調用,我們再來看這張Dubbo的基本架構圖
在這裏插入圖片描述
首先Provider也就是服務的提供者會先運行,然後將服務註冊到註冊中心,這裏的註冊中心通常指的是Zookeeper,當Provider運行之後,會在Provider項目的Spring容器中生成相對應的服務對象,注意了!Provider和Consumer分別是在不同的JVM上運行的,因此Consumer是無法直接引用Provider中的對象的,這就是爲什麼我們要使用RPC框架來調用對象中的方法的根本原因了!
在這裏插入圖片描述
如上圖,當Provider將AddServiceImpl服務註冊到註冊中心之後,其實並不是把這個對象傳輸給Zookeeper,而是註冊了這個服務的 “身份信息”,我們通過 “身份信息” 可以找到這個方法, “身份信息” 具體包含哪些內容呢?
我這裏就先賣個關子… 哈哈哈,下面手寫框架會重點講到。
當AddServiceImpl服務的信息被註冊之後,Consumer會向註冊中心訂閱這個服務,此時Consumer就相當於是擁有了AddServiceImpl服務的所有信息,你可以把Consumer理解成你女朋友,Provider就是你了,你們倆約定了,做飯的事情你來,那麼就好比在註冊中心註冊了 “做飯” 這個服務,當然可能還有其他的特殊服務我就不多說了… 當服務註冊之後,接下來女朋友想去 “調用” 你去做飯,就只用打電話傳呼你了,此時的電話號碼就是服務的註冊信息(當然這麼說可能有些牽強,大致是這個意思啦!) 。

手寫RPC框架

講了這麼多,主要是讓大家熟悉一下Dubbo是如何工作的,以及核心功能是什麼?接下來就進入今天的正題了,此次手寫RPC框架,包含以下4個項目。
在這裏插入圖片描述
rpc是父項目,父項目怎麼創建我就不多贅述啦,其餘的幾個項目都是子項目(Maven Moduel),rpc-core是核心項目,包含RPC調用的核心代碼,rpc-myself扮演consumer的角色,rpc-roommate則扮演的是provider的角色,爲了便於大家理解,我這裏就以考試爲例。
相信這麼多年了,大家或多或少會有些抄襲的獨門看家本領吧!(別說沒有!我不信!) 那麼此次呢,你將扮演的就是成績很差的 “菜比”,而你的室友呢扮演的就是學霸的角色,你答應學霸,考試過了,就請吃飯!但前提是我這次複試你一定要把答案發給我,手機一定要保持開機狀態,並且要一直盯着手機看!這次考試能不能過就看大神的了…
學霸一般都是老實人,就答應了,故事就這麼開始了…

第一步:創建rpc父項目

注意要創建Maven的pom項目

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.marco</groupId>
  <artifactId>rpc</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>
  <name>遠程調用</name>
  <description>遠程調用</description>
  <modules>
  	<module>rpc-roommate</module>
  	<module>rpc-myself</module>
  	<module>rpc-domain</module>
  </modules>
</project>
第二步:創建rpc-core項目

rpc-core項目包含手寫Dubbo框架的核心代碼,這裏先簡單介紹一下這個包裏面的各個包的作用吧。anno是註解包,coder是用於對象的二進制字節碼的轉化,domain就不多解釋啦,關於這裏面的兩個類我們下面會詳細做介紹,invoker專門處理方法的調用,包含本地方法的調用,以及遠程方法的調用,net主要用於網絡傳輸,proxy則存放的代理類,service嘛主要是模擬服務接口。
在這裏插入圖片描述
rpc-core的內容就是上面這些啦!先看簡單的anno註解包吧。
anno
@Expose 實則就是Dubbo中的@Service@Reference含義不變。@Expose 作用於類上,@Reference則作用於屬性上。

package com.marco.anno;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(ElementType.TYPE)
public @interface Expose {

}
package com.marco.anno;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(ElementType.FIELD)
public @interface Reference {

}

coder
coder包的主要用途是爲了將對象轉換爲二進制流,以及將二進制流轉爲對象,方便對象在網絡上進行傳輸。

package com.marco.coder;

import java.io.Serializable;

public interface Coder {
	/**
	 * 將對象編碼爲字節碼
	 * @param object
	 * @return
	 */
	byte[] code(Serializable object);
	
	/**
	 * 將字節數組轉換爲對象
	 * @return
	 */
	Object decode(byte[] bt);
}

JDKCoderCoder的實現類,這裏我們使用的還是最傳統的JDK序列化方式傳輸對象,但是此種方式在實際開發中並不推薦,我們可以使用Hessian、Jackson…等等序列化框架優化對象的序列化,使之序列化後的二進制流成倍的縮小,提升傳輸的速度,當然,這裏我們就不去引用別的框架啦!

package com.marco.coder;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class JDKCoder implements Coder {

	@Override
	public byte[] code(Serializable object) {
		try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
				ObjectOutputStream oos = new ObjectOutputStream(baos)) {
			// 將對象轉爲字節,寫入到內存中並返回
			oos.writeObject(object);
			//爲了避免客戶端阻塞,因此採取在二進制流的最末尾設置標記(-1)的措施來規避此問題發生
			oos.writeByte(-1);
			return baos.toByteArray();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public Object decode(byte[] bt) {
		try (ByteArrayInputStream bais = new ByteArrayInputStream(bt);
				ObjectInputStream ois = new ObjectInputStream(bais)) {
			// 將對象寫入到內存中並返回
			Object object = ois.readObject();
			return object;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}

domain
上面咱們講到,當Provider將AddServiceImpl服務註冊到註冊中心之後,其實並不是把這個對象傳輸給Zookeeper,而是註冊了這個服務的 “身份信息”,我們通過 “身份信息” 可以找到這個方法,這個 “身份信息” 其實就是下面的Request對象(包含接口名稱,方法名稱,傳入的參數信息),當然還包含你需要請求的服務註冊在Zookeeper裏的主機的IP地址和port端口號。
Reponse對象主要是用於回覆信息給發送請求的消費端。
在這裏插入圖片描述
在這裏插入圖片描述
invoker
定義MethodInvoker接口,實現類爲本地調用對象LocalInvoker,和遠程調用對象RPCInvoker

package com.marco.invoker;

import com.marco.domain.Request;
import com.marco.domain.Response;

public interface MethodInvoker {
	
	Response invoker(Request request);
}
package com.marco.invoker;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.spring.provider.RPCContextAware;

/**
 * 本地調用
 * @author Marco
 *
 */
public class LocalInvoker implements MethodInvoker {
	
	/**
	 * 用於緩存Class<?>對象
	 */
	private static Map<String,Class<?>> classCache = new HashMap<>();

	@Override
	public Response invoker(Request request) {
		//獲取request對象中本地調用的參數信息
		String interfaceName = request.getInterfaceName();
		String methodName = request.getMethod();
		Object[] args = request.getArgs();
		//獲取被調用接口的實現類的完全限定名
		String implClassName = getImplClassName(interfaceName);
		Object answer = null;
		String msg = "success";
		Class<?> clazz = null;
		try {
			//通過反射獲取到該實現類的Class對象
			if(classCache.containsKey(interfaceName)) {
				clazz = classCache.get(interfaceName);
			} else {
				clazz = Class.forName(implClassName);
				classCache.put(interfaceName,clazz);
			}
			Object obj = RPCContextAware.getBean(clazz);
			//將被調用方法的參數封裝到paramTypes數組中
			Class<?>[] parameterTypes = new Class<?>[args.length];
			for (int i = 0; i < args.length; i++) {
				parameterTypes[i] = args[i].getClass();
			}
			//通過反射原理獲取被調用方法的Method對象
			Method method = clazz.getMethod(methodName, parameterTypes);
			//通過invoke方法獲取最終的結果並返回結果
			answer = method.invoke(obj,args);
		} catch (Exception e) {
			msg = e.getMessage();
			e.printStackTrace();
		}
		return new Response(answer, msg);
	}
	
	/**
	 * 獲取被調用接口的實現類的完全限定名,例如將com.marco.service.UserService
	 * 轉化爲com.marco.service.impl.UserServiceImpl
	 * @param interfaceName
	 * @return
	 */
	private String getImplClassName(String interfaceName) {
		int lastIndexOf = interfaceName.lastIndexOf(".");
		String simpleClassName = interfaceName.substring(lastIndexOf);
		return interfaceName.replaceAll(simpleClassName, ".impl"+simpleClassName+"Impl");
	}
}
package com.marco.invoker;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import com.marco.coder.Coder;
import com.marco.coder.JDKCoder;
import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.loadbalance.LoadBalance;
import com.marco.loadbalance.RandomLoadBalance;
import com.marco.net.ConsumerNet;
import com.marco.net.client.ConsumerNetImpl;
import com.marco.register.ServerDiscovery;
import com.marco.register.impl.ServerDiscoveryImpl;

public class RPCInvoker implements MethodInvoker {
	
	private ConsumerNet consumerNet = new ConsumerNetImpl();

	private Coder coder = new JDKCoder();
	
	private LoadBalance loadBalance = new RandomLoadBalance();
	
	private ServerDiscovery discovery = new ServerDiscoveryImpl();

	@Override
	public Response invoker(Request request) {
		//獲取服務中所有的服務提供者
		List<String> servers = discovery.discovery(request.getInterfaceName());
		// 連接提供者
		String select = loadBalance.select(servers);
		String host = select.split(":")[0];
		Integer port = Integer.valueOf(select.split(":")[1]);
		consumerNet.connect(host, port);
		byte[] result = null;
		try (OutputStream outputStream = consumerNet.getOutputStream();
				BufferedOutputStream bos = new BufferedOutputStream(outputStream);
				InputStream inputStream = consumerNet.getInputStream();
				BufferedInputStream bis = new BufferedInputStream(inputStream)) {
			// 將request對象轉成字節(二進制)並寫出
			byte[] code = coder.code(request);
			bos.write(code);
			bos.flush();

			byte[] by = new byte[1024];
			int len = 0;
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			while ((len = bis.read(by)) != -1) {
				baos.write(by, 0, len);
				// 爲了避免客戶端阻塞,因此採取在最末尾設置標記(-1)的措施規避此問題發生
				byte lastByte = by[len - 1];
				if (lastByte == -1) {
					break;
				}
			}
			result = baos.toByteArray();
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 將獲取到的字節流轉爲對象
		Object decode = coder.decode(result);
		return (Response) decode;
	}
}

net
net包下的類,主要是封裝了客戶端和服務端的遠程網絡訪問(Socket)方法,方便調用,當然Dubbo的底層肯定不是使用原生的Socket來進行遠程通信,而是使用Netty框架來實現通信,畢竟Socket的效率實在是太低啦!
關於Netty的原理和解析大家可以參考博文 Netty高性能原理和框架架構解析

package com.marco.net;

import java.io.InputStream;
import java.io.OutputStream;

public interface ConsumerNet {
	
	/**
	 * 連接提供者
	 * @param ip
	 * @param port
	 */
	void connect(String ip, int port);
	
	/**
	 * 輸入
	 * @return
	 */
	InputStream getInputStream();

	/**
	 * 輸出
	 * @return
	 */
	OutputStream getOutputStream();
}
package com.marco.net.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import com.marco.net.ConsumerNet;

public class ConsumerNetImpl implements ConsumerNet {
	
	private Socket client;

	@Override
	public void connect(String ip, int port) {
		try {
			//創建Socket客戶對象
			client = new Socket(ip, port);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public InputStream getInputStream() {
		InputStream inputStream = null;
		try {
			inputStream = client.getInputStream();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return inputStream;
	}

	@Override
	public OutputStream getOutputStream() {
		OutputStream outputStream = null;
		try {
			outputStream = client.getOutputStream();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return outputStream;
	}
}
package com.marco.net.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import com.marco.net.ProviderNet;

public class ProviderNetImpl implements ProviderNet {
	
	private ServerSocket serverSocket = null;
	
	private Socket server;
	
	public ProviderNetImpl(int port) {
		//創建Socket服務對象
		try {
			serverSocket = new ServerSocket(port);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void listen() {
		try {
			//開啓監聽
			server = serverSocket.accept();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public InputStream getInputStream() {
		InputStream inputStream = null;
		try {
			inputStream = server.getInputStream();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		return inputStream;
	}

	@Override
	public OutputStream getOutputStream() {
		OutputStream outputStream = null;
		try {
			outputStream = server.getOutputStream();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		return outputStream;
	}
}

proxy
定義遠程調用代理類接口RPCProxy

package com.marco.proxy;

/**
 * 遠程調用代理
 * @author Marco
 *
 */
public interface RPCProxy{
	
	/**
	 * 獲取代理對象
	 * @param <T>
	 * @param clazz
	 * @return
	 */
	<T> T getProxy(Class<T> clazz);
}

創建代理類RPCObjectProxy實現RPCProxy接口,當消費者執行方法,例如addService.add(1,2)的時候會調用invoke方法,進而通過遠程調用獲取執行完成之後的結果。

package com.marco.proxy;

import java.lang.reflect.Proxy;

import com.marco.domain.Request;
import com.marco.invoker.MethodInvoker;

public class RPCObjectProxy implements RPCProxy {
	
	private MethodInvoker methodInvoker;
	
	public RPCObjectProxy(MethodInvoker methodInvoker) {
		super();
		this.methodInvoker = methodInvoker;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getProxy(Class<T> clazz) {
		return (T) Proxy.newProxyInstance(
				RPCObjectProxy.class.getClassLoader(), 
				new Class<?>[] { clazz },
				(proxy, method, args) -> {
					//創建請求對象
					Request request = new Request();
					request.setArgs(args);
					request.setInterfaceName(clazz.getName());
					request.setMethod(method.getName());
					return methodInvoker.invoker(request).getAnswer();
				});
	}
}

register
註冊和發現服務的接口類ServerRegisterServerDiscovery

package com.marco.register;

import java.util.List;

public interface ServerDiscovery {
	List<String> discovery(String serverName);
}
package com.marco.register;

public interface ServerRegister {
	void register(String serverName, String serverPath);
}

以上接口的實現類ServerRegisterImplServerDiscoveryImpl,顧名思義,用途就是方法的註冊和方法的發現(引用)。

package com.marco.register.impl;

import com.marco.register.ServerRegister;
import com.marco.utils.ZookeeperUtils;

public class ServerRegisterImpl implements ServerRegister {
	
	@Override
	public void register(String serverName, String serverPath) {
		ZookeeperUtils.addEphemeralNode(serverName, serverPath);
	}
}
package com.marco.register.impl;

import java.util.List;

import com.marco.register.ServerDiscovery;
import com.marco.utils.ZookeeperUtils;

public class ServerDiscoveryImpl implements ServerDiscovery {

	@Override
	public List<String> discovery(String serverName) {
		return ZookeeperUtils.getSubNode(serverName);
	}
}

utils
大家也發現了,上面的ServerRegisterImpl以及ServerDiscoveryImpl本質上就是調用了ZookeeperUtils中的addEphemeralNode()創建臨時節點和getSubNode()獲取子節點的方法,ZookeeperUtils中封裝了我們手寫Dubbo中所有的關於和Zookeeper集成的操作。
並且使用一個簡單的 “緩存” serverCache,將被暴露並註冊在Zookeeper的服務進行緩存處理,適當的提高了框架的性能,但是需要注意的一點是,當Provider服務端因爲某種原因,導致服務停止之後,Zookeeper會監控到這個異常,並且會通知到Consumer,那麼此時我們的緩存serverCache就必須得更新或者刪除節點得信息。
否則,如果不更新緩存,假設當Provider的服務子節點有更新,從localhost:8888變成localhost:9999,那麼此時我們不更新緩存讀出來的數據依然是localhost:8888,此時我們根據這個 “身份信息” 再次去連接服務端時,必定是會報 java.net.ConnectException: Connection refused 等連接異常的。
在這裏插入圖片描述
因此在以下得代碼中,我通過zkClient.subscribeChildChanges()的監聽子節點方法,解決了緩存的髒數據讀取問題。

package com.marco.utils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.I0Itec.zkclient.ZkClient;

/**
 * 操作一: zk 創建持久節點
 * 操作二: zk 創建臨時節點
 * 操作三: zk 獲取父節點裏面的子節點
 * 操作四: zk 訂閱節點
 * @author Marco
 *
 */
public class ZookeeperUtils {
	
	/**
	 * Zookeeper的連接地址
	 */
	private static final String ZK_URL = "127.0.0.1:2181";
	
	/**
	 * 聲明ZkClient
	 */
	private static ZkClient zkClient = null;
	
	/**
	 * 緩存,key爲服務名稱,value爲該服務下的類的全類名
	 */
	private static Map<String,List<String>> serverCache = new HashMap<String,List<String>>();
	
	/**
	 * 初始化ZkClient
	 */
	static {
		zkClient = new ZkClient(ZK_URL, 5*1000, 20*1000);
	}
	
	/**
	 * 創建持久節點
	 * @param nodeName
	 */
	public static void addPersistentNode(String nodeName) {
		if(!zkClient.exists("/" + nodeName)) {
			//若節點不存在,則創建新的節點
			zkClient.createPersistent("/" + nodeName);
		}
		System.out.println("節點創建完成");
	}
	
	/**
	 * 創建臨時節點
	 * @param parentNodeName
	 * @param nodeName
	 */
	public static void addEphemeralNode(String parentNodeName, String nodeName) {
		//若父節點不存在,則創建新的節點
		addPersistentNode(parentNodeName);
		zkClient.createEphemeral("/" + parentNodeName + "/" + nodeName);
		System.out.println("臨時節點創建完成");
		/*try {
			System.in.read();
		} catch (IOException e) {
			e.printStackTrace();
		}*/
	}
	
	/**
	 * 獲取子節點
	 * @param nodeName
	 * @return
	 */
	public static List<String> getSubNode(String serverName) {
		//如果緩存中有serverName的服務,則直接從緩存中獲取
		if(serverCache.containsKey(serverName)) {
			return serverCache.get(serverName);
		}
		if(!zkClient.exists("/" + serverName)) {
			throw new RuntimeException("沒有" + serverName + "的服務提供者");
		}
		List<String> servers = zkClient.getChildren("/" + serverName);
		//如果緩存中沒有serverName的服務,則存入緩存中
		serverCache.put(serverName, servers);
		/**
		 * 採用節點訂閱的方式,解決節點緩存的數據髒讀問題
		 */
		zkClient.subscribeChildChanges("/" + serverName, (parentPath, currentChilds) -> {
			System.out.println("已解決緩存的髒讀問題!");
			serverCache.put(serverName, currentChilds);
		});
		return servers;
	}
}

spring
接下來就是我們的集成spring的包,可以算是咱們核心包中的核心了,直接亮出代碼吧!
在這裏插入圖片描述
ReferenceBeanContext類中,我們實現了spring的BeanPostProcessor接口,並重寫postProcessBeforeInitialization()postProcessAfterInitialization(),這兩個方法很類似,但是依然有區別,前者是在bean實例化,依賴注入之後及自定義初始化方法(例如:配置文件中bean標籤添加init-method屬性指定Java類中初始化方法、@PostConstruct註解指定初始化方法,Java類實現InitailztingBean接口)之前調用。
而後置處理器的postProcessorAfterInitailization方法是在bean實例化、依賴注入及自定義初始化方法之後調用,實現BeanPostProcessor並借用此方法的主要目的是爲了查找IoC容器中的是否擁有屬性上設置@Reference註解的對象,如果有,則獲取這個屬性的名稱,得到它的接口,並從IoC容器中取出這個和接口對應的實現類對象,而這個實現類對象,恰巧是我們使用@Expose(等同於Dubbo中的@Service)註解暴露並註冊的服務類對象。

package com.marco.spring.consumer;

import java.lang.reflect.Field;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import com.marco.anno.Reference;
import com.marco.invoker.RPCInvoker;
import com.marco.proxy.RPCObjectProxy;
import com.marco.proxy.RPCProxy;

@Component
public class ReferenceBeanContext implements BeanPostProcessor{
	
	private RPCProxy rpcProxy = new RPCObjectProxy(new RPCInvoker());
	
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		Field[] declaredFields = bean.getClass().getDeclaredFields();
		if(null != declaredFields && declaredFields.length > 0) {
			for (Field field : declaredFields) {
				//檢查該屬性上是否有@Reference註解
				Reference reference = field.getAnnotation(Reference.class);
				//如果屬性上有@Reference註解,則將代理對象注入給該屬性
				if(null != reference) {
					System.out.println(beanName + "類中的屬性" + field.getName() + "上有" + reference + "註解");
					//設置強制注入
					field.setAccessible(true);
					Class<?> clazz = field.getType();
					//獲取代理對象
					Object proxy = rpcProxy.getProxy(clazz);
					//注入代理對象
					try {
						field.set(bean, proxy);
					} catch (IllegalArgumentException | IllegalAccessException e) {
						e.printStackTrace();
					}
				}
			}
		}
		return bean;
	}
}
package com.marco.spring.provider;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import com.marco.anno.Expose;
import com.marco.coder.Coder;
import com.marco.coder.JDKCoder;
import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.invoker.LocalInvoker;
import com.marco.invoker.MethodInvoker;
import com.marco.net.ProviderNet;
import com.marco.net.server.ProviderNetImpl;
import com.marco.register.ServerRegister;
import com.marco.register.impl.ServerRegisterImpl;

@Component
public class RPCContextAware implements ApplicationContextAware, InitializingBean{
	
	/**
	 * IoC容器對象
	 */
	private static ApplicationContext applicationContext = null;
	
	/**
	 * 用於保存被暴露的服務
	 */
	private Map<String,String> exposedServers = new HashMap<String, String>();
	
	/**
	 * 用於將服務註冊到註冊表中
	 */
	private ServerRegister register = new ServerRegisterImpl();
	
	/**
	 * 用於編碼、解碼
	 */
	private Coder coder = new JDKCoder();
	
	/**
	 * 本地調用處理器
	 */
	private MethodInvoker methodInvoker = new LocalInvoker();
	
	/**
	 * 需要監聽端口,默認不設置則爲6666
	 */
	@Value("${port:6666}")
	private int port;
	
	/**
	 * 提供服務者的網絡連接裝置
	 */
	private ProviderNet providerNet;
	
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		/**
		 * 在IoC容器中搜索類上有Expose註解的類 
		 * key 類的名稱,如addServiceImpl  
		 * value 對象
		 */
		RPCContextAware.applicationContext = applicationContext;
		Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Expose.class);
		if(!beans.isEmpty()) {
			beans.forEach((k,v) -> {
				Class<?>[] interfaces = v.getClass().getInterfaces();
				//添加需要被暴露的服務
				exposedServers.put(interfaces == null ? v.getClass().getName():interfaces[0].getName(), "localhost:" + port);
			});
		}
		System.out.println("需要註冊的服務有" + exposedServers);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		/**
		 * 將暴露的服務註冊到Zookeeper中
		 */
		exposedServers.forEach((k,v) -> {
			register.register(k, v);
			System.out.println(k + "服務已經註冊成功,暴露服務的地址是" + v);
		});
		listen();
	}
	
	private void listen() {
		System.out.println("*************監聽就緒***************");
		providerNet = new ProviderNetImpl(port);
		// 循環監聽port端口(等待菜比隊友的通知),解決服務端被請求一次就死機的問題
		while(true) {
			providerNet.listen();
			// 當收到消息之後,監聽中止,取消阻塞狀態
			Request request = null;
			System.out.println("收到菜比室友的題目了...");
			try (InputStream inputStream = providerNet.getInputStream();
					BufferedInputStream bis = new BufferedInputStream(inputStream);
					OutputStream outputStream = providerNet.getOutputStream();
					BufferedOutputStream bos = new BufferedOutputStream(outputStream)) {
				byte[] by = new byte[1024];
				int len = 0;
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				while ((len = bis.read(by)) != -1) {
					baos.write(by, 0, len);
					//字節數組長度沒有1024表示這是最後一次讀取數據了
					if(len < 1024) {
						break;
					}
				}
				// 獲取request對象,通過coder的decode解碼方法將二進制流轉爲Request對象
				request = (Request) coder.decode(baos.toByteArray());
				System.out.println("題目爲" + request.toString());
				// 執行invoker方法獲取結果
				Response answer = methodInvoker.invoker(request);
				System.out.println("計算出來的答案爲" + answer);
				bos.write(coder.code(answer));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	/**
	 * 通過clazz從IoC容器中獲取已經被創建出來的對象
	 * @param clazz
	 * @return
	 */
	public static Object getBean(Class<?> clazz) {
		return applicationContext.getBean(clazz);
	}
}

loadbalance
在這裏插入圖片描述
loadbalance包主要是爲了實現負載均衡,目前只添加了Random隨機選擇的策略,有興趣的朋友可以試着添加輪詢RoundRobin 、ConsistentHash等策略。

package com.marco.loadbalance;

import java.util.List;

public interface LoadBalance {
	
	/**
	 * 從list集合中挑選一個服務出來
	 * @param servers
	 * @return
	 */
	String select(List<String> servers);
}
package com.marco.loadbalance;

import java.util.List;
import java.util.Random;

public class RandomLoadBalance implements LoadBalance {

	private static Random random = new Random();
	
	@Override
	public String select(List<String> servers) {
		if(null == servers || servers.isEmpty()) {
			throw new RuntimeException("當前註冊服務列表爲空!");
		}
		if(servers.size() == 1) {
			return servers.get(0);
		}
		int nextInt = random.nextInt(servers.size());
		return servers.get(nextInt);
	}
}

service
AddService類主要是模擬被暴露的服務接口,就暫且先放在這裏吧。

package com.marco.service;

public interface AddService {
	Integer add(Integer a, Integer b);
}

最後,我們需要導入rpc-core項目所依賴的jar包spring-context

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.marco</groupId>
		<artifactId>rpc</artifactId>
		<version>1.0</version>
	</parent>
	<artifactId>rpc-core</artifactId>
	<dependencies>
		<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
		<dependency>
			<groupId>com.101tec</groupId>
			<artifactId>zkclient</artifactId>
			<version>0.11</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>4.3.16.RELEASE</version>
		</dependency>
	</dependencies>
</project>

第三步:創建rpc-roommate項目

以上就是我們的難點部分,接下來創建的這兩個子項目主要是爲了測試我們的手寫Dubbo框架是否能正常運行,達到我們想要的效果。
在這裏插入圖片描述
大家注意看AddServiceImpl類上的註解,添加@Expose註解的目的正是將服務暴露出去,並在Zookeeper中註冊,關於這一塊的邏輯業務代碼請參照上面的RPCContextAware類的實現。

package com.marco.service.impl;

import com.marco.anno.Expose;
import com.marco.service.AddService;

@Expose
public class AddServiceImpl implements AddService {

	@Override
	public Integer add(Integer a, Integer b) {
		return a + b;
	}
}

Roommate可以把它當作是一個啓動類,因爲provider的核心業務邏輯代碼在RPCContextAware類中,但是它和我們當前的Roommate並不在一個項目裏面,因此需要利用xml文件將RPCContextAware加載並置入Spring的IoC容器中,這樣才能使RPCContextAware中得方法被執行調用,完成服務得註冊,以及服務端的監聽。
所以我們通過new ClassPathXmlApplicationContext()加載spring-rpc-provider.xml配置文件。

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Roommate {
	public static void main(String[] args) {
		System.out.println("等待菜比室友發題目過來");
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-rpc-provider.xml");
		context.start();
	}
}

spring-rpc-provider.xml配置文件內容如下。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	<context:component-scan base-package="com.marco.service.impl"></context:component-scan>
	<context:property-placeholder location="classpath:/properties/app.properties"/>
	<bean name="rPCContextAware" class="com.marco.spring.provider.RPCContextAware"></bean>
</beans>

除了RPCContextAware需要被放入IoC容器中,我們的AddServiceImpl也應該被掃描,置入容器中,這就是爲何我在@Expose註解中添加了@Component元註解的原因。
在這裏插入圖片描述
另外還有一個小細節,就是properties/app.properties文件的加載。
在這裏插入圖片描述
不知道大家有沒有留意RPCContextAware中的這段代碼,通過SpEL表達式,動態的獲取端口的值,僅需要在app.properties文件中添加你要運行的服務的端口號(例如:port=6666)就可以了,:6666的意思是當配置文件什麼都沒有填寫的時候,默認會以port=6666來啓動我們的服務。

/**
 * 需要監聽端口,默認不設置則爲6666
 */
@Value("${port:6666}")
private int port;

我們大可以嘗試運行一下,比方說端口號是7777,運行之後的結果如下
在這裏插入圖片描述
假如我們沒有寫任何端口號,則執行port=6666
在這裏插入圖片描述
我們接着再運行一個port=8888的服務,並查看我們的Zookeeper註冊中心,發現服務註冊成功了!並且子節點有三個!顯然是正常的狀態。
在這裏插入圖片描述


第四步:創建rpc-myself項目

終於到我們的最後一步了。rpc-myself項目模擬的就是消費端,結構如下
在這裏插入圖片描述
AddController類主要爲了模擬遠程獲取AddService服務,相關的業務邏輯核心代碼請參照上面的ReferenceBeanContext類。

package com.marco.controller;

import org.springframework.stereotype.Component;

import com.marco.anno.Reference;
import com.marco.service.AddService;

@Component
public class AddController {
	
	@Reference
	private AddService addService;
	
	public Integer add(Integer a, Integer b) {
		return addService.add(a, b);
	}
}

最後是Consumer端的啓動類Myself

package com.marco;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.marco.controller.AddController;

public class Myself {
		
	public static void main(String[] args){
		System.out.println("這道題目不會做 1+1=? 我要求教我的室友");
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-rpc-consumer.xml");
//		AddService addService = new RPCObjectProxy(new RPCInvoker()).getProxy(AddService.class);
		AddController addController = context.getBean(AddController.class);
		while(true) {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			Integer answer = addController.add(1, 4);
			System.out.println("室友發送過來的答案是" + answer.toString());
		}
	}
}

同樣的,Myself中也是通過new ClassPathXmlApplicationContext("classpath:spring-rpc-consumer.xml")的方式將ReferenceBeanContext類和AddController類注入到IoC容器中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	
	<context:component-scan base-package="com.marco.controller"></context:component-scan>
	<bean name="referenceBeanContext" class="com.marco.spring.consumer.ReferenceBeanContext"></bean>
</beans>

終於完成了!趕緊啓動看看效果吧!
在這裏插入圖片描述
通過測試發現,我們的random策略着實生效了,每間隔5秒鐘,消費端就會隨機挑選一個服務端,並通過Socket向服務戶端發送一次請求,收到Request請求的服務戶端,則會解析Request對象,找到對應的服務方法,將方法執行完成的結果,通過Socket返回給消費端,通過這種方式,將遠程調用的所有過程全部 “隱藏”。這就是神奇的RPC框架!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章