如何在 Spring Boot 应用程序中预编译 jsp?

Posted

技术标签:

【中文标题】如何在 Spring Boot 应用程序中预编译 jsp?【英文标题】:How to precompile jsp in a spring boot application? 【发布时间】:2016-04-28 21:27:08 【问题描述】:

我使用的是 Spring boot,之前我们使用的是 Spring 和 Tomcat。 两年前我们使用Spring和Tomcat的时候,我们使用了一个maven插件来预编译jsp。 避免在部署后的每次首次访问时都进行此编译非常有用。

然而,我们所知道的所有 maven 插件都会转储一个 web.xml 文件,其中列出了所有 jsp 和相关的生成的 servlet。 使用 Spring boot,它不再使用 web.xml,因此该文件被忽略。

我们仍然有汇编,这是一条安全带,但每次首次访问每个页面都会受到处罚。

有人知道是否可以在 Spring Boot 应用程序中预编译 jsp 吗?

【问题讨论】:

您创建的ServletRegistrationBeans 等同于web.xml 中的条目。手动操作的实用程度取决于您拥有多少 JSP。 那些 servlet 将由 maven 插件生成。所以,要么插件也能够生成这个文件(这是有道理的),要么这个 bean 能够列出类路径中的所有 servlet 类(但我不知道如何) 我建议您使用现有插件来执行编译,然后为它生成的每个 Servlet 手动创建 ServletRegistration bean 配置。不过,这很可能是不切实际的。或者,如果编译后的Servlets 带有@WebServlet 注释,您可以使用Boot 的@ServletComponentScan 自动找到它们。 第二种解决方案似乎很有趣。我会尝试这样的。感谢您的建议 【参考方案1】:

我在服务器启动时(不必使用 JspC,所以构建文件更简单)和构建时(更快的服务器启动时间)都进行了预编译。我动态注册生成的 servlet,因此如果您添加/删除 JSP,您不必手动更改任何文件。

在服务器启动时

使用ServletRegistration.Dynamic 为每个JSP 注册一个JSP_SERVLET_CLASS Servlet。 使用initParameterjspFile设置JSP文件名(ref)

例如对于 ServletContextInitializer (ref) 中的 SpringBoot:

@Bean
public ServletContextInitializer preCompileJspsAtStartup() 
    return servletContext -> 
        getDeepResourcePaths(servletContext, "/WEB-INF/jsp/").forEach(jspPath -> 
            log.info("Registering JSP: ", jspPath);
            ServletRegistration.Dynamic reg = servletContext.addServlet(jspPath, Constants.JSP_SERVLET_CLASS);
            reg.setInitParameter("jspFile", jspPath);
            reg.setLoadOnStartup(99);
            reg.addMapping(jspPath);
        );
    ;


private static Stream<String> getDeepResourcePaths(ServletContext servletContext, String path) 
    return (path.endsWith("/")) ? servletContext.getResourcePaths(path).stream().flatMap(p -> getDeepResourcePaths(servletContext, p))
            : Stream.of(path);

在构建时

使用JspC (ref) 为每个 JSP 和带有它们的 servlet 映射的 web.xml 生成 Java 源文件。

