1. 程式人生 > >spring原始碼擴充套件點與實戰(一)

spring原始碼擴充套件點與實戰(一)

前言

我們在使用 spring 框架的時候,有時候需要做一些定製化開發,這個時候就有必要對 spring 進行一些個性化擴充套件。spring 的程式碼本身就是一門藝術,可以非常方便進行擴充套件,但是有時候應用場景比較複雜,可能會覺得無從下手,筆者也曾有這樣的困惑,因此,本文總結了一些常用的擴充套件點,希望能起到拋磚引玉的作用,開拓大家的思路。

spring 擴充套件點

BeanPostProcessor

首先,我們來看下官方的註釋,解釋得非常到位

Factory hook that allows for custom modification of new bean instances, e.g. checking for marker interfaces or wrapping them with proxies. ApplicationContexts can autodetect BeanPostProcessor beans in their bean definitions and apply them to any beans subsequently created. Plain bean factories allow for programmatic registration of post-processors, applying to all beans created through this factory.

BeanPostProcessor 這是一個勾子,允許對 bean 的例項進行個些自定義的個性,比如檢查標記介面、使用代理包裝 bean 例項。spring 可以自動檢測容器中定義的 BeanPostProcessor,後續建立的 bean 便會被該 BeanPostProcessor 處理。此外,BeanFactory 還允許通過編碼的方式註冊 BeanPostProcessor

BeanPostProcessor 介面定義如下所示:

public interface BeanPostProcessor {

    // 初始化之後被呼叫,已完成注入,但是尚未執行 InitializingBean#afterPropertiesSet() 方法,或者自定義的 init 方法
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; // 初始化之後被呼叫 Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }

由於 BeanPostProcessor 可以修改 bean 的例項物件,因此在 spring 框架中得到了非常廣泛的應用,比如註解注入、AOP 等等。這是一個非常重要的擴充套件點,就拿註解注入來說吧,spring 根據 BeanDefinition

對 bean 進行例項化之後,會遍歷容器內部註冊的 InstantiationAwareBeanPostProcessor(BeanPostProcessor的子類)進行屬性填充,其中就包括 AutowiredAnnotationBeanPostProcessor,它會處理 @Autowired@Value 註解,從而完成注入的功能,而 CommonAnnotationBeanPostProcessor 也是如此,只是處理不同的註解而已

spring 有很多內建的 BeanPostProcessor,我們來看個簡單的例子,幫助我們理解這個介面的作用。

我們知道 spring 提供了很多 Aware 幫助我們注入 spring 內建的 bean,比如 ApplicationContextAwareResourceLoaderAware,那麼 spring 又是如何實現該功能的呢?非常的簡單,只要寫一個 BeanPostProcessor,判斷下這個 bean 是否實現了該 Aware 介面,如果是則呼叫 setXXX() 方法進行賦值即可。這個類在 spring-context 模組,叫做 ApplicationContextAwareProcessor,我們來看下原始碼

class ApplicationContextAwareProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
        // 省略 AccessControlContext 處理的程式碼
        invokeAwareInterfaces(bean);
        return bean;
    }

    private void invokeAwareInterfaces(Object bean) {
        if (bean instanceof Aware) {
            if (bean instanceof ApplicationEventPublisherAware) {
                ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
            }
            // 省略部分程式碼……
        }
    }
}

通過這個簡單的例子,我們可以感受到 BeanPostProcessor 的強大,不過,這僅僅是 spring 的冰山一角,還有很多更高階的應用,比如 AnnotationAwareAspectJAutoProxyCreator 在 bean 的例項化、或者初始化之後建立代理,從而實現 aop 功能。再比如,spring 對註解注入的檢查機制是由 RequiredAnnotationBeanPostProcessor 完成的,也是一個 InstantiationAwareBeanPostProcessor 實現,只不過它執行的優先順序較低(通過 Ordered 控制),因為需要等待所有的 InstantiationAwareBeanPostProcessor 完成注入之後才能進行注入檢查,否則會有邏輯上的問題

BeanFactoryPostProcessor

BeanFactoryPostProcessor 允許對 Bean 的定義進行修改,還可以往 ConfigurableListableBeanFactory 中註冊一些 spring 的元件,比如新增 BeanPostProcessor、ApplicationListener、型別轉換器等等,介面定義如下:

public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

spring 利用 BeanFactoryPostProcessor 來處理佔位符 ${...},關鍵的實現類是 PropertySourcesPlaceholderConfigurer,它會遍歷所有的 BeanDefinition,如果 PropertyValue 存在這樣的佔位符,則會進行解析替換,下面我們來簡單的分析下實現原理。

我們可以看到 PropertySourcesPlaceholderConfigurer 重寫了 BeanFactoryPostProcessorpostProcessBeanFactory 方法,遍歷了 BeanFactory 內部註冊的 BeanDefinition,並且使用 BeanDefinitionVisitor 修改其屬性值,從而完成佔位符的替換。如果,我們要對某些配置進行加解密處理,也可以使用這種方式進行處理

PropertySourcesPlaceholderConfigurer.java

