歡迎光臨
每天分享高質量文章

深入 Spring Boot:那些註入不了的 Spring 佔位符 ( ${} 運算式 )

(點選上方公眾號,可快速關註)


來源:hengyunabc ,

blog.csdn.net/hengyunabc/article/details/75453307

Spring裡的佔位符

spring裡的佔位符通常表現的形式是:

或者

@Configuration

@ImportResource(“classpath:/com/acme/properties-config.xml”)

public class AppConfig {

    @Value(“${jdbc.url}”)

    private String url;

}

Spring應用在有時會出現佔位符配置沒有註入,原因可能是多樣的。

本文介紹兩種比較複雜的情況。

佔位符是在Spring生命週期的什麼時候處理的

Spirng在生命週期裡關於Bean的處理大概可以分為下麵幾步:

  1. 載入Bean定義(從xml或者從@Import等)

  2. 處理BeanFactoryPostProcessor

  3. 實體化Bean

  4. 處理Bean的property註入

  5. 處理BeanPostProcessor

當然這隻是比較理想的狀態,實際上因為Spring Context在構造時,也需要建立很多內部的Bean,應用在介面實現裡也會做自己的各種邏輯,整個流程會非常複雜。

那麼佔位符(${}運算式)是在什麼時候被處理的?

  • 實際上是在org.springframework.context.support.PropertySourcesPlaceholderConfigurer裡處理的,它會訪問了每一個bean的BeanDefinition,然後做佔位符的處理

  • PropertySourcesPlaceholderConfigurer實現了BeanFactoryPostProcessor介面

  • PropertySourcesPlaceholderConfigurer的 order是Ordered.LOWEST_PRECEDENCE,也就是最低優先順序的

結合上面的Spring的生命週期,如果Bean的建立和使用在PropertySourcesPlaceholderConfigurer之前,那麼就有可能出現佔位符沒有被處理的情況。

例子1:Mybatis 的 MapperScannerConfigurer引起的佔位符沒有處理

例子程式碼:mybatis-demo.zip

https://github.com/hengyunabc/hengyunabc.github.io/files/1158339/mybatis-demo.zip

首先應用自己在程式碼裡建立了一個DataSource,其中${db.user}是希望從application.properties裡註入的。程式碼在執行時會打印出user的實際值。

@Configuration

public class MyDataSourceConfig {

    @Bean(name = “dataSource1”)

    public DataSource dataSource1(@Value(“${db.user}”) String user) {

        System.err.println(“user: ” + user);

        JdbcDataSource ds = new JdbcDataSource();

        ds.setURL(“jdbc:h2:˜/test”);

        ds.setUser(user);

        return ds;

    }

}

然後應用用程式碼的方式來初始化mybatis相關的配置,依賴上面建立的DataSource物件

@Configuration

public class MybatisConfig1 {

 

    @Bean(name = “sqlSessionFactory1”)

    public SqlSessionFactory sqlSessionFactory1(DataSource dataSource1) throws Exception {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        org.apache.ibatis.session.Configuration ibatisConfiguration = new org.apache.ibatis.session.Configuration();

        sqlSessionFactoryBean.setConfiguration(ibatisConfiguration);

 

        sqlSessionFactoryBean.setDataSource(dataSource1);

        sqlSessionFactoryBean.setTypeAliasesPackage(“sample.mybatis.domain”);

        return sqlSessionFactoryBean.getObject();

    }

 

    @Bean

    MapperScannerConfigurer mapperScannerConfigurer(SqlSessionFactory sqlSessionFactory1) {

        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();

        mapperScannerConfigurer.setSqlSessionFactoryBeanName(“sqlSessionFactory1”);

        mapperScannerConfigurer.setBasePackage(“sample.mybatis.mapper”);

        return mapperScannerConfigurer;

    }

}

當程式碼執行時,輸出結果是:

user: ${db.user}

為什麼會user這個變數沒有被註入?

分析下Bean定義,可以發現MapperScannerConfigurer它實現了BeanDefinitionRegistryPostProcessor。這個介面在是Spring掃描Bean定義時會回呼的,遠早於BeanFactoryPostProcessor。

所以原因是:

  • MapperScannerConfigurer它實現了BeanDefinitionRegistryPostProcessor,所以它會Spring的早期會被建立

  • 從bean的依賴關係來看,mapperScannerConfigurer依賴了sqlSessionFactory1,sqlSessionFactory1依賴了dataSource1

  • MyDataSourceConfig裡的dataSource1被提前初始化,沒有經過PropertySourcesPlaceholderConfigurer的處理,所以@Value(“${db.user}”) String user 裡的佔位符沒有被處理

要解決這個問題,可以在程式碼裡,顯式來處理佔位符:

environment.resolvePlaceholders(“${db.user}”)

例子2:Spring boot自身實現問題,導致Bean被提前初始化

