注:本系列源码分析基于springboot 2.2.2.RELEASE,对应的spring版本为5.2.2.RELEASE,源码的gitee仓库仓库地址:funcy/spring-boot.
在springboot 自动装配之条件注解(一)一文中,在分析@ConditionalOnBean/@ConditionalOnMissingBean 注解的条件判断时,官方强烈建议我们在自动装配类中使用这两个注解,并且@ConditionalOnBean/@ConditionalOnMissingBean标记的类要在指定的类之后初始化,那springboot如何来控制自动装配顺序呢?本文将来研究下。
1. springboot 处理自动装配类的过程
需要明确的是,本文探讨的自动装配顺序是指将class注册到beanFactory的顺序,springboot 处理自动装配类的大致过程如下:
- 加载自动装配类,在springboot 自动装配之加载自动装配类 一文中已分析过;
- 对自动装配类进行排序,这是本文将要分析的内容;
- 遍历自动装配类,对每个自动装配类逐一进行以下操作:
- 根据条件注解判断当前自动装配类是否满足装配条件;
- 如果当前自动装配类满嘴装配条件,注册到
beanFactory中。
再回到@ConditionalOnBean/@ConditionalOnMissingBean,对如下两个自动装配类:
// A是自动装配类
@Configuration
public class A {
@Bean
@ConditionalOnMissingBean("b1")
public A1 a1() {
return new A1();
}
}
// B是自动装配类
@Configuration
public class B {
@Bean
public B1 b1() {
return new b1();
}
}
a1与b1在两个不同的自动装配类中初始化,且a1只有在b1不存在时,才会初始化,根据上面总结的 springboot 处理自动装配类的步骤,我们只要指定b1在a1之前初始化就不会产生异常了。
那么,自动装配类的顺序如何指定呢?
2. 自动装配类的顺序控制注解
springboot 为我们提供了两种自动装配类的排序手段:
- 绝对自动装配顺序——
@AutoConfigOrder - 相对自动装配顺序——
@AutoConfigureBefore与@AutoConfigureAfter
这三个注解就是用来处理自动装配类的排序的了,@AutoConfigOrder 指定了装配顺序,同 spring 提供的@Order类似,@AutoConfigureBefore 与 @AutoConfigureAfter可以指定class,表示在哪个class之前或之后装配。
回到示例,我们可以这样指定装配顺序:
// A是自动装配类
@Configuration
// 在B.class之后自动装配
@AutoConfigureAfter(B.class)
public class A {
@Bean
@ConditionalOnMissingBean("b1")
public A1 a1() {
...
}
}
// B是自动装配类
@Configuration
public class B {
...
}
3. 自动装配类的排序
前面我们提到,@AutoConfigOrder、@AutoConfigureBefore 与 @AutoConfigureAfter可以控制自动装配类的装配顺序,那么它们是在哪里进行排序的呢?在 springboot 自动装配之加载自动装配类 一文中,我们提总结了获取自动装配类的步骤有6步:
- 调用
AutoConfigurationImportSelector#getAutoConfigurationEntry(...)方法加载自动装配类; - 将得到的自动装配类保存到
autoConfigurationEntries中; - 得到过滤类,这些过滤类就是由
@EnableAutoConfiguration的exclude或excludeName指定的; - 将
autoConfigurationEntries转换为LinkedHashSet,结果为processedConfigurations; - 去除
processedConfigurations需要过滤的类; - 将第5步得到的类排序后,返回。
而对自动装配类的排序正是在第6步,对应的方法是AutoConfigurationImportSelector.AutoConfigurationGroup#sortAutoConfigurations,代码如下:
private List<String> sortAutoConfigurations(Set<String> configurations,
AutoConfigurationMetadata autoConfigurationMetadata) {
// 先创建了 AutoConfigurationSorter 对象,
// 然后调用 AutoConfigurationSorter.getInPriorityOrder 进行排序
return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata)
.getInPriorityOrder(configurations);
}
这个方法的处理分为两步:
- 创建了
AutoConfigurationSorter对象 - 调用
AutoConfigurationSorter.getInPriorityOrder进行排序
我们先来看看AutoConfigurationSorter的创建操作:
class AutoConfigurationSorter {
private final MetadataReaderFactory metadataReaderFactory;
private final AutoConfigurationMetadata autoConfigurationMetadata;
/**
* 构造方法
* 仅仅只是对传入的参数进行赋值,将他们赋值为成员变量
*/
AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory,
AutoConfigurationMetadata autoConfigurationMetadata) {
Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null");
this.metadataReaderFactory = metadataReaderFactory;
this.autoConfigurationMetadata = autoConfigurationMetadata;
}
...
}
可以看到,AutoConfigurationSorter的构造方法并没有做什么实质性的操作,看来排序的关键还得看AutoConfigurationSorter.getInPriorityOrder 方法,该方法的代码如下:
List<String> getInPriorityOrder(Collection<String> classNames) {
// 1. 将 classNames 包装成 AutoConfigurationClasses
AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory,
this.autoConfigurationMetadata, classNames);
List<String> orderedClassNames = new ArrayList<>(classNames);
// 2. 按类名排序
Collections.sort(orderedClassNames);
// 3. 使用 @AutoConfigureOrder 排序
orderedClassNames.sort((o1, o2) -> {
int i1 = classes.get(o1).getOrder();
int i2 = classes.get(o2).getOrder();
return Integer.compare(i1, i2);
});
// 4. 使用 @AutoConfigureBefore,@AutoConfigureAfter 排序
orderedClassNames = sortByAnnotation(classes, orderedClassNames);
return orderedClassNames;
}
从代码来 看,这个方法的执行步骤如下:
- 将
classNames包装成AutoConfigurationClasses - 按类名排序
- 使用
@AutoConfigureOrder排序 - 使用
@AutoConfigureBefore,@AutoConfigureAfter排序
这个方法排序共进行了3次,都是对orderedClassNames进行排序,这样一来,后面的排序会打乱前面的排序,最先的排序是按类名排序,也就是说,如果没有指定@AutoConfigureOrder、@AutoConfigureBefore等注解,就会使用类名进行排序。
接下来我们具体分析这几个操作吧。
4. 将 classNames 包装成 AutoConfigurationClasses
该操作位于AutoConfigurationSorter.AutoConfigurationClasses#AutoConfigurationClasses方法,代码如下:
private static class AutoConfigurationClasses {
// 保存结果
private final Map<String, AutoConfigurationClass> classes = new HashMap<>();
/**
* 构造方法
*/
AutoConfigurationClasses(MetadataReaderFactory metadataReaderFactory,
AutoConfigurationMetadata autoConfigurationMetadata, Collection<String> classNames) {
// 进行方法调用
addToClasses(metadataReaderFactory, autoConfigurationMetadata, classNames, true);
}
/**
* 添加类,就是将类包装成 AutoConfigurationClass,添加到名为 classes 的 Map 中
* classNames 就是去除了排除类的所有自动装配类
*/
private void addToClasses(MetadataReaderFactory metadataReaderFactory,
AutoConfigurationMetadata autoConfigurationMetadata, Collection<String> classNames,
boolean required) {
for (String className : classNames) {
if (!this.classes.containsKey(className)) {
// 将 className 包装成 AutoConfigurationClass
AutoConfigurationClass autoConfigurationClass = new AutoConfigurationClass(
className, metadataReaderFactory, autoConfigurationMetadata);
boolean available = autoConfigurationClass.isAvailable();
// @AutoConfigureBefore 与 @AutoConfigureAfter 标记的类的 required 为 false
if (required || available) {
this.classes.put(className, autoConfigurationClass);
}
if (available) {
// 递归调用
addToClasses(metadataReaderFactory, autoConfigurationMetadata,
autoConfigurationClass.getBefore(), false);
addToClasses(metadataReaderFactory, autoConfigurationMetadata,
autoConfigurationClass.getAfter(), false);
}
}
}
}
...
}
从以上代码来看,
AutoConfigurationClasses包含一个成员变量:classes,类型是Map,key是String(也就是className),value是AutoConfigurationClass(也就是className的包含类);AutoConfigurationClasses的构造方法会调用addToClasses(...)该方法会遍历传入的classNames,将其包装成AutoConfigurationClass后,再保存到classes中。
在分析addToClasses(...)的具体逻辑前,我们先来看看AutoConfigurationClass是个啥:
可以看到 ,AutoConfigurationClass 是类名的包装,并且还保存了@AutoConfigureBefore与@AutoConfigureAfter指定的类,以及提供了跟@AutoConfigureOrder、 @AutoConfigureBefore、@AutoConfigureAfter 相关的一些方法。
我们再回过头看addToClasses(...)的执行流程,该方法的执行流程如下:
- 遍历传入的
classNames,对其中每一个className,进行下面的操作; - 创建
AutoConfigurationClass,传入className; - 调用
AutoConfigurationSorter.AutoConfigurationClass#isAvailable方法,得到available; - 判断
available与required的值,如果其一为ture,就将其添加到classes; - 如果
available为true,递归处理className由@AutoConfigureBefore与@AutoConfigureAfter指定的类。
流程看着不复杂,不过有几个就去需要分析下 :
AutoConfigurationSorter.AutoConfigurationClass#isAvailable:判断当前class是否存在AutoConfigurationSorter.AutoConfigurationClass#getBefore:获取class:当前class需要在这些class之前处理AutoConfigurationSorter.AutoConfigurationClass#getAfter:获取class:当前class需要在这些class之后处理
接下来我们一一来分析下这几个方法。
4.1 AutoConfigurationSorter.AutoConfigurationClass#isAvailable
这个方法是用来判断当前class是否在当前项目的classpath中,看代码:
boolean isAvailable() {
try {
if (!wasProcessed()) {
getAnnotationMetadata();
}
return true;
}
catch (Exception ex) {
return false;
}
}
这个方法代码不多,先是调用wasProcessed()方法,再调用getAnnotationMetadata(),需要注意的是,getAnnotationMetadata() 可能会抛出异常,招聘异常也会返回false.
我们继续跟进 AutoConfigurationSorter.AutoConfigurationClass#wasProcessed 方法:
private boolean wasProcessed() {
return (this.autoConfigurationMetadata != null
// 判断 META-INF/spring-autoconfigure-metadata.properties 文件中是否存在该配置
&& this.autoConfigurationMetadata.wasProcessed(this.className));
}
这个方法里主要调用了AutoConfigurationMetadataLoader.PropertiesAutoConfigurationMetadata#wasProcessed方法来判断:
@Override
public boolean wasProcessed(String className) {
// 判断 properties 是否存在对应的 className
return this.properties.containsKey(className);
}
可以看到,这个方法仅是判断properties里是否包含传入的className,properties 的内容来自于META-INF/spring-autoconfigure-metadata.properties,内容示例如下:
需要注意的是,该文件在源码中是不存在的,它是在编译时写入的,关于该文件的写入、加载到properties的流程,本文就不展开分析了,这里提供个大概思路:
-
文件的写入:在代码编译时,springboot 会将自动装配类的一些信息(例如,
@ConditionalOnClass指定的class,@ConditionalOnBean指定的bean,@AutoConfigureBefore与@AutoConfigureAfter的指定的class等) 写入到META-INF/spring-autoconfigure-metadata.properties文件中,处理类为AutoConfigureAnnotationProcessor,这个类是javax.annotation.processing.AbstractProcessor的子类,而AbstractProcessor由jdk提供,可以在编译期对注解进行处理; -
文件的加载:在
AutoConfigurationImportSelector.AutoConfigurationGroup#process方法中调用AutoConfigurationImportSelector#getAutoConfigurationEntry时,会传入AutoConfigurationMetadata,文件META-INF/spring-autoconfigure-metadata.properties中的内容就是从这里加载到AutoConfigurationMetadataLoader.PropertiesAutoConfigurationMetadata#properties中的;
由些可见,AutoConfigurationMetadataLoader.PropertiesAutoConfigurationMetadata#wasProcessed 方法实际上就是判断 META-INF/spring-autoconfigure-metadata.properties 文件中是否有className的配置。
让我们回到AutoConfigurationSorter.AutoConfigurationClass#isAvailable,再来看看另一个方法:getAnnotationMetadata(),该方法位于 AutoConfigurationSorter.AutoConfigurationClass 中,代码如下:
private AnnotationMetadata getAnnotationMetadata() {
if (this.annotationMetadata == null) {
try {
// 加载`className`对应的资源,当 className 对应的资源不存在时,会抛出异常
MetadataReader metadataReader = this.metadataReaderFactory
.getMetadataReader(this.className);
this.annotationMetadata = metadataReader.getAnnotationMetadata();
}
catch (IOException ex) {
throw new IllegalStateException(...);
}
}
return this.annotationMetadata;
}
继续进入SimpleMetadataReaderFactory#getMetadataReader(String):
@Override
/**
* 这个方法会获取 className 对应的 .class 文件
* 如果 .class 文件不存在,就报异常了:IOException
*/
public MetadataReader getMetadataReader(String className) throws IOException {
try {
// 转换名称:"classpath:xxx/xxx/Xxx.class"
String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX
+ lassUtils.convertClassNameToResourcePath(className)
+ ClassUtils.CLASS_FILE_SUFFIX;
// 获取资源,默认的 resourceLoader 为 classLoader
Resource resource = this.resourceLoader.getResource(resourcePath);
// 将 resource 转换成 MetadataReader 对象,不存在就会抛出异常:IOException
return getMetadataReader(resource);
}
catch (FileNotFoundException ex) {
// 有可能是内部类,再按内部类的命名方式处理一次
int lastDotIndex = className.lastIndexOf('.');
if (lastDotIndex != -1) {
String innerClassName = className.substring(0, lastDotIndex) + '$'
+ className.substring(lastDotIndex + 1);
// 转换名称:"classpath:xxx/Xxx$Xxx.class"
String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(innerClassName)
+ ClassUtils.CLASS_FILE_SUFFIX;
Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath);
// 判断是否存在,不存在还是会报异常的:IOException
if (innerClassResource.exists()) {
return getMetadataReader(innerClassResource);
}
}
throw ex;
}
}
这个方法的处理流程如下:
- 将传入的
className转换为classpath:xxx/xxx/Xxx.class的形式,然后去加载对应的资源,如果资源不存在即className对应的.class文件不存在,则抛出异常; - 在异常的
catch块中,为了防止className是内部类,会将className转换为classpath:xxx/Xxx$Xxx.class的形式,然后再加载一次资源,如果资源存在,直接返回,否则将异常往外抛;
到了这里,我们就明白了,getAnnotationMetadata() 就是用来判断当前className对应的.class在项目的classpath路径中是否存在。
对这两个方法,我们总结如下:
AutoConfigurationSorter.AutoConfigurationClass#wasProcessed:当前className是否在META-INF/spring-autoconfigure-metadata.properties文件中AutoConfigurationSorter.AutoConfigurationClass#isAvailable:当前className对应的.class文件是否存在
最终的结论:AutoConfigurationSorter.AutoConfigurationClass#isAvailable 就是用来判断当前className对应的.class文件在项目的classpath路径中.
4.2 AutoConfigurationSorter.AutoConfigurationClass#getBefore/getAfter
接下来我们来看看AutoConfigurationSorter.AutoConfigurationClass 类的两个方法:getAfter() 与 getBefore():
Set<String> getBefore() {
if (this.before == null) {
this.before = (wasProcessed()
// 如果存在于 `META-INF/spring-autoconfigure-metadata.properties` 文件中,直接获取值
? this.autoConfigurationMetadata.getSet(this.className, "AutoConfigureBefore",
Collections.emptySet())
// 否则从 @AutoConfigureBefore 注解上获取
: getAnnotationValue(AutoConfigureBefore.class));
}
return this.before;
}
Set<String> getAfter() {
if (this.after == null) {
this.after = (wasProcessed()
// 如果存在于 `META-INF/spring-autoconfigure-metadata.properties` 文件中,直接获取值
? this.autoConfigurationMetadata.getSet(this.className, "AutoConfigureAfter",
Collections.emptySet())
// 否则从 @AutoConfigureAfter 注解上获取
: getAnnotationValue(AutoConfigureAfter.class));
}
return this.after;
}
/**
* 从 @AutoConfigureBefore/@AutoConfigureAfter 注解中获取值:value 与 name 指定的值
*/
private Set<String> getAnnotationValue(Class<?> annotation) {
Map<String, Object> attributes = getAnnotationMetadata()
.getAnnotationAttributes(annotation.getName(), true);
if (attributes == null) {
return Collections.emptySet();
}
Set<String> value = new LinkedHashSet<>();
Collections.addAll(value, (String[]) attributes.get("value"));
Collections.addAll(value, (String[]) attributes.get("name"));
return value;
}
这两个方法在代码形式基本一致,先看getBefore()的流程:
-
如果当前
className存在于META-INF/spring-autoconfigure-metadata.properties文件中,直接取值,前面分析也提到,springboot在编译时,会把一些注解的信息写入到META-INF/spring-autoconfigure-metadata.properties文件中; -
如果第1步不成功,则从当前
class的@AutoConfigureBefore取值;
getAfter() 方法的流程与getBefore()的流程基本一致,就不分析了。
5. 使用 @AutoConfigureOrder 排序
让我们回到AutoConfigurationSorter#getInPriorityOrder方法,我们来看看@AutoConfigureOrder的排序过程:
List<String> getInPriorityOrder(Collection<String> classNames) {
...
orderedClassNames.sort((o1, o2) -> {
int i1 = classes.get(o1).getOrder();
int i2 = classes.get(o2).getOrder();
return Integer.compare(i1, i2);
});
...
}
这个排序操作使用的是List#sort,sort(...) 里的参数为Comparator,指定了排序规则。从代码来看,通过getOrder()获取到当前类的顺序后,再使用的是Integer的比较规则进行排序,因此getOrder()是排序的关键,它所对就的方法是AutoConfigurationSorter.AutoConfigurationClass#getOrder,代码如下:
private int getOrder() {
// 判断 META-INF/spring-autoconfigure-metadata.properties 文件中是否存在当前 className
if (wasProcessed()) {
// 如果存在,就使用文件中指定的顺序,否则就使用默认顺序
return this.autoConfigurationMetadata.getInteger(this.className,
"AutoConfigureOrder", AutoConfigureOrder.DEFAULT_ORDER);
}
// 处理不存在的情况:获取 @AutoConfigureOrder 注解指定的顺序
Map<String, Object> attributes = getAnnotationMetadata()
.getAnnotationAttributes(AutoConfigureOrder.class.getName());
// 如果 @AutoConfigureOrder 未配置,就使用默认顺序
return (attributes != null) ? (Integer) attributes.get("value")
: AutoConfigureOrder.DEFAULT_ORDER;
}
这个方法还是比较简单的,就是获取 @AutoConfigureOrder 注解指定的顺序,如果没有@AutoConfigureOrder 注解,就使用默认顺序,默认顺序AutoConfigureOrder.DEFAULT_ORDER的值为0。
6. 使用 @AutoConfigureBefore,@AutoConfigureAfter 排序
接下来就是最激动人心的@AutoConfigureBefore与@AutoConfigureAfter注解的排序了,对应的方法为AutoConfigurationSorter#sortByAnnotation,代码如下:
/**
* 进行排序,
* 实际上这个方法里只是准备了一些数据,真正干活的是 doSortByAfterAnnotation(...)
*/
private List<String> sortByAnnotation(AutoConfigurationClasses classes, List<String> classNames) {
// 需要排序的 className
List<String> toSort = new ArrayList<>(classNames);
toSort.addAll(classes.getAllNames());
// 排序好的 className
Set<String> sorted = new LinkedHashSet<>();
// 正在排序中的 className
Set<String> processing = new LinkedHashSet<>();
while (!toSort.isEmpty()) {
// 真正处理排序的方法
doSortByAfterAnnotation(classes, toSort, sorted, processing, null);
}
// 存在于集合 sorted 中,但不存在于 classNames 中的元素将会被移除
sorted.retainAll(classNames);
return new ArrayList<>(sorted);
}
/**
* 具体进行排序的方法
*/
private void doSortByAfterAnnotation(AutoConfigurationClasses classes, List<String> toSort,
Set<String> sorted, Set<String> processing, String current) {
if (current == null) {
current = toSort.remove(0);
}
// 使用 processing 来判断是否存在循环比较,比如,类A after 类B,而 类B 又 after 类A
processing.add(current);
// classes.getClassesRequestedAfter:当前 className 需要在哪些 className 之后执行
for (String after : classes.getClassesRequestedAfter(current)) {
Assert.state(!processing.contains(after),
"AutoConfigure cycle detected between " + current + " and " + after);
if (!sorted.contains(after) && toSort.contains(after)) {
// 递归调用
doSortByAfterAnnotation(classes, toSort, sorted, processing, after);
}
}
processing.remove(current);
// 添加到已排序结果中
sorted.add(current);
}
AutoConfigurationSorter#sortByAnnotation 提供了保存数据的结构,而AutoConfigurationSorter#doSortByAfterAnnotation才是真正处理排序的方法,排序操作不太好懂,大致流程如下:
-
查找当前
className需要在哪些className之后装配,将其保存为afterClasses,也就是说,afterClasses中的每一个className都要在当前className之前装配; -
遍历
afterClasses,对其中每一个className,继续查找其afterClasses,这样递归下去,不考虑循环比较的情况下,最终必然会存在一个className,它的afterClasses为空,这里就把className加入到已完成排序的结构中。
我们再来看看获取afterClasses的操作,方法为AutoConfigurationSorter.AutoConfigurationClasses#getClassesRequestedAfter,代码如下:
Set<String> getClassesRequestedAfter(String className) {
// 当前类:获取在哪些类之后执行,就是获取 @AutoConfigureAfter 注解指定的类
Set<String> classesRequestedAfter = new LinkedHashSet<>(get(className).getAfter());
// 其他类:需要前置执行的类中
this.classes.forEach((name, autoConfigurationClass) -> {
if (autoConfigurationClass.getBefore().contains(className)) {
classesRequestedAfter.add(name);
}
});
return classesRequestedAfter;
}
从代码来的来看,这个afterClasses包含两个内容:
- 获取在哪些类装配完成之后装配,就是获取
@AutoConfigureAfter注解指定的类 - 获取哪些类需要在当前类装配之前进行装配
7. 再来看:@ConditionalOnBean/@ConditionalOnMissingBean
前面提到了@ConditionalOnBean/@ConditionalOnMissingBean的坑,了解完自动装配的顺序后,就能很好规避这些坑了:
- 两个
bean都是自动装配类:避坑方式是,使用@AutoConfigureBefore/@AutoConfigureAfter或@AutoConfigureOrder指定条件顺序,保证条件注解中的bean先装配即可; - 一个是普通
spring bean,一个是自动装配类:如果条件注解中的bean是普通spring bean,另一个是自动装配类,这种情况下不用处理,自动装配的处理类是DeferredImportSelector的子类,先天决定自动装配类在普通spring bean之后处理;反之 ,条件注解中的bean是自动装配类,另一个是普通spring bean,这种一定会出错,不要使用; - 两个都是普通
spring bean:无避坑方法,spring bean注册到beanFactory的顺序不可控,不建议在这种情况下使用;
8. 总结
本文总结了自动装配类的装配顺序,主要介绍了如下内容:
- 对自动装配类排序:
AutoConfigurationImportSelector.AutoConfigurationGroup#sortAutoConfigurations - 指定自动装配类的装配顺序:使用
@AutoConfigureBefore/@AutoConfigureAfter或@AutoConfigureOrder - 排序方式有三种,依次是:
- 按className排序,由
String提供排序规则 - 根据
@AutoConfigureOrder指定的值进行排序,由Integer提供排序规则 - 根据
@AutoConfigureBefore/@AutoConfigureAfter进行排序 需要注意的是,以上三种排序方式先后进行,以最后排序完的结果为最终顺序
- 按className排序,由
- 关于
@ConditionalOnBean/@ConditionalOnMissingBean避坑指南:- 两个
bean都是自动装配类:避坑方式是,使用@AutoConfigureBefore/@AutoConfigureAfter或@AutoConfigureOrder指定条件顺序,保证条件注解中的bean先装配即可; - 一个是普通
spring bean,一个是自动装配类:条件注解中的bean必须为普通的spring bean; - 其他情况不可控,不建议使用。
- 两个
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。