一、創建項目
二、設計目標
2.1 概論
設計一個基於接口的調用方式的遠程調用框架,和spring 整合在一起
2.2 形式
2.2.1 導入spring的依賴
<?xml version="1.0" encoding="UTF-8"?>
<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.sxt</groupId>
<artifactId>link</artifactId>
<version>2.0</version>
<properties>
<spring.version>4.3.16.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
2.2.2 服務的提供者的啓動類和配置文件
啓動類:
/**
* 服務提供者的啓動類
*/
public class ProviderApp {
/**
* 我們和spring 整合在了一起
* @param args
*/
public static void main(String[] args) {
// 加載spring的容器
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("classpath:provider.xml");
app.start();
// 掛起jvm
try {
System.in.read() ;
} catch (IOException e) {
e.printStackTrace();
}
}
}
配置文件:
<?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.sxt.test.service.impl"></context:component-scan>
</beans>
2.2.3 接口和實現類的準備
2.2.4 服務消費者的啓動類和配置文件
啓動類:
/**
* 消費者
*/
public class ConsumerApp {
public static void main(String[] args) {
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("consumer.xml");
app.start();
ConsumerService consumerService = app.getBean(ConsumerService.class);
consumerService.consumer(); // consumer 底層會觸發遠程調用
try {
// 掛起jvm
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
配置文件:
<?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.sxt.test.consumer.service.impl"></context:component-scan>
</beans>
2.2.5 服務消費者的接口和實現類
三、設計註解和功能
3.1 對於服務的提供者
要註冊該服務,已經暴露該服務!
在dubbo 裏面使用@service來完成該功能
對於link:
使用該註解:
代表該服務以後將被暴露出去
3.2 對於服務的消費者
去遠程調用一個服務
使用該註解:
3.3 設計工具和接口
3.3.1 負載均衡的功能定義
3.3.2 註冊中心的功能
3.3.3 編碼器(影響性能的關鍵)
我們在遠程調用時,不可避免要把對象轉換爲字節去在socket 上面發送?
我們昨天的Request 還實現序列化的接口了
我們把對象->字節
字節-> 對象
3.3.4 模型對象
3.3.5 代理對象的工具類
/**
* 代理對象的工具類
*/
public class ProxyUtil {
/**
* 通過接口創建代理對象
* @param interfacee
* @param <T>
* @return
*/
public static <T> T createProxy(Class<T> interfacee){
return (T) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class<?>[]{interfacee},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 1 普通方法的處理
*/
boolean isCommonMethod = false ;
if(isCommonMethod){
return commonInvoker(method.getName());
}
/**
* 2 遠程調用的方法的處理
*/
Request request = new Request();
request.setInterfaceName(interfacee.getName());
request.setMethodName(method.getName());
request.setArgs(args);
return rpcInvoker(request); // 遠程調用
}
}
);
}
/**
* 完成遠程的調用
* @param request
* @return
*/
private static Object rpcInvoker(Request request) {
return null ;
}
/**
* 完成普通方法的調用
* @param name
* @return
*/
private static Object commonInvoker(String name) {
return null ;
}
}
3.3.6 方法的調用
方法的調用在2 個場景發生:
1 服務提供者而言
需要一個本地的調用
2 服務的消費者而言
需要遠程的方法調用
使用起來:
3.3.7 網絡的能力
網絡的能力無非就是輸入和輸出(IO)
四、實現接口和工具
牛逼的公司,做設計(標準),菜(實現)
4.1 實現負載均衡
/**
* 隨機的負載均衡實現
*/
public class RandomLoadBalance implements LoadBalance{
/**
* 單例對象
*/
public static final RandomLoadBalance INSTANCE = new RandomLoadBalance();
public Random rdm = new Random() ;
private RandomLoadBalance(){}
@Override
public String loadbalance(List<String> servers) {
if(servers==null || servers.isEmpty()){
throw new RuntimeException("輸入的集合爲null") ;
}
return servers.get(rdm.nextInt(servers.size()));
}
}
4.2 實現註冊中心的功能
註冊中心不僅僅可以使用zk 吧,redis 也能實現?在dubbo 裏面講過吧
使用zk 實現註冊中心,需要導入zkclient的依賴
public class ZkRegister implements Register{
private ZkClient zkClient = null ;
private String zkUrl = "" ;
private int connectionTime = 300000;
private int sessionTime = 5000 ;
{
zkClient = new ZkClient(zkUrl,sessionTime,connectionTime) ;
}
private Map<String,List<String>> cache = new HashMap<>();
public static final ZkRegister INSTANCE = new ZkRegister() ;
private ZkRegister (){}
@Override
public void register(String serviceName, String serviceAddress) {
if(!zkClient.exists("/"+serviceName)){
zkClient.createPersistent("/"+serviceName);
}
if(!zkClient.exists("/"+serviceName+"/"+serviceAddress)){
zkClient.createEphemeral("/"+serviceName+"/"+serviceAddress);
System.out.println("服務"+serviceName+"註冊成功,地址爲"+serviceAddress);
}
}
@Override
public List<String> discovery(String serviceName) {
if(cache.containsKey(serviceName)){
return cache.get(serviceName) ;
}
List<String> servers = zkClient.getChildren("/" + serviceName);
if(servers!=null && !servers.isEmpty()){
cache.put(serviceName,servers);
zkClient.subscribeChildChanges("/" + serviceName, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
System.out.println("服務"+serviceName+"的提供者有改變");
if(currentChilds==null || currentChilds.isEmpty()){
cache.remove(serviceName);
}else{
cache.put(serviceName,currentChilds) ;// 更新緩存
System.out.println("服務列表已經更新完畢");
}
}
}) ;
return servers ;
}
return Collections.emptyList();
}
}
4.3 實現編碼器的功能
/**
* 利用jdk的序列化實現
*/
public class JDKCoder implements Coder{
public static final JDKCoder INSTANCE = new JDKCoder() ;
private JDKCoder(){}
/**
* 將對象轉爲爲字節
* @param object
* @return
*/
@Override
public byte[] code(Object object) {
// 對象-> 字節 對象的流
if(object==null){
return new byte[0] ;
}
byte[] bytes = new byte[0];
ByteArrayOutputStream byteArrayOutputStream = null ;
ObjectOutputStream objectOutputStream = null ;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
bytes = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 關閉資源的操作
}
return bytes ; // 將內存裏面的字節讀取出來
}
/**
* 將字節轉換爲對象
* @param bs
* @return
*/
@Override
public Object deCode(byte[] bs) {
if(bs==null || bs.length==0){
return null ;
}
Object object = null;
ByteArrayInputStream byteArrayInputStream = null ;
ObjectInputStream objectInputStream = null ;
try {
byteArrayInputStream = new ByteArrayInputStream(bs);
objectInputStream = new ObjectInputStream(byteArrayInputStream);
object = objectInputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
}
return object;
}
}
4.4 方法的調用實現
4.4.1 遠程方法調用的實現
public class RpcMethodInvoker implements MethodInvoker{
private Net net ;
private Coder coder ;
/**
* 遠程調用的實現
* @param request
* @return
*/
@Override
public Object invoker(Request request) {
net.write(coder.code(request)); // 發送請求對象給服務的提供者
byte[] bs = net.read(); // 接受服務提供者的答案
return coder.deCode(bs);
}
}
4.4.2 本地方法調用的實現
public class LocalMethodInvoker implements MethodInvoker {
private ApplicationContext applicationContext ;// 它就是ioc的容器
/**
* 完成真實方法的調用
* @param request
* @return
*/
@Override
public Object invoker(Request request) {
String interfaceName = request.getInterfaceName();
Object result = null;
try {
Class<?> interfacee = Class.forName(interfaceName);
// 怎麼通過接口得到實現類的對象 我們現在位於ioc 裏面
Object bean = applicationContext.getBean(interfacee); // 真實對象
String methodName = request.getMethodName();
Object[] args = request.getArgs();
Class<?> types[] = null ;
if(args!=null && args.length > 0){ // 得到方法的參數類型
types = new Class[args.length] ;
for (int i = 0; i < args.length; i++) {
types[i] = args[i].getClass() ;
}
}
Method method = interfacee.getMethod(methodName, types);
result = method.invoke(bean, args);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return result;
}
}
4.5 網絡能力的實現
實現io
public class SocketNet implements Net{
@Override
public void write(byte[] bs) {
OutputStream outputStream = null ;
try {
outputStream.write(bs); // 可以直接寫
} catch (IOException e) {
e.printStackTrace();
}finally {
// TODO 關閉資源的操作
}
}
@Override
public byte[] read() {
InputStream inputStream = null ;
/**
* 讀取的操作
*/
byte[] buff = new byte[1024];
int len = 0 ;
byte[] bytes = new byte[0];
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while((len=inputStream.read(buff)) != -1){ // 從input 讀取到buff ,長度爲0-len
byteArrayOutputStream.write(buff,0,len);
}
bytes = byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
//TODO 關閉資源的操作
}
return bytes;
}
}
4.5.1 針對服務端
ServerSockert
4.5.2 針對客戶端
Socket
public class ClientSocketNet extends SocketNet{
private Register register ;
private LoadBalance loadBalance ;
// private Socket socket ;
// 答案是不行的,因爲我們ClientSocketNet 是個單例對象,那單例對象裏面的屬性也是唯一的,若有線程給它賦值後,它就有值了
// 和springmvc 裏面的對象的變量是一個道理 // 怎麼解決springMVC 裏面變量的安全問題?
/**
* 輸出流用於發送一個數據
* @return
*/
@Override
protected OutputStream getOutputStream() {
// 利用ThreadLocal 取出當前要調用的服務的名稱
//TODO
String serviceName = "" ;
/**
* 服務的發現
*/
List<String> servers = register.discovery(serviceName);
/**
* 服務的負載均衡
*/
String server = loadBalance.loadbalance(servers);
String[] serverAddress = server.split(":");
OutputStream outputStream = null;
Socket socket = null ;
try {
socket = new Socket(serverAddress[0],Integer.valueOf(serverAddress[1])); //ip :port ->服務的地址-> 服務的發現+ 負載均衡
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
// 把Socket 存儲起來 TODO
// 使用ThreadLocal 做
return outputStream;
}
@Override
protected InputStream getInputStream() {
Socket socket = null ;// // 從ThreadLocal 裏面獲取Socket
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
}
4.6 ThreadLocal 利用起來
/**
* ThreadLocal的工具類
*/
public class ThreadLocalUtil {
private static ThreadLocal<Map<String,Object>> threadLocal = new ThreadLocal<Map<String, Object>>(){
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
/**
* 從ThreadLocal 裏面取值
* @param name
* @return
*/
public static Object get(String name){
Map<String, Object> objectMap = threadLocal.get();
if(objectMap.containsKey(name)){
return objectMap.get(name) ;
}
return null ;
}
/**
* 給ThreadLocal 裏面保存值
* @param name
* @param value
*/
public static void set(String name,Object value){
Map<String, Object> objectMap = threadLocal.get();
objectMap.put(name,value) ;
}
/**
* 清理ThreadLocal 裏面的值
*/
public static void clear(){
Map<String, Object> stringObjectMap = threadLocal.get();
stringObjectMap.clear();
}
}
4.6.1 利用ThreadLocal 在一個線程之間來共享值
public class ClientSocketNet extends SocketNet{
private Register register ;
private LoadBalance loadBalance ;
// private Socket socket ;
// 答案是不行的,因爲我們ClientSocketNet 是個單例對象,那單例對象裏面的屬性也是唯一的,若有線程給它賦值後,它就有值了
// 和springmvc 裏面的對象的變量是一個道理 // 怎麼解決springMVC 裏面變量的安全問題?
/**
* 輸出流用於發送一個數據
* @return
*/
@Override
protected OutputStream getOutputStream() {
// 利用ThreadLocal 取出當前要調用的服務的名稱
String serviceName = (String)ThreadLocalUtil.get("serviceName");
/**
* 服務的發現
*/
List<String> servers = register.discovery(serviceName);
/**
* 服務的負載均衡
*/
String server = loadBalance.loadbalance(servers);
String[] serverAddress = server.split(":");
OutputStream outputStream = null;
Socket socket = null ;
try {
socket = new Socket(serverAddress[0],Integer.valueOf(serverAddress[1])); //ip :port ->服務的地址-> 服務的發現+ 負載均衡
outputStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
// 把Socket 存儲起來 TODO
// 使用ThreadLocal 做
ThreadLocalUtil.set("client",socket); // 往線程裏面設置值
return outputStream;
}
@Override
protected InputStream getInputStream() {
Socket socket = (Socket)ThreadLocalUtil.get("client") ;// // 從ThreadLocal 裏面獲取Socket
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
}
4.7 ServerSocketNet的實現
/**
* serverSocket
* ServerSocket 只需要一個就有可以了,不管有多少個客戶端,只需要一個ServerSocket 就可以了
* 在服務端和客戶不同的點,是服務端需要監聽
*
*/
public class ServerSocketNet extends SocketNet{
private Coder coder ;
private ServerSocket serverSocket ;
private MethodInvoker methodInvoker ;
public ServerSocketNet(int port){
try {
this.serverSocket = new ServerSocket(port) ;
// 服務端一啓動就需要監聽,而且是死循環的監聽
while(true){
listener();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void listener(){
Socket socket = null ;
try {
socket = this.serverSocket.accept(); // 代表監聽
// 該socket 就是客戶端 , 客戶端會被請求對象發送過來,我們在本地調用成功後,又把該結果發個client
ThreadLocalUtil.set("client",socket); // 把該變量放在線程裏面
byte[] read = read(); // 調用父類裏面的讀取數據的方法,因爲服務的消費者要把請求對象發送過來
Request request = (Request)coder.deCode(read);
// 服務的提供者裏面,需要本地調用
Object result = methodInvoker.invoker(request);
write(coder.code(result)); // 向服務的消費者寫答案過去
// 一次調用結束
ThreadLocalUtil.clear(); // 調用結束,釋放線程裏面的數據
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
socket = null ;
}
}
}
}
@Override
protected OutputStream getOutputStream() {
Socket client =(Socket) ThreadLocalUtil.get("client");// 把該變量放在線程裏面
OutputStream outputStream = null;
try {
outputStream = client.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
return outputStream;
}
/**
* 獲取輸入流
* @return
*/
@Override
protected InputStream getInputStream() {
Socket client = (Socket)ThreadLocalUtil.get("client");
InputStream inputStream = null;
try {
inputStream = client.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
}
五、完成和spring的整合(重點內容)
5.1 使用ExposeService 註解標記的類,我們怎麼獲取到它
5.2.1 修改ExposeService 註解,讓Spring 來管理該對象
@Service @Controller
5.2.2 服務提供者和spring 整合的類
public class ProviderSpringStater implements ApplicationContextAware , InitializingBean {
private ApplicationContext applicationContext ; // 這就是ioc
private Register register ;
private String ipAddr ;
private int port ;
Map<String,String> regiesters = new HashMap<>();
/**
* ioc 容器裏面的對象已經創建完畢了 我們使用ExposeService 標記的類,它的對象也創建完畢了
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext ;
// 取出使用ExposeService 標記的對象
/**
* String 代表對象的名稱
* Object:代表實例對象
* 使用ExposeService標記的類,我們需要把他註冊到註冊中心上面
* 註冊時需要2 個屬性
* 1 服務的名稱 一般就是接口的名稱
* 2 服務的地址
*/
Map<String, Object> beansWithAnnotation = this.applicationContext.getBeansWithAnnotation(ExposeService.class);
if(!beansWithAnnotation.isEmpty()){
beansWithAnnotation.forEach((name,object)->{
Class<?> interfacee = object.getClass().getInterfaces()[0];
regiesters.put(interfacee.getName(),ipAddr + ":" +port) ;// 把以後要註冊到註冊中心裏面的服務都放在map 集合裏面
/**
* 爲什麼不直接註冊呢?
* 因爲現在對象的屬性好沒有注入,如果你的服務已經上線了,但是你的功能實現不了,那就是ppt的模型而已
* 那什麼時候註冊:
* 我們完事具備後,才上線,讓別人使用
*/
});
}
}
/**
* 就是當ioc 容器裏面所以的屬性都被注入完畢後執行的方法,那代表我們已經準備好了
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
if(!this.regiesters.isEmpty()){ // 不管你list 還是map ,都有foreach
this.regiesters.forEach((serviceName,serviceAddress)->{
register.register(serviceName,serviceAddress); // 完成註冊了
});
}
/**
* 服務註冊完畢後幹啥?
* 就開始要幹活了,就有開始監聽了
*/
new Thread(
()->{
new ServerSocketNet(port); // 我們ServerSocketNet 有個死循環,代碼還能往下走嗎?不能走了,那有什麼問題發生
}
).start();
}
}
老師,我們寫代碼的風格不一樣,可以,只要你掌握思想,你怎麼都行。我教你你的只是思想!
5.2 @Ref的完成屬性注入實現
@Autowired
代表屬性的注入,那怎麼注入屬性,就有利用 BeanPostProcessor
public class ConsumerProxyInject implements BeanPostProcessor {
/**
* <bean init-method=""></bean>
* 初始化方法之前執行
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; // 這裏不能返回null
}
/**
* 我們注入代理對象就在init-method 方法之後執行
* 初始化方法之後
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 只有使用@Ref 標記的屬性,我們要給他注入一個代理的對象
Class<?> clazz = bean.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Ref ref = field.getAnnotation(Ref.class);
if(ref!=null){ // 該屬性由ref 標記
field.setAccessible(true); // 暴力反射
Class<?> type = field.getType();
try {
field.set(bean, ProxyUtil.createProxy(type)); // ref 注入的是代理對象
} catch (IllegalAccessException e) {
e.printStackTrace();
}
field.setAccessible(false);// 開啓安全策略
}
}
return bean;
}
}
六、代碼的完善
6.1 本地調用獲取ioc 容器LocalMethodInvoker
6.2 遠程方法的調用裏面RpcMethodInvoker
6.3 ClientSocketNet 裏面
單例的實現:
6.4 ProxyUtil 裏面
6.5 RpcMethodInvoker
6.6 使用ThreadLocal 獲取服務名稱的地方
6.7 ProviderSpringStater的補充
6.8 ServerSocketNet的補充
6.9 LocaMethodInvoker的補充
6.10 LocalMethodInvoker 的ioc 的獲取
因爲LocalMethodInvoker 是我們自己new ,導致,setApplicationContext不執行,我們獲取不到ioc 容器。
我們在ProviderSpringStater 添加一個:
,就可以使用這裏面的了
七、開始測試
7.1 修改provider.xml 文件
7.2 啓動測試
服務提供者沒有任何問題
7.3 修改conusmer.xml
7.4 嘗試遠程調用的實現
7.5 bug的修復
爲啥長度只有288 個,已經讀取完了,爲啥還再次循環讀了
讀取的最後一個不是-1,導致又循環讀取了
我們需要給他添加一個結束的標記
我多加了一個-1 ,對我反序列化又影響嗎?
沒有影響,因爲-1 在計算機是個非常特殊的標識,我們的程序讀取到-1 ,一般任務它已經結束了,而且會把-1 去掉