I have a file in my Netbeans 14 Jetty 11.0.11 project at srcmainwebappstaticindex.html that I want to serve to web-browsers using Jetty.
Here is my pom.xml:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>verishare</groupId>
<artifactId>verdi</artifactId>
<version>12-JDK17</version>
<packaging>jar</packaging>
<name>verdi</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jettyVersion>11.0.11</jettyVersion>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<!--<version>2.11.0</version>-->
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<!--<version>2.11.0</version>-->
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-client</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>apache-jsp</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>apache-jstl</artifactId>
<version>11.0.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.0.4</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<!--<version>2.0.1</version>-->
<version>2.5.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901-1.jdbc4</version>
</dependency>
<!--
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.8.6</version>
</dependency>
-->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>6.4.0.jre8</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<!--<version>1.1.8</version>-->
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-jetty-http</artifactId>
<version>2.22.1</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.10</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.53</version>
</dependency>
<dependency>
<groupId>ie.corballis</groupId>
<artifactId>sox-java</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.5.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.3</version>
</dependency>
<!-- Required so Verdi can compile in JDK's higher than 1.8 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<!--<scope>provided</scope>-->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.5</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>verishare.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.2</version>
<configuration>
<archive>
<manifest>
<mainClass>verishare.App</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!--this is used for inheritance merges-->
<phase>package</phase> <!--bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/webapp</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
</project>
Here’s how I start Jetty:
public boolean startJetty(Server server) throws Exception, InterruptedException {
boolean retVal = false;
try {
server = new Server(AppSettings.getJettyServerPort());
jettyServer = server;
server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize", -1);
String webDir = this.getClass().getClassLoader().getResource("static").toExternalForm();
SecurityHandler basicSecurity = getBasicAuthHandler("abc", "def");
WebAppContext waContext = new WebAppContext(webDir, "/");
waContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
waContext.setSecurityHandler(basicSecurity);
waContext.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*servlet-api-[^/]*\.jar$|.*/javax.servlet.jsp.jstl-.*\.jar$|.*/[^/]*taglibs.*\.jar$");
ServletContextHandler servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
servletContext.setMaxFormKeys(1000000000);
servletContext.setContextPath("/api");
servletContext.addServlet(new ServletHolder(new WebApiServlet()), "/*");
String cn = "";
Reflections rf = new Reflections("proj");
Set<Class<?>> classWithPath = rf.getTypesAnnotatedWith(javax.ws.rs.Path.class);
for (Class c : classWithPath) {
if (cn.length() > 0) {
cn += ";";
}
cn += c.getCanonicalName();
localLogger.info((String) logEntryRefNumLocal.get() + "Adding class: " + c.getCanonicalName());
}
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{servletContext, waContext});
server.setHandler(handlers);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSendServerVersion(false);
HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfig);
ServerConnector httpConnector = new ServerConnector(server, httpFactory);
httpConnector.setPort(AppSettings.getJettyServerPort());
server.setConnectors(new Connector[]{httpConnector});
server.setStopAtShutdown(true);
server.setStopTimeout(0x2710L);
server.start();
} catch (InterruptedException iex) {
localLogger.error((String) logEntryRefNumLocal.get() + "InterrupedException in WebHost.java startJetty method.", iex);
retVal = false;
throw iex;
} catch (RuntimeException rex) {
localLogger.error((String) logEntryRefNumLocal.get() + "Runtime exception in WebHost.java startJetty method.", rex);
retVal = false;
throw rex;
} catch (Exception ex) {
localLogger.error((String) logEntryRefNumLocal.get() + "General exception in WebHost.java startJetty method.", ex);
retVal = false;
throw ex;
}
return retVal;
}
If I run the above in Windows inside Netbeans 14, I can see the content of the .html file in /usr/src/webapp/static by visiting
http://127.0.0.1:8086/index.html
However, if I run the .jar in Ubuntu under the same version of the official Oracle JDK (JDK 17) using
/usr/lib/jvm/jdk-17/bin/java -Djavax.net.ssl.trustStore=/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts -cp /usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar verishare.App
and I try to visit
Jetty 11.0.11 replies
HTTP ERROR 404 Not Found
URI: /
STATUS: 404
MESSAGE: Not Found
SERVLET: org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-726a17c4
What am I doing wrong that the above works in Windows inside NetBeans 14 with JDK 17, but it gives a 404 error with the compiled JAR in Linux (Ubuntu 20.04 LTS) with JDK 17?
Thanks
EDIT: I have refined the issue further. When run under Windows inside NetBeans 14 in JDK 17, in the code above, the "webDir" becomes
file:/D:/Projects/verdi_2/target/classes/static
When run under Linux in JDK 17, in the code above, the "webDir" becomes
jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static
It appears that the Linux versions of neither OpenJDK 17 nor the Oracle JDK 17, can interpret the above jar:file reference, leading to the 404.
How can this be fixed, though??? It is as if jar:file is completely opaque to Linux JREs of either flavor. No exceptions are raised, and I have confirmed over and over that the directory /static exists in the .JAR concerned.
EDIT: As I understand the answers below, this is then clearly invalid and the wrong way to access a directory full of html, Javascript, and images in a Jetty webAppContext that Jetty needs to serve to browser:
jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static
I still have no idea how to correct this, but can someone maybe comment: if the above is INCORRECT, what would a correct reference look like for a /static folder in the root of a JAR file?
EDIT: I have confirmed that if I regress the Jetty version back far enough to older versions, the reference jar:file:/usr/src/verdi/verdi-12-JDK17-jar-with-dependencies.jar!/static -does- start working and functions perfectly, and the older Jetty instance can serve the resources in the /static folder to any web-browsers that request them
EDIT: GitHub issue opened
2
Answers
The answer to this question is in the long series of posts beneath this GitHub issue for jetty:
https://github.com/eclipse/jetty.project/issues/8549
In essence, eventually I had to first clean up my Maven pom.xml (see this thread for the discussion and for links to a pom.xml example that is compliant with Maven Shade plugin and Jetty 11.0.11 requirements and standards) then at the end of the day hardcode a link to the JAR file to find the HTML, JS, etc. resources Jetty was to serve out as a webpage. Also put in a conditional where, on compiling, I need to specify if the code will run "in-IDE" (in my case, Netbeans 14) or "in-JAR" - e. g. in a detached JRE elsewhere than the Netbeans 14 IDE.
Also dropped using the Jetty WebAppContext class and started rendering web content out of a normal ServletContextHandler.
Hopefully this may help someone upgrading Jetty from Jetty 9.xxx to 11 and finding that it all falls apart.
For details as to why they changed so much, see the GitHub link (the last few entries are apropos.)
The github discussion also contains full working source code (startJettyc method) that solved the issue of getting a 404 in a detached, non-IDE modality where the JAR was being run in an JRE separate from an IDE.
Stefan
This code has all sorts of massive problems with it. It’s not supposed to work, but it’s crazy enough that depending on a ton of tricky, hard to guarantee caveats, it will work. The usual solution to ‘hey this code is incredibly fragile and if so much as the phase of the moon changes, this code is likely to break, please enumerate all the conditions I need to ensure so that it does not’ is to say: "Forget that, write it differently, this is too fragile to continue to use!".
The simple stuff
Let’s first fix the simple things and explain how this works. These don’t immediately solve the problem, but are steps on the way to properly solve the problem.
this.getClass()
is bad.getClass()
gets you your actual class. This is bad – the point of this class is to serve as ‘context’ for where to look for a given resource. Your intent is clearly is to refer to the very class whose definition is the source file you are writing for. In other words, if you haveclass Foo { ..... getClass().getResource... }
your intent is to look up whatever resource you’re looking up in the same placeFoo.class
is at, as you’re writing in Foo.That is not what
getClass()
actually does. If someone subclasses Foo, thengetClass()
would give you that class. In modular setups, that context is unlikely to work. This is needlessly fragile. The fix is to writeFoo.class
(exotic, but valid java: Gets you a reference to a class), and notgetClass()
.getClassLoader()
is badMostly, it’s needless: the class object itself already has a
.getResource
method, there is no need to type.getClassLoader().getResource
. In addition,getClassLoader()
returnsnull
(thus, causing NullPointerException) in exotic cases. Why limit your code so that it breaks in certain scenarios for no good reason?There is one slight difference:
someClass.getClassLoader().getResource
resolves the stated resource relative to the root of the classpath. WhereassomeClass.getResource
includes the ‘package’ of thesomeClass
as prefix. You can tellgetResource
not to do that by prefixing a slash. so,someClass.getClassLoader().getResource("static")
can be simplified and improved tosomeClass.getResource("/static")
.So far we’re at..
ClassThisWasIn.class.getResource("/static")
instead ofthis.getClass().getClassLoader().getResource("static")
Now for the hard part
The class loader resource stuff is an abstraction for the concept of loading resources (in that sense
ClassLoader
is a misnomer: aClassLoader
can load any resource; classes are just one kind of resource.ResourceLoader
is the correct name, but java does not break backwards compat lightly, so once this thing was released with theClassLoader
name, fixing that brainfart became too much cost for the meagre gain).As with any abstraction, the higher level you keep the abstraction the more bizarre scenarios can be represented by it. As a consequence, even though e.g. directories and jar files, which is how 99.9% of ‘java resources’ are shipped, all clearly have the concept of ‘listing the contents’ (you can
ls
a directory, you canjar tvf
orunzip -l
a jar file), the ClassLoader abstraction DOES NOT. Same applies to directories, and this is crucial: The ClassLoader abstraction simply does not have any concept about directories whatsoever. Hence, assuming/static
is a dir, this entire principle is a hack that the spec does not guarantee will ever work.That’s in large part the problem here, but partly the authors of jetty are to blame, perhaps, for making you think you can write code like this without breaking spec.
At any rate, this hackery goes some way to explain what’s wrong: Given that the classloader infra doesn’t have any concept of directories, asking for a directory as a resource doesn’t actually give you anything useful.
We can trivially give this a quick test!
When you run this, it dutifully prints:
Which look weird perhaps, but
jrt
is referring to ‘java run time’, and the reason you get this instead of ajar
orfile
URL is because the core classes, starting with Java9, are not in jars anymore. Go scan your JDK dir, you won’t find it. Until Java8, that would print something like:jar:file:/absolute/path/to/your/JDK/classes/rt.jar!java/lang/String.class
.Now let’s try to get the lang package:
This prints
null
. Because you can’t do that.Now, if we try this stuff on jar/file classpaths, even on modern JVMs, it does ‘work’:
For me this prints:
But as the
jrt:
example showed, whilst that first result is as expected per the java specs, that second is a wild stab in the dark. It worked… more or less by accident.Now to core problem
You’re asking for a resource named
/static
anywhere in the root classpath of whatever classpath is responsible for finding theTest.class
resource. Often, this ‘root’ is the entire classpath. You don’t generally get into multiple separate roots unless you use module systems. Thus, the problem becomes clear: You’re asking for any occurrence of a dir named ‘static’ in the root of any of your classpath path entries, so you’re getting a random one, and on various JDKs, they ship their own/static
, and thus, you get the wrong one.Alternatively, because you’re asking for a dir and that is not a valid principle, the
getResource
API is just giving you the first classpath path entry and sticking/static
at the end of it. You’re beyond what the spec guarantees, so if instead it played Beethoven’s 5th from the speakers that would be weird but not a bug either. Optimally the java spec should be more clear about how directories operate (and probably say: They don’t, and then stop returning anything butnull
, though that would break lots of existing code).Solutions
Instead, ask for a resource (a file, not a directory) that you know exists and whose name is unique. Then use string manipulation to lop the file part off so you are now left with a directory.
Let’s say you know that
/static/header.png
definitely exists. That name is likely to be unique, though I’d consider going with/name-of-my-site/static/header.png
instead to increase the odds of this, or to put thatstatic
dir in your package structure, e.g. if your class haspackage com.stefan.myapp
at the top (inMyApp.java
), to put this stuff in/com/stefan/myapp/static/header.png
and now you can refer to that usingMyApp.class.getResource("static/header.png")
, because without the prefix / ingetResource
it’s relative to the package. And now you have a guarantee of unique names.Now just lop off
/header.png
and voila.How does it work?
What jetty does is this:
/res/images/foo.png
/res
is for static files, and these are to be found via the classloader atjar:file:/whatever/foo/bar.jar!/a/b/c
./res
, and append the rest straight onto that path, giving mejar:file:/whatever/foo/bar.jar!/a/b/c/images/foo.png
. I will then just toss that straight at the classloader and I’ll run with whatever it gives me. I don’t care what these strings are, they can befloobargle:/hootenanny/
as far as jetty is concerned.Hence, you’re really passing a ‘prefix’, as in: Take any resource, prefix this, ask the classloader for the end result. Thus, using the classloader to find a known resource and then lopping that resource off of the end of the string exactly matches what jetty will do with this string.