在项目中使用Spring boot开发微服务,需要从application.properties读取一个配置项,示例如下:

    @Value("${test.boolean}")
    private boolean testBoolean;

结果一次升级过程中,误将配置项test.boolean的值写成空字符串"",直接导致服务启动失败!
启动异常信息:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testController': Unsatisfied dependency expressed through field 'testBoolean'; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'boolean'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value []
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1411) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:845) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141) ~[spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744) [spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391) [spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) [spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204) [spring-boot-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at com.example.demo.DemoApplication.main(DemoApplication.java:17) [classes/:na]
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'boolean'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value []
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1199) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1171) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:593) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	... 19 common frames omitted
Caused by: java.lang.IllegalArgumentException: Invalid boolean value []
	at org.springframework.beans.propertyeditors.CustomBooleanEditor.setAsText(CustomBooleanEditor.java:154) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:429) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:402) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:155) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73) ~[spring-beans-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	... 22 common frames omitted

@Value原理

搜索了一大堆资料,关于Spring @Value原理描述最清楚的应该是这篇博客:springboot中@Value的工作原理,非常感谢作者。看了这篇博客,基本上对@Value的原理和过程有个大概理解。因为我的示例中使用到了类型转换,从异常堆栈看,是String到boolean的类型转换抛出异常了。
直接查看Spring framework社区的CustomBooleanEditor.java源码,输入text为空字符串的情况下,如果allowEmpty是true,则可以转换成null,否则抛出IllegalArgumentException异常。

	@Override
	public void setAsText(@Nullable String text) throws IllegalArgumentException {
		String input = (text != null ? text.trim() : null);
		if (this.allowEmpty && !StringUtils.hasLength(input)) {
			// Treat empty String as null value.
			setValue(null);
		else if (this.trueString != null && this.trueString.equalsIgnoreCase(input)) {
			setValue(Boolean.TRUE);
		else if (this.falseString != null && this.falseString.equalsIgnoreCase(input)) {
			setValue(Boolean.FALSE);
		else if (this.trueString == null &&
				(VALUE_TRUE.equalsIgnoreCase(input) || VALUE_ON.equalsIgnoreCase(input) ||
						VALUE_YES.equalsIgnoreCase(input) || VALUE_1.equals(input))) {
			setValue(Boolean.TRUE);
		else if (this.falseString == null &&
				(VALUE_FALSE.equalsIgnoreCase(input) || VALUE_OFF.equalsIgnoreCase(input) ||
						VALUE_NO.equalsIgnoreCase(input) || VALUE_0.equals(input))) {
			setValue(Boolean.FALSE);
		else {
			throw new IllegalArgumentException("Invalid boolean value [" + text + "]");

问题倒是明白了。
可为什么Spring没有把allowEmpty默认设置为false?如果设置true,健壮性不是更好吗?继续深挖。。。

CustomBooleanEditor的allowEmpty来自于构造函数入参。

public CustomBooleanEditor(boolean allowEmpty) {

那Spring framework则是什么时候构造CustomBooleanEditor并设置allowEmpty=false呢,又是一顿搜索,原来默认的转换方式定义在PropertyEditorRegistrySupport类中createDefaultEditors方法内。

* Actually register the default editors for this registry instance. private void createDefaultEditors() { this.defaultEditors = new HashMap<Class<?>, PropertyEditor>(64); // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));

从createDefaultEditors的代码中发现了一个新情况,Spring framework对于基础类型boolean注册的CustomBooleanEditor转换器是不允许空串转换,但是对于包装类型Boolean则是允许空串转换的。
好大一个坑啊!
不仅boolean和Boolean,所有基础类型和包装类型都是类似的:

		// The JDK does not contain a default editor for char!
		this.defaultEditors.put(char.class, new CharacterEditor(false));
		this.defaultEditors.put(Character.class, new CharacterEditor(true));
		// Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
		this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
		this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
		// The JDK does not contain default editors for number wrapper types!
		// Override JDK primitive number editors with our own CustomNumberEditor.
		this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
		this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
		this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
		this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
		this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
		this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
		this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
		this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
		this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
		this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
		this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
		this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
		this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
		this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));

实在不明白Spring framework为什么做这样的区分。。。

不用基础类型

第一个办法就是用到@Value注解且需要类型转换时,优先使用包装类型。

自定义转换器

参考两篇StackOverFlow的文章(https://stackoverflow.com/questions/12266050/register-many-property-editors,https://stackoverflow.com/questions/26063171/spring-value-property-for-custom-class),可以自定义boolean转换器,并注册到spring的custom editor中。

import org.springframework.beans.propertyeditors.CustomBooleanEditor
class MyBooleanEditor extends CustomBooleanEditor {
    MyBooleanEditor() {
        super(true)
    void setAsText(String text) {
        try {
            super.setAsText(text);
        } catch (Exception e) {
            setValue(false);
@Bean
public CustomEditorConfigurer customEditorConfigurer() {
    Map<Class<?>, Class<? extends PropertyEditor>> customEditors = 
            new HashMap<Class<?>, Class<? extends PropertyEditor>>(1);
    customEditors.put(boolean.class, MyBooleanEditor.class);
    CustomEditorConfigurer configurer = new CustomEditorConfigurer();
    configurer.setCustomEditors(customEditors);
    return configurer;

自定义转换器的好处是除了做到空串转换,还可以对非法字符串转换做保护。
但是类型转换器除了用在@Value注解中,有可能在Spring framework其他地方都在使用,确定要替换默认类型转换器前一定要弄清楚影响范围。

自定义注解

假如对@Value不满意,可以自定义一个实现增强功能的注解,例如@SecValue。可以参考SpringBoot之自定义注解(基于BeanPostProcessor接口实现)这篇博客。
不过,要达到@Value的能力,能从application.properties读取,能实现类型转换,基本上等于重新实现Spring framework的AutowiredAnnotationBeanPostProcessor,不得不说这是一个浩大的工程。

Spring @Value引发的一次“血案”“血案”在项目中使用Spring boot开发微服务,需要从application.properties读取一个配置项,示例如下: @Value("${test.boolean}") private boolean testBoolean;结果一次升级过程中,误将配置项test.boolean的值写成空字符串"",直接导致服务启动... 昨天在开发过程中,遇到一个比较无语的问题:重新拉一份代码搭框架,在启动的时候读取properties配置文件报类型转换错误,因为代码是一样的,所以排除代码的问题,在百度谷歌搜索了一遍没有找到能够解决的办法。现已解决,给大家分享解决办法,希望能帮助各位。 先贴上报错信息: Unsatisfied dependency expressed through field 'testWhileI...
@Autowire可以用来注入bean依赖,@Value则用于注入属性值。本章介绍@Value的使用方法,属性值来源以及属性值类型转换的原理。 回顾PropertySourcesPlaceholderConfigurer 在介绍BeanFactoryPostProcessor的时候,我们讲过,可以通过向容器注册一个PropertySourcesPlaceholderConfigurer来替换属性值...
查资料源码 太复杂了不想看,因为其他项目也使用过@Value,赋值是正常的,然后对比发现springboot版本不一致,原来使用4.3.8,现在的项目使用4.2.5,更换版本后赋值成功,boot更新换代这么快跟不上步伐啊。 Spring 的 @Vaule 注解提供了一种便捷的方法可以让属性值注入到组件中,当属性值不存在的时候提供一个默认值也是非常好用的 这就是我们这篇文章所专注的,如何给 @Vaule 注解指定一个默认值。对于更多的关于 @Vaule 的教程看 这篇文章 2.String 默认值 让我们看看对于 String 类型的值,给定一个默认值得基础语法 @Value("${some.key:my default value}") private String stringWithDefaultValue; 对于从事java开发工作的小伙伴来说,spring框架肯定再熟悉不过了。spring给开发者提供了非常丰富的api,满足我们日常的工作需求。 如果想要创建bean实例,可以使用@Controller、@Service、@Repository、@Component等注解。 如果想要依赖注入某个对象,可以使用@Autowired和@Resource注解。 如果想要开启事务,可以使用@Transactional注解。 如果想要动态读取配置文件中的某个系统属性,可以使用@Value注解。 等等,还有很多。。。
Springboot项目打包启动后报错org.springframework.beans.factory.UnsatisfiedDependencyException描述Mac 电脑上idea启动日志Mac 电脑上打包后命令启动日志 项目pull下来后idea启动正常(macOS Big Sur),打包部署后启动一直失败,切换window10拉取项目启动正常,打包部署确正常启动。现在搞得部署项目得去window上打包部署,现在(mac)除了打包部署后启动失败 其它正常。 mvn clean inst
既然上面已经创建了springboot项目,pom文件里面也导入了redis的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency&g
`@Value` 是 Spring 框架中的一个注解,用于从配置文件中获取值并注入到对应的属性中。其使用方式如下: 1. 在需要注入值的属性上添加 `@Value` 注解。 2. 在 `@Value` 注解中指定属性对应的配置文件中的键值,例如 `@Value("${jdbc.url}")`。 3. 在 Spring 配置文件中定义对应的键值对,例如 `jdbc.url=jdbc:mysql://localhost:3306/test`。 示例代码如下: ```java @Component public class MyComponent { @Value("${myapp.title}") private String appTitle; public String getAppTitle() { return appTitle; public void setAppTitle(String appTitle) { this.appTitle = appTitle; 在上面的代码中,`@Value("${myapp.title}")` 表示从配置文件中获取名为 `myapp.title` 的属性值,并将其注入到 `appTitle` 属性中。在 Spring 配置文件中,可以定义如下键值对: ```yaml myapp.title: "My Application" 在这个例子中,`appTitle` 属性将被注入值 `"My Application"`。 另外,`@Value` 注解还支持 SpEL 表达式,可以在注解中使用表达式来计算属性值。例如: ```java @Value("#{T(java.lang.Math).PI}") private double pi; 在这个例子中,属性将被注入值 `3.141592653589793`,这是通过 SpEL 表达式 `#{T(java.lang.Math).PI}` 计算得出的。