Using nashorn
I'm using version OpenJDK 11. Would this be much of an issue?
I'd say so. JDK15 is the first JDK that does not include nashorn. The latest 'long term support release is JDK17, and the current OpenJDK release is 18, so at this point, yes, this is a big issue.
and if so what would be the best alternative for what I'm trying to do?
Instead of relying on a javascript engine shipped with the JDK, you should pick a third-party javascript engine and ship it with your app. Java apps should be having many many dependencies (hundreds, perhaps) - JDK doesn't ship web template engines, web routing, DB interaction libraries (JDBC ships with the JDK but it isn't intended to be directly used, it's more the low-level library that things like JDBI, JOOQ, and Hibernate are built on top of), and so much more. On purpose - the JDK can never break backward compatibility, and that's too much handcuffed for many kinds of libraries. The solution is to use the robust and extensive open-source library. Every java build tool that is popular (be it Maven, Gradle, or even ant+ivy) makes it extremely simple; just put the GAV (Group::Artifact::Version) ID of the library you want in your build file and the tool takes care of everything (downloading it from a network of mirrored servers, tools to keep it up to date, shipping it, etc).
Here is a tutorial that adds the GraalVM javascript engine to a project. It's not the only javascript engine, you have alternatives. But it shows the principle.
I can't get it to work when it's in a jar file
new FileReader("/Script.js")
In java, File means File. An entry in a jar file is not a file. Hence, FileReader
? You've already lost, you can't use that stuff for resources.
What you want to use is the GRAS mechanism instead: Ask java's classloader infrastructure to load resources from the same place it loads class files (think about it as follows: class files are resources - your java application doesn't work without them, and the JVM itself needs to load them as your app runs. Why should, say, an icon file for your GUI, or some javascript you want to throw through a javascript engine be any different?)
The only way to do that is GRAS. It comes in 2 forms:
Form 1: When the API takes a URL object.
Some APIs have a method that takes a URL. For example, ImageIcon and most other things in the GUI libraries java offers (swing, JavaFX, etc). For these:
class Example {
void example() {
URL resource = Example.class.getResource("foo.png");
new ImageIcon(resource);
}
}
Form 2: InputStream
If the API doesn't offer a method variant that takes a URL, surely there's one that takes either an InputStream
(for byte-based things, like image files) or a Reader
(for char-based things, like javascript). You can trivially turn an inputstream into a reader by specifying a charset encoding:
try (InputStream in = ...) {
Reader r = new InputStreamReader(in, StandardCharsets.UTF_8);
somethingThatNeedsAReader(r);
}
Given that these are resources, you must safely close them, hence try-with-resources is required (or roll your try/finally if you must). Otherwise, your app will eventually start crashing because it ran out of file handles.
So, all we need to know is how to obtain InputStream objects - as we can use the above trick to turn them into readers. It works like this:
try (InputStream in = Example.class.getResourceAsStream("foo.js")) {
....
}
That path string
The string you pass to getResource
and getResourceAsStream
is not quite a file path. It looks in the same place that Example.class
(the class file that has the code for your Example class) is at, even if that is in a jar file. Even if it's generated on the fly, downloaded on the fly - java's classloader system is an abstract concept, who knows how it's loading these. That's the point: You just load from wherever that might be.
Such entries have a 'root' - and you can go from the root as well, by prefixing it with a slash.
Example:
- You have a class named
Example
, in package com.foo
.
- It's packed into a jar file together with the js file.
- Therefore, in that jar file, there's an entry
/com/foo/Example.class
.
- Let's say the js file is in
/com/foo/js/myapp.js
Then you can load that resource as an inputstream using either of these:
Example.class.getResource("js/myapp.js"); // relative to com/foo
Example.class.getResource("/com/foo/js/myapp.js"); // relative to root
It's not a file system - ..
does not work, for example. Had it been in /js/myapp.js
, then the leading-slash-strategy is the only option.
Alternatives that suck
These are common in tutorials and SO answers but you should never use them:
getClass().getResource
instead of Example.class.getResource
- the getClass trick breaks if someone subclasses. It's unlikely, but why write code that is strictly worse, right? Avoid the style guide debate and use the version that works in strictly more cases.
Example.class.getClassLoader().getResource()
- this is needlessly longer, removes the ability for using relative paths, and breaks in certain cases. For example, classes loaded as boot load don't have a classloader, and the above code would therefore throw NullPointerException
. But e.g. String.class.getResource("String.class")
works fine.
Things you can't do
The resource loader system only has the 'primitive' of 'load a resource'. There is no list() operation! - a few libraries suggest it exists but these are hacks that will break in certain circumstances. Hence, if you want something like 'can I get a list of all js files in the js dir?' the answer is simple: No, that is not possible.
The general solution to this is to make a text file that lists the resources you're interested in, saves that into a text file with a known name. Now to 'fake' a "list all resources" operation, you load that file using getResource
, process it, loading each entry with .getResource
in turn. This is exactly how SPI works (which is how e.g. java itself discovers available JDBC drivers, charset providers, crypto providers, etc). Now you know what to search the web for if you need 'list all resources of some kind' :)