Can I serve JSPs from inside a JAR in lib, or is there a workaround?
I have a web application deployed as a WAR file in Tomcat 7. The application is build as a multi-module project:
- core - packaged as JAR, contains most of the backend code
- core-api - packaged as JAR, contains interfaces toward core
- webapp - packaged as WAR, contains frontend code and depends on core
- customer-extensions - optional module, packaged as JAR
Normally, we can put our JSP files in the webapp project, and reference them relative to the context:
/WEB-INF/jsp/someMagicalPage.jsp
The question is what we do about JSP files that are specific to the customer-extensions project, that should not always be included in the WAR. Unfortunately, I cannot refer to JSPs inside JAR files, it appears. Attempting classpath:jsp/customerMagicalPage.jsp
results in a file not found in the JspServlet, since it uses ServletContext.getResource()
.
Traditionally, we "solved" this having maven unpack the customer-extensions JAR, locate the JSPs, and put them in the WAR when building it. But an ideal situation is where you just drop a JAR in the exploded WAR in Tomcat and the extension is discovered - which works for everything but the JSPs.
Is there anyway to solve this? A standard way, a Tomcat-specific way, a hack, or a workaround? For example, I've been thinking of unpacking the JSPs on application startup...
Solution 1:
Servlet 3.0 which Tomcat 7 supports includes the ability to package jsps into a jar.
You need to:
- place your jsps in
META-INF/resources
directory of your jar - optionally include a
web-fragment.xml
in theMETA-INF
directory of your jar - place the jar in
WEB-INF/lib
directory of your war
You should then be able to reference your jsps in your context. For example if you have a jsp META-INF/resources/test.jsp
you should be able reference this at the root of your context as test.jsp
Solution 2:
As a workaround, I created a class that opens up a jar file, finds files matching a certain pattern, and extracts those files to a given location relative to the context path.
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.ServletContextAware;
/**
* Allows extraction of contents of a JAR file. All files matching a given Ant path pattern will be extracted into a
* specified path.
*/
public class JarFileResourcesExtractor implements ServletContextAware {
private String resourcePathPattern;
private String jarFile;
private String destination;
private ServletContext servletContext;
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* Creates a new instance of the JarFileResourcesExtractor
*
* @param resourcePathPattern
* The Ant style path pattern (supports wildcards) of the resources files to extract
* @param jarFile
* The jar file (located inside WEB-INF/lib) to search for resources
* @param destination
* Target folder of the extracted resources. Relative to the context.
*/
private JarFileResourcesExtractor(String resourcePathPattern, String jarFile, String destination) {
this.resourcePathPattern = resourcePathPattern;
this.jarFile = jarFile;
this.destination = destination;
}
/**
* Extracts the resource files found in the specified jar file into the destination path
*
* @throws IOException
* If an IO error occurs when reading the jar file
* @throws FileNotFoundException
* If the jar file cannot be found
*/
@PostConstruct
public void extractFiles() throws IOException {
try {
String path = servletContext.getRealPath("/WEB-INF/lib/" + jarFile);
JarFile jarFile = new JarFile(path);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (pathMatcher.match(resourcePathPattern, entry.getName())) {
String fileName = entry.getName().replaceFirst(".*\\/", "");
File destinationFolder = new File(servletContext.getRealPath(destination));
InputStream inputStream = jarFile.getInputStream(entry);
File materializedJsp = new File(destinationFolder, fileName);
FileOutputStream outputStream = new FileOutputStream(materializedJsp);
copyAndClose(inputStream, outputStream);
}
}
}
catch (MalformedURLException e) {
throw new FileNotFoundException("Cannot find jar file in libs: " + jarFile);
}
catch (IOException e) {
throw new IOException("IOException while moving resources.", e);
}
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
public static int IO_BUFFER_SIZE = 8192;
private static void copyAndClose(InputStream in, OutputStream out) throws IOException {
try {
byte[] b = new byte[IO_BUFFER_SIZE];
int read;
while ((read = in.read(b)) != -1) {
out.write(b, 0, read);
}
} finally {
in.close();
out.close();
}
}
}
And then I configure it as a bean in my Spring XML:
<bean id="jspSupport" class="se.waxwing.util.JarFileResourcesExtractor">
<constructor-arg index="0" value="jsp/*.jsp"/>
<constructor-arg index="1" value="myJarFile-1.1.0.jar"/>
<constructor-arg index="2" value="WEB-INF/classes/jsp"/>
</bean>
It's not an optimal solution to a really annoying problem. The question now becomes, will the guy who maintains this code come and murder me while I sleep for doing this?
Solution 3:
There is such workaround - you can precompile your JSPs into servlets. So you'll get .class files you can put into JAR and map in web.xml to some URLs.
Solution 4:
Struts 2 team added a plugin for embedded JSP. Maybe it may be used ad a base.
https://struts.apache.org/plugins/embedded-jsp/