5

I have been using a reflection technique from https://apimeister.com/2015/06/27/add-jar-to-the-classpath-at-runtime-in-jjs.html to load classes at runtime in java's nashorn jjs.

It works in java 8, but in java 9 it doesn't. I know about the recommended command line workaround mentioned in https://stackoverflow.com/a/41265267/5891192

And according to https://stackoverflow.com/a/45970885/5891192 this alternative syntax of using = instead of spaces between the flag and its args seems like it should also be valid (needed because of the nashorn method of passing jvm args through jjs via -J--...

Any hints?


This works... (java 8) ...

$ wget -q http://central.maven.org/maven2/org/apache/poi/poi/4.0.0/poi-4.0.0.jar
$ /usr/lib/jvm/java-1.8.0/bin/jjs -scripting loadit.js -- poi-4.0.0.jar
DONE

This doesn't... (java 9) ...

$ wget -q http://central.maven.org/maven2/org/apache/poi/poi/4.0.0/poi-4.0.0.jar
$ /usr/lib/jvm/java-9/bin/jjs -J--add-opens=java.base/java.net=ALL-UNNAMED -scripting loadit.js -- poi-4.0.0.jar

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected void java.net.URLClassLoader.addURL(java.net.URL) accessible: module java.base does not "opens java.net" to module jdk.scripting.nashorn.scripts
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)...

And here is loadit.js ...

// loadit.js 
function addUrlToClasspath(pth) {
  var s = java.lang.ClassLoader.getSystemClassLoader();
  var C = Java.type("java.lang.Class[]");
  var p = new C(1); p[0]  = java.net.URL.class;
  var m = java.net.URLClassLoader.class.getDeclaredMethod("addURL", p);
  var O = Java.type("java.lang.Object[]"); var a = new O(1); var f = new java.io.File(pth); m.setAccessible(true);
  var u = f.toURL(); a[0] = u; m.invoke(s, a);
}

addUrlToClasspath($ARG[0]);
print("DONE")

Edit: 23 Oct 2018: corrected the "This doesn't (java 9)" example command line

J. Brazile
  • 781
  • 4
  • 9
  • 1
    The system class loader is not a URLClassLoader so this hack isn't going to work. Have you looked at creating your own URLClassLoader instead? – Alan Bateman Oct 22 '18 at 16:41
  • I don't completely follow - I think you are saying the system classloader in java9 is different than in java8 - can you give an additional hint or link if I were to try this approach? -thx – J. Brazile Oct 23 '18 at 06:01
  • 1
    Here's the JDK 9 release note that covers this: https://www.oracle.com/technetwork/java/javase/9-relnote-issues-3704069.html#JDK-8142968 . There is no supported way to extend the class path at run-time (only tool agents can do that). So I think you need to stand back from what you are doing, probably create your own URLClassLoader instance and call it to load the classes you are looking for. – Alan Bateman Oct 23 '18 at 06:42
  • 1
    Oh that skepticism… How about `System.out.println(ClassLoader.getSystemClassLoader() instanceof URLClassLoader);`? – Holger Oct 23 '18 at 14:22
  • 1
    https://stackoverflow.com/a/30251930/2711488 – Holger Oct 23 '18 at 14:27
  • Thanks @AlanBateman In the meantime, now I see what I thought was 1 problem is actually 3 problems. 1. the original question as per the title 2. thanks to your hint I see that even if that were solved, the approach itself doesn't work anymore starting with java-9 - https://stackoverflow.com/q/46694600/5891192 - 3. starting with java-11, jjs itself is deprecated :-( Shame. Up until now, this approach had been an easy way to send short scripts as text files that anyone could run as long as they had a jdk installed (and an internet connection if external jars were needed). – J. Brazile Oct 24 '18 at 12:40
  • Thanks @Holger - so I think I understand the suggestion in the link you posted to be that inside jjs I could instantiate my own nashorn engine from within a self-created classloader and that should be able to dynamically load additional classes. – J. Brazile Oct 24 '18 at 12:52
  • 1
    Starting with JDK 11, you don’t need to use JavaScript for that, as now, `java` itself can launch simple Java source code files. See https://dzone.com/articles/launch-single-file-source-code-programs-in-jdk-11 for example. Within these “scripts”, the entire Java API is available, including class loaders. – Holger Oct 24 '18 at 12:57

