William_Shi 发表于 2020-8-10 18:50:07

关于修改主线程类加载器

众所周知,Bukkit插件所使用的类加载器,是PluginClassLoader
这个类加载器,没有双亲委派,有利也有弊
以往我都采用一种办法测试JS代码


    ScriptEngineManager m;
    ScriptEngine e;
    m = new ScriptEngineManager(Thread.currentThread().getContextClassLoader());
    e = m.getEngineByName("js");


为什么要给Nashorn指定类加载器呢?因为写脚本只要涉及到Java.type,取到另外Java插件的一些什么类
在不指定类加载器的情况下都会报错
Caused by: java.lang.ClassNotFoundException: xxxxxxx
      at java.net.URLClassLoader.findClass(Unknown Source) ~[?:1.8.0_241]


但是刚才,我即使是按照上述写法,仍然报了ClassNotFoundException
于是我干了一件这样的事

Thread.currentThread().setContextClassLoader(getClassLoader());


当我再次运行new ScriptEngineManager(Thread.currentThread().getContextClassLoader())
并且尝试Java.type 访问另外Java插件的类的时候成功了

然而,我以往测试的时候,似乎发现如果采用PluginClassLoader
存在一个getEngineFactories为空的问题,而直接采用AppClassLoader则没有问题

此次测试的结果是,采用PluginClassLoader没有问题
但是采用AppClassLoader却出现了问题

以往我是在1.15进行测试,现在则是1.12,是不是版本导致了什么问题?
或者说1.12以上莫非对PluginClassLoader作出了什么改动?
JavaScript脚本的加载插件是如何解决这一问题的?(别和我说PVPIN,那玩意倒闭前就最高支持到1.11.2,第二种办法就是那边看到的)
把主线程AppClassLoader改成PluginClassLoader会不会有问题?
希望知道的大佬能不吝分享下自己的经验

NekokeCore 发表于 2020-8-10 19:09:04

冒泡{:694:}

阴阳师元素祭祀 发表于 2020-8-10 19:28:18

本帖最后由 阴阳师元素祭祀 于 2020-8-10 19:30 编辑

首先我不会bukkit 然后我也不会JavaScript

Nashorn我记得弃用了
本身就不推荐使用

指定ScriptEngineManager加载器我懂
为什么连线程context的loader都要改
Thread.currentThread().setContextClassLoader(getClassLoader());
因为eval js的东西的时候需要相关context么?
不可以给engine的ScriptContext自行设置相关东西么(?)


然而,我以往测试的时候,似乎发现如果采用PluginClassLoader
存在一个getEngineFactories为空的问题,而直接采用AppClassLoader则没有问题

此次测试的结果是,采用PluginClassLoader没有问题
但是采用AppClassLoader却出现了问题
采用AppClassLoader出现了问题 指 getEngineFactories返回空?
请原谅我理解能力 因为我觉得就离谱(?)

改主线程的AppClassLoader成PluginClassLoader
我觉得是野蛮的
非常野蛮 而且绝对大概率行不通的
希望不要有这种行为出现 不是希望 是呵斥

JavaScript脚本的加载插件是如何解决这一问题的?
没用过 但是我觉得完全可以在脚本里面调用自己的java代码 然后用代码再去执行相关操作
而不是纯js加载(?)

Karlatemp 发表于 2020-8-10 19:41:23

这是我对你的问题的相关回答

