深入学习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
// 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));
继续加油哦!