欢迎关注公众号:冬瓜白
相关文章:
- 多模式 Web 应用开发记录<一>背景&全局变量优化
 - 多模式 Web 应用开发记录<二>自己动手写一个 Struts
 - 多模式 Web 应用开发记录<三>预初始化属性
 
经过一段时间的开发和测试,我们成功地将项目从 “Spring + FreeMarker + Struts2 + Resin” 迁移到了 “Spring Boot + Spring Web MVC + Embedded Tomcat + FreeMarker”。但是在上线后发现了一个问题:视图首次渲染的速度非常慢,甚至可以达到数十秒。

使用 Trace 工具,发现大部分时间都耗费在了 Render 操作上。在 FreeMarker 中,Render 是指将数据模型和模板文件结合起来,生成最终的输出结果的过程。

这种时候使用 Arthas Trace 要找出造成渲染慢的底层原因还是相当困难的,但是可以使用 Async-profiler 采样第一次请求的火焰图,但是这里有个细节,在进行火焰图采样之前,可以先请求其他的非涉及 FreeMarker 渲染的接口,避免受到 Spring MVC 底层资源初始化的影响。
我选择了一个相对“干净”的 FreeMarker 渲染接口进行采样,这个接口没有复杂的业务逻辑,仅仅是渲染一个视图。采样结果显示,大量的 CPU 时间都花费在了 org.apache.catalina.webresources.JarWarResourceSet.getArchiveEntries 上。这可能是因为 Tomcat 在处理 FreeMarker 视图时需要加载大量的资源,这些资源主要包括 FreeMarker 模板和静态资源。

针对这个问题,有三种可能的优化策略:
- 减少资源数量:可以尝试减少 FreeMarker 模板和静态资源的数量,但是这种方法并不现实。考虑到我们的项目背景,我们并不想在前端处理上花费太多的精力。
 - 预加载资源:可以在应用启动时预加载一些资源。
 - 使用 JAR 文件:一般来说,从 JAR 文件加载资源通常比从 WAR 文件加载资源更快。
 
由于服务已经使用了 JAR 文件,所以我决定采用“资源预加载”的策略进行优化。
那么,如何进行资源预加载呢?我的思路是:提前渲染那些需要渲染的 FreeMarker 视图。换句话说,提前执行那些执行速度较慢的操作。这种思路其实在很多框架的预处理和预请求优化中都有应用。可以使用 Spring 提供的 MockHttpServletRequest 来实现这个目标。
首先定义一个注解,用来标注需要提前预加载的视图:
/**
 * @author dongguabai
 * @date 2024-04-16 21:38
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreloadOnStartup {
}
 
在 Spring Boot 启动后异步并发预加载:
/**
 * @author dongguabai
 * @date 2024-04-17 10:31
 */
@Component
public class Preloader implements ApplicationRunner {
    private static final Logger log = LoggerFactory.getLogger(Preloader.class);
    private final ApplicationContext applicationContext;
    private final RequestMappingHandlerAdapter handlerAdapter;
    private final RequestMappingHandlerMapping handlerMapping;
    private final ViewResolver viewResolver;
    private static final int QUEUE_SIZE = 200;
    public Preloader(ApplicationContext applicationContext,
                     RequestMappingHandlerAdapter handlerAdapter,
                     RequestMappingHandlerMapping handlerMapping,
                     ViewResolver viewResolver) {
        this.applicationContext = applicationContext;
        this.handlerAdapter = handlerAdapter;
        this.handlerMapping = handlerMapping;
        this.viewResolver = viewResolver;
    }
    @Override
    public void run(ApplicationArguments args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        try {
            Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
            for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
                RequestMappingInfo mappingInfo = entry.getKey();
                HandlerMethod handlerMethod = entry.getValue();
                Method method = handlerMethod.getMethod();
                if (method.isAnnotationPresent(PreloadOnStartup.class)) {
                    Set<RequestMethod> methods = mappingInfo.getMethodsCondition().getMethods();
                    Set<String> patterns = mappingInfo.getPatternsCondition().getPatterns();
                    RequestMethod requestMethod = methods.isEmpty() ? RequestMethod.POST : methods.iterator().next();
                    for (String pattern : patterns) {
                        executorService.execute(() -> {
                            try {
                                MockHttpServletRequest request = new MockHttpServletRequest(requestMethod.name(), pattern);
                                Parameter[] methodParams = method.getParameters();
                                for (Parameter param : methodParams) {
                                    Class<?> type = param.getType();
                                    Object value = null;
                                    if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
                                        value = type.getConstructor().newInstance();
                                    }
                                    request.addParameter(param.getName(), value != null ? value.toString() : null);
                                }
                                MockHttpServletResponse response = new MockHttpServletResponse();
                                ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
                                RequestContextHolder.setRequestAttributes(attributes);
                                HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
                                Object handler = handlerExecutionChain.getHandler();
                                ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
                                String viewName = modelAndView.getViewName();
                                View view = viewResolver.resolveViewName(viewName, Locale.getDefault());
                                view.render(modelAndView.getModel(), request, response);
                                log.info("Successfully loaded method: {}", method.getName());
                            } catch (Exception e) {
                                log.error("Failed to load method: {}", method.getName(), e);
                            } finally {
                                RequestContextHolder.resetRequestAttributes();
                            }
                        });
                    }
                }
            }
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                    log.warn("Preloader did not finish within 60 seconds");
                }
            } catch (InterruptedException e) {
                log.error("Preloader was interrupted", e);
                Thread.currentThread().interrupt();
            }
        } finally {
            executorService.shutdownNow();
        }
    }
}
 
看下效果对比:
| 优化前 | 优化后 | 
|---|---|
| 4s+ | 0.2s+ | 
总结
本文探讨了一个在项目迁移过程中遇到的性能问题:FreeMarker 视图首次渲染速度慢。
使用了 Trace 分析和 Async-profiler 工具定位到问题后,然后提出了三种可能的优化策略。最终选择了预加载资源的策略,并使用 Spring 提供的 MockHttpServletRequest 来实现。优化结果非常显著,首次渲染的时间从4秒以上降低到了0.2秒以上。



