public static IService getService(ClassLoader classLoader) {
    if (classLoader == null) {
      classLoader = Thread.currentThread().getContextClassLoader();
      // 当没有指定 classLoader 的时候, 大部分(不排除有部分弱智库不这样做) 库
      // 都会使用 Thread.currentThread().getContextClassLoader() 作为目标 ClassLoader
      //
      // 由于 Java 的类加载本身并不依赖 Thread.currentThread().getContextClassLoader(),
      // 而是依赖需要使用的类他自己的唯一类加载器, 所以, 线程上下文加载器你设置成什么, 对java本身并没有任何影响
      // 换句话来说, 线程上下文类加载器无法影响类本身的加载机制
      // 上下文类加载器只是各种类库的缺省设置
      // 这个值影响的只有类库的加载
      //
      // 对于 Bukkit Plugin System
      // Bukkit 加载插件的时候, 并不会去修改 Thread.currentThread().getContextClassLoader(),
      // 也就是说, 默认情况下, contextClassLoader = ClassLoader.getSystemClassLoader()
      //
      // 此时, SystemCL 没法找到 PluginCL,
      // 你需要临时把上下文加载器设置成 PluginCL
      // 然后, 为了避免系统不稳定性, 我们在调用完毕后需要设置回去
      //
      // 就像下面这样
      /*
      ClassLoader old = Thread.currentThread().getContextClassLoader();
      try {
            Thread.currentThread().setContextClassLoader(Bootstrap.class.getClassLoader());
            // Service loading.
      } finally {
            Thread.currentThread().setContextClassLoader(old);
      }
      */
    }
    return getServiceImpl( /* @NotNull */ classLoader)
}

William_Shi 发表于 2020-8-10 19:45:54

阴阳师元素祭祀 发表于 2020-8-10 19:28
首先我不会bukkit 然后我也不会JavaScript

Nashorn我记得弃用了


Nashorn在javax包下没有弃用,至少针对Java8完全没有问题

为什么连线程context的loader都要改
这个是我有疑问的地方,只能说是一种解决方法

因为eval js的东西的时候需要相关context么?
准确的来说,PluginClassLoader没双亲委派导致的,逻辑太复杂
总之以往1.15测试时,不指定ClassLoader,没有办法通过Java.type访问其他插件的类

采用AppClassLoader出现了问题 指 getEngineFactories返回空?
不是,没做这个测试
我说的是使用AppClassLoader,js中调用Java.type会报错ClassNotFound

在脚本里面调用自己的java代码
Java.type就是获取Java中的类,调用Java的代码

阴阳师元素祭祀 发表于 2020-8-10 19:50:19

本帖最后由 阴阳师元素祭祀 于 2020-8-10 19:51 编辑

William_Shi 发表于 2020-8-10 19:45
Nashorn在javax包下没有弃用,至少针对Java8完全没有问题

为什么连线程context的loader都要改

不指定ClassLoader,没有办法通过Java.type访问其他插件的类
这个不指定 包括ContextClassLoader (?)某个解决方法么 那没事莉



总之以往1.15测试时,不指定ClassLoader,没有办法通过Java.type访问其他插件的类
没法访问目标类的时候 目标类加载了么

Java.type就是获取Java中的类,调用Java的代码
你之前说的是无法访问 其他插件的类 那么自己插件的类 是能的(?)
那么脚本里面调用自己代码操作就对了(确信)



William_Shi 发表于 2020-8-10 19:53:37

阴阳师元素祭祀 发表于 2020-8-10 19:50


这个不指定 包括ContextClassLoader (?)某个解决方法么 那没事莉

你之前说的是无法访问 其他插件的类 那么自己插件的类 是能的(?)
那么脚本里面调用自己代码操作就对了(确信)

都不能。如果能的话我当然可以采用自己的类封装别的插件的类

没法访问目标类的时候 目标类加载了么
加载了。Class.forName强行加载
Class.forName未报错,返回的Class<?>不为空。

洞穴夜莺 发表于 2020-8-10 19:57:42

个人觉得不要用Nashorn
其一,Nashorn在高版本Java已经弃用
其二,Nashorn仍然在执行ES5标准,而火狐谷歌之流都奔着ES7标准去了

William_Shi 发表于 2020-8-10 20:00:44

Karlatemp 发表于 2020-8-10 19:41
这是我对你的问题的相关回答