然后使用ServletContext 注册这些(通过使用Tomcat 的WebXmlParser 解析web.xml,例如对于SpringBoot:

@Value("classpath:precompiled-jsp-web.xml")
private Resource precompiledJspWebXml;

@Bean
public ServletContextInitializer registerPreCompiledJsps() 
    return servletContext -> 
        // Use Tomcat's web.xml parser (assume complete XML file and validate).
        WebXmlParser parser = new WebXmlParser(false, true, true);
        try (InputStream is = precompiledJspWebXml.getInputStream()) 
            WebXml webXml = new WebXml();
            boolean success = parser.parseWebXml(new InputSource(is), webXml, false);
            if (!success) 
                throw new RuntimeException("Error parsing Web XML " + precompiledJspWebXml);
            
            for (ServletDef def :  webXml.getServlets().values()) 
                log.info("Registering precompiled JSP:  =  -> ", def.getServletName(), def.getServletClass());
                ServletRegistration.Dynamic reg = servletContext.addServlet(def.getServletName(), def.getServletClass());
                reg.setLoadOnStartup(99);
            

            for (Map.Entry<String, String> mapping : webXml.getServletMappings().entrySet()) 
                log.info("Mapping servlet:  -> ", mapping.getValue(), mapping.getKey());
                servletContext.getServletRegistration(mapping.getValue()).addMapping(mapping.getKey());
            
         catch (IOException e) 
            throw new RuntimeException("Error registering precompiled JSPs", e);
        
    ;

生成和编译 JSP 类的示例 Maven 配置,并生成 precompiled-jsp-web.xml

<!-- Needed to get the jasper Ant task to work (putting it in the plugin's dependencies didn't work) -->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina-ant</artifactId>
    <version>8.0.32</version>
    <scope>provided</scope>
</dependency>

<!-- ... -->

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.8</version>
    <executions>
        <execution>
            <id>precompile-jsp-generate-java</id>
            <!-- Can't be generate-sources because we need the compiled Henry taglib classes already! -->
            <phase>compile</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <tasks>
                    <echo message="Precompiling JSPs"/>
                    <property name="compile_classpath" refid="maven.compile.classpath"/>
                    <property name="target_dir" value="$project.basedir/generated-sources/jspc" />
                    <path id="jspc_classpath">
                        <path path="$compile_classpath"/>
                    </path>

                    <typedef resource="org/apache/catalina/ant/catalina.tasks" classpathref="jspc_classpath"/>

                    <mkdir dir="$target_dir/java"/>
                    <mkdir dir="$target_dir/resources"/>
                    <jasper
                            validateXml="false"
                            uriroot="$project.basedir/src/main/webapp"
                            compilertargetvm="1.8"
                            compilersourcevm="1.8"
                            failonerror="true"
                            javaencoding="UTF-8"
                            webXml="$target_dir/resources/precompiled-jsp-web.xml"
                            outputDir="$target_dir/java/" >
                    </jasper>
                    <!-- Can't use Maven to compile the JSP classes because it has already compiled the app's classes
                         (needed to do that becuase JspC needs compiled app classes) -->
                    <javac srcdir="$target_dir/java" destdir="$project.build.outputDirectory" classpathref="jspc_classpath" fork="true"/>
                    <!-- Have to copy the web.xml because process-resources phase has already finished (before compile) -->
                    <copy todir="$project.build.outputDirectory">
                        <fileset dir="$target_dir/resources"/>
                    </copy>
                </tasks>
            </configuration>
        </execution>
    </executions>
</plugin>
<!-- Not strictly necessary, because Ant does the compilation, but at least attempts to keep it in sync with Maven -->
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>add-precompiled-jsp-java-sources</id>
            <phase>generate-sources</phase>
            <goals><goal>add-source</goal></goals>
            <configuration>
                <sources>
                    <source>$project.basedir/generated-sources/jspc/java</source>
                </sources>
            </configuration>
        </execution>
        <execution>
            <id>add-precompiled-jsp-resources</id>
            <phase>generate-resources</phase>
            <goals><goal>add-resource</goal></goals>
            <configuration>
                <resources>
                    <resource>
                        <directory>$project.basedir/generated-sources/jspc/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

【讨论】:

你是个天才,我希望我能给这篇文章投票 10 倍 不适用于我的 spring-boot 2.1 和 tomcat-version 9.0.12。由于 未正确关闭,catalina ant 进程正在生成无效的 xml。起始标签缺少结束“>”。奇怪...【参考方案2】:

基于 paulcm 的出色回答,我想出了自己的解决方案,因为上述解决方案对我不起作用,我无法追踪错误。也许上面的答案对于tomcat9来说已经过时了。或者多模块设置有问题。但是:所有学分都属于paulcm

这只是编译时的解决方案。

将这两个插件添加到您的 pom.xml 中

<plugin>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-jspc-maven-plugin</artifactId>
  <version>9.4.15.v20190215</version>
  <executions>
    <execution>
      <id>jspc</id>
      <goals>
        <goal>jspc</goal>
      </goals>
      <configuration>
        <mergeFragment>true</mergeFragment>
        <sourceVersion>1.8</sourceVersion>
        <targetVersion>1.8</targetVersion>
      </configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <configuration>
    <webXml>$project.basedir/target/web.xml</webXml>
  </configuration>
</plugin>

添加一个空的 web.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
   version="4.0"
   metadata-complete="true">
  <session-config>
    <cookie-config>
    </cookie-config>
  </session-config>
</web-app>

添加注册表

import org.apache.tomcat.util.descriptor.web.ServletDef;
import org.apache.tomcat.util.descriptor.web.WebXml;
import org.apache.tomcat.util.descriptor.web.WebXmlParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.xml.sax.InputSource;

import javax.servlet.ServletRegistration;
import java.io.InputStream;
import java.util.Map;

@Configuration
public class PreCompileJspRegistry 

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Bean
    public ServletContextInitializer registerPreCompiledJsps() 
        return servletContext -> 
            InputStream inputStream = servletContext.getResourceAsStream("/WEB-INF/web.xml");
            if (inputStream == null) 
                logger.info("Could not read web.xml");
                return;
            
            try 
                WebXmlParser parser = new WebXmlParser(false, false, true);
                WebXml webXml = new WebXml();
                boolean success = parser.parseWebXml(new InputSource(inputStream), webXml, false);
                if (!success) 
                    logger.error("Error registering precompiled JSPs");
                
                for (ServletDef def : webXml.getServlets().values()) 
                    logger.info("Registering precompiled JSP:  =  -> ", def.getServletName(), def.getServletClass());
                    ServletRegistration.Dynamic reg = servletContext.addServlet(def.getServletName(), def.getServletClass());
                    reg.setLoadOnStartup(99);
                

                for (Map.Entry<String, String> mapping : webXml.getServletMappings().entrySet()) 
                    logger.info("Mapping servlet:  -> ", mapping.getValue(), mapping.getKey());
                    servletContext.getServletRegistration(mapping.getValue()).addMapping(mapping.getKey());
                
             catch (Exception e) 
                logger.error("Error registering precompiled JSPs", e);
            
        ;
    

【讨论】:

我相信这是针对 Spring Boot,您对如何为 Spring MVC 实施此解决方案有任何想法/提示吗?我试过这个,但得到“ java.lang.UnsupportedOperationException:Servlet 3.0 规范的第 4.4 节不允许从 web.xml 中未定义的 ServletContextListener 调用此方法,web-fragment.xml 文件也没有用 @ 注释WebListener “当我在 Spring 5 MVC 中坚持使用“@WebListener”时 我为 WebMvcConfigurer 实现添加了“@WebListener”【参考方案3】:

上面概述的“在服务器启动时”的注释:如果应用程序打包在可执行 jar 中,您创建的 servlet 默认处于开发模式,因此如果您在生产模式下使用它,您还应该设置development = false ++ 防止jsps再次被编译:

reg.setInitParameter("genStringAsCharArray", "true");
reg.setInitParameter("trimSpaces", "true");
reg.setInitParameter("development", "false");

【讨论】:

以上是关于如何在 Spring Boot 应用程序中预编译 jsp?的主要内容,如果未能解决你的问题,请参考以下文章

我们如何在 Cheetah 中预编译基本模板,以便 #include、#extends 和 #import 在 Weby 中正常工作

如何在 ASP.NET Core 2.0 中预编译视图?

C中预编译详解

rails4 rake assets 在生产环境中预编译生成错误的javascript文件

在spring boot3中使用native image

浅谈VC++中预编译的头文件放那里的问题分析