SpringBoot怎么把yml中的"1200"怼成Duration类型的

878 阅读3分钟

好奇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是有值的,那就看看这个这个值是怎么来的。 image.png

跳过所有的invoke方法,直接看Spring/SpringBoot的方法

org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty#setValue

能看到这是反射设置值了,继续往下点,看看这个value是哪来的 image.png

org.springframework.boot.context.properties.bind.JavaBeanBinder#bind 在这里转换完成的 image.png

看下这个bindProperty方法 image.png

喔噢,就是它完成的从配置文件到实体类属性的转换

看看这propertyBinder里都有什么 image.png

找到这configurationProperty,这就是在yml里配置的值

点开org.springframework.boot.context.properties.source.ConfigurationProperty,类,在getValue方法上下断点,看看在调用了

找到了,在org.springframework.boot.context.properties.bind.Binder#bindProperty方法里调用了 image.png

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)

image.png 还真是。

看看具体怎么实现的

逐步debug到org.springframework.boot.context.properties.bind.BindConverter.CompositeConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)方法里 image.png

可以看到这里遍历了List<ConversionService> delegates集合, 通过canConert方法判断是否能够把一个Integer转换为Duration image.png

继续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 image.png 找到了一个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,就可以这么干:

image.png

结束