天天看點

學習 @ConfigurationProperties 注解

作者:JU幫

我們在開發 SpringBoot 項目時,經常會配置 application.properties 或 application.yml 配置檔案,我們以 spring-boot-starter-data-redis 為例,配置如下:

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123123           

而且配置這些配置項時,IDEA 會有友好的提示資訊,如下所示:

學習 @ConfigurationProperties 注解

那麼在配置檔案中的配置項是如何被 spring-boot-starter-data-redis 使用的,IDEA 又是如何能友好的提示配置資訊的,下面我們詳細的學習一下。

在正式學習後續内容之前,為了避免語言表達中可能産生的偏差,做如下約定:

配置檔案:指 SpringBoot 項目中的 application.properties 或 application.yml 檔案

配置項:指配置檔案中配置的内容

配置屬性類:指使用 @ConfigurationProperties 與配置項綁定的類

如何綁定配置項

首先研究一下,在配置檔案中的配置項是如何使用的。借用 IDEA 強大的功能,在配置檔案中按住 Ctrl + 滑鼠左鍵 會被導航到 org.springframework.boot.autoconfigure.data.redis.RedisProperties 類中,如下所示:

@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisProperties {
  private int database = 0;
  private String url;
  private String host = "localhost";
  private String username;
  private String password;
  private int port = 6379;
  // 省略後續屬性和對應 Getter、Setter 方法
}           

發現 @ConfigurationProperties 注解屬性 prefix 的值和 RedisProperties 類的屬性值與配置檔案中的配置項是一緻的,是以 @ConfigurationProperties 注解是關鍵,下面學習一下該注解。

@ConfigurationProperties 注解源碼如下:

/**
 * Annotation for externalized configuration. Add this to a class definition or a
 * {@code @Bean} method in a {@code @Configuration} class if you want to bind and validate
 * some external Properties (e.g. from a .properties file).
 * <p>
 * Binding is either performed by calling setters on the annotated class or, if
 * {@link ConstructorBinding @ConstructorBinding} is in use, by binding to the constructor
 * parameters.
 * <p>
 * Note that contrary to {@code @Value}, SpEL expressions are not evaluated since property
 * values are externalized.
 *
 * @author Dave Syer
 * @since 1.0.0
 * @see ConfigurationPropertiesScan
 * @see ConstructorBinding
 * @see ConfigurationPropertiesBindingPostProcessor
 * @see EnableConfigurationProperties
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {

	@AliasFor("prefix")
	String value() default "";

	/**
	 * The prefix of the properties that are valid to bind to this object. Synonym for
	 * {@link #value()}. A valid prefix is defined by one or more words separated with
	 * dots (e.g. {@code "acme.system.feature"}).
	 * @return the prefix of the properties to bind
	 */
	@AliasFor("value")
	String prefix() default "";

	boolean ignoreInvalidFields() default false;

	boolean ignoreUnknownFields() default true;
}           

通過注解注釋可以了解到, @ConfigurationProperties 注解的作用是将 SpringBoot 外部化配置的配置項綁定到其标注的對象,通過通路該對象就能擷取配置檔案中配置項的值。

通過 RedisProperties 類,我們學習到,如果要将配置項綁定到結構化對象,需要做兩件事:

  1. 使用 @ConfigurationProperties 注解标注需要綁定的類
  2. 設定 @ConfigurationProperties 注解的 prefix 或 value 屬性,用于指定該配置的字首,友善管理和防止與其他元件配置沖突

将配置項指派給配置屬性類執行個體的方式

通過上面我們知道配置屬性類執行個體綁定了配置檔案中的指定字首的配置項,那麼配置屬性類執行個體化時,配置項的值是如何指派給配置屬性類執行個體對應屬性的呢?

我們知道給對象屬性指派的方式有兩種:

  1. 通過屬性的 Setter 方法指派
  2. 通過有參構造器指派

第一種:屬性的 Setter 方法指派

就是建立完成配置屬性類對象後,通過使用屬性的 Setter 方法對屬性指派,這種方式需要配置屬性類提供 Setter 方法,或使用 Lombok 提供的 @Data / @Setter 注解生成 Setter 方法。

在 SpringBoot 各種 Starter 綁定配置項多數使用的就是這種方式,例如 spring-boot-starter-data-redis 的配置屬性類 RedisProperties。

第二種:有參構造器指派

