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
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:
tomcat.getHost().setAppBase(".");
call is to avoid the code crashing at startup looking up for the webapps
directorytomcat.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 /