在项目中使用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
搜索了一大堆资料,关于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,不得不说这是一个浩大的工程。