I have a file in my Netbeans 14 Jetty 11.0.11 project at \src\main\webapp\static\index.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
CodePudding user response:
this.getClass().getClassLoader().getResource("static").toExternalForm();
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 have class Foo { ..... getClass().getResource... }
your intent is to look up whatever resource you're looking up in the same place Foo.class
is at, as you're writing in Foo.
That is not what getClass()
actually does. If someone subclasses Foo, then getClass()
would give you that class. In modular setups, that context is unlikely to work. This is needlessly fragile. The fix is to write Foo.class
(exotic, but valid java: Gets you a reference to a class), and not getClass()
.
getClassLoader()
is bad
Mostly, it's needless: the class object itself already has a .getResource
method, there is no need to type .getClassLoader().getResource
. In addition, getClassLoader()
returns null
(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. Whereas someClass.getResource
includes the 'package' of the someClass
as prefix. You can tell getResource
not to do that by prefixing a slash. so, someClass.getClassLoader().getResource("static")
can be simplified and improved to someClass.getResource("/static")
.
So far we're at..
ClassThisWasIn.class.getResource("/static")
instead of this.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: a ClassLoader
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 the ClassLoader
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 can jar tvf
or unzip -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!
class Example {
public static void main(String[] args) {
System.out.println(String.class.getResource("/java/lang/String.class"));
}
}
When you run this, it dutifully prints:
jrt:/java.base/java/lang/String.class
Which look weird perhaps, but jrt
is referring to 'java run time', and the reason you get this instead of a jar
or file
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:
class Example {
public static void main(String[] args) {
System.out.println(String.class.getResource("/java/lang"));
}
}
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':
class Test {
public static void main(String[] args) {
System.out.println(Test.class.getResource("/Test.class"));
System.out.println(Test.class.getResource("/"));
}
}
For me this prints:
file:/Users/rzwitserloot/test/Test.class
file:/Users/rzwitserloot/test/
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 the Test.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 but null
, 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 that static
dir in your package structure, e.g. if your class has package com.stefan.myapp
at the top (in MyApp.java
), to put this stuff in /com/stefan/myapp/static/header.png
and now you can refer to that using MyApp.class.getResource("static/header.png")
, because without the prefix / in getResource
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:
- User agent asked for
/res/images/foo.png
- My config says that anything in
/res
is for static files, and these are to be found via the classloader atjar:file:/whatever/foo/bar.jar!/a/b/c
. - So, I lop off
/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.