概述
springCloud feign主要對netflix feign進行了增強和包裝,本篇從源碼角度帶你過一遍裝配流程,揭開feign底層的神秘面紗。
主要包括feign整合ribbon,hystrix,sleuth,以及生成的代理類最終注入到spring容器的過程。篇幅略長,耐心讀完,相信你會有所收獲。
Feign架構圖
一些核心類及大致流程:
大體步驟:
一、注冊FeignClient配置類和FeignClient BeanDefinition
二、實例化Feign上下文對象FeignContext
三、創建 Feign.builder 對象
四、生成負載均衡代理類
五、生成默認代理類
六、注入到spring容器
源碼分析
主要圍繞上面6個步驟詳細分析。
一、注冊FeignClient配置類和FeignClient BeanDefinition
從啟動類注解開始,來看下@EnableFeignClients
注解:
@EnableFeignClients public class MyApplication { }
這是在啟動類開啟feign裝配的注解,跟進該注解,
@Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {
看看做了什么:
@Import(FeignClientsRegistrar.class) public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware { // patterned after Spring Integration IntegrationComponentScanRegistrar // and RibbonClientsConfigurationRegistgrar private final Logger logger = LoggerFactory.getLogger(FeignClientsRegistrar.class); private ResourceLoader resourceLoader; private ClassLoader classLoader; public FeignClientsRegistrar() { } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //1、先注冊默認配置 registerDefaultConfiguration(metadata, registry); //2、注冊所有的feignClient beanDefinition registerFeignClients(metadata, registry); } //... }
我們分別來看一下上面registerBeanDefinitions
中的兩個方法:
1) 注冊默認配置方法:registerDefaultConfiguration
:
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } //name 默認以default開頭,后續會根據名稱選擇配置 registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } }
上述方法為讀取啟動類上面@EnableFeignClients
注解中聲明feign相關配置類,默認name為default,一般情況下無需配置。用默認的FeignAutoConfiguration
即可。
上面有個比較重要的方法:注冊配置registerClientConfiguration
,啟動流程一共有兩處讀取feign的配置類,這是第一處。
跟進該registerClientConfiguration方法看一下:
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(FeignClientSpecification.class); builder.addConstructorArgValue(name); builder.addConstructorArgValue(configuration); registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); }
上面將bean配置類包裝成FeignClientSpecification
,注入到容器。該對象非常重要,包含FeignClient需要的重試策略,超時策略,日志等配置,如果某個服務沒有設置,則讀取默認的配置。
2、掃描FeignClient
該方法主要是掃描類路徑,對所有的FeignClient生成對應的BeanDefinition
:
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); if (clients == null || clients.length == 0) { scanner.addIncludeFilter(annotationTypeFilter); basePackages = getBasePackages(metadata); } else { final Set<String> clientClasses = new HashSet<>(); basePackages = new HashSet<>(); for (Class<?> clazz : clients) { basePackages.add(ClassUtils.getPackageName(clazz)); clientClasses.add(clazz.getCanonicalName()); } AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { @Override protected boolean match(ClassMetadata metadata) { String cleaned = metadata.getClassName().replaceAll("\\$", "."); return clientClasses.contains(cleaned); } }; scanner.addIncludeFilter( new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } //獲取掃描目錄下面所有的bean deanDefinition for (String basePackage : basePackages) { Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes); //這里是第二處 registerClientConfiguration(registry, name, attributes.get("configuration")); //注冊feignClient registerFeignClient(registry, annotationMetadata, attributes); } } } }
可以看到上面又調用了registerClientConfiguration
注冊配置的方法,這里是第二處調用。這里主要是將掃描的目錄下,每個項目的配置類加載的容器當中。
注冊到容器中,什么時候會用到呢?具體又如何使用呢?別着急,后面會有介紹。
我們先會回到繼續主流程,繼續看注冊feignClient的方法,跟進registerFeignClient
:
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); //FeignClientFactoryBean的名稱 BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); //將annotationMetadata(注解中的參數)通過addPropertyValue方式設置到FeignClientFactoryBean里 definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = name + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); //將bean definition加入到spring容器 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
划重點,上面出現了一行相當關鍵代碼:
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
springCloud FeignClient其實是利用了spring的代理工廠來生成代理類,所以這里將所有的feignClient
的描述信息BeanDefinition
設定為FeignClientFactoryBean
類型,該類又繼承FactoryBean
,很明顯,這是一個代理類。
在spring中,FactoryBean
是一個工廠bean,用作創建代理bean,所以得出結論,feign將所有的feignClient bean包裝成FeignClientFactoryBean
。掃描方法到此結束。
代理類什么時候會觸發生成呢?
在spring刷新容器時,當實例化我們的業務service時,如果發現注冊了FeignClient,spring就會去實例化該FeignClient,同時會進行判斷是否是代理bean,如果為代理bean,則調用FeignClientFactoryBean
的T getObject() throws Exception;
方法生成代理bean。
先來隆重介紹一下FeignClientFactoryBean
,后面四步都基於此類。
先看一下代理feignClient代理生成入口:getObject
方法:
@Override public Object getObject() throws Exception { // 二、實例化Feign上下文對象FeignContext FeignContext context = applicationContext.getBean(FeignContext.class); // 三、生成builder對象,用來生成feign Feign.Builder builder = feign(context); // 判斷生成的代理對象類型,如果url為空,則走負載均衡,生成有負載均衡功能的代理類 if (!StringUtils.hasText(this.url)) { String url; if (!this.name.startsWith("http")) { url = "http://" + this.name; } else { url = this.name; } url += cleanPath(); // 四、生成負載均衡代理類 return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } //如果指定了url,則生成默認的代理類 if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not lod balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient)client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); // 五、生成默認代理類 return targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url)); }
getObject()
邏輯比較多,每一行都會做一些初始化配置,來逐步分析。
二、實例化Feign上下文對象FeignContext
上述方法中第一行便是實例化FeignContext
:
FeignContext context = applicationContext.getBean(FeignContext.class);
獲取FeignContext
對象,如果沒有實例化,則主動實例化,如下:
@Configuration @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class}) public class FeignAutoConfiguration { @Autowired(required = false) private List<FeignClientSpecification> configurations = new ArrayList<>(); @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; }
可以看到feign的配置類設置到feign的容器當中,而configurations集合中的元素 正是上面我們提到的兩處調用registerClientConfiguration
方法添加進去的,前后呼應。
然而,當我們引入了sleuth
之后,獲取的feignContext
確是TraceFeignClientAutoConfiguration
中配置的實例sleuthFeignContext
:
可以看到上面創建了一個TraceFeignContext
實例,因為該對象繼承FeignContext
,同時又加了@Primary
注解,所以在上面第2步中通過類型獲取:applicationContext.getBean(FeignContext.class);
,最終拿到的是TraceFeignContext
。
三、構造FeignBuilder
繼續跟進FeignClientFactoryBean.getObject()方法:
Feign.Builder builder = feign(context);
feign()
protected Feign.Builder feign(FeignContext context) { Logger logger = getOptional(context, Logger.class); if (logger == null) { logger = new Slf4jLogger(this.type); } // 1、構造 Feign.Builder Feign.Builder builder = get(context, Feign.Builder.class) // required values .logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // 2、設置重試策略,log等組件 //設置log級別 Logger.Level level = getOptional(context, Logger.Level.class); if (level != null) { builder.logLevel(level); } //設置重試策略 Retryer retryer = getOptional(context, Retryer.class); if (retryer != null) { builder.retryer(retryer); } //feign的錯誤code解析接口 ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class); if (errorDecoder != null) { builder.errorDecoder(errorDecoder); } //超時時間設置,連接超時時間:connectTimeout默認10s,請求請求超時時間:readTimeout默認60s Request.Options options = getOptional(context, Request.Options.class); if (options != null) { builder.options(options); } //攔截器設置,可以看出攔截器也是可以針對單獨的feignClient設置 Map<String, RequestInterceptor> requestInterceptors = context.getInstances( this.name, RequestInterceptor.class); if (requestInterceptors != null) { builder.requestInterceptors(requestInterceptors.values()); } if (decode404) { builder.decode404(); } return builder; }
上述代碼有兩處邏輯,分別來看:
1、Feign.Builder builder = get(context, Feign.Builder.class)
,又會有以下三種情況:
1)單獨使用Feign,沒有引入 sleuth
、hystrix
:
通過加載FeignClientsConfiguration的配置創建Feign
的靜態內部類:Feign.Builder
@Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); }
2)引入了hystrix
,沒有引入sleuth
:
通過加載FeignClientsConfiguration
的配置創建HystrixFeign
的靜態內部類:HystrixFeign.Builder
@Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } }
3)同時引入hystrix
和 sleuth
:
加載TraceFeignClientAutoConfiguration
的配置創建:HystrixFeign.Builder
:
注意:
-
TraceFeignClientAutoConfiguration
的配置類加載一定是在FeignClientsConfiguration
之前(先加載先生效),而FeignClientsConfiguration
加載是通過FeignAutoConfiguration
完成的,所以上圖中引入了條件注解:@AutoConfigureBefore({FeignAutoConfiguration.class})
-
創建創建的
builder
對象和第二種情況一下,只是做了一層包裝:final class SleuthFeignBuilder { private SleuthFeignBuilder() {} static Feign.Builder builder(Tracer tracer, HttpTraceKeysInjector keysInjector) { return HystrixFeign.builder() //各組件`client,retryer,decoder`進行增強,裝飾器模式。 .client(new TraceFeignClient(tracer, keysInjector)) .retryer(new TraceFeignRetryer(tracer)) .decoder(new TraceFeignDecoder(tracer)) .errorDecoder(new TraceFeignErrorDecoder(tracer)); } }
2、設置重試策略,log等組件
Feign.builder在獲取之后又分別指定了重試策略,日志級別,錯誤代碼code等,在上一步中調用SleuthFeignBuilder.build()
時已經設置過默認值了,這里為什么要重復設置呢?
我們跟進去get()方法,一探究竟:
protected <T> T get(FeignContext context, Class<T> type) { //根據name,也就是服務名稱來生成builder T instance = context.getInstance(this.name, type); if (instance == null) { throw new IllegalStateException("No bean found of type " + type + " for " + this.name); } return instance; } public <T> T getInstance(String name, Class<T> type) { //這里獲取AnnotationConfigApplicationContext容器 AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type); } return null; } private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>(); protected AnnotationConfigApplicationContext getContext(String name) { if (!this.contexts.containsKey(name)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name)) { //這里創建容器createContext(name) this.contexts.put(name, createContext(name)); } } } return this.contexts.get(name); }
重點來了,上述代碼將FeignContext做了緩存,每個服務對應一個FeignContext,服務名作為key。
繼續跟進createContext(name)
方法:
protected AnnotationConfigApplicationContext createContext(String name) { //注意:這里的容器並不是spring的容器,而是每次都重新創建一個 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); //加載每個服務對應的配置類 if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name) .getConfiguration()) { context.register(configuration); } } //加載啟動類@EnableFeignClients注解指定的配置類 for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } //注冊默認的配置類:FeignClientsConfiguration context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( this.propertySourceName, Collections.<String, Object> singletonMap(this.propertyName, name))); if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); } //刷新容器 context.refresh(); return context; }
可以看到上述AnnotationConfigApplicationContext容器並非spring容器,只是利用了spring刷新容器的方法來實例化配置類,以服務名作為key,配置隔離。
重點來了,上面加載配置的順序為:先加載每個服務的配置類,然后加載啟動類注解上的配置類,最后加載默認的配置類。這樣做有什么好處?
spring刷新容器的方法也是對所有的bean進行了緩存,如果已經創建,則不再實例化。所以優先選取每個FeignClient的配置類,最后默認的配置類兜底。
所以這也證明了sleuth
的配置一定在feign
的配置類之前加載。
至此,FeignBuilder
構造流程結束。
四、生成負載均衡代理類
再貼一下生成代理類的入口:
//判斷url是否為空 if (!StringUtils.hasText(this.url)) { //...... return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url)); } //...... return targeter.target(this, builder, context, new HardCodedTarget<>( this.type, this.name, url));
這里有個重要判斷:判斷FeignClient聲明的url是否為空,來判斷具體要生成的代理類。如下:
這么做有什么意義?
1)如果為空,則默認走Ribbon代理,也就是這個入口,會有加載ribbon的處理。@FeignClient("MyFeignClient")
2)如果不為空,指定url,則走默認生成代理類的方式,也就是所謂的硬編碼。@FeignClient(value = "MyFeignClient",url = "http://localhost:8081")
這樣處理方便開發人員進行測試,無需關注注冊中心,直接http調用,是個不錯的開發小技巧。
生產環境也可以用上述第二種方式,指定域名的方式。
我們跟進loadBalance
方法:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { //獲得FeignClient Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?"); }
Client client = getOptional(context, Client.class);
這里會從FeignContext
上下文中獲取Client
對象,該對象有三種實例,具體是哪個實現呢?
這里又會有三種情況:
1)沒有整合ribbon
、sleuth
:
獲取默認的Client
:Default
實例。
2)整合了ribbon
,沒有整合sleuth
:
獲取LoadBalanceFeignClient
實例。
3)整合了ribbon
和 sleuth
:
會獲取TraceFeignClient
實例,該實例是對LoadBalanceFeignClient
的一種包裝,實現方式通過BeanPostProcessor
實現:FeignBeanPostProcessor
中定義了包裝邏輯:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return this.traceFeignObjectWrapper.wrap(bean);
}
通過wrap
方法最終返回TraceFeignClient
實例。
繼續回到主流程,先來看下Targeter
接口:
interface Targeter {
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
HardCodedTarget<T> target);
}
該對象定義在FeignClientFactoryBean
靜靜態代碼塊中:
private static final Targeter targeter;
static {
Targeter targeterToUse;
//判斷類路徑是否引入了hystrixFeign
if (ClassUtils.isPresent("feign.hystrix.HystrixFeign",
FeignClientFactoryBean.class.getClassLoader())) {
targeterToUse = new HystrixTargeter();
}
else {
targeterToUse = new DefaultTargeter();
}
targeter = targeterToUse;
}
這里會初始化Targeter
,該類是生成feign代理類的工具類,有兩種實現,正是上面的HystrixTargeter
,DefaultTargeter
。
因為我們引入了hystrix
,所以Targeter
實現類為HystrixTargeter
。我們繼續跟進targeter.target
方法:
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
上面通過build()
方法獲取生成代理類的工具類ReflectiveFeign
,再通過newInstance
正式創建代理類。
繼續跟進:
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
}
這里會創建Feign的方法工廠synchronousMethodHandlerFactory
,Feign
通過該工廠為每個方法創建一個methodHandler
,每個methodHandler
中包含Feign對應的配置:retryer
、requestInterceptors
等。
繼續跟進newInstance
方法:
public <T> T newInstance(Target<T> target) {
//創建所有的 MethodHandler
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
//判斷是否啟用默認handler
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
//創建InvocationHandler,接收請求,轉發到methodHandler
InvocationHandler handler = factory.create(target, methodToHandler);
//生成代理類
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
//將默認方法綁定到代理類
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
InvocationHandler
最終創建的實例為HystrixInvocationHandler
,核心方法如下:
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
@Override
protected Object run() throws Exception {
try {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw (Error) t;
}
}
@Override
protected Object getFallback() {
//......
}
};
整個流程:Feign調用方發起請求,發送至hystrix的HystrixInvocationHandler,通過服務名稱,找到對應方法的methodHandler,methodHandler中封裝了loadBalanceClient、retryer、RequestInterceptor等組件,如果引入了sleuth,這幾個組件均是sleuth的包裝類。然后通過以上組件構造http
請求完成整個過程。
五、生成默認代理類
理解了第四步的邏輯,生成默認代理類就很容易理解了,唯一不同點就是client
的實現類為loadBalanceClient
。
注意:不管是哪種代理類,最終發起請求還是由
Feign.Default
中的execute
方法完成,默認使用HttpUrlConnection
實現。
六、注入spring容器
總結:通過spring refresh()
方法,觸發FeignClientFactoryBean.getObject()
方法獲得了代理類,然后完成注入spring
容器的過程。該實現方式同Dubbo
的實現方式類似,有興趣的可以自行研究噢。
轉:http://springcloud.cn/view/409