ConfigurationProperties使用详解

简介与注意项
为啥要用ConfigurationProperties

参考SpringBoot官方文档:24.8 Type-safe Configuration Properties

Using the @Value("${property}") annotation to inject configuration properties can sometimes be cumbersome, especially if you are working with multiple properties or your data is hierarchical in nature.Spring Boot provides an alternative method of working with properties that lets strongly typed beans govern and validate the configuration of your application.

使用@Value(“ $ {property}”)批注来注入配置属性有时会很麻烦,尤其是当您使用多个属性或数据本质上是分层的时。SpringBoot提供了另一种使用属性的方法来实现强类型的Bean控制和验证应用程序的配置。

上面所说的方法就是通过@ConfigurationProperties注解指定的类。也就是说更加安全,强类型控制和应用程序验证。

@ConfigurationProperties("acme")
public class AcmeProperties {
    private boolean enabled;
    private InetAddress remoteAddress;
    ...
}

比如上面的类,就强制增加了如下的类型控制和默认值。

acme.enabled, with a value of false by default.类型为boolean,并且有个默认值false

acme.remote-address, with a type that can be coerced from String.类型为可以转换为InetAddress的String

@ConfigurationProperties vs. @Value方式的异同点

通过@Value方式注入属性是容器的基本属性,但是不提供安全类型配置。以下表格展示了二者的异同。

Feature@ConfigurationProperties@Value
Relaxed binding (灵活绑定)YesNo
Meta-data support(元数据支持)YesNo
SpEL evaluation(SPEL语言支持)NoYes
If you define a set of configuration keys for your own components, we recommend you group them in a POJO annotated with @ConfigurationProperties. You should also be aware that, since @Value does not support relaxed binding, it is not a good candidate if you need to provide the value by using environment variables

如果为自己的组件定义了一组配置键,建议将这些配置通过一个注解了@ConfigurationProperties的POJO进行包装,第一,通过@Value不支持动态绑定,第二,通过@Value无法获取environment中的变量。

配置类注意点
The properties that map to @ConfigurationProperties classes available in Spring Boot,
which are configured via properties files, YAML files, environment variables etc., are public API but the accessors (getters/setters) of the class itself are not meant to be used directly.

可以通过properties文件、YAML文件、环境属性等配置@ConfigurationProperties注解类的属性,这些都是公共的API,但是并不会意味着getter/setter方法可以直接使用。主要有如下的一些原因。

Getters and setters are usually mandatory, since binding is through standard Java Beans property descriptors, just like in Spring MVC. A setter may be omitted in the following cases:
• Maps, as long as they are initialized, need a getter but not necessarily a setter, since they can be mutated by the binder.
• Collections and arrays can be accessed either through an index (typically with YAML) or by using a single comma-separated value (properties). In the latter case, a setter is mandatory.We recommend to always add a setter for such types. If you initialize a collection, make sure it is not immutable (as in the preceding example).  
• If nested POJO properties are initialized (like the Security field in the preceding example), a setter is not required. If you want the binder to create the instance on the fly by using its default constructor, you need a setter.Some people use Project Lombok to add getters and setters automatically. Make sure that Lombok does not generate any particular constructor for such a type, as it is used automatically by the container to instantiate the object.

Finally, only standard Java Bean properties are considered and binding on static properties is not supported.  

作为一个标准的javabean的属性,getter方法和setter方法是强制要求的,但是在有一些类中可能会被舍弃。比如Map类、Collection类、数组,因为它们是可以被局部修改的,比如通过索引。另外如果属性是一个pojo类,而且已经实例化过,setter方法也是可以不需要的,但是如果希望在绑定的过程中通过默认的构造器进行实例化,那么是需要setter方法的。另外还有像Lombok这些插件也带来了额外的复杂性。

最后明确一下,仅考虑标准Java Bean属性,并且不支持对静态属性的绑定。标准编程很重要…

使用方式
方式一
@ConfigurationProperties("acme")
public class AcmeProperties {
    private boolean enabled;    // acme.enabled, with a value of false by default.
    private InetAddress remoteAddress;  // acme.remote-address, with a type that can be coerced from String.
    ...
}
You also need to list the properties classes to register in the @EnableConfigurationProperties annotation, as shown in the following example  

必须在配置类上面通过@EnableConfigurationProperties注解来注册属性类。

@Configuration
@EnableConfigurationProperties(AcmeProperties.class)
public class MyConfiguration {
}