這種方式需要配置屬性類提供有參構造器,并且該構造器使用 @ConstructorBinding 注解标注,如下所示:

@Data
@ConfigurationProperties("person")
public class PersonProperties {

    private String id;
    private String name;
    private Integer age;

    @ConstructorBinding
    public PersonProperties(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}           

@ConstructorBinding 注解源碼如下:

/**
 * Annotation that can be used to indicate which constructor to use when binding
 * configuration properties using constructor arguments rather than by calling setters. A
 * single parameterized constructor implicitly indicates that constructor binding should
 * be used unless the constructor is annotated with `@Autowired`.
 * <p>
 * Note: To use constructor binding the class must be enabled using
 * {@link EnableConfigurationProperties @EnableConfigurationProperties} or configuration
 * property scanning. Constructor binding cannot be used with beans that are created by
 * the regular Spring mechanisms (e.g.
 * {@link org.springframework.stereotype.Component @Component} beans, beans created via
 * {@link org.springframework.context.annotation.Bean @Bean} methods or beans loaded using
 * {@link org.springframework.context.annotation.Import @Import}).
 *
 * @author Phillip Webb
 * @since 2.2.0
 * @see ConfigurationProperties
 * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
 * {@link org.springframework.boot.context.properties.bind.ConstructorBinding}
 */
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Deprecated(since = "3.0.0", forRemoval = true)
@org.springframework.boot.context.properties.bind.ConstructorBinding
public @interface ConstructorBinding {}           

該注解的作用是綁定配置項時使用其标注的構造器,而不是使用 Setter 方法。

啟用配置屬性類

想要啟用配置屬性類(也就是能夠通過自動注入使用配置屬性類),需要 Spring 管理配置屬性類并将其注入到 IOC 容器中。有下面 4 中方式可以實作,在學習這些方式之前,先定義一些公共代碼,如下所示:

配置檔案 application.yml:

person:
  id: 1001
  name: 張三
  age: 21           

測試類:

@SpringBootTest
class ConfigurationPropertiesTests {

	@Autowired
	private PersonProperties personProperties;


	/**
	 * 測試通過 setting 方法綁定屬性
	 */
	@Test
	public void testPropertiesSettingBinding() {
		System.out.println(personProperties);
	}
}           

第一種:使用 @Component 注解

注意:使用這種方式,不能使用有參構造器的方式指派

配置屬性類使用 @Component 注解标注,如下:

@Data
@Component
@ConfigurationProperties("person")
public class PersonProperties {

    private String id;

    private String name;

    private Integer age;
}           

執行測試方法結果如下:

PersonProperties(id=1001, name=張三, age=21)           

第二種:使用 @EnableConfigurationProperties 注解

@EnableConfigurationProperties 注解作用是啟動對 @ConfigurationProperties 标注的 Bean 的支援,這是一種比較友善的方式,在 SpringBoot Starter 中使用的比較多,較為推薦使用該種方式。

較第一種方式,删除 @Component 注解,如下所示:

@Data
@ConfigurationProperties("person")
public class PersonProperties {

    private String id;

    private String name;

    private Integer age;
}           

在啟動類上使用 @EnableConfigurationProperties 注解,并指定配置屬性類:

@SpringBootApplication
@EnableConfigurationProperties(PersonProperties.class)
public class SpringAnnotationApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringAnnotationApplication.class, args);
	}
}           

通常将 @EnableConfigurationProperties 注解标注在對應的配置類上,以 spring-boot-starter-data-redis 自動配置為例,如下所示:

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		return new StringRedisTemplate(redisConnectionFactory);
	}

}           
可以看到 Redis 自動配置類上标注了 @EnableConfigurationProperties(RedisProperties.class),并且指定配置屬性類 RedisProperties

第三種:使用 @ConfigurationPropertiesScan 注解

@ConfigurationPropertiesScan 注解作用是掃描配置的包路徑下所有标注 @ConfigurationProperties 的 Bean,如果沒有顯示指定包路徑,預設掃描标注該注解的類對應的包和子包。

配置屬性類,如下所示:

@Data
@ConfigurationProperties("person")
public class PersonProperties {

    private String id;

    private String name;

    private Integer age;
}           

在啟動類上使用 @ConfigurationPropertiesScan 注解,可以顯示指定待掃描的包路徑:

@SpringBootApplication
@ConfigurationPropertiesScan("com.demo")
public class SpringAnnotationApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringAnnotationApplication.class, args);
	}
}           

