闖關經驗:
袋鼠走過了第一關,順利搭建出了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
// URL對象是整個框架的核心對象,它保存了一系列配置,分為注冊URL和服務URL,注冊URL是指到Registry服務的地址,服務URL則是具體使用的服務串 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后,需要去掉該參數 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
方法中主要做了兩件事,前60行解析生成的配置實體類轉換成URL類,60行以后的代碼,代理給ConfigHandler,並生成Exporter對象。
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));
繼續加油哦!