好奇SpringBoot怎么把yml中的"1200"怼成Duration类型的,就DEBUG了一下
环境准备
application.yml
my:
read-timeout: 1200
session-timeout: 12s
MyProperties.java
@ConfigurationProperties(prefix = "my")
public class MyProperties {
@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);
private Duration readTimeout = Duration.ofMillis(1000);
public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
}
在setter上打上断点,启动SpringBoot
开始DEBUG(猜)
代码走到setter方法上了,打开看一下,参数readTimeout是有值的,那就看看这个这个值是怎么来的。
跳过所有的invoke方法,直接看Spring/SpringBoot的方法
org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty#setValue
能看到这是反射设置值了,继续往下点,看看这个value是哪来的
org.springframework.boot.context.properties.bind.JavaBeanBinder#bind
在这里转换完成的
看下这个bindProperty方法
喔噢,就是它完成的从配置文件到实体类属性的转换
看看这propertyBinder里都有什么
找到这configurationProperty,这就是在yml里配置的值
点开org.springframework.boot.context.properties.source.ConfigurationProperty,类,在getValue方法上下断点,看看在调用了
找到了,在org.springframework.boot.context.properties.bind.Binder#bindProperty方法里调用了
private <T> Object bindProperty(Bindable<T> target, Context context, ConfigurationProperty property) {
context.setConfigurationProperty(property);
Object result = property.getValue();
result = this.placeholdersResolver.resolvePlaceholders(result); // 这行是解析占位符,咱这没有,不管它
result = context.getConverter().convert(result, target); //这是重点
return result;
}
这个convert应该就是转换的一个方法,执行一下context.getConverter().convert(result, target),
还真是。
看看具体怎么实现的
逐步debug到org.springframework.boot.context.properties.bind.BindConverter.CompositeConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)方法里
可以看到这里遍历了List<ConversionService> delegates集合,
通过canConert方法判断是否能够把一个Integer转换为Duration
继续debug进到
org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)方法里
@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) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); // 这里执行转换的方法
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
进getConverter瞅一眼,逻辑挺简单,先创建一个缓存ConverterCacheKey(类里重写了eq和hashcode了)用key去缓存里找,找不到就从converters里找,无论converters有没有都会缓存起来
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
继续debug
找到了一个
org.springframework.boot.convert.NumberToDurationConverter转换器。
打开瞅瞅
final class NumberToDurationConverter implements GenericConverter {
private final StringToDurationConverter delegate = new StringToDurationConverter();
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Number.class, Duration.class)); // 转换器能处理的类型
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return this.delegate.convert((source != null) ? source.toString() : null, TypeDescriptor.valueOf(String.class),
targetType); // 这里调用了StringToDurationConverter进行转换,防止NPE好评
}
}
直接转了String然后丢给了StringToDurationConverter处理,那就再看看StringToDurationConverter
@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);
}
可以看到最后一个convert方法里通过匹配配置的字符串,拿到了合适的DurationStyle,然后把String转换成了Duration,并且这个DurationStyle枚举是可以通过DurationFormat注解配置在字段上的。
如果想把一个字符串/数字直接转成Duration,就可以这么干: