Dubbo源码分析之 SPI(一)
一、概述
dubbo spi 在dubbo的作用是基础性的,要想分析研究dubbo的实现原理、dubbo源码,都绕不过 dubbo spi,掌握dubbo spi 是征服dubbo的必经之路。
本篇文章会详细介绍dubbo spi相关的内容,通过源码分析,目标是让读者能通过本篇文章,彻底征服dubbo spi。
文章的组织方式是先介绍spi 的概念,通过java spi 让大家了解spi 是什么,怎么用,有一个初步的概念,dubbo的spi是扩展了java spi的内容,自己实现了一个spi。
二、spi概念介绍
spi全称 service provider interface,是一种服务发现机制。我们编程实现一个功能时,经常先抽象一个interface,内部再定一些方法。具体的实现交给 implments了此接口的类,实现了功能和实现类的分离。
我们设想一下,如果一个描述汽车功能的interface car,存在多个实现类bmw、benz、volvo,某个场景调用car的行驶方法,程序就需要确认到底是需要bmw、benz、volvo中的那个类对象。如果硬编码到程序中,固然可以,但是出现了接口和实现类的耦合,缺点也显而易见。
有办法做到在调用代码中不涉及bmw、benz、volvo字符,也随意的指定实现类么?当然,spi就是解决这个问题。
spi的实现方式是,在指定的路径下增加一个文本文件,文件的名称是interface的全限定名(包名+接口名),文件内容每行是一个此接口的实现类的全限定名,多个实现类就会有多行。接口进行调用时,根据接口全限定名,先读取文本文件,解析出具体的实现类,通过反射进行实例化,再调用之。如果增加新的实现类,不需要修改调用代码,只需要在文本文件中增加一行实现类的全限定名即可,删除实现类同理。
三、java spi 介绍
我们先看看java的spi怎么实现的,通过一个demo,进行了解。
先定一个小车的接口,有一个方法 gobeijing():
1 package cn.hui.spi 2 //car 接口 3 public interface car { 4 5 // 开车去北京 6 void gobeijing(); 7 8 }
我们建三个实现类:
1 package cn.hui.spi.impl; 2 public class bmw implements car{ 3 @override 4 public void gobeijing() { 5 // todo auto-generated method stub 6 system.out.println("开着宝马去北京......"); 7 } 8 } 9 10 package cn.hui.spi.impl; 11 public class benz implements car{ 12 @override 13 public void gobeijing() { 14 // todo auto-generated method stub 15 system.out.println("开着奔驰去北京........"); 16 } 17 } 18 19 package cn.hui.spi.impl; 20 public class volvo implements car { 21 @override 22 public void gobeijing() { 23 // todo auto-generated method stub 24 system.out.println("开着沃尔沃去北京......"); 25 } 26 }
我们在 "meta-inf/services" 文件夹下新建一个文件,名称为“cn.hui.spi.car",文件内容:
1 cn.hui.spi.impl.bmw 2 cn.hui.spi.impl.benz 3 cn.hui.spi.impl.volvo
方法调用的代码如下:
1 import java.util.iterator; 2 import java.util.serviceloader; 3 public class app { 4 5 public static void main(string[] args) { 6 serviceloader<car> serviceloader = serviceloader.load(car.class); 7 iterator<car> iterator = serviceloader.iterator(); 8 while(iterator.hasnext()) { 9 car car = iterator.next(); 10 car.gobeijing(); 11 } 12 } 13 }
打印结果:
1 开着宝马去北京...... 2 开着奔驰去北京........ 3 开着沃尔沃去北京......
这个就是java spi简单实现方式。
三、dubbo spi介绍
dubbo 在java spi的基础上进行了功能扩展,我们再看上面的java spi示例,可以发现很明显的问题,对文本文件的加载后,实例化对象是一次性全部进行实例化,得到一个实现类的对象集合,调用的时候循环执行。不能唯一指定一个实现类进行唯一调用。dubbo通过在文本文件中指定每个实现类的key,唯一标识出每个实现类,调用的时候可以指定唯一的一个实现类。同样实例化也不需要一次性全部实例化了,只需要实例化需要调用的类即可。
同时dubbo还实现了ioc和aop的功能,接口的实现类之间可以进行相互的注入,了解spring的同学,应该很清楚ioc和aop的逻辑,下面我们现在熟悉下dubbo spi的相关概念,之后在通过一个简单的样例,了解dubbo spi 的使用。
四、dubbo spi关键点
dubbo spi的功能主要是通过extensionloader类实现,dubbo启动时,默认扫描三个目录:meta-inf/services/、meta-inf/dubbo/、meta-inf/internal/,在这三个目录下的文本文件都会加载解析,文本文件的内容:key=实现类的全限定名。
dubbo把接口定义为 扩展点,实现类定义为 扩展点实现,所有的扩展点(接口)需要进行@spi注解,更多的功能和注解我们逐步介绍。
dubbo在启动的时候扫描文本文件,对文件内容进行解析,但是不会全部进行实例化,只有在调用到具体的扩展点实现时,才会进行特定扩展点的实例化。
同时dubbo spi提供自适应扩展、默认扩展、自动激活扩展等功能,我们后面介绍。
五、dubbo spi示例
我们把上面car接口的例子,改造成基于dubbo spi的实现。进行配置的文本文件内容。
在扩展点实现类前都加上key,改为:
1 bmw=cn.hui.spi.impl.bmw 2 benz=cn.hui.spi.impl.benz 3 volvo=cn.hui.spi.impl.volvo
car接口改造为:
1 @spi 2 public interface car { 3 // 开车去北京 4 void gobeijing(); 5 }
扩展点,暂时不做修改,我们看看调用方法:
1 public class app { 2 public static void main(string[] args) { 3 car car = extensionloader.getextensionloader(car.class).getextension("bmw"); 4 car.gobeijing(); 5 car = extensionloader.getextensionloader(car.class).getextension("benz"); 6 car.gobeijing(); 7 car = extensionloader.getextensionloader(car.class).getextension("volvo"); 8 car.gobeijing(); 9 } 10 }
此时,控制台会出现:
1 开着宝马去北京...... 2 开着奔驰去北京........ 3 开着沃尔沃去北京......
这个就是简单dubbo使用,复杂的功能我们放到源码分析的时候进行。
六、dubbo spi 源码分析
dubbo spi的功能主要几种在extensionloader类中实现,分析源码也就主要分析此类,我们通过extensionloader对外提供的方法作为入口进行源码分析。
需要注意:一个type接口对应一个extensionloader 实例。
上面的示例中,我们通过 getextensionloader(..)方法,获得extensionloader实例,extensionloader类的构造方法是私有的,只能通过此方法获取实例。
我们先看看此方法:
1 @suppresswarnings("unchecked") 2 public static <t> extensionloader<t> getextensionloader(class<t> type) { 3 if (type == null) { 4 throw new illegalargumentexception("extension type == null"); 5 } 6 // 必须是接口 7 if (!type.isinterface()) { 8 throw new illegalargumentexception("extension type(" + type + ") is not interface!"); 9 } 10 // 必须被@spi注解 11 if (!withextensionannotation(type)) { 12 throw new illegalargumentexception("extension type(" + type + ") is not extension, because without @" + spi.class.getsimplename() + " annotation!"); 13 } 14 // extension_loaders 为成员变量,是 type---> extensionloader 实例的缓存 15 extensionloader<t> loader = (extensionloader<t>) extension_loaders.get(type); 16 if (loader == null) { 17 // putifabsent put不覆盖 18 extension_loaders.putifabsent(type, new extensionloader<t>(type)); 19 loader = (extensionloader<t>) extension_loaders.get(type); 20 } 21 return loader; 22 }
我们看到该方法主要是先对type进行校验,再根据type为key,从缓存extension_loaders中获取extensionloader实例,如果缓存没有,则新建一个extensionloader实例,并放入缓存。
注意,我们说过一个type对应一个extensionloader实例,为什么还需要缓存呢,我们再看看 extension_loaders的定义:
// 扩展点接口和对应extensionloader实例的缓存 private static final concurrentmap<class<?>, extensionloader<?>> extension_loaders = new concurrenthashmap<class<?>, extensionloader<?>>();
没错,extension_loaders 是一个static、final修饰的类静态变量。
我们接着看上面,看一下extensionloader的构造方法:
1 private extensionloader(class<?> type) { 2 this.type = type; 3 // type 为extensionfactory时,objectfactory为空 4 if (type == extensionfactory.class) { 5 objectfactory = null; 6 } else { 7 // type为普通接口时,objectfactory为adaptiveextensionfactory,负责dubbo spi 的ioc 功能 8 objectfactory = extensionloader.getextensionloader(extensionfactory.class).getadaptiveextension(); 9 } 10 // objectfactory = (type == extensionfactory.class ? null 11 // : extensionloader.getextensionloader(extensionfactory.class).getadaptiveextension()); 12 }
构造方法私有,不能直接在外部new出实例。
方法内部,参数type赋值给成员变量type,还会进行extensionfactory类判断,extensionfactory是实现ioc功能的,我们此处暂时绕过,后面进行介绍。
我们总结一下getextensionloader(..)方法,绕开extensionfactory,就是new 了一个extensionloader对象实例,为成员变量type赋值为扩展点type,对象实例放入extension_loaders 缓存中。
现在我们有了extensionloader实例对象,我们再看看获取type实例的方法:getextension(..):
1 @suppresswarnings("unchecked") 2 public t getextension(string name) { 3 if (name == null || name.length() == 0) 4 throw new illegalargumentexception("extension name == null"); 5 if ("true".equals(name)) { 6 // 获取默认的扩展实现类 7 return getdefaultextension(); 8 } 9 // holder仅用于持有目标对象,没有其他逻辑 10 holder<object> holder = cachedinstances.get(name); 11 if (holder == null) { 12 cachedinstances.putifabsent(name, new holder<object>()); 13 holder = cachedinstances.get(name); 14 } 15 object instance = holder.get(); 16 if (instance == null) { 17 synchronized (holder) { 18 instance = holder.get(); 19 if (instance == null) { 20 // 创建扩展实例,并设置到holder中 21 instance = createextension(name); 22 holder.set(instance); 23 } 24 } 25 } 26 return (t) instance; 27 }
方法的入参name为提供配置的文本文件中的key,还记得我们的文本文件中的内容吧,其中一行:bmw=cn.hui.spi.impl.bmw,此处的name 就是 bmw。 如果name为true,返回getdefaultextension(),这个方法我们暂时绕过。
我们看到13行,根据name从cachedinstances中获取holder对象,很明显 cachedinstances就是一个存放对象的缓存,缓存中没有new一个新的实例,至于holder,我们看下这个类:
1 // 持有目标对象 2 public class holder<t> { 3 4 private volatile t value; 5 6 public void set(t value) { 7 this.value = value; 8 } 9 10 public t get() { 11 return value; 12 } 13 14 }
只是存放对象,没有任何逻辑。
我们接着看到extensionloader类的代码,在拿到holder实例后,我们要从hodler中获取扩展点的实例:
1 object instance = holder.get(); 2 if (instance == null) { 3 synchronized (holder) { 4 instance = holder.get(); 5 if (instance == null) { 6 // 创建扩展实例,并设置到holder中 7 instance = createextension(name); 8 holder.set(instance); 9 } 10 } 11 }
如果holder中没有扩展点的实例,通过双检锁,通过调用 createextension方法 返回扩展点实例。并放入holder对象中。
到此,我们发现new扩展点实例进到 createextension方法中。
我们接着分析此方法:
1 // 创建扩展对象实例 2 @suppresswarnings("unchecked") 3 private t createextension(string name) { 4 // 从配置文件中加载所有的扩展类,形成配置项名称到配置类clazz的映射关系 5 class<?> clazz = getextensionclasses().get(name); 6 if (clazz == null) { 7 throw findexception(name); 8 } 9 try { 10 t instance = (t) extension_instances.get(clazz); 11 if (instance == null) { 12 // 通过反射创建实例 13 extension_instances.putifabsent(clazz, clazz.newinstance()); 14 instance = (t) extension_instances.get(clazz); 15 } 16 // 向实例中注入依赖,ioc实现 17 injectextension(instance); 18 // 包装处理 19 // cachedwrapperclasses 加载@spi配置时赋值,此处进行实例化 20 set<class<?>> wrapperclasses = cachedwrapperclasses; 21 if (wrapperclasses != null && !wrapperclasses.isempty()) { 22 // 循环创建wrapper实例 23 for (class<?> wrapperclass : wrapperclasses) { 24 // 将当前instance作为参数创建wrapper实例,然后向wrapper实例中注入属性值, 25 // 并将wrapper实例赋值给instance 26 instance = injectextension((t) wrapperclass.getconstructor(type).newinstance(instance)); 27 } 28 } 29 return instance; 30 } catch (throwable t) { 31 throw new illegalstateexception("extension instance(name: " + name + ", class: " + type + ") could not be instantiated: " + t.getmessage(), t); 32 } 33 }
我们看到方法开始就通过 class<?> clazz = getextensionclasses().get(name); 获取class对象,可以直观的看出通过name获得的这个clazz是在配置的文本文件中name对应的扩展点实现类的class对象,关于getextensionclasses方法,我们稍后分析,接着往下看:
1 t instance = (t) extension_instances.get(clazz); 2 if (instance == null) { 3 // 通过反射创建实例 4 extension_instances.putifabsent(clazz, clazz.newinstance()); 5 instance = (t) extension_instances.get(clazz); 6 }
通过clazz对象,从extension_instances获取缓存的实例,如果获取不到,通过反射clazz.newinstance() new一个新的实例对象,并放入extension_instances中。
我们可以看到,扩展点的实现类 必须要有一个默认无参的构造函数。
接着往下看:
1 // 向实例中注入依赖,ioc实现 2 injectextension(instance);
此方法是实现ioc功能,我们暂且绕过。
接下来,我们看到:
1 // 包装处理 2 // cachedwrapperclasses 加载@spi配置时赋值,此处进行实例化 3 set<class<?>> wrapperclasses = cachedwrapperclasses; 4 if (wrapperclasses != null && !wrapperclasses.isempty()) { 5 // 循环创建wrapper实例 6 for (class<?> wrapperclass : wrapperclasses) { 7 // 将当前instance作为参数创建wrapper实例,然后向wrapper实例中注入属性值, 8 // 并将wrapper实例赋值给instance 9 instance = injectextension((t) wrapperclass.getconstructor(type).newinstance(instance)); 10 } 11 }
此处是处理包装类的,我们也暂且绕过。下面就是直接返回扩展点的instance实例了
1 return instance;
现在我们还有一个方法没有分析,就是加载扩展点实现类的class对象的方法getextensionclasses()。我们现在来看这个方法:
1 private map<string, class<?>> getextensionclasses() { 2 map<string, class<?>> classes = cachedclasses.get(); 3 if (classes == null) { 4 synchronized (cachedclasses) { 5 classes = cachedclasses.get(); 6 if (classes == null) { 7 classes = loadextensionclasses(); 8 cachedclasses.set(classes); 9 } 10 } 11 } 12 return classes; 13 }
我们看到,这个方法返回的是一个map对象,可以确认的是,这个map存放的是扩展点的所有实现类的class,map的key就是配置的文本文件的name。如果缓存cachedclasses 中存在,即返回,如果没有,通过loadextensionclasses()加载,并设置到cachedclasses中。
我们接着看loadextensionclasses方法:
1 private map<string, class<?>> loadextensionclasses() { 2 // 获取注解 spi的接口 3 // type为传入的扩展接口,必须有@spi注解 4 final spi defaultannotation = type.getannotation(spi.class); 5 // 获取默认扩展实现value,如果存在,赋值给cacheddefaultname 6 if (defaultannotation != null) { 7 string value = defaultannotation.value(); 8 if ((value = value.trim()).length() > 0) { 9 // @spi value 只能是一个,不能为逗号分割的多个 10 // @spi value为默认的扩展实现 11 string[] names = name_separator.split(value); 12 if (names.length > 1) { 13 throw new illegalstateexception("more than 1 default extension name on extension " + type.getname() + ": " + arrays.tostring(names)); 14 } 15 if (names.length == 1) 16 cacheddefaultname = names[0]; 17 } 18 } 19 // 加载三个目录配置的扩展类 20 map<string, class<?>> extensionclasses = new hashmap<string, class<?>>(); 21 // meta-inf/dubbo/internal 22 loaddirectory(extensionclasses, dubbo_internal_directory); 23 // meta-inf/dubbo 24 loaddirectory(extensionclasses, dubbo_directory); 25 // meta-inf/services/ 26 loaddirectory(extensionclasses, services_directory); 27 return extensionclasses; 28 }
我们看到方法内部的逻辑,首先判断扩展点接口type是否用@spi注解,在前面的方法中,已经判断,如果没有@spi注解,抛出异常,此处type必定存在@spi注解。
根据注解获取到defaultannotation 对象,目的是拿到@spi中的value,且value值不能用逗号分隔,只能有一个,赋值给cacheddefaultname。
接着定一个了map对象extensionclasses,作为方法的返回值,我们知道,这个方法的返回值最后设置到了缓存cachedclasses中。我们看看这个extensionclasses是怎么赋值的。这个对象主要是”经历“了三个方法(其实是同一个方法loaddirectory,只是入参不同)。这三个方法的入参是extensionclasses 和一个目录参数,就是前面我们介绍的dubbo默认三个目录:
1 meta-inf/services/ 2 meta-inf/dubbo/ 3 meta-inf/dubbo/internal/
我们再具体看方法loaddirectory的内容:
1 private void loaddirectory(map<string, class<?>> extensionclasses, string dir) { 2 // 扩展配置文件完整文件路径+文件名 3 string filename = dir + type.getname(); 4 try { 5 enumeration<java.net.url> urls; 6 // 获取类加载器 7 classloader classloader = findclassloader(); 8 if (classloader != null) { 9 urls = classloader.getresources(filename); 10 } else { 11 urls = classloader.getsystemresources(filename); 12 } 13 if (urls != null) { 14 while (urls.hasmoreelements()) { 15 java.net.url resourceurl = urls.nextelement(); 16 // 加载 17 loadresource(extensionclasses, classloader, resourceurl); 18 } 19 } 20 } catch (throwable t) { 21 logger.error("exception when load extension class(interface: " + type + ", description file: " + filename + ").", t); 22 } 23 }
首先组合目录参数和type名称,作为文件的真实路径名,通过加载器进行加载,之后调用loadresource方法,同时extensionclasses 传入该方法。
1 private void loadresource(map<string, class<?>> extensionclasses, classloader classloader, java.net.url resourceurl) { 2 try { 3 bufferedreader reader = new bufferedreader(new inputstreamreader(resourceurl.openstream(), "utf-8")); 4 try { 5 string line; 6 while ((line = reader.readline()) != null) { 7 // 字符#是注释开始标志,只取#前面的字符 8 final int ci = line.indexof('#'); 9 if (ci >= 0) 10 line = line.substring(0, ci); 11 line = line.trim(); 12 if (line.length() > 0) { 13 try { 14 string name = null; 15 int i = line.indexof('='); 16 if (i > 0) { 17 // 解析出 name 和 实现类 18 name = line.substring(0, i).trim(); 19 line = line.substring(i + 1).trim(); 20 } 21 if (line.length() > 0) { 22 loadclass(extensionclasses, resourceurl, class.forname(line, true, classloader), name); 23 } 24 } catch (throwable t) { 25 illegalstateexception e = new illegalstateexception("failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceurl + ", cause: " + t.getmessage(), t); 26 exceptions.put(line, e); 27 } 28 } 29 } 30 } finally { 31 reader.close(); 32 } 33 } catch (throwable t) { 34 logger.error("exception when load extension class(interface: " + type + ", class file: " + resourceurl + ") in " + resourceurl, t); 35 } 36 }
这个方法就简单多了,解析文件流,拿到配置文本文件中的key、value,同时通过class.forname(..)加载解析出来的扩展点实现类,传入方法loadclass,注意这个方法传入的参数还有存放key、class的map对象extensionclasses,以及配置文本文件中的key,我们再看这个方法:
1 private void loadclass(map<string, class<?>> extensionclasses, java.net.url resourceurl, class<?> clazz, string name) throws nosuchmethodexception { 2 // type是否为clazz的超类,clazz是否实现了type接口 3 // 此处clazz 是扩展实现类的class 4 if (!type.isassignablefrom(clazz)) { 5 throw new illegalstateexception("error when load extension class(interface: " + type + ", class line: " + clazz.getname() + "), class " + clazz.getname() + "is not subtype of interface."); 6 } 7 // clazz是否注解了 adaptive 自适应扩展 8 // 不允许多个类注解adaptive 9 // 注解adaptive的实现类,赋值给cachedadaptiveclass 10 if (clazz.isannotationpresent(adaptive.class)) { 11 if (cachedadaptiveclass == null) { 12 cachedadaptiveclass = clazz; 13 // 不允许多个实现类都注解@adaptive 14 } else if (!cachedadaptiveclass.equals(clazz)) { 15 throw new illegalstateexception("more than 1 adaptive class found: " + cachedadaptiveclass.getclass().getname() + ", " + clazz.getclass().getname()); 16 } 17 // 是否为包装类,判断扩展类是否提供了参数是扩展点的构造函数 18 } else if (iswrapperclass(clazz)) { 19 set<class<?>> wrappers = cachedwrapperclasses; 20 if (wrappers == null) { 21 cachedwrapperclasses = new concurrenthashset<class<?>>(); 22 wrappers = cachedwrapperclasses; 23 } 24 wrappers.add(clazz); 25 // 普通扩展类 26 } else { 27 // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 28 clazz.getconstructor(); 29 // 此处name为 spi配置中的key 30 // @spi配置中key可以为空,此时key为扩展类的类名(getsimplename())小写 31 if (name == null || name.length() == 0) { 32 // 兼容旧版本 33 name = findannotationname(clazz); 34 if (name.length() == 0) { 35 throw new illegalstateexception("no such extension name for the class " + clazz.getname() + " in the config " + resourceurl); 36 } 37 } 38 // 逗号分割 39 string[] names = name_separator.split(name); 40 if (names != null && names.length > 0) { 41 // 获取activate注解 42 activate activate = clazz.getannotation(activate.class); 43 if (activate != null) { 44 cachedactivates.put(names[0], activate); 45 } 46 for (string n : names) { 47 if (!cachednames.containskey(clazz)) { 48 cachednames.put(clazz, n); 49 } 50 // name不能重复 51 class<?> c = extensionclasses.get(n); 52 if (c == null) { 53 extensionclasses.put(n, clazz); 54 } else if (c != clazz) { 55 throw new illegalstateexception("duplicate extension " + type.getname() + " name " + n + " on " + c.getname() + " and " + clazz.getname()); 56 } 57 } 58 } 59 } 60 }
方法参数clazz就是传过来的扩展点实现类的class对象,首先判断是否实现了扩展点type接口。接着判断是否注解了@adaptive以及是否为包装类iswrapperclass(clazz),这两个分支逻辑 我们暂且绕过,接下来会进行构造器检查,判断是否存在无参构造器,如果name为空,为了兼容老版本 会进行一次name赋值。
此处会再进行一次name的分隔,前门已经知道,name中不会存在逗号的,但经过上面兼容老版本的重新赋值,会再进行一次判断。@activate注解的判断,我们也暂且绕过。
循环解析过的name字符串,把加载的扩展点实现class对象和name存放到入参extensionclasses中。
至此,解析、加载配置文本文件的逻辑已经结束。最后的结果主要是有:把加载到的扩展点class和key存入到缓存对象extensionclasses中,同时设置cacheddefaultname为扩展点注解@spi中的value。
我们重新回到方法createextension中,现在我们已经拿到了特定name对应的扩展点实现类的class对象,如果对象为空,抛出异常。
接着,我们从缓存对象extension_instances中,通过class对象获取实例,如果实例为空,通过clazz.newinstance()创建,并放入extension_instances中。
createextension方法的后面的逻辑:
1 // 向实例中注入依赖,ioc实现 2 injectextension(instance); 3 // 包装处理 4 // cachedwrapperclasses 加载@spi配置时赋值,此处进行实例化 5 set<class<?>> wrapperclasses = cachedwrapperclasses; 6 if (wrapperclasses != null && !wrapperclasses.isempty()) { 7 // 循环创建wrapper实例 8 for (class<?> wrapperclass : wrapperclasses) { 9 // 将当前instance作为参数创建wrapper实例,然后向wrapper实例中注入属性值, 10 // 并将wrapper实例赋值给instance 11 instance = injectextension((t) wrapperclass.getconstructor(type).newinstance(instance)); 12 } 13 }
是拿到扩展点的实例之后,后期的处理,包括对ioc的实现,包装类的处理等功能逻辑,这些知识点,我们稍后进行分析。
七、总结
总结一下,本篇文章,我们分析了dubbo spi的主流程,从入门介绍、示例描述到源码分析,主流程基本介绍完了,中间涉及到的@adaptive、@activate注解,以及包装类、扩展点实现类的ioc功能等知识点,我们都暂且绕过了,后面我们会在下一篇文章中逐一介绍。
上一篇: 中植系万亿金融集团何去何从(谁能掌舵)
下一篇: java多线程实现奇数和偶数的交叉打印