通过上面这种方式,容器中注入了一个Bean,它的名称为acme-com.example.AcmeProperties。强烈建议,@ConfigurationProperties只考虑environment,不要从容器中注入其他的bean.

When the @ConfigurationProperties bean is registered that way, the bean has a
conventional name: <prefix>-<fqn>, where <prefix> is the environment key prefix specified in the @ConfigurationProperties annotation and <fqn> is the fully qualified name of the bean. If the annotation does not provide any prefix, only the fully qualified name of the bean is used
Keep in mind that the @EnableConfigurationProperties annotation is also automatically applied to your project so that any existing bean annotated with @ConfigurationProperties is configured from the Environment.
方式二

直接将AcmeProperties作为一个Bean注入到容器中

@Component
@ConfigurationProperties(prefix="acme")
public class AcmeProperties {
// ... see the preceding example
}
方式三

在public类型标注了@Bean的方法上使用,这样可以用于绑定属性到第三方组件上。比如在属性配置文件中有如下配置:

# Druid 数据源 1 配置,继承spring.datasource.druid.* 配置,相同则覆盖
spring.datasource.druid.one.url=jdbc:mysql://127.0.0.1:3306/ms_user
spring.datasource.druid.one.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.one.password=root
spring.datasource.druid.one.username=root
spring.datasource.druid.one.max-active=10
spring.datasource.druid.one.max-wait=10000
@Configuration
public class DruidConfiguration {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.one")
    public DataSource dataSourceOne() {
        return DruidDataSourceBuilder.create().build();
    }
}

通过以上方式就可以通过读取属性配置注入一个Druid的数据源。

Relaxed Binding 灵活绑定
Spring Boot uses some relaxed rules for binding Environment properties to
@ConfigurationProperties beans, so there does not need to be an exact match between the Environment property name and the bean property name. Common examples where this is useful include dash-separated environment properties (for example, context-path binds to contextPath),and capitalized environment properties (for example, PORT binds to port)  .
package com.example.durid.demo.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "acme.my-project.person")
public class OwnerProperties {
    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}

在配置文件中通过如下配置的效果一致

# Standard camel case syntax
acme.my-project.person.firstName=Steven Rogers
# Kebab case
acme.my-project.person.first_name=Steven Rogers
# Underscore notation
acme.my_project.person.first_name=Steven Rogers
# 以下这种模式只能用于Environment变量在properites或yaml文件中无效
# ACME_MYPROJECT_PERSON_FIRSTNAME=Steven Rogers
Property SourceSimpleList
Properties FilesCamel case, kebab case, or underscore notationStandard list syntax using [ ] or comma-separated values
YAML FilesCamel case, kebab case, or underscore notationStandard YAML list syntax or comma separated values
Environment VariablesUpper case format with underscore as the delimiter. _ should not be used within a property nameNumeric values surrounded by underscores, such as MY_ACME_1_OTHER = my.acme[1].other
System propertiesCamel case, kebab case, or underscore notationStandard list syntax using [ ] or comma-separated values

对于一些复杂属性的写法,具体参照文档:Merging Complex Types

Properties Conversion 属性转换
Spring Boot attempts to coerce the external application properties to the right type when it binds to the @ConfigurationProperties beans. If you need custom type conversion, you can provide a ConversionService bean (with a bean named conversionService) or custom property editors (through a CustomEditorConfigurer bean) or custom Converters (with bean definitions annotated as @ConfigurationPropertiesBinding)  

从配置文件中的字符串最终转换为对应类中的属性,必须要通过类型转换,Spring Boot已经提供了必须的转换器。如果需要自定义类型转换器,有如下三种方式:

  1. 定义一个名称为conversionService的Bean
  2. 自定义属性编辑器(通过CustomEditorConfigurer)
  3. 自定义的转换器(通过标注有@ConfigurationPropertiesBinding的Bean)

比如:

@ConfigurationProperties(prefix = "acme.my-project.person")
public class OwnerProperties {
    private String firstName;
    private Duration sessionTimeout = Duration.ofSeconds(30);
    private Duration readTimeout = Duration.ofMillis(1000);
    private DataSize bufferSize = DataSize.ofMegabytes(2);
    private DataSize sizeThreshold = DataSize.ofBytes(512);
}
acme.my_project.person.first_name=Steven Rogers
# 单位为ms
acme.my_project.person.sessionTimeout=100ms
# 单位为s
acme.my_project.person.readTimeout=1s
# 单位为MB
acme.my_project.person.bufferSize=10MB
# 单位为KB
acme.my_project.person.sizeThreshold=1KB
OwnerProperties{firstName='Steven Rogers', sessionTimeout=PT0.1S, readTimeout=PT1S, bufferSize=10485760B, sizeThreshold=1024B}

