SpringBoot + Duration
背景: 在SpringBoot项目中,我们经常需要配置时间参数,作为某一动作的间隔。以往我们通常是在配置文件中定义字段后,直接设置对应的秒或毫秒值,遇到计算时,直接在此基础上做运算。这种方式虽然也能实现基本需求,但不够优美,有时我们更需要5s、4m、3h这样带单位的配置,这种方式更符合我们实际的需要。如果你经常看源码,就会发现SpringBoot中某些内部配置就是使用这种方式,此篇将在源码的基础上对此机制做简要分析。
格式化时间使用的核心是Duration对象
在实际使用时,我们在配置类中或其它类中定义Duration对象来接收此时间格式配置参数,此类代表时间间隔对象,除自身简单的API外,还可以与JAVA自身的相关事件API结合运算,如LocalDateTime。
LocalDateTime.now().plus([Duration]).atZone(ZoneId.systemDefault()).toInstant().getEpochSecond()
从字符串到Duration,是怎么处理的?有哪些注意事项?
熟悉Spring的话,我们知道框架在解析完类生成对应的BeanDefinition后,会将此类实例化和初始化,如果存在属性操作,会在createBean中调用其populateBean方法完成属性注入,其底层依赖AutowiredAnnotationBeanPostProcessor 等Processor完成此类操作。
public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 解析得到类中所有@Autowired修饰的变量元数据
InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
// 属性注入操作
metadata.inject(bean, beanName, pvs);
return pvs;
} catch (BeanCreationException var6) {
throw var6;
} catch (Throwable var7) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);
}
}
}
@Nullable
private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet(1);
Assert.state(AutowiredAnnotationBeanPostProcessor.this.beanFactory != null, "No BeanFactory available");
TypeConverter typeConverter = AutowiredAnnotationBeanPostProcessor.this.beanFactory.getTypeConverter(); // SimpleTypeConverter
Object value;
try {
// 获取属性值
value = AutowiredAnnotationBeanPostProcessor.this.beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
} catch (BeansException var12) {
throw new UnsatisfiedDependencyException((String)null, beanName, new InjectionPoint(field), var12);
}
*************省略************
return value;
}
}
在ContextAnnotationAutowireCandidateResolver解析类中,根据变量Field对象获取其注解列表,后抽取其具体配置值。
public Object getSuggestedValue(DependencyDescriptor descriptor) {
Object value = this.findValue(descriptor.getAnnotations());
if (value == null) {
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
value = this.findValue(methodParam.getMethodAnnotations());
}
}
return value;
}
protected Object findValue(Annotation[] annotationsToSearch) {
if (annotationsToSearch.length > 0) {
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
if (attr != null) {
return this.extractValue(attr);
}
}
return null;
}
protected Object extractValue(AnnotationAttributes attr) {
Object value = attr.get("value");
if (value == null) {
throw new IllegalStateException("Value annotation must have a value attribute");
} else {
return value;
}
}
上面说了变量参数解析与值获取的过程,但此时获取的是String对象,需要转换为Duration对象,是调用Duration的静态parse方法吗?
没看源码之前,我以为转换器就是执行此parse方法,但是这里有个问题,parse方法其实仅支持ISO8601标准,使用类似PT15M,我们配置的3M等参数不符合此类标准,直接使用会报错。下面看源码的使用:
// source即为我们配置的3M
// DurationStyle是枚举类,除ISO8601时间格式外,适配了简约模式,也就是我们参数配置的格式
DurationStyle.detect(source)
// unit可为null
Duration value = style.parse(source, unit);
当把String解析成Duration对象,就可以直接反射完成属性注入了。
InjectedElement element = (InjectedElement)var6.next();
element.inject(target, beanName, pvs);
总结:
1、借助Duration类,实现时间配置值单位的适配,不再被数字值实际单位所困扰
2、Duration实际时间格式有简约版,相比ISO8601更符合我们的使用习惯【具体参考DurationStyle源码】
3、时间格式中,M、H、S、D等单位同时支持大小写,原理是解析正则配置了Pattern.CASE_INSENSITIVE
4、上文简述了注入变量解析的时机、位置及流程
除了时间外,我们还会遇到一个常见配置和其类似,就是文件大小配置,此配置通常配置为字节数,但是我们也可指定单位为:KB、MB、GB等
multipart场景下代码示例:
@ConfigurationProperties(
prefix = "spring.servlet.multipart",
ignoreUnknownFields = false
)
public class MultipartProperties {
private boolean enabled = true;
private String location;
private DataSize maxFileSize = DataSize.ofMegabytes(1L);
private DataSize maxRequestSize = DataSize.ofMegabytes(10L);
private DataSize fileSizeThreshold = DataSize.ofBytes(0L);
private boolean resolveLazily = false;
}
可以看到各大小指标均被DataSize类型修饰,其parse方法涉及DataUnit枚举类,
public enum DataUnit {
BYTES("B", DataSize.ofBytes(1L)),
KILOBYTES("KB", DataSize.ofKilobytes(1L)),
MEGABYTES("MB", DataSize.ofMegabytes(1L)),
GIGABYTES("GB", DataSize.ofGigabytes(1L)),
TERABYTES("TB", DataSize.ofTerabytes(1L));
}
在内部转换器的支持下,配置文件中就可配置带单位的大小阈值了。
PS1:和时间Duration封装类不同,DataUnit不支持单位小写,也不支持K、M、G等简写形式,默认单位为字节数
PS2:@MultipartConfig注解和MultipartProperties字段名相同,但类型为基本数据类型,使用此注解不能设置单位