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

12
我正在使用Spring Boot,并且以前使用Spring和Tomcat。两年前,我们在使用Spring和Tomcat时,使用了一个Maven插件来预编译JSP。这对于避免每次部署后的第一次访问进行此编译非常有用。
然而,我们所知道的所有Maven插件都会转储一个web.xml文件,列出所有JSP和相关生成的servlet。在Spring Boot中,它不再使用web.xml,因此忽略此文件。
我们仍然需要编译,但这会影响每个页面的首次访问速度。
是否有人知道在Spring Boot应用程序中是否可以预编译JSP?

您可以创建与 web.xml 条目等效的 ServletRegistrationBean。手动完成这个过程的实际可行性取决于您有多少 JSP。 - Andy Wilkinson
这些servlet将由Maven插件生成。因此,要么插件也能够生成此文件(这是有意义的),要么此bean能够列出类路径中的所有servlet类(但我不知道如何实现)。 - Hugo Lassiège
我建议您使用现有的插件进行编译,然后手动为每个生成的“Servlet”创建ServletRegistration bean配置。虽然这可能不切实际。或者,如果编译的“Servlet”带有@WebServlet注解,您可以使用Boot的@ServletComponentScan自动发现它们。 - Andy Wilkinson
第二个解决方案似乎很有趣。我会尝试那种方式。感谢您的建议。 - Hugo Lassiège
3个回答

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

在服务器启动时

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

例如,在ServletContextInitializer参考资料)中使用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参考文献)为每个JSP生成Java源文件,并使用它们的Servlet映射生成web.xml

然后使用Tomcat的WebXmlParser解析web.xml,并将其与ServletContext注册,例如对于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);
        }
    };
}

示例Maven配置生成和编译JSP类,并生成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>

6
你是个天才,我希望我能给这篇文章点赞十次。 - Hiro2k
1
在我的spring-boot 2.1和tomcat版本9.0.12中无法工作。catalina ant进程生成的xml无效,因为<web-app>没有正确关闭。起始标签缺少闭合符号“>”。奇怪... - Janning Vygen
由于ant任务/编译jsp期间出现错误,导致xml没有结束标记。在<configuration>和<task>(新版本中的<target>)之间添加<failOnError>false</failOnError>后,xml将具有关闭标记。 - hang321

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);
            }
        };
    }
}

1
我认为这是针对Spring Boot的,你有没有关于如何在Spring MVC中实现此解决方案的任何想法/提示?我尝试了这个,但出现了“java.lang.UnsupportedOperationException:Servlet 3.0规范的第4.4节不允许从未在web.xml、web-fragment.xml文件中定义或未用@WebListener注释的ServletContextListener调用此方法”的错误。当我被迫在我的Spring 5 MVC中使用“@WebListener”时,我陷入了困境。 - Pruthviraj
我解决了这个问题,为WebMvcConfigurer实现添加了"@WebListener"。 - Pruthviraj

1
上面提到的“At server start time”的评论:如果应用程序打包在可执行的jar文件中,则您创建的servlet默认情况下将处于开发模式,因此,如果您在生产模式下使用它,还应设置development = false ++以防止重新编译jsps。
reg.setInitParameter("genStringAsCharArray", "true");
reg.setInitParameter("trimSpaces", "true");
reg.setInitParameter("development", "false");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接