Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to precompile jsp in a spring boot application?

I'm using Spring boot and we were using Spring with Tomcat before that. When we used Spring and Tomcat two years ago, we used a maven plugin to precompile the jsp. It was really useful to avoid this compilation to be made for every first visits after a deployement.

However all maven plugin that we know dumps a web.xml file that list all jsp and associated generated servlets. With Spring boot, it don't use web.xml anymore, so this file is ignored.

We still have the compilation and that's a security belt but there is a penalty for every first visit on each page.

Does anybody know if it's possible to precompile jsp in a Spring boot application ?

like image 594
Hugo Lassiège Avatar asked Jan 22 '16 10:01

Hugo Lassiège


People also ask

How do I Precompile a JSP file?

Add jsp_precompile as a request parameter and send a request to the JSP file. This will make the jsp pre-compile. Why it is mentioned as pre compile instead of compilation is that, the request is not served. That is, the JSP will not be executed and the request will not be serviced.

Where do I put JSP in spring boot?

As per convention, we place our JSP files in the ${project. basedir}/main/webapp/WEB-INF/jsp/ directory.

Does spring boot support JSP?

It clear that JSP has known limitations only with embedded servlet containers but not with integration so you can also use JSP with spring boot if you make a jar or war type packaging and deploy it in supporting web containers ( Jetty and Tomcat).


1 Answers

I got precompiling to work either at server start time (don't have to use JspC, so simpler build file) and at build time (much quicker server start time). I register the resulting servlets dynamically, so you don't have to manually change any files if you add/remove JSPs.

At server start time

Use ServletRegistration.Dynamic to register a JSP_SERVLET_CLASS Servlet for each JSP. Use the initParameter jspFile to set the JSP filename (ref)

e.g. for SpringBoot in a ServletContextInitializer (ref):

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

At build time

Generate Java source files for each JSP and a web.xml with their servlet mappings using JspC (ref).

Then register these with the ServletContext (by parsing the web.xml with Tomcat's WebXmlParser, e.g. for 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);
        }
    };
}

Example Maven config to generate and compile the JSP classes, and generate the 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>
like image 97
paulcm Avatar answered Oct 07 '22 16:10

paulcm