public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerSupport implements EnvironmentAware {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 省略對 PropertySources 的處理邏輯......
        processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
        this.appliedPropertySources = this.propertySources;
    }

    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            final ConfigurablePropertyResolver propertyResolver) throws BeansException {

        propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
        propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
        propertyResolver.setValueSeparator(this.valueSeparator);
        StringValueResolver valueResolver = new StringValueResolver() {
            @Override
            public String resolveStringValue(String strVal) {
                String resolved = (ignoreUnresolvablePlaceholders ?
                        propertyResolver.resolvePlaceholders(strVal) :
                        propertyResolver.resolveRequiredPlaceholders(strVal));
                if (trimValues) {
                    resolved = resolved.trim();
                }
                return (resolved.equals(nullValue) ? null : resolved);
            }
        };

        // 呼叫父類 PlaceholderConfigurerSupport 進行 properties 處理
        // 遍歷每一個 BeanDefinition,並將其交給 BeanDefinitionVisitor 修改內部的屬性
        doProcessProperties(beanFactoryToProcess, valueResolver);
    }
}

BeanDefinitionRegistryPostProcessor

BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor 的子類,可以通過編碼的方式,改變、新增類的定義,甚至刪除某些 bean 的定義

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}

這也是一個非常有用的擴充套件點,比如 spring 整合 mybatis,我們可以使用 spring 提供的掃描功能,為我們的 Dao 介面生成實現類而不需要編寫具體的實現類,簡化了大量的冗餘程式碼,mybatis-spring 框架就是利用 BeanDefinitionRegistryPostProcessor 通過編碼的方式往 spring 容器中新增 bean。

關鍵程式碼如下所示,MapperScannerConfigurer 重寫了 postProcessBeanDefinitionRegistry 方法,掃描 Dao 介面的 BeanDefinition,並將 BeanDefinition 註冊到 spring 容器中。我們也可以借鑑這種思路,通過具體的應用場景,從 spring 容器中刪除或者新增 BeanDefinition

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, 
    InitializingBean, ApplicationContextAware, BeanNameAware {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        if (this.processPropertyPlaceHolders) {
            processPropertyPlaceHolders();
        }

        // ClassPathMapperScanner 持有 BeanDefinitionRegistry 引用,可以新增 BeanDefinition
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

        // 省略部分程式碼……,設定 ClassPathMapperScanner 各種屬性

        // 掃描並註冊 Dao 介面的 BeanDefinition,當然是通過動態代理實現
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
}

ApplicationListener

用於接收spring的事件通知,比如常用的 ContextRefreshedEvent 事件,spring 在成功完成 refresh 動作之後便會發出該事件,代表 spring 容器已經完成初始化了,可以做一些額外的處理了,比如開啟 spring 定時任務、拉取 MQ 訊息,等等。

下面列出了 spring 處理 @Scheduled 註解的部分實現,在收到 Refresh 事件之後對 ScheduledTaskRegistrar 進行額外的設定,並開啟定時任務

public class ScheduledAnnotationBeanPostProcessor
        implements MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
        Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
        SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (event.getApplicationContext() == this.applicationContext) {
            finishRegistration();   // 對 ScheduledTaskRegistrar 進行額外的設定,並開啟定時任務
        }
    }

    // 其它程式碼......
}

ApplicationContextInitializer

ApplicationContext#refresh() 之前呼叫,用於對 ApplicationContext 進行一些初始化設定,也可以做一些前期的初始化工作,比如設定環境變數

我們知道,dubbo 可以通過註冊中心發現服務,也可以通過直連的方式,直連的方式通過設定環境變數即可,下面的程式碼演示了通過 dubbo-resolve-local.properties 檔案指定的直連配置

public class DubboContextInitializer implements ApplicationContextInitializer {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        String[] activeProfiles = applicationContext.getEnvironment().getActiveProfiles();
        if (activeProfiles.length == 0) {
            return;
        }

        // 初始化Dubbo直連檔案
        initResolveEnv(activeProfiles[0]);
    }

    private void initResolveEnv(String activeProfile) {
        // 省略獲取配置檔案的程式碼......
        Properties properties = new Properties();
        properties.load(inputStream);
        for (String name : properties.stringPropertyNames()) {
            // dubbo 指定直連 url 
            System.setProperty(name, properties.getProperty(name));
        }
    }
}

dubbo-resolve-local.properties 檔案指定了直連的配置資訊:

net.dwade.dubbo.UserService=dubbo://127.0.0.1:20881
net.dwade.dubbo.ItemService=dubbo://127.0.0.1:20881

上面只是實現了 ApplicationContextInitializer 的具體邏輯,還需要告訴 spring 有這麼個 DubboContextInitializer 實現類
- 如果是 spring boot,則新增個 META-INF/spring.factories 檔案,並且指定實現類即可

org.springframework.context.ApplicationContextInitializer=net.dwade.spring.dubbo.DubboContextInitializer
  • 如果是 web 環境,還可以通過配置 ServletContext 引數的方式,引數名為 "contextInitializerClasses",例如 web.xml 配置
<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>net.dwade.spring.dubbo.DubboContextInitializer</param-value>
</context-param>

總結

這篇文章簡單地介紹了 spring 常用的擴充套件點,如有錯誤的地方還請各位大佬加以斧正哦。在接下來的這篇文章,將會總結本人在專案中的實戰經驗,《spring原始碼擴充套件點與實戰(二)》,期待與您再會!