Last active
February 4, 2021 07:03
-
-
Save dmikurube/f8df821d4527c8f1ab18efceba159173 to your computer and use it in GitHub Desktop.
Load JRuby via a sub ClassLoader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*~ | |
/.gradle/ | |
/build/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
apply plugin: "java" | |
repositories { | |
mavenCentral() | |
} | |
configurations { | |
jruby | |
} | |
dependencies { | |
compile "joda-time:joda-time:2.9.2" | |
compile "com.google.inject:guice:4.0" | |
compile "com.google.inject.extensions:guice-multibindings:4.0" | |
jruby "org.jruby:jruby-complete:9.1.15.0" | |
} | |
sourceSets { | |
main { | |
java { | |
srcDir "." | |
} | |
} | |
} | |
task installGems1(type: JavaExec) { | |
doFirst { | |
delete("${buildDir}/dependencyGems1") | |
mkdir("${buildDir}/dependencyGems1") | |
} | |
classpath = configurations.jruby | |
main = "org.jruby.Main" | |
args = ["-rjars/setup", "-S", "gem", "install", "msgpack:1.1.0"] | |
environment "GEM_HOME": "${buildDir}/dependencyGems1" | |
} | |
task installGems2(type: JavaExec) { | |
doFirst { | |
delete("${buildDir}/dependencyGems2") | |
mkdir("${buildDir}/dependencyGems2") | |
} | |
classpath = configurations.jruby | |
main = "org.jruby.Main" | |
args = ["-rjars/setup", "-S", "gem", "install", "sigdump:0.2.4"] | |
environment "GEM_HOME": "${buildDir}/dependencyGems2" | |
} | |
task run(type: JavaExec, dependsOn: ["installGems1", "installGems2"]) { | |
main = "Main" | |
classpath = sourceSets.main.runtimeClasspath | |
systemProperty "jruby", configurations.jruby.files.collect { file -> file.toURI() }.join(";") | |
systemProperty "gemHome", "${buildDir}/dependencyGems1" | |
systemProperty "gemPath", "${buildDir}/dependencyGems1:${buildDir}/dependencyGems2" | |
} | |
task run100(dependsOn: ["installGems1", "installGems2"]) { | |
doFirst { | |
for (int i = 0; i < 100; i++) { | |
javaexec { | |
main = "Main" | |
classpath = sourceSets.main.runtimeClasspath | |
systemProperty "jruby", configurations.jruby.files.collect { file -> file.toURI() }.join(";") | |
systemProperty "gemHome", "${buildDir}/dependencyGems1" | |
systemProperty "gemPath", "${buildDir}/dependencyGems1:${buildDir}/dependencyGems2" | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.io.File; | |
class JRuby { | |
JRuby(final JRubyClassLoader classLoader) throws Exception { | |
this.classLoader = classLoader; | |
this.scriptingContainer = createScriptingContainer(); | |
this.method_runScriptlet_LString = scriptingContainer.getClass().getMethod("runScriptlet", String.class); | |
this.method_callMethod_ALObject = scriptingContainer.getClass().getMethod( | |
"callMethod", Object.class, String.class, Object[].class); | |
} | |
Object runScriptlet(final String script) throws Exception { | |
return this.method_runScriptlet_LString.invoke(this.scriptingContainer, script); | |
} | |
void setGemPaths(final String gemHome, final String gemPath) throws Exception { | |
final String[] gemArgs; | |
// An empty string in GEM_PATH is still effective as GEM_PATH. | |
// https://svn.ruby-lang.org/cgi-bin/viewvc.cgi/tags/v2_6_2/lib/rubygems/path_support.rb?revision=67232&view=markup#l34 | |
// https://svn.ruby-lang.org/cgi-bin/viewvc.cgi/tags/v2_6_2/lib/rubygems/path_support.rb?revision=67232&view=markup#l51 | |
if (gemPath != null) { | |
// It is believed that Ruby's File::PATH_SEPARATOR is the same with Java's File.pathSeparator. | |
// https://github.com/jruby/jruby/blob/9.2.6.0/core/src/main/java/org/jruby/RubyFile.java#L122-L125 | |
final String[] gemPaths = gemPath.split("\\" + File.pathSeparator); | |
gemArgs = new String[gemPaths.length + 1]; | |
gemArgs[0] = gemHome; | |
for (int i = 0; i < gemPaths.length; ++i) { | |
gemArgs[i + 1] = gemPaths[i]; | |
} | |
} else { | |
gemArgs = new String[2]; | |
if (gemHome != null) { | |
gemArgs[0] = gemHome; | |
gemArgs[1] = gemHome; | |
} else { | |
gemArgs[0] = null; | |
gemArgs[1] = null; | |
} | |
} | |
this.callMethodArray(this.runScriptlet("Gem"), "use_paths", gemArgs); | |
} | |
private Object createScriptingContainer() throws Exception { | |
final Object object_LocalContextScope = this.createLocalContextScopeSINGLETHREAD(); | |
final Object object_LocalVariableBehavior = this.createLocalVariableBehaviorPERSISTENT(); | |
final Class<?> clazz = classLoader.loadClass("org.jruby.embed.ScriptingContainer"); | |
final Constructor<?> constructor = | |
clazz.getConstructor(object_LocalContextScope.getClass(), object_LocalVariableBehavior.getClass()); | |
return constructor.newInstance(object_LocalContextScope, object_LocalVariableBehavior); | |
} | |
private Object createLocalContextScopeSINGLETHREAD() throws Exception { | |
final Class<?> clazz = this.classLoader.loadClass("org.jruby.embed.LocalContextScope"); | |
final Method valueOf = clazz.getMethod("valueOf", String.class); | |
return valueOf.invoke(null, "SINGLETHREAD"); | |
} | |
private Object createLocalVariableBehaviorPERSISTENT() throws Exception { | |
final Class<?> clazz = classLoader.loadClass("org.jruby.embed.LocalVariableBehavior"); | |
final Method valueOf = clazz.getMethod("valueOf", String.class); | |
return valueOf.invoke(null, "PERSISTENT"); | |
} | |
public Object callMethodArray(final Object receiver, final String methodName, final Object[] args) throws Exception { | |
return this.method_callMethod_ALObject.invoke(this.scriptingContainer, receiver, methodName, args); | |
} | |
private final JRubyClassLoader classLoader; | |
private final Object scriptingContainer; | |
private final Method method_runScriptlet_LString; | |
private final Method method_callMethod_ALObject; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import java.util.Collection; | |
import java.util.Enumeration; | |
import java.util.Vector; | |
final class JRubyClassLoader extends URLClassLoader { | |
JRubyClassLoader(final Collection<URL> jarUrls, final ClassLoader parent) { | |
// The delegation parent ClassLoader is processed by the super class URLClassLoader. | |
super(jarUrls.toArray(new URL[0]), parent); | |
} | |
@Override | |
protected void addURL(final URL url) { | |
throw new UnsupportedOperationException("JRubyClassLoader does not support addURL."); | |
} | |
@Override | |
public void close() throws IOException { | |
super.close(); | |
} | |
@Override | |
public URL[] getURLs() { | |
return super.getURLs(); | |
} | |
/** | |
* Loads the class with the specified binary name from JRuby. | |
* | |
* <p>It prioritizes a class found by this class loader over a class loaded by the parent class loader. | |
*/ | |
@Override | |
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException { | |
synchronized (this.getClassLoadingLock(name)) { | |
// If a class of the specified name has already been loaded by this class loader, or the parent class loader, | |
// find the loaded class, and return it. | |
final Class<?> loadedClass = this.findLoadedClass(name); | |
if (loadedClass != null) { | |
return this.resolveClassIfNeeded(loadedClass, resolve); | |
} | |
// JRuby should use Joda-Time of embulk-core (on the top-level class loader), not of jruby-complete. | |
// Otherwise, embulk-core uses its own, and JRuby uses its own, then they wouldn't match. | |
// | |
// TODO: Remove the condition when embulk-core removes Joda-Time from its dependencies. | |
if (!name.startsWith("org.joda.time.")) { | |
// If a class of the specified name has not been loaded yet, and is found by this (not parent) class loader, | |
// find it, and return it. | |
try { | |
return this.resolveClassIfNeeded(this.findClass(name), resolve); | |
} catch (final ClassNotFoundException ignored) { | |
// Passing through intentionally. | |
} | |
} | |
// If a class of the specified name is found by this class loader (not by the parent class loader), | |
// find it, and return it. | |
try { | |
return this.resolveClassIfNeeded(this.getParent().loadClass(name), resolve); | |
} catch (final ClassNotFoundException ignored) { | |
// Passing through intentionally. | |
} | |
throw new ClassNotFoundException(name); | |
} | |
} | |
/** | |
* Finds the resource with the given name from JRuby. | |
* | |
* <p>It prioritizes a resource found by this class loader over a class loaded by the parent class loader. | |
*/ | |
@Override | |
public URL getResource(final String name) { | |
final URL parentUrl = this.getParent().getResource(name); | |
if (parentUrl != null) { | |
return parentUrl; | |
} | |
return this.findResource(name); | |
} | |
@Override | |
public Enumeration<URL> getResources(final String name) throws IOException { | |
final Vector<URL> resources = new Vector<>(); | |
final Enumeration<URL> parentResources = this.getParent().getResources(name); | |
while (parentResources.hasMoreElements()) { | |
resources.add(parentResources.nextElement()); | |
} | |
final Enumeration<URL> childResources = this.findResources(name); | |
while (childResources.hasMoreElements()) { | |
resources.add(childResources.nextElement()); | |
} | |
return resources.elements(); | |
} | |
private Class<?> resolveClassIfNeeded(final Class<?> clazz, final boolean resolve) { | |
if (resolve) { | |
this.resolveClass(clazz); | |
} | |
return clazz; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.util.ArrayList; | |
public class Main { | |
public static void main(final String[] args) throws Exception { | |
final String jrubyPath = System.getProperty("jruby"); | |
System.out.println(jrubyPath); | |
final String gemHome = System.getProperty("gemHome"); | |
System.out.println(gemHome); | |
final String gemPath = System.getProperty("gemPath"); | |
System.out.println(gemPath); | |
final ArrayList<URL> jrubyUrls = new ArrayList<>(); | |
for (final String url : jrubyPath.split(";")) { | |
jrubyUrls.add(new URL(url)); | |
} | |
final JRubyClassLoader classloader = new JRubyClassLoader(jrubyUrls, Main.class.getClassLoader()); | |
final JRuby jruby = new JRuby(classloader); | |
jruby.setGemPaths(gemHome, gemPath); | |
jruby.runScriptlet("require 'sigdump'"); | |
jruby.runScriptlet("require 'msgpack'"); | |
jruby.runScriptlet("puts 'foo'"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment