1. 程式人生 > >深入學習Motan系列(二)——服務釋出

深入學習Motan系列(二)——服務釋出

闖關經驗:

袋鼠走過了第一關,順利搭建出了Demo,信心爆棚。不過之後,心想怎麼去研究這個框架呢。查了一下,官方文件,好像沒什麼東西可以研究啊。後來,又搜了搜部落格,因為這是微博的框架嘛,所以搜尋時用百度進行搜尋。後來發現,原始碼工程motan-demo-server中的MotanApiExportDemo類,它用程式碼的形式完整了表述了服務端啟動的過程,這不正是思路嗎。袋鼠,找到了方向,摸了摸下巴,點了點頭,開幹。

 

1 服務釋出

不多說廢話,先上demo

 1 package com.weibo.motan.demo.server;
 2 
 3 import com.weibo.api.motan.common.MotanConstants;
4 import com.weibo.api.motan.config.ProtocolConfig; 5 import com.weibo.api.motan.config.RegistryConfig; 6 import com.weibo.api.motan.config.ServiceConfig; 7 import com.weibo.api.motan.util.MotanSwitcherUtil; 8 import com.weibo.motan.demo.service.MotanDemoService; 9 10 public class MotanApiExportDemo {
11 12 public static void main(String[] args) throws InterruptedException { 13 ServiceConfig<MotanDemoService> motanDemoService = new ServiceConfig<MotanDemoService>(); 14 15 // 設定介面及實現類 16 motanDemoService.setInterface(MotanDemoService.class); 17 motanDemoService.setRef(new
MotanDemoServiceImpl()); 18 19 // 配置服務的group以及版本號 20 motanDemoService.setGroup("motan-demo-rpc"); 21 motanDemoService.setVersion("1.0"); 22 23 // 配置註冊中心直連呼叫 24 RegistryConfig registry = new RegistryConfig(); 25 26 //use local registry 27 //registry.setRegProtocol("local"); 28 29 // use ZooKeeper registry 30 registry.setRegProtocol("zookeeper"); 31 registry.setAddress("127.0.0.1:2181"); 32 33 // registry.setCheck("false"); //是否檢查是否註冊成功 34 motanDemoService.setRegistry(registry); 35 36 // 配置RPC協議 37 ProtocolConfig protocol = new ProtocolConfig(); 38 protocol.setId("motan"); 39 protocol.setName("motan"); 40 motanDemoService.setProtocol(protocol); 41 42 motanDemoService.setExport("motan:8002");
// 服務的釋出及註冊的核心 這裡是重點 接下來,深入分析
43 motanDemoService.export(); 44 45 MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true); 46 47 System.out.println("server start..."); 48 } 49 50 }

 

 motanDemoService.export()

方法中整體的邏輯很清晰明瞭,我已經用紅字給出解釋了。先對這個方法有個整體認識,然後再逐步分析。

 1 public synchronized void export() {
       // 判斷介面是否釋出,如果已經發布,出log,並結束此方法
2 if (exported.get()) { 3 LoggerUtil.warn(String.format("%s has already been expoted, so ignore the export request!", interfaceClass.getName())); 4 return; 5 } 6      // 對介面和方法進行檢查 7 (interfaceClass, methods);
8      // 獲取註冊的URL地址,之後進行check 9 List<URL> registryUrls = loadRegistryUrls(); 10 if (registryUrls == null || registryUrls.size() == 0) { 11 throw new IllegalStateException("Should set registry config for service:" + interfaceClass.getName()); 12 } 13
       // 獲取協議和埠號,之後進行釋出 14 Map<String, Integer> protocolPorts = getProtocolAndPort(); 15 for (ProtocolConfig protocolConfig : protocols) { 16 Integer port = protocolPorts.get(protocolConfig.getId()); 17 if (port == null) { 18 throw new MotanServiceException(String.format("Unknow port in service:%s, protocol:%s", interfaceClass.getName(), 19 protocolConfig.getId())); 20 }
         // 實際的釋出和註冊的操作都是方法doExport裡。類似doXXX,方法裡都是關於XXX的核心的操作,這樣的編碼習慣在Spring框架原始碼中大量出現
// 同時也是很值得大家借鑑的
21 doExport(protocolConfig, port, registryUrls); 22 } 23
       // 服務釋出後的後續處理 24 afterExport(); 25 }

 分析:

①第二行,exported的宣告如下,private AtomicBoolean exported = new AtomicBoolean(false);這是使用了JDK提供的原子類,保證變數的原子性

