NO IMAGE

1. 背景介紹

Spring現在幾乎已經成為了Java開發的必備框架,在享受Spring框架本身強大能力的同時,有時我們也會希望自己研發的元件和Spring進行整合,從而使得元件更易於上手,而且配合Spring使用能發揮更強大的作用。

Apollo配置中心的Java客戶端在前一段時間也提供了和Spring整合的功能,詳細程式碼改動可以參考PR543

Apollo既支援傳統的基於XML的配置,也支援目前比較流行的基於Java的配置。下面就以Apollo為例,簡單介紹一下擴充套件Spring的幾種方式。

2. 基於XML配置的擴充套件

相信從事Java開發有一些年頭的人一定會對Spring的xml配置方式非常熟悉。不管是bean的定義,還是Spring自身的配置,早期都是通過xml配置完成的。相信還是有一大批遺留專案目前還是基於xml配置的,所以支援xml的配置方式是一個必選項。

2.1 定義schema

要支援XML的配置方式,首先需要定義一套XML Schema來描述元件所提供的功能。

Apollo提供了向Spring Property Sources注入配置的功能,所以schema中就需要描述我們期望使用者提供的namespace以及namespace之間的排序等後設資料。

下面就是Apollo的schema示例,可以看到xml的配置節點名字是config,並且有兩個可選屬性:namespacesorder,型別分別是stringint

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.ctrip.com/schema/apollo"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.ctrip.com/schema/apollo"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:annotation>
<xsd:documentation><![CDATA[ Namespace support for Ctrip Apollo Configuration Center. ]]></xsd:documentation>
</xsd:annotation>
<xsd:element name="config">
<xsd:annotation>
<xsd:documentation>
<![CDATA[ Apollo configuration section to integrate with Spring.]]>
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:attribute name="namespaces" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
The comma-separated list of namespace names to integrate with Spring property sources.
If not specified, then default to application namespace.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="order" type="xsd:int" use="optional">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
The order of the config, default to Ordered.LOWEST_PRECEDENCE, which is Integer.MAX_VALUE.
If there are properties with the same name in different apollo configs, the config with smaller order wins.
]]>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>

2.2 建立NamespaceHandler

除了XML Schema,我們還需要建立一個自定義的NamespaceHandler來負責解析使用者在XML中的配置。

2.2.1 繼承NamespaceHandlerSupport

為了簡化程式碼,我們一般會繼承一個helper類:NamespaceHandlerSupport,然後在init方法中註冊處理我們自定義節點的BeanDefinitionParser。

下面的示例告訴Spring由我們自定義的的BeanParser來處理xml中的config節點資訊。

public class NamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("config", new BeanParser());
}
}

2.2.2 自定義BeanDefinitionParser

自定義的BeanDefinitionParser負責解析xml中的config節點資訊,記錄使用者的配置資訊,為後面和Spring整合做好鋪墊。

Apollo的自定義BeanDefinitionParser主要做了兩件事情:

  1. 記錄使用者配置的namespace和order
  2. 向Spring註冊Bean:ConfigPropertySourcesProcessor,這個bean後面會實際處理使用者配置的namespace和order,從而完成配置注入到Spring中的功能
public class BeanParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return ConfigPropertySourcesProcessor.class;
}
@Override
protected boolean shouldGenerateId() {
return true;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String namespaces = element.getAttribute("namespaces");
//default to application
if (Strings.isNullOrEmpty(namespaces)) {
namespaces = ConfigConsts.NAMESPACE_APPLICATION;
}
int order = Ordered.LOWEST_PRECEDENCE;
String orderAttribute = element.getAttribute("order");
if (!Strings.isNullOrEmpty(orderAttribute)) {
try {
order = Integer.parseInt(orderAttribute);
} catch (Throwable ex) {
throw new IllegalArgumentException(
String.format("Invalid order: %s for namespaces: %s", orderAttribute, namespaces));
}
}
PropertySourcesProcessor.addNamespaces(NAMESPACE_SPLITTER.splitToList(namespaces), order);
}
}

2.3 註冊Spring handler和Spring schema

基於XML配置擴充套件Spring的主體程式碼基本就是上面這些,剩下的就是要讓Spring解析xml配置檔案的過程中識別我們的自定義節點,並且轉交到我們的NamespaceHandler處理。

2.3.1 META-INF/spring.handlers

首先需要在META-INF目錄下建立一個spring.handlers檔案,來配置我們自定義的XML Schema Namespace到我們自定義的NamespaceHandler對映關係。

http\://www.ctrip.com/schema/apollo=com.ctrip.framework.apollo.spring.config.NamespaceHandler

注意,:需要轉義

2.3.2 META-INF/spring.schemas

我們還需要在META-INF目錄下建立一個spring.schemas,來配置我們自定義的XML Schema地址到實際Jar包中的classpath對映關係(避免Spring真的去伺服器上下載不存在的檔案)。

為了簡單起見,Apollo把實際的schema檔案放在了META-INF目錄下。

http\://www.ctrip.com/schema/apollo-1.0.0.xsd=/META-INF/apollo-1.0.0.xsd
http\://www.ctrip.com/schema/apollo.xsd=/META-INF/apollo-1.0.0.xsd

注意,:需要轉義

2.4 樣例目錄結構

按照上面的方式,最終Apollo和Spring整合的相關程式碼結構如下圖所示:

apollo-spring-xml-config-hierarchy

2.5 使用樣例

基於XML配置的使用樣例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:apollo="http://www.ctrip.com/schema/apollo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
<apollo:config namespaces="application" order="1"/>
</beans>

3. 基於Java配置的擴充套件

從Spring 3.0開始,一種新的基於Java的配置方式出現了。