最后的方法就类似PVPINJSRT(也是set回原来的CL
但是关键在于1.15发生了什么使得指定PluginClassLoader加载Nashorn无法再获取到Engine了
这与什么有关?
(1.12可以getEngineByName("js"),但是1.15getEngineFactories返回了空列表

感谢提供完整操作,问题差不多解决了

Karlatemp 发表于 2020-8-10 20:07:42

针对 Nashorn, 我翻了 JDK 的源码, 希望能解决你的问题

ScriptEngineManager manager = new ScriptEngineManager(Thread.currentThread().getContextClassLoader());
ScriptEngine engine = manager.getEngineByName("js");

首先到 `new ScriptEngineManager` 查看 JDK 其源码, 可以发现,
可以发现, 这个 ClassLoader 参数
public ScriptEngineManager(ClassLoader loader) {
    init(loader);
}
private void init(final ClassLoader loader) {
    globalScope = new SimpleBindings();
    engineSpis = new HashSet<ScriptEngineFactory>();
    nameAssociations = new HashMap<String, ScriptEngineFactory>();
    extensionAssociations = new HashMap<String, ScriptEngineFactory>();
    mimeTypeAssociations = new HashMap<String, ScriptEngineFactory>();
    initEngines(loader);
}

private void initEngines(final ClassLoader loader) {
    Iterator<ScriptEngineFactory> itr = null;
    try {
      ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged(
            new PrivilegedAction<ServiceLoader<ScriptEngineFactory>>() {
                @Override
                public ServiceLoader<ScriptEngineFactory> run() {
                  return getServiceLoader(loader);
                }
            });
      itr = sl.iterator();
    } catch (ServiceConfigurationError err) {
      System.err.println("Can't find ScriptEngineFactory providers: " +
                      err.getMessage());
      if (DEBUG) {
            err.printStackTrace();
      }
      // do not throw any exception here. user may want to
      // manage his/her own factories using this manager
      // by explicit registratation (by registerXXX) methods.
      return;
    }
    try {
      while (itr.hasNext()) {
            try {
                ScriptEngineFactory fact = itr.next();
                engineSpis.add(fact);
            } catch (ServiceConfigurationError err) {
                System.err.println("ScriptEngineManager providers.next(): "
                           + err.getMessage());
                if (DEBUG) {
                  err.printStackTrace();
                }
                // one factory failed, but check other factories...
                continue;
            }
      }
    } catch (ServiceConfigurationError err) {
      System.err.println("ScriptEngineManager providers.hasNext(): "
                        + err.getMessage());
      if (DEBUG) {
            err.printStackTrace();
      }
      // do not throw any exception here. user may want to
      // manage his/her own factories using this manager
      // by explicit registratation (by registerXXX) methods.
      return;
    }
}

详细逻辑请自行前往 JDK 源码查看

不难看出, 这个 Manager 会尝试加载全部他自己找得到的 `ScriptEngineFactory`

然后已知 JDK8 中, Java的JavaScript的Engine为 Nashorn
他的 ScriptEngineFactory 为 NashornScriptEngineFactory,

@Exported
public final class NashornScriptEngineFactory implements ScriptEngineFactory {
    private static final String[] DEFAULT_OPTIONS = new String[]{"-doe"};
    private static final List<String> names = immutableList("nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript");
    private static final List<String> mimeTypes = immutableList("application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript");
    private static final List<String> extensions = immutableList("js");

    public NashornScriptEngineFactory() {
    }

通过查看 NashornScriptEngineFactory 的源码可以看出, Nashorn的 Factory 和 Engine 之间的创建并没有任何关联

来到 `manager.getEngineByName("js")`, 我们查看其源码, 可以得出

public ScriptEngine getEngineByName(String shortName) {
    if (shortName == null) throw new NullPointerException();
    //look for registered name first
    Object obj;
    if (null != (obj = nameAssociations.get(shortName))) {
      ScriptEngineFactory spi = (ScriptEngineFactory)obj;
      try {
            ScriptEngine engine = spi.getScriptEngine();
            engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
            return engine;
      } catch (Exception exp) {
            if (DEBUG) exp.printStackTrace();
      }
    }
    for (ScriptEngineFactory spi : engineSpis) {
      List<String> names = null;
      try {
            names = spi.getNames();
      } catch (Exception exp) {
            if (DEBUG) exp.printStackTrace();
      }
      if (names != null) {
            for (String name : names) {
                if (shortName.equals(name)) {
                  try {
                        ScriptEngine engine = spi.getScriptEngine();
                        engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
                        return engine;
                  } catch (Exception exp) {
                        if (DEBUG) exp.printStackTrace();
                  }
                }
            }
      }
    }
    return null;
}


public ScriptEngine getEngineByName(String shortName) {
    // ....
      try {
            ScriptEngine engine = spi.getScriptEngine();
            engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
            return engine;
    // ...
    // ...
                        ScriptEngine engine = spi.getScriptEngine();
                        engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
    // ...
}


可以找到, 核心就是 getScriptEngine(), 接下来我们查看 NashornScriptEngineFactory#getScriptEngine


    public ScriptEngine getScriptEngine() {
      try {
            return new NashornScriptEngine(this, DEFAULT_OPTIONS, getAppClassLoader(), (ClassFilter)null);
      } catch (RuntimeException var2) {
            if (Context.DEBUG) {
                var2.printStackTrace();
            }

            throw var2;
      }
    }

    private static ClassLoader getAppClassLoader() {
      ClassLoader ccl = Thread.currentThread().getContextClassLoader();
      return ccl == null ? NashornScriptEngineFactory.class.getClassLoader() : ccl;
    }


让我们注意一下, getAppClassLoader, 他做了什么,
他直接使用了 Thread.currentThread().getContextClassLoader(),
这说明 NashornScriptEngine 的创建 本身 和 NashornScriptEngineFactory 没有任何关联!
结合我之前发的解释, 这也完美的说明了


为什么要给Nashorn指定类加载器呢?因为写脚本只要涉及到Java.type,取到另外Java插件的一些什么类
在不指定类加载器的情况下都会报错
Caused by: java.lang.ClassNotFoundException: xxxxxxx
      at java.net.URLClassLoader.findClass(Unknown Source) ~[?:1.8.0_241]


但是刚才,我即使是按照上述写法,仍然报了ClassNotFoundException
于是我干了一件这样的事

Thread.currentThread().setContextClassLoader(getClassLoader());


当我再次运行new ScriptEngineManager(Thread.currentThread().getContextClassLoader())
并且尝试Java.type 访问另外Java插件的类的时候成功了


没有 Thread.currentThread().setContextClassLoader(getClassLoader()); 的时候,
Nashorn 引擎使用的是 System.getClassLoader(), 自然无法找到位于 PluginClassLaoder 的任何类,

此刻, 证毕.

William_Shi 发表于 2020-8-10 20:07:58

洞穴夜莺 发表于 2020-8-10 19:57
个人觉得不要用Nashorn
其一,Nashorn在高版本Java已经弃用
其二,Nashorn仍然在执行ES5标准,而火狐谷歌之 ...

据说Nashorn将会被一个新的JS引擎所替代
叫做Graal(GraalVM)
并且是ECMAScript 2020 compatible
还支持JDK15
可以说是非常非常新
但是我还不会用。。。

William_Shi 发表于 2020-8-10 20:25:44

Karlatemp 发表于 2020-8-10 20:07
针对 Nashorn, 我翻了 JDK 的源码, 希望能解决你的问题

ScriptEngineManager manager = new ScriptEngine ...

感谢大佬的回复
也就是说在新建了Manager时传参只能确保加载到ScriptEngineFactory
调整Engine相关还得靠改ContextClassLoader,除非它被强行设为空

那么得出这样的结论
创建Manager使用App Class Loader确保能加载到EngineFactory
创建Engine调整ContextClassLoader为PluginClassLoader确保能访问到插件

已经完全理清,再次感谢大佬的回复!

2280761425 发表于 2020-8-22 17:46:37

本帖最后由 2280761425 于 2020-8-22 17:48 编辑

之前我在开发时直接设置ContextClassLoader也遇到过这问题
但是不这样又不好调用其他插件的类
所以我的做法是在必要时先获取到对应插件的ClassLoader再进行加载(如果觉得麻烦也可以获取到classloader后保存,之后直接使用)
具体可以参考一下这里https://github.com/Miraclexc/Jav ... sources/lib/util.js
页: [1]
查看完整版本: 关于修改主线程类加载器