@ConfigurationPropertiesScan 注解通常用于該服務中具有多個配置屬性類時使用,如果隻有一個,推薦使用 @EnableConfigurationProperties 注解。

第四種:@Configuration 和 @Bean 注解

配置屬性類,如下所示:

@Data
@ConfigurationProperties("person")
public class PersonProperties {

    private String id;

    private String name;

    private Integer age;
}           

定義配置類,使用 @Bean 将配置屬性類注冊到 IOC 容器中:

@Configuration
public class PropertiesConfig {

    @Bean
    public PersonProperties personProperties() {
        return new PersonProperties();
    }
}           

上面這種方式使用的不多,下面的方式可以在将第三發元件中的 Bean 作為配置屬性類時使用。

配置屬性類較上面删除 @ConfigurationProperties("person") 注解,假設為第三方元件種普通的 Bean,如下所示:

@Data
public class PersonProperties {

    private String id;

    private String name;

    private Integer age;
}           

定義配置類,使用 @ConfigurationProperties("person") 注解綁定字首為 person 的配置,同時使用 @Bean 将配置屬性類注冊到 IOC 容器中,如下所示:

@Configuration
public class PropertiesConfig {

    @Bean
    @ConfigurationProperties("person")
    public PersonProperties personProperties() {
        return new PersonProperties();
    }
}           

友好的提示資訊

在我們編寫配置屬性類,添加上 @ConfigurationProperties 注解時,IDEA 會有如下提示

學習 @ConfigurationProperties 注解

點選打開文檔會被連結到 SpringBoot 官方文檔:https://docs.spring.io/spring-boot/docs/2.7.5/reference/html/configuration-metadata.html

首先我們先了解一下什麼是配置中繼資料?

注解處理器的作用是在編譯時通過标注 @ConfigurationProterties 注解的配置項自動生成配置中繼資料(Configuration Metadata),如果自動生成的不夠詳細,可以手動編寫部分中繼資料。

配置中繼資料提供了所有支援的配置屬性詳細資訊,這可以幫助開發人員在配置 application.properties 或 application.yml 檔案時,提供上下文資訊。配置中繼資料檔案在 jar 包 META-INF/spring-configuration-metadata.json 中。

詳細内容可以查詢上面官網連結

如何使用注解處理器?

使用注解處理器,可以很容易的從帶有 @ConfigurationProperties 注解的類生成配置中繼資料檔案。

1、首先添加注解處理器依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>           

2、執行 maven 代碼打包,生成配置中繼資料:

在生成配置中繼資料之前,我們先修改配置屬性類,為 id 設定預設值,如下所示:

/**
 * 使用者資訊配置
 */
@Data
@ConfigurationProperties("person")
public class PersonProperties {

    /**
     * 使用者編号
     */
    private String id = "1002";

    /**
     * 使用者姓名
     */
    private String name;

    /**
     * 使用者年齡
     */
    private Integer age;
}           
注意:建議添加屬性的注釋,可以在生成配置中繼資料時,生成屬性的描述(description),在配置時,可以看到,友善知道配置含義。

執行 maven 編譯,編譯完成後,打開路徑 target/classes/META-INF/spring-configuration-metadata.json 發現,自動生成了配置中繼資料,如下所示:

學習 @ConfigurationProperties 注解

配置中繼資料 spring-configuration-metadata.json 内容如下:

{
  "groups": [
    {
      "name": "person",
      "type": "com.yage.boot.annotation.PersonProperties",
      "sourceType": "com.yage.boot.annotation.PersonProperties"
    }
  ],
  "properties": [
    {
      "name": "person.age",
      "type": "java.lang.Integer",
      "description": "使用者年齡",
      "sourceType": "com.yage.boot.annotation.PersonProperties"
    },
    {
      "name": "person.id",
      "type": "java.lang.String",
      "description": "使用者編号",
      "sourceType": "com.yage.boot.annotation.PersonProperties",
      "defaultValue": "1002"
    },
    {
      "name": "person.name",
      "type": "java.lang.String",
      "description": "使用者姓名",
      "sourceType": "com.yage.boot.annotation.PersonProperties"
    }
  ],
  "hints": []
}           

有了這個配置中繼資料檔案之後,我們在編寫 application.properties 和 application.yml 檔案時,就會有提示資訊,如下所示:

學習 @ConfigurationProperties 注解

繼續閱讀