源码分析:

当容易需要注入OwnerProperties类型Bean的时候最后会触发initializeBean方法:

调用栈如下:

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();

getBean(beanName);

return doGetBean(name, null, null, false);

sharedInstance = getSingleton(beanName, () -> {
	try {
		return createBean(beanName, mbd, args);
	}
	catch (BeansException ex) {
		// Explicitly remove instance from singleton cache: It might have been put there
		// eagerly by the creation process, to allow for circular reference resolution.
		// Also remove any beans that received a temporary reference to the bean.
		destroySingleton(beanName);
		throw ex;
	}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

singletonObject = singletonFactory.getObject();

// 调用getObject()会进入getSingleton的匿名内部类方法
return createBean(beanName, mbd, args);

Object beanInstance = doCreateBean(beanName, mbdToUse, args);

exposedObject = initializeBean(beanName, exposedObject, mbd);

进入到初始化bean的方法:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
		if (System.getSecurityManager() != null) {
			AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
				invokeAwareMethods(beanName, bean);
				return null;
			}, getAccessControlContext());
		}
		else {
			invokeAwareMethods(beanName, bean);
		}

		Object wrappedBean = bean;
		if (mbd == null || !mbd.isSynthetic()) {
            // 初始化前回调处理
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		try {
            // 调用初始化方法
			invokeInitMethods(beanName, wrappedBean, mbd);
		}
		catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}
		return wrappedBean;
	}

最后会遍历后置处理器,其中的ConfigurationPropertiesBindingPostProcessor就是负责进行属性绑定操作的。
在这里插入图片描述
此处使用的转换器为:BindConverter内部属性conversionService
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从上面可以看出,在BindConverter内部的转换器存在两个代理,遍历每个代理去查找转换器,如果能查找到转换器,则进行转换,首选被找到的转换器优先级最高,后续的转换器也不再起作用。

	@Override
	@Nullable
	public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
		Assert.notNull(targetType, "Target type to convert to cannot be null");
		if (sourceType == null) {
			Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
			return handleResult(null, targetType, convertNullSource(null, targetType));
		}
		if (source != null && !sourceType.getObjectType().isInstance(source)) {
			throw new IllegalArgumentException("Source to convert from must be an instance of [" +sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
		}
		GenericConverter converter = getConverter(sourceType, targetType);
		if (converter != null) {
            // 查找到转换器 通过ConversionUtils进行转换
			Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
			return handleResult(sourceType, targetType, result);
		}
		return handleConverterNotFound(source, sourceType, targetType);
	}
/**
 * Internal utilities for the conversion package.
 *
 * @author Keith Donald
 * @author Stephane Nicoll
 * @since 3.0
 */
abstract class ConversionUtils {

	@Nullable
	public static Object invokeConverter(GenericConverter converter, @Nullable Object source,
			TypeDescriptor sourceType, TypeDescriptor targetType) {

		try {
			return converter.convert(source, sourceType, targetType);
		}
		catch (ConversionFailedException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new ConversionFailedException(sourceType, targetType, source, ex);
		}
	}
}
/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.convert;

import java.util.Collections;
import java.util.Set;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.ObjectUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

/**
 * {@link Converter} to convert from a {@link String} to a {@link DataSize}. Supports
 * {@link DataSize#parse(CharSequence)}.
 *
 * @author Stephane Nicoll
 * @see DataSizeUnit
 */
final class StringToDataSizeConverter implements GenericConverter {

	@Override
	public Set<ConvertiblePair> getConvertibleTypes() {
		return Collections.singleton(new ConvertiblePair(String.class, DataSize.class));
	}

	@Override
	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		if (ObjectUtils.isEmpty(source)) {
			return null;
		}
		return convert(source.toString(), getDataUnit(targetType));
	}

	private DataUnit getDataUnit(TypeDescriptor targetType) {
		DataSizeUnit annotation = targetType.getAnnotation(DataSizeUnit.class);
		return (annotation != null) ? annotation.value() : null;
	}

	private DataSize convert(String source, DataUnit unit) {
		return DataSize.parse(source, unit);
	}
}

