Search code examples
javatomcatfatjarembedded-tomcat-10

Embedded Tomcat 10 serving static resources from fat jar


The following code works correctly when fired from the IDE (Eclipse); that is, the documents in src/main/resources/static are being served to the browser:

import org.apache.catalina.Context;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.catalina.startup.Tomcat;

public class MainTomcat {
    
    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.getConnector(); // silly needed call
        tomcat.setPort(8080);
        String rootPath = MainTomcat.class.getClassLoader().getResource("static").getFile();
        System.out.println("root path=" + rootPath);
        Context context = tomcat.addContext("/", rootPath);
        tomcat.addServlet("/", "default", new DefaultServlet());
        context.addServletMappingDecoded("/", "default");
        tomcat.start();
        tomcat.getServer().await();
    }
}

The console shows on start:

root path=/home/user/eclipse-workspace/tom1/target/classes/static
18:04:02.950 WARN  o.a.catalina.core.StandardContext - A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
class org.apache.catalina.core.StandardContext
18:04:03.415 INFO  o.a.catalina.core.StandardService - Starting service [Tomcat]
18:04:03.416 INFO  o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.0.27]

So far, so good. Then I build a fatjar using the following configuration:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.americati</groupId>
  <artifactId>tom1</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>5.0.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.3.14</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>10.0.27</version>
    </dependency>
  </dependencies>
  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <id>build-a</id>
                        <configuration>
                            <archive><manifest><mainClass>com.americati.awfemb.MainTomcat</mainClass></manifest></archive>
                            <descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs>
                            <appendAssemblyId>false</appendAssemblyId>
                            <finalName>awfemb</finalName>
                        </configuration>
                        <phase>package</phase>
                        <goals><goal>single</goal></goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

After a mvn clean package, I copy the fatjar to another directory (/tmp/abc) and run java -jar awfemb.jar

root path=file:/tmp/abc/awfemb.jar!/static
20:11:02.466 WARN  o.a.catalina.core.StandardContext - A context path must either be an empty string or start with a '/' and do not end with a '/'. The path [/] does not meet these criteria and has been changed to []
class org.apache.catalina.core.StandardContext
20:11:03.179 INFO  o.a.catalina.core.StandardService - Starting service [Tomcat]
20:11:03.179 INFO  o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.0.27]
20:11:03.219 ERROR o.apache.catalina.core.ContainerBase - A child container failed during start
java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [org.apache.catalina.webresources.StandardRoot@53fe15ff]
    at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:926)
    at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:886)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919)
    at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:265)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.StandardService.startInternal(StandardService.java:432)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.startup.Tomcat.start(Tomcat.java:486)
    at com.americati.awfemb.MainTomcat.main(MainTomcat.java:39)
Caused by: org.apache.catalina.LifecycleException: Failed to start component [org.apache.catalina.webresources.StandardRoot@53fe15ff]
    at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:440)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:198)
    at org.apache.catalina.core.StandardContext.resourcesStart(StandardContext.java:4835)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4973)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919)
    ... 16 common frames omitted
Caused by: java.lang.IllegalArgumentException: The main resource set specified [/tmp/abc/tomcat.8080/webapps/file:/tmp/abc/awfemb.jar!/static] is not valid
    at org.apache.catalina.webresources.StandardRoot.createMainResourceSet(StandardRoot.java:762)
    at org.apache.catalina.webresources.StandardRoot.startInternal(StandardRoot.java:719)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    ... 25 common frames omitted

This is repeated three times (I suspect once per thread.) The relevant message is:

The main resource set specified [/tmp/abc/tomcat.8080/webapps/file:/tmp/abc/awfemb.jar!/static] is not valid

Seems like Tomcat is joining the non-existent /tmp/abc/tomcat.8080/webapps directory to the specified resource file:/tmp/abc/awfemb.jar!/static, and producing an invalid resource. Some idea to fix it? I saw some (more complex) examples using a WebResourceRoot class, but I'm not sure if it is needed at all.

BTW, the following structure is indeed automatically created inside /tmp/abc, which is in line with the documentation of the Tomcat.setBaseDir() method.

$ pwd
/tmp/abc
$ find tomcat.8080/
tomcat.8080/
tomcat.8080/work
tomcat.8080/work/Tomcat
tomcat.8080/work/Tomcat/localhost
tomcat.8080/work/Tomcat/localhost/ROOT

Solution

  • Taking ideas from https://julianjupiter.com/blog/java-web-application-with-embedded-tomcat/ and How to get the path of a running JAR file?, I got this code which is working:

    public class MainTomcat {
    
        public static String getSourceFile() {
            try {
                return new File(MainTomcat.class.getProtectionDomain().getCodeSource().
                        getLocation().toURI()).getPath();
            } catch(Exception e) {
                throw new IllegalStateException(e);
            }
        }
        
        public static void main(String[] args) throws Exception {
            String jarFile = getSourceFile();
            Tomcat tomcat = new Tomcat();
            tomcat.getHost().setAppBase(".");
            tomcat.setPort(8080);
            tomcat.getConnector(); // silly needed call
            Context context = tomcat.addContext("/", ".");
            WebResourceRoot resRoot = new StandardRoot(context);
            WebResourceSet webRes;
            if(jarFile.endsWith(".jar")) {
                System.out.println("Jar file=" + jarFile);
                webRes = new JarResourceSet(resRoot, "/", jarFile, "/static");
            } else {
                String rootPath = MainTomcat.class.getResource("/static").getFile();
                System.out.println("root path=" + rootPath);
                webRes = new DirResourceSet(resRoot, "/", rootPath, "/");
            }
            resRoot.addPreResources(webRes);
            context.setResources(resRoot);
            Tomcat.addServlet(context, "default", new DefaultServlet());
            context.addServletMappingDecoded("/", "default");
            tomcat.start();
            tomcat.getServer().await();
        }
    

    Some additional comments:

    • The tomcat.getHost().setAppBase("."); call is to avoid the code crashing at startup looking up for the webapps directory
    • Instead of tomcat.addContext() I also tested using tomcat.addWebapp(): the later required the tomcat-embed-jasper artifact for JSP (here unneeded.) Also, the "web app" way apparently defined a (non working, maybe unmapped) "default" named servlet, but I was able to add a "default2" DefaultServlet mapped to /