通過這種方式,我們在開發Spring專案的過程中再也不需要去配置繁瑣的xml檔案了,只需要在Configuration類中配置就可以了,大大的簡化了Spring的使用。

另外,這也是Spring Boot預設的配置方式,所以建議也支援這一特性。

3.1 @Import註解

支援Java配置擴充套件的關鍵點就是@Import註解,Spring 3.0提供了這個註解用來支援在Configuration類中引入其它的配置類,包括Configuration類, ImportSelector和ImportBeanDefinitionRegistrar的實現類。

我們可以通過這個註解來引入自定義的擴充套件Bean。

3.2 自定義註解

和基於XML配置類似的,我們需要提供給使用者一個註解來配置需要注入到Spring Property Sources的namespaces和order。

下面就是Apollo提供的@EnableApolloConfig註解,允許使用者傳入namespaces和order資訊。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApolloConfigRegistrar.class)
public @interface EnableApolloConfig {
/**
* Apollo namespaces to inject configuration into Spring Property Sources.
*/
String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};
/**
* The order of the apollo config, default is {@link Ordered#LOWEST_PRECEDENCE}, which is Integer.MAX_VALUE.
* If there are properties with the same name in different apollo configs, the apollo config with smaller order wins.
*/
int order() default Ordered.LOWEST_PRECEDENCE;
}

這裡的關鍵點是在註解上使用了@Import(ApolloConfigRegistrar.class),從而Spring在處理@EnableApolloConfig時會例項化並呼叫ApolloConfigRegistrar的方法。

3.3 自定義ImportBeanDefinitionRegistrar實現

ImportBeanDefinitionRegistrar介面定義了registerBeanDefinitions方法,從而允許我們向Spring註冊必要的Bean。

Apollo的自定義ImportBeanDefinitionRegistrar實現(ApolloConfigRegistrar)主要做了兩件事情:

  1. 記錄使用者配置的namespace和order
  2. 向Spring註冊Bean:PropertySourcesProcessor,這個bean後面會實際處理使用者配置的namespace和order,從而完成配置注入到Spring中的功能
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata
.getAnnotationAttributes(EnableApolloConfig.class.getName()));
String[] namespaces = attributes.getStringArray("value");
int order = attributes.getNumber("order");
PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(),
PropertySourcesProcessor.class);
}
}

3.4 樣例目錄結構

按照上面的方式,最終Apollo和Spring整合的相關程式碼結構如下圖所示:

apollo-spring-java-config-hierarchy

3.5 使用樣例

基於Java配置的使用樣例如下:

@Configuration
@EnableApolloConfig(value = "application", order = 1)
public class AppConfig {}

4. Spring容器的擴充套件點

前面兩節簡單介紹了擴充套件Spring的兩種方式:基於XML和基於Java的配置。通過這兩種方式,我們可以在執行時收集到使用者的配置資訊,同時向Spring註冊實際處理這些配置資訊的Bean。

但這些註冊進去的Bean實際上是如何工作的呢?我們通過什麼方式能使我們的程式邏輯和Spring的容器緊密合作,並無縫插入到使用者bean的生命週期中呢?

這裡簡單介紹Spring容器最常用的兩個擴充套件點:BeanFactoryPostProcessorBeanPostProcessor

4.1 BeanFactoryPostProcessor

BeanFactoryPostProcessor提供了一個方法:postProcessBeanFactory

這個方法會被Spring在容器初始化過程中呼叫,呼叫時機是所有bean的定義資訊都已經初始化好,但是這些bean還沒有例項化。

Apollo就利用這個時間點把配置資訊注入到Spring Property Sources中,從而使用者的bean在真正例項化時,所有需要的配置資訊已經準備好了。

public class PropertySourcesProcessor implements BeanFactoryPostProcessor {
private static final AtomicBoolean PROPERTY_SOURCES_INITIALIZED = new AtomicBoolean(false);
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (!PROPERTY_SOURCES_INITIALIZED.compareAndSet(false, true)) {
//already initialized
return;
}
//initialize and inject Apollo config to Spring Property Sources
initializePropertySources();
}
}

4.3 BeanPostProcessor

BeanPostProcessor提供了兩個方法:postProcessBeforeInitializationpostProcessAfterInitialization,主要針對bean初始化提供擴充套件。

  • postProcessBeforeInitialization會在每一個bean例項化之後、初始化(如afterPropertiesSet方法)之前被呼叫。
  • postProcessAfterInitialization則在每一個bean初始化之後被呼叫。

我們常用的@Autowired註解就是通過postProcessBeforeInitialization實現的(AutowiredAnnotationBeanPostProcessor)。

Apollo提供了@ApolloConfig註解來實現例項化時注入Config物件例項,所以實現邏輯和@Autowired類似。

public class ApolloAnnotationProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class clazz = bean.getClass();
processFields(bean, clazz.getDeclaredFields());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private void processFields(Object bean, Field[] declaredFields) {
for (Field field : declaredFields) {
ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
if (annotation == null) {
continue;
}
Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
"Invalid type: %s for field: %s, should be Config", field.getType(), field);
String namespace = annotation.value();
Config config = ConfigService.getConfig(namespace);
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, config);
}
}
}

仔細閱讀上面的程式碼就會發現Apollo在使用者bean初始化前會根據@ApolloConfig的配置注入對應namespace的Config例項。

5. 總結

本文簡單介紹了擴充套件Spring的幾種方式,下面簡單小結一下,希望對大家有所幫助:

  1. 通過基於XML和基於Java的配置擴充套件,可以使使用者通過Spring使用我們研發的元件,提供很好的易用性。
  2. 通過Spring容器最常用的兩個擴充套件點:BeanFactoryPostProcessorBeanPostProcessor,可以使我們的程式邏輯和Spring容器緊密合作,無縫插入到使用者bean的生命週期中,發揮更強大的作用。