例子程式碼:demo.zip

https://github.com/spring-projects/spring-boot/files/773587/demo.zip

Spring Boot裡提供了@ConditionalOnBean,這個方便使用者在不同條件下來建立bean。裡面提供了判斷是否存在bean上有某個註解的功能。

@Target({ ElementType.TYPE, ElementType.METHOD })

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Conditional(OnBeanCondition.class)

public @interface ConditionalOnBean {

    /**

     * The annotation type decorating a bean that should be checked. The condition matches

     * when any of the annotations specified is defined on a bean in the

     * {@link ApplicationContext}.

     * @return the class-level annotation types to check

     */

    Class extends Annotation>[] annotation() default {};

比如使用者自己定義了一個Annotation:

@Target({ ElementType.TYPE })

@Retention(RetentionPolicy.RUNTIME)

public @interface MyAnnotation {

}

然後用下麵的寫法來建立abc這個bean,意思是當使用者顯式使用了@MyAnnotation(比如放在main class上),才會建立這個bean。

@Configuration

public class MyAutoConfiguration {

    @Bean

    // if comment this line, it will be fine.

    @ConditionalOnBean(annotation = { MyAnnotation.class })

    public String abc() {

        return “abc”;

    }

}

這個功能很好,但是在spring boot 1.4.5 版本之前都有問題,會導致FactoryBean提前初始化。

在例子裡,透過xml建立了javaVersion這個bean,想獲取到java的版本號。這裡使用的是spring提供的一個呼叫static函式建立bean的技巧。

 

 

 

 

 

 

我們在程式碼裡獲取到這個javaVersion,然後打印出來:

@SpringBootApplication

@ImportResource(“classpath:/demo.xml”)

public class DemoApplication {

 

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        System.err.println(context.getBean(“javaVersion”));

    }

}

在實際執行時,發現javaVersion的值是null。

這個其實是spring boot的鍋,要搞清楚這個問題,先要看@ConditionalOnBean的實現。

  • @ConditionalOnBean實際上是在ConfigurationClassPostProcessor裡被處理的,它實現了BeanDefinitionRegistryPostProcessor

  • BeanDefinitionRegistryPostProcessor是在spring早期被處理的

  • @ConditionalOnBean的具體處理程式碼在org.springframework.boot.autoconfigure.condition.OnBeanCondition裡

  • OnBeanCondition在獲取bean的Annotation時,呼叫了beanFactory.getBeanNamesForAnnotation

private String[] getBeanNamesForAnnotation(

    ConfigurableListableBeanFactory beanFactory, String type,

    ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {

  String[] result = NO_BEANS;

  try {

    @SuppressWarnings(“unchecked”)

    Class extends Annotation> typeClass = (Class extends Annotation>) ClassUtils

        .forName(type, classLoader);

    result = beanFactory.getBeanNamesForAnnotation(typeClass);

  • beanFactory.getBeanNamesForAnnotation 會導致FactoryBean提前初始化,創建出javaVersion裡,傳入的${java.version.key}沒有被處理,值為null。

  • spring boot 1.4.5 修複了這個問題:https://github.com/spring-projects/spring-boot/issues/8269

實現spring boot starter要註意不能導致bean提前初始化

使用者在實現spring boot starter時,通常會實現Spring的一些介面,比如BeanFactoryPostProcessor介面,在處理時,要註意不能呼叫類似beanFactory.getBeansOfType,beanFactory.getBeanNamesForAnnotation 這些函式,因為會導致一些bean提前初始化。

而上面有提到PropertySourcesPlaceholderConfigurer的order是最低優先順序的,所以使用者自己實現的BeanFactoryPostProcessor介面在被回呼時很有可能佔位符還沒有被處理。

對於使用者自己定義的@ConfigurationProperties物件的註入,可以用類似下麵的程式碼:

@ConfigurationProperties(prefix = “spring.my”)

public class MyProperties {

    String key;

}

public static MyProperties buildMyProperties(ConfigurableEnvironment environment) {

  MyProperties myProperties = new MyProperties();

 

  if (environment != null) {

    MutablePropertySources propertySources = environment.getPropertySources();

    new RelaxedDataBinder(myProperties, “spring.my”).bind(new PropertySourcesPropertyValues(propertySources));

  }

 

  return myProperties;

}

總結

  • 佔位符(${}運算式)是在PropertySourcesPlaceholderConfigurer裡處理的,也就是BeanFactoryPostProcessor介面

  • spring的生命週期是比較複雜的事情,在實現了一些早期的介面時要小心,不能導致spring bean提前初始化

  • 在早期的介面實現裡,如果想要處理佔位符,可以利用spring自身的api,比如 environment.resolvePlaceholders(“${db.user}”)

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,看技術乾貨

贊(0)

分享創造快樂