Spring Boot启动时如何初始化Jackson以提升首次请求响应速度?
解决方案
核心失效原因
- Jackson默认对每个需要序列化/反序列化的Java类型独立生成
JsonSerializer/JsonDeserializer实例,采用懒加载机制,只有首次用到对应类型时才会生成并缓存,之前只调用少量测试接口只能覆盖有限类型,无法覆盖全量业务DTO - 若项目存在多
ObjectMapper实例配置,直接注入的ObjectMapper可能和Spring MVC的MappingJackson2HttpMessageConverter内部持有的实例不是同一个,预热错实例自然不会生效 - 手动调用
ObjectMapper序列化的操作如果没有主动触发反序列化器的初始化,对应接口的请求参数解析阶段还是会触发首次初始化
通用预热实现方案
不需要新增任何业务测试接口,通过扫描全量业务DTO+主动触发Jackson元数据初始化即可实现全覆盖,代码实现如下:
import org.springframework.context.event.EventListener; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Objects; @Component public class JacksonPreheatConfig { @Autowired private MappingJackson2HttpMessageConverter jsonHttpMessageConverter; @EventListener(ApplicationReadyEvent.class) public void preheatJackson() throws Exception { // 替换为实际业务DTO所在的包路径,支持配置多个包 List<String> dtoPackageList = List.of("com.xxx.biz.dto", "com.xxx.common.domain"); ObjectMapper mapper = jsonHttpMessageConverter.getObjectMapper(); for (String basePackage : dtoPackageList) { List<Class<?>> dtoClasses = scanPackageClasses(basePackage); for (Class<?> dtoClazz : dtoClasses) { // 主动触发当前类的序列化器初始化 mapper.getSerializerProvider().findValueSerializer(dtoClazz); // 主动触发当前类的反序列化器初始化 mapper.getDeserializationContext().findRootValueDeserializer( mapper.getTypeFactory().constructType(dtoClazz) ); // 若存在通用泛型包装类如Result<T>、PageResult<T>,取消注释下面的逻辑预热泛型类型 // Class<?> wrapperClazz = Result.class; // mapper.getSerializerProvider().findValueSerializer( // mapper.getTypeFactory().constructParametricType(wrapperClazz, dtoClazz) // ); } } } /** * 扫描指定包下所有Class类 * 也可引入org.reflections:reflections工具包简化扫描逻辑 */ private List<Class<?>> scanPackageClasses(String basePackage) throws IOException, ClassNotFoundException, URISyntaxException { List<Class<?>> classList = new ArrayList<>(); String packagePath = basePackage.replace('.', '/'); Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(packagePath); while (resources.hasMoreElements()) { URL url = resources.nextElement(); File packageDir = new File(url.toURI()); for (File classFile : Objects.requireNonNull(packageDir.listFiles(f -> f.getName().endsWith(".class")))) { String className = basePackage + "." + classFile.getName().replace(".class", ""); classList.add(Class.forName(className)); } } return classList; } }
注意事项
- 如果项目中存在内部类DTO、继承体系复杂的DTO,上述扫描逻辑需要做对应调整,也可通过自定义
@JacksonPreheat注解标记需要预热的类,扫描时只取标记了注解的类即可 - 若业务接口使用了
@RequestBody接收集合/泛型类型参数,需要在预热逻辑中补充对应集合类型、泛型类型的初始化逻辑 - 预热逻辑只会增加应用启动耗时,不会影响运行期性能,启动完成后所有接口的首次请求和后续请求耗时完全一致
内容的提问来源于stack exchange,提问作者Sebi
相关产品推荐
相关产品推荐