②介面的檢查

 1 protected void checkInterfaceAndMethods(Class<?> interfaceClass, List<MethodConfig> methods) {
 2         if (interfaceClass == null) {
 3             throw new IllegalStateException("interface not allow null!");
 4         }
 5         if (!interfaceClass.isInterface()) {
 6             throw new IllegalStateException("The interface class " + interfaceClass + " is not a interface!");
 7         }
 8         // 檢查方法是否在介面中存在
// methods值為null(沒有設定),所以不會進入下面方法裡
9 if (methods != null && !methods.isEmpty()) { 10 for (MethodConfig methodBean : methods) { 11 String methodName = methodBean.getName(); 12 if (methodName == null || methodName.length() == 0) { 13 throw new IllegalStateException("<motan:method> name attribute is required! Please check: <motan:service interface=\"" 14 + interfaceClass.getName() + "\" ... ><motan:method name=\"\" ... /></<motan:referer>"); 15 } 16 java.lang.reflect.Method hasMethod = null; 17 for (java.lang.reflect.Method method : interfaceClass.getMethods()) { 18 if (method.getName().equals(methodName)) { 19 if (methodBean.getArgumentTypes() != null 20 && ReflectUtil.getMethodParamDesc(method).equals(methodBean.getArgumentTypes())) { 21 hasMethod = method; 22 break; 23 } 24 if (methodBean.getArgumentTypes() != null) { 25 continue; 26 } 27 if (hasMethod != null) { 28 throw new MotanFrameworkException("The interface " + interfaceClass.getName() + " has more than one method " 29 + methodName + " , must set argumentTypes attribute.", MotanErrorMsgConstant.FRAMEWORK_INIT_ERROR); 30 } 31 hasMethod = method; 32 } 33 } 34 if (hasMethod == null) { 35 throw new MotanFrameworkException("The interface " + interfaceClass.getName() + " not found method " + methodName, 36 MotanErrorMsgConstant.FRAMEWORK_INIT_ERROR); 37 } 38 methodBean.setArgumentTypes(ReflectUtil.getMethodParamDesc(hasMethod)); 39 } 40 } 41 }

③取得註冊的URL地址

內容很簡單,原始碼的註釋已經很簡明瞭。

 1 protected List<URL> loadRegistryUrls() {
 2         List<URL> registryList = new ArrayList<URL>();
       // 這裡的registries是在demo中registry.setRegProtocol("zookeeper"); registry.setAddress("127.0.0.1:2181");進行設定的
3 if (registries != null && !registries.isEmpty()) { 4 for (RegistryConfig config : registries) { 5 String address = config.getAddress(); 6 if (StringUtils.isBlank(address)) { 7 address = NetUtils.LOCALHOST + ":" + MotanConstants.DEFAULT_INT_VALUE; 8 } 9 Map<String, String> map = new HashMap<String, String>(); 10 config.appendConfigParams(map); 11 12 map.put(URLParamType.application.getName(), getApplication()); 13 map.put(URLParamType.path.getName(), RegistryService.class.getName()); 14 map.put(URLParamType.refreshTimestamp.getName(), String.valueOf(System.currentTimeMillis())); 15 16 // 設定預設的registry protocol,parse完protocol後,需要去掉該引數
// 上面說的parse完protocol後,再去掉protoclo引數,這裡的目的,一時沒看明白。有明白的的前輩望賜教
17 if (!map.containsKey(URLParamType.protocol.getName())) { 18 if (address.contains("://")) { 19 map.put(URLParamType.protocol.getName(), address.substring(0, address.indexOf("://"))); 20 } else { 21 map.put(URLParamType.protocol.getName(), MotanConstants.REGISTRY_PROTOCOL_LOCAL); 22 } 23 } 24 // address內部可能包含多個註冊中心地址 25 List<URL> urls = UrlUtils.parseURLs(address, map); 26 if (urls != null && !urls.isEmpty()) { 27 for (URL url : urls) { 28 url.removeParameter(URLParamType.protocol.getName()); 29 registryList.add(url); 30 } 31 } 32 } 33 } 34 return registryList; 35 }

④服務釋出和註冊的核心操作 doExport

 1 private void doExport(ProtocolConfig protocolConfig, int port, List<URL> registryURLs) {
       // 首先是,收集服務的協議的各種資訊 host 埠 procotol
// 吐槽一下,方法的前部分,這些應該算是準備工作,這些準備工作不應該放在doExport方法裡。
2 String protocolName = protocolConfig.getName(); 3 if (protocolName == null || protocolName.length() == 0) { 4 protocolName = URLParamType.protocol.getValue(); 5 } 6 7 String hostAddress = host; 8 if (StringUtils.isBlank(hostAddress) && basicService != null) { 9 hostAddress = basicService.getHost(); 10 } 11 if (NetUtils.isInvalidLocalHost(hostAddress)) { 12 hostAddress = getLocalHostAddress(registryURLs); 13 } 14 15 Map<String, String> map = new HashMap<String, String>(); 16 17 map.put(URLParamType.nodeType.getName(), MotanConstants.NODE_TYPE_SERVICE); 18 map.put(URLParamType.refreshTimestamp.getName(), String.valueOf(System.currentTimeMillis())); 19 20 collectConfigParams(map, protocolConfig, basicService, extConfig, this); 21 collectMethodConfigParams(map, this.getMethods()); 22
       // 生成URL地址,之後check是否已經發布該地址的服務 23 URL serviceUrl = new URL(protocolName, hostAddress, port, interfaceClass.getName(), map); 24 25 if (serviceExists(serviceUrl)) { 26 LoggerUtil.warn(String.format("%s configService is malformed, for same service (%s) already exists ", interfaceClass.getName(), 27 serviceUrl.getIdentity())); 28 throw new MotanFrameworkException(String.format("%s configService is malformed, for same service (%s) already exists ", 29 interfaceClass.getName(), serviceUrl.getIdentity()), MotanErrorMsgConstant.FRAMEWORK_INIT_ERROR); 30 } 31 32 List<URL> urls = new ArrayList<URL>(); 33 34 // injvm 協議只支援註冊到本地,其他協議可以註冊到local、remote 35 if (MotanConstants.PROTOCOL_INJVM.equals(protocolConfig.getId())) { 36 URL localRegistryUrl = null; 37 for (URL ru : registryURLs) { 38 if (MotanConstants.REGISTRY_PROTOCOL_LOCAL.equals(ru.getProtocol())) { 39 localRegistryUrl = ru.createCopy(); 40 break; 41 } 42 } 43 if (localRegistryUrl == null) { 44 localRegistryUrl = 45 new URL(MotanConstants.REGISTRY_PROTOCOL_LOCAL, hostAddress, MotanConstants.DEFAULT_INT_VALUE, 46 RegistryService.class.getName()); 47 } 48 49 urls.add(localRegistryUrl); 50 } else {
         // 我們用的是Motan協議 所以會進到else分支
51 for (URL ru : registryURLs) { 52 urls.add(ru.createCopy()); 53 } 54 } 55 56 for (URL u : urls) { 57 u.addParameter(URLParamType.embed.getName(), StringTools.urlEncode(serviceUrl.toFullStr())); 58 registereUrls.add(u.createCopy()); 59 }
// 到這裡為止,算是各種準備工作就緒了,接下來是SPI的部分。我們休息五分鐘。袋鼠吃個茶蛋去,回來接著幹。
60 61 ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE); 62 63 exporters.add(configHandler.export(interfaceClass, ref, urls)); 64 }

 大家到這裡可能累了,建議休息一會,稍後打起精神學習下面的東西。

 

SPI

JDK的SPI

關於SPI,我先說JDK的SPI,然後在分析Motan的SPI。

JDK的SPI:https://my.oschina.net/11450232/blog/700146    

可以先閱讀文章的demo部分,後面的內容先不讀,接下來,看下面原理分析的文章

SPI原理分析:https://blog.csdn.net/a910626/article/details/78811273   

看這篇文章的時候,建議自己第一遍看完後,瞭解大概,之後自己動手debug進行跟蹤實踐。就用上面demo的程式碼進行除錯就可以。注意,在方法hasNext中,pending = parse(service, configs.nextElement());這裡是獲取檔案一行內容;在方法next中,c = Class.forName(cn, false, loader)利用發射,生成實現介面的類。這裡的兩個地方需要打斷點自己實踐。

我也尚有疑問,configs = loader.getResources(fullName);這裡的返回值不是很明白,希望前輩們賜教。

上面的內容消化掉,理解了JDK的SPI的使用和原理後,可以看下面的文章,裡面的有個關於jdbc的例項分析,自己也動手實踐了,真的很棒!!!

------------------------------------------------------------------------------------------------------------------------------

http://www.myexception.org/program/1355384.html

------------------------------------------------------------------------------------------------------------------------------

 下面呢,留下一段程式碼,大家自己驗證跑一下,驗證自己的預期值是否正確。(我認為上面的文章寫的足夠詳細了,自己一定要動手debug跑跑看,再又不懂的可以留言,如果到這裡,有不懂的地方,一定要停下來,好好消化弄懂,不要往下看,內功不夠,後面就該走火入魔了!!!)

 1 import java.sql.Driver;
 2 import java.sql.DriverManager;
 3 import java.util.Enumeration;
 4 
 5 public class JDBC_SPI {
 6     public static void main(String[] args) {
 7         Enumeration<Driver> drivers = DriverManager.getDrivers();
 8         Driver driver;
 9         while (drivers.hasMoreElements()) {
10             driver = drivers.nextElement();
11             System.out.println(driver.getClass());
12         }
13     }
14 }

 

JDK的SPI不足之處:
1.ServiceLoader使用延遲載入,但是隻能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。如果你並不想用某些實現類,它也被載入並例項化了,這就造成了浪費;
2.獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個引數來獲取對應的實現類;
3.不是單例。
那麼,我們接下來繼續研究Motan的SPI,看看他有哪些高明之處。

 

Motan的SPI

分析原始碼前,先看Motan的兩個註解
Spi:在Motan中如果要使用SPI機制,則暴露出來的介面要使用@Spi註解標註,並且可以指定該介面的的實現者在建立物件時是用單例還是多例建立。
SpiMeta:這個註解是載入實現類上的,用來標註該實現類的SPI名稱,後續可以通過該名稱來獲取一個服務。(一個介面會有很多實現類,可以標註每個實現類自己的名稱)


提示:
java的SPI只能通過型別獲取實現者,最後要根據型別來確定使用哪個實現類來處理業務;Motan通過SpiMeta註解增加類實現類的名稱,所以可以根據名稱來獲取,能更好的解耦。

 

到了,到這裡,具備了對SPI的認識後,我們返回到ServiceConfig的doExport()方法,繼續分析。

前面部分程式碼省略。。。

1
ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE); 2 3 exporters.add(configHandler.export(interfaceClass, ref, urls));
getExtensionLoader的引數是介面類,在getExtensionLoader方法中,對class引數進行check,然後對ExtensionLoader初始化
 1 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
 2         checkInterfaceType(type);
 3 
       // extensionLoaders的定義,是一個ConcurrentMap。我認為這個map,做快取用。 4 ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoaders.get(type); 5 6 if (loader == null) { 7 loader = initExtensionLoader(type); 8 } 9 return loader; 10 }

接下來,我們看loader是如何進行初始化的。

注意,因為是多執行緒操作它,所以方法前的synchronized 關鍵字很重要。此外,initExtensionLoader方法內部和外部,分別執行了一次loader為null的判斷。這裡是進行單例雙重檢測???如果這樣的話,上面的方法中,在loader的定義前面,應該加一個volatile關鍵字。這個地方有疑問,不知道大傢什麼看法????

(關於單例雙重檢測,附上一篇文章,寫的不錯:https://www.jianshu.com/p/45885e50d1c4

 1 public static synchronized <T> ExtensionLoader<T> initExtensionLoader(Class<T> type) {
 2         ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoaders.get(type);
 3 
 4         if (loader == null) {
 5             loader = new ExtensionLoader<T>(type);
 6 
 7             extensionLoaders.putIfAbsent(type, loader);
 8 
 9             loader = (ExtensionLoader<T>) extensionLoaders.get(type);
10         }
11 
12         return loader;
13     }

接下來,我們再分析getExtension(MotanConstants.DEFAULT_VALUE)的操作。

 1 public T getExtension(String name) {
       // 第一步,是初始化的check。具體裡面check什麼東西,稍後深入分析
2 checkInit(); 3 4 if (name == null) { 5 return null; 6 } 7 8 try { 9 Spi spi = type.getAnnotation(Spi.class); 10
         // 第二步,判斷是否生成單例的例項,然後建立例項 11 if (spi.scope() == Scope.SINGLETON) { 12 return getSingletonInstance(name); 13 } else { 14 Class<T> clz = extensionClasses.get(name); 15 16 if (clz == null) { 17 return null; 18 } 19 20 return clz.newInstance(); 21 } 22 } catch (Exception e) { 23 failThrows(type, "Error when getExtension " + name, e); 24 } 25 26 return null; 27 }
1 private void checkInit() {
2         if (!init) {
3             loadExtensionClasses();
4         }
5     }
 1 private synchronized void loadExtensionClasses() {
 2         if (init) {
 3             return;
 4         }
 5 
 6         extensionClasses = loadExtensionClasses(PREFIX);
 7         singletonInstances = new ConcurrentHashMap<String, T>();
 8 
 9         init = true;
10     }

接下來進入loadExtensionClasses方法

首先是獲取全名(META-INF/services/com.weibo.api.motan.config.handler.ConfigHandler),然後進入第10行,獲取url(file:/C:/Users/46617/git/motan/motan-core/target/classes/META-INF/services/com.weibo.api.motan.config.handler.ConfigHandler),接下來,第20行,解析這個地址,獲取裡面的內容(介面的實現類),最後,第27行,匯入類。

 1 private ConcurrentMap<String, Class<T>> loadExtensionClasses(String prefix) {
 2         String fullName = prefix + type.getName();
 3         List<String> classNames = new ArrayList<String>();
 4 
 5         try {
 6             Enumeration<URL> urls;
 7             if (classLoader == null) {
 8                 urls = ClassLoader.getSystemResources(fullName);
 9             } else {
10                 urls = classLoader.getResources(fullName);
11             }
12 
13             if (urls == null || !urls.hasMoreElements()) {
14                 return new ConcurrentHashMap<String, Class<T>>();
15             }
16 
17             while (urls.hasMoreElements()) {
18                 URL url = urls.nextElement();
19 
20                 parseUrl(type, url, classNames);
21             }
22         } catch (Exception e) {
23             throw new MotanFrameworkException(
24                     "ExtensionLoader loadExtensionClasses error, prefix: " + prefix + " type: " + type.getClass(), e);
25         }
26 
27         return loadClass(classNames);
28     }

 

 1 private ConcurrentMap<String, Class<T>> loadClass(List<String> classNames) {
 2         ConcurrentMap<String, Class<T>> map = new ConcurrentHashMap<String, Class<T>>();
 3 
 4         for (String className : classNames) {
 5             try {
 6                 Class<T> clz;
 7                 if (classLoader == null) {
 8                     clz = (Class<T>) Class.forName(className);
 9                 } else {
10                     clz = (Class<T>) Class.forName(className, true, classLoader);
11                 }
12 
// 檢查下面三項內容
           // 1) is public class
          // 2) contain public constructor and has not-args constructor
           // 3) check extension clz instanceof Type.class 13 checkExtensionType(clz); 14
           // 獲取到Spi的名稱,default 15 String spiName = getSpiName(clz); 16 17 if (map.containsKey(spiName)) { 18 failThrows(clz, ":Error spiName already exist " + spiName); 19 } else { 20 map.put(spiName, clz); 21 } 22 } catch (Exception e) { 23 failLog(type, "Error load spi class", e); 24 } 25 } 26 27 return map; 28 29 }

到這為止,只是將實現介面的具體類,進行匯入,仍然沒有對其例項化。彆著急,接著往下看。

回到getExtension方法

 1  public T getExtension(String name) {
// 剛才所做的所有操作都是這裡完成的,就做了一件事,匯入擴充套件類(匯入介面的實現類)
2 checkInit(); 。。。 8 try { 9 Spi spi = type.getAnnotation(Spi.class); 10 11 if (spi.scope() == Scope.SINGLETON) { 12 return getSingletonInstance(name); 13 } else { 。。。 20 return clz.newInstance(); 21 } 。。。 26 return null; 27 }

進入獲取單例例項getSingletomInstance方法

 1 private T getSingletonInstance(String name) throws InstantiationException, IllegalAccessException {
 2         T obj = singletonInstances.get(name);
 3 
 4         if (obj != null) {
 5             return obj;
 6         }
 7 
 8         Class<T> clz = extensionClasses.get(name);
 9 
10         if (clz == null) {
11             return null;
12         }
13 
14         synchronized (singletonInstances) {
15             obj = singletonInstances.get(name);
16             if (obj != null) {
17                 return obj;
18             }
19         
// 在這裡對剛才去到的實現類,進行例項化 20 obj = clz.newInstance();
// 快取操作,將例項化的物件放入map中
21 singletonInstances.put(name, obj); 22 } 23
       // 終於返回生成的例項化物件了,Motan的SPI部分算是OK了。還是比較簡單的。第一次看,知識量感覺可能有些大,再捋順一遍,就發現很清晰明瞭。
// 以前自己看Sping原始碼,哭的心都有了,可惜中途放棄了,現在看這種原始碼,發現很簡單。以後繼續專研Sping原始碼,收益多多。跑題了!!!
// 能看到這裡的,絕對是老鐵了。
24 return obj; 25 }

現在,終於又回到doExport方法中了,彆著急,累了的話,喝杯水。我們下一篇,分析export方法。(考慮到篇幅過長,所以進行分割了)

1 ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE);
2 
3         exporters.add(configHandler.export(interfaceClass, ref, urls));

 繼續加油哦!