在这里插入图片描述
最后会调用DataSize的解析类

/**
 * The pattern for parsing.
 */
private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$");

public static DataSize parse(CharSequence text, @Nullable DataUnit defaultUnit) {
	Assert.notNull(text, "Text must not be null");
	try {
        // 验证字符串格式
		Matcher matcher = PATTERN.matcher(text);
		Assert.state(matcher.matches(), "Does not match data size pattern");
        // 判断单位 根据最后的单位
		DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit);
        // 转换数值
		long amount = Long.parseLong(matcher.group(1));
        // 根据数值和单位创建DataSize对象
		return DataSize.of(amount, unit);
	}
	catch (Exception ex) {
		throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex);
	}
}

在Converter类上打断点,以后有转换则会进入此处
在这里插入图片描述
此时使用的转换器(StringToDurationConverter)如下:

/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.convert;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.ObjectUtils;

/**
 * {@link Converter} to convert from a {@link String} to a {@link Duration}. Supports
 * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form.
 *
 * @author Phillip Webb
 * @see DurationFormat
 * @see DurationUnit
 */
final class StringToDurationConverter implements GenericConverter {

	@Override
	public Set<ConvertiblePair> getConvertibleTypes() {
		return Collections.singleton(new ConvertiblePair(String.class, Duration.class));
	}

	@Override
	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		if (ObjectUtils.isEmpty(source)) {
			return null;
		}
		return convert(source.toString(), getStyle(targetType), getDurationUnit(targetType));
	}

	private DurationStyle getStyle(TypeDescriptor targetType) {
		DurationFormat annotation = targetType.getAnnotation(DurationFormat.class);
		return (annotation != null) ? annotation.value() : null;
	}

	private ChronoUnit getDurationUnit(TypeDescriptor targetType) {
		DurationUnit annotation = targetType.getAnnotation(DurationUnit.class);
		return (annotation != null) ? annotation.value() : null;
	}

	private Duration convert(String source, DurationStyle style, ChronoUnit unit) {
		style = (style != null) ? style : DurationStyle.detect(source);
		return style.parse(source, unit);
	}
}
自定义conversionService名称的Bean
package com.example.durid.demo.config;

import org.springframework.boot.convert.DurationFormat;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.ObjectUtils;

import java.time.Duration;
import java.time.temporal.ChronoUnit;

@Configuration
public class ConversionConfig {

    @Bean
    public ConversionService conversionService() {
        return new ConversionService() {
            @Override
            public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
                return String.class == sourceType && Duration.class == targetType;
            }

            @Override
            public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
                if(canConvert(sourceType.getType(),targetType.getType())){
                    return true;
                }
                return false;
            }

            @Override
            public <T> T convert(Object source, Class<T> targetType) {
                String sourceStr = source.toString();
                return (T) DurationStyle.detect(sourceStr).parse(sourceStr,null);
            }

            @Override
            public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
                if (ObjectUtils.isEmpty(source)) {
                    return null;
                }
                return convert(source.toString(), getStyle(targetType), getDurationUnit(targetType));
            }

            private DurationStyle getStyle(TypeDescriptor targetType) {
                DurationFormat annotation = targetType.getAnnotation(DurationFormat.class);
                return (annotation != null) ? annotation.value() : null;
            }

            private ChronoUnit getDurationUnit(TypeDescriptor targetType) {
                DurationUnit annotation = targetType.getAnnotation(DurationUnit.class);
                return (annotation != null) ? annotation.value() : null;
            }

            private Duration convert(String source, DurationStyle style, ChronoUnit unit) {
                style = (style != null) ? style : DurationStyle.detect(source);
                return style.parse(source, unit);
            }
        };
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面可见,使用了自定义的转换器进行转换

自定义属性编辑器(通过CustomEditorConfigurer)

在配置类中添加如下bean定义

// 自定义CustomEditorConfigurer
@Bean
public CustomEditorConfigurer customEditorConfigurer() {
	CustomEditorConfigurer customEditorConfigurer = new CustomEditorConfigurer();
	Map<Class<?>, Class<? extends PropertyEditor>> customEditors = new HashMap<Class<?>, Class<? extends PropertyEditor>>();
	customEditors.put(Duration.class, DurationCustomEditor.class);
	customEditorConfigurer.setCustomEditors(customEditors);
	return customEditorConfigurer;
}
package com.example.durid.demo.custom;