1 Answers1

1

As I commented above, there are actually 3 problems.

  1. the question as asked - Answer: a. doesn't help (see next point) b. the system level add-opens command line option doesn't make its way to the Nashorn engine used by jjs)
  2. the appending-to-the-system-class-loader approach doesn't work anyway starting with java9 - Java 9, compatability issue with ClassLoader.getSystemClassLoader
  3. starting with java-11, jjs itself is declared deprecated

However, thanks to hints from @Alan , @Holger and my colleague @Philippe , I got what I want with workarounds.

  1. You can create your own URLClassLoader using the desired jars and create a second nashorn engine passing in this classloader (and e.g. command line arguments from jjs).
  2. Add another hack for implementing a so-called "here document" for the script-within-a-script

... and here is a complete example:

// jjs -scripting ora2csv.js -- "select 'hi' from dual" jdbc:oracle:thin:@host:1521:XE user pass

function newJjsEngineWith (jars) {
  var ua = Java.type("java.net.URL[]"); var urls = new ua(jars.length);
  for(var i=0; i<jars.length; i++) {
    var u=new java.net.URL(new java.io.File(jars[i]).toURL());
    urls[i] = u;
  }
  var loader = new java.net.URLClassLoader(urls);
  java.lang.Thread.currentThread().setContextClassLoader(loader);
  var nsef = Java.type("jdk.nashorn.api.scripting.NashornScriptEngineFactory");
  var sa = Java.type("java.lang.String[]"); var args = new sa(2 + $ARG.length);
  args[0] = "-scripting"; args[1] = "--";
  for(var i=0;i<$ARG.length;i++) { args[i+2] = $ARG[i]; }
  return new nsef().getScriptEngine(args, loader);
}

var jjs = newJjsEngineWith(["ojdbc8.jar"]);

function hereDoc(f) { return f.toString().slice(14,-3); }

var code = hereDoc(function(){/*
var q = $ARG[0]; // select 'hello' from dual
var c = $ARG[1]; // jdbc:oracle:thin:@host:1521:XW
var u = $ARG[2]; // username
var p = $ARG[3]; // password
var conn = java.sql.DriverManager.getConnection(c, u, p);
var stmt = conn.createStatement(); rset = stmt.executeQuery(q);
var row=0;
while (rset.next()) {
  row++; var rowBuf = new java.lang.StringBuilder();
  var meta = rset.getMetaData();
  for(var col=0; col< meta.getColumnCount(); col++) {
    var cell = rset.getString(col+1);
    if (cell == null) { cell = ""; }
    rowBuf.append(cell.replaceAll(";",","));
    if (col < meta.getColumnCount()-1) { rowBuf.append(";"); }
  }
  print(rowBuf.toString());
}
stmt.close();
*/});

jjs.eval(code);

This works for java 8 through java 11.

$ /usr/lib/jvm/java-1.8.0/bin/jjs -scripting ora2csv.js -- "select 'hi' from dual" jdbc:oracle:thin:@host:1521:XE user pass
hi
$ /usr/lib/jvm/java-9/bin/jjs -scripting ora2csv.js -- "select 'hi' from dual" jdbc:oracle:thin:@host:1521:XE user pass
hi
$ /usr/lib/jvm/java-10/bin/jjs -scripting ora2csv.js -- "select 'hi' from dual" jdbc:oracle:thin:@host:1521:XE user pass
hi
$ /usr/lib/jvm/java-11/bin/jjs -scripting ora2csv.js -- "select 'hi' from dual" jdbc:oracle:thin:@host:1521:XE user pass
Warning: The jjs tool is planned to be removed from a future JDK release
Warning: Nashorn engine is planned to be removed from a future JDK release
hi

I welcome any suggestions for making this more concise and less pukey

J. Brazile
  • 781
  • 4
  • 9