import org.springframework.boot.convert.DurationFormat;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.core.convert.TypeDescriptor;

import java.beans.PropertyEditorSupport;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

/**
 * @author :guanglai.zhou
 * @date :Created in 2020/3/10 14:47
 * @description:
 */
public class DurationCustomEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        // 进行String到指定类型的转换
        Object source = convert(text,null,null);
        super.setSource(source);
    }

    private DurationStyle getStyle(TypeDescriptor targetType) {
        DurationFormat annotation = targetType.getAnnotation(DurationFormat.class);
        return (annotation != null) ? annotation.value() : null;
    }

    private ChronoUnit getDurationUnit(TypeDescriptor targetType) {
        DurationUnit annotation = targetType.getAnnotation(DurationUnit.class);
        return (annotation != null) ? annotation.value() : null;
    }

    private Duration convert(String source, DurationStyle style, ChronoUnit unit) {
        style = (style != null) ? style : DurationStyle.detect(source);
        return style.parse(source, unit);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面也可以看出,通过自定义属性编辑器的方式加入的编辑器优先级要高于自定义的conversionService。

自定义的转换器(通过标注有@ConfigurationPropertiesBinding的Bean)

首先在源码中查找该注解的使用案例

可以找到如下使用在org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration中定义

@Bean
@ConfigurationPropertiesBinding
public StringOrNumberToMigrationVersionConverter stringOrNumberMigrationVersionConverter() {
	return new StringOrNumberToMigrationVersionConverter();
}

可以看出StringOrNumberToMigrationVersionConverter实现了GenericConverter接口

自定义转换器

package com.example.durid.demo.custom;

import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.convert.DurationFormat;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.Set;

/**
 * @author :guanglai.zhou
 * @date :Created in 2020/3/10 15:14
 * @description:
 */
@Component
@ConfigurationPropertiesBinding
public class StringToDurationConverter implements GenericConverter {
    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        Set<ConvertiblePair> pairSet = new HashSet<ConvertiblePair>();
        pairSet.add(new ConvertiblePair(String.class, Duration.class));
        return pairSet;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (ObjectUtils.isEmpty(source)) {
            return null;
        }
        return doConvert(source.toString(), null, null);
    }
    
    private Duration doConvert(String source, DurationStyle style, ChronoUnit unit) {
        style = (style != null) ? style : DurationStyle.detect(source);
        return style.parse(source, unit);
    }
}

这种方式定义的转换器优先级最低,所以要注释掉上面两种方式定义的转换器或属性编辑器
在这里插入图片描述
在这里插入图片描述

@ConfigurationProperties Validation 验证

Spring Boot attempts to validate @ConfigurationProperties classes whenever they are annotated
with Spring’s @Validated annotation. You can use JSR-303 javax.validation constraint
annotations directly on your configuration class. To do so, ensure that a compliant JSR-303
implementation is on your classpath and then add constraint annotations to your fields, as shown in
the following example:

每当使用Spring的@Validated注释对@ConfigurationProperties类进行注释时,Spring Boot就会尝试对其进行验证。

您可以在配置类上直接使用JSR-303 javax.validation约束注释。 为此,请确保在类路径上有兼容的JSR-303实现,然后将约束注释添加到字段中,如以下示例所示:

@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
    @NotNull
    private InetAddress remoteAddress;
    // ... getters and setters
}

You can also trigger validation by annotating the @Bean method that creates the configuration
properties with @Validated.

您还可以通过使用@Validated注释创建配置属性的@Bean方法来触发验证。

To ensure that validation is always triggered for nested properties, even when no properties are found,
the associated field must be annotated with @Valid. The following example builds on the preceding
AcmeProperties example:

为了确保始终为嵌套属性触发验证,即使未找到任何属性,也必须使用@Valid注释关联的字段。 以下示例基于前面的AcmeProperties示例:

@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
    @NotNull
    private InetAddress remoteAddress;
    @Valid  // 使用@Vaild注释关联的字段
    private final Security security = new Security();
    // ... getters and setters
    public static class Security {
        @NotEmpty
        public String username;
        // ... getters and setters
    }
}

You can also add a custom Spring Validator by creating a bean definition called
configurationPropertiesValidator. The @Bean method should be declared static. The configuration properties validator is created very early in the application’s lifecycle, and declaring the
@Bean method as static lets the bean be created without having to instantiate the @Configuration
class. Doing so avoids any problems that may be caused by early instantiation.

@Component
@ConfigurationProperties(prefix = "sample")
@Validated
public class SampleProperties {

	/**
	 * Sample host.
	 */
	private String host;

	/**
	 * Sample port.
	 */
	private Integer port = 8080;

	public String getHost() {
		return this.host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public Integer getPort() {
		return this.port;
	}

	public void setPort(Integer port) {
		this.port = port;
	}

}
import java.util.regex.Pattern;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class SamplePropertiesValidator implements Validator {

	final Pattern pattern = Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");

	@Override
	public boolean supports(Class<?> type) {
		return type == SampleProperties.class;
	}

	@Override
	public void validate(Object o, Errors errors) {
		ValidationUtils.rejectIfEmpty(errors, "host", "host.empty");
		ValidationUtils.rejectIfEmpty(errors, "port", "port.empty");
		SampleProperties properties = (SampleProperties) o;
		if (properties.getHost() != null && !this.pattern.matcher(properties.getHost()).matches()) {
			errors.rejectValue("host", "Invalid host");
		}
	}

}
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;

@Component
public class SamplePropertyValidationRunner implements CommandLineRunner {

    private final SampleProperties properties;

    public SamplePropertyValidationRunner(SampleProperties properties) {
        this.properties = properties;
    }

    @Bean
    public static Validator configurationPropertiesValidator() {
        return new SamplePropertiesValidator();
    }

    @Override
    public void run(String... args) {
        System.out.println("=========================================");
        System.out.println("Sample host: " + this.properties.getHost());
        System.out.println("Sample port: " + this.properties.getPort());
        System.out.println("=========================================");
    }

}

在配置文件中配置参数

sample.host=192.168.0.1
sample.port=7070

输出结果为

2020-03-10 17:02:31.888  INFO 5196 --- [           main] com.example.durid.demo.DemoApplication   : Started DemoApplication in 19.291 seconds (JVM running for 26.164)
=========================================
Sample host: 192.168.0.1
Sample port: 7070
=========================================

修改配置文件参数

sample.host=xxxxxx
sample.port=7070

输出结果为


***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'sample' to com.example.durid.demo.properties.SampleProperties failed:

    Property: sample.host
    Value: xxxxxx
    Origin: class path resource [application.properties]:48:13
    Reason: null


Action:

Update your application's configuration
健康检查

首先增加配置:

# 健康监控配置
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

请求地址:http://localhost:8083/actuator/configprops
在这里插入图片描述

总结:通过使用ConfigurationProperties,可以将一类的属性提取到一起,并通过强类型的属性控制,还可以通过验证逻辑进行额外的验证,以及灵活的属性配置方式,驼峰、下划线等。 将ConfigurationProperties修饰的类作为一个Bean,在实例化的过程中通过后置处理器ConfigurationPropertiesBindingPostProcessor进行属性的绑定操作,其中还涉及到属性转换以及安全验证。

当在Spring Boot项目中使用@ConfigurationProperties注解时,可能会看到红色波浪线的警告。这通常是因为在当前类中没有添加@Component注解,或者@Component注解的作用范围不正确。 @ConfigurationProperties注解用于将配置文件中的属性绑定到一个Java对象上。为了使其正常工作,我们需要确保以下几点: 1. 在需要使用@ConfigurationProperties注解的类上添加@Component注解,以将其实例化到Spring容器中。 2. 确保@ConfigurationProperties注解所在的类被正确地扫描到。可以通过在主应用程序类上添加@ComponentScan注解来指定需要扫描的包。 3. 确保@ConfigurationProperties注解的作用范围正确。通常情况下,它应该标注在需要绑定属性的类或类的字段上。 根据您提供的引用内容,可以使用@Component注解来解决@ConfigurationProperties报红的问题。请确保在需要使用@ConfigurationProperties注解的类上添加@Component注解,并确保@Component所在的包被正确地扫描到。这样就可以解决@ConfigurationProperties报红的问题了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [@ConfigurationProperties报红](https://blog.csdn.net/weixin_45987928/article/details/123186290)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [解决@ConfigurationProperties注解红色波浪线问题](https://blog.csdn.net/weixin_43296313/article/details/120766097)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [SpringBoot @ConfigurationProperties使用详解](https://download.csdn.net/download/weixin_38607971/12743468)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值