【问题标题】:How to load jars dynamically from different locations into current class loader?如何将 jar 从不同位置动态加载到当前类加载器中?
【发布时间】:2017-01-23 08:01:42
【问题描述】:

每当我在 tomcat 中部署 web 应用程序时,WEB-INF/lib 中的所有 jar 都会被加载到应用程序 ClassLoader 中。

我几乎没有其他位置有一些 jar 集,例如 WEB-INF/ChildApp1/*.jar、WEB-INF/ChildApp2/*.jar ..... 根据用户请求,我想将一组 jar 加载到当前的类加载器中。

注意:我不想创建任何子类加载器。

我真正的要求是,如何以编程方式将 jars 添加到当前的类加载器中。

【问题讨论】:

  • 我的要求是将不同位置的 jar 加载到当前的类加载器中。不想创建子类加载器。
  • 你想要实现的不是标准的,如果没有自定义类加载器,你将无法做到
  • 非标准位置列表会在运行时改变还是固定?
  • 位置是固定的。但我无法将这些 jar 复制到 WEB-INF/lib 文件夹中。

标签: java tomcat jar classloader dynamic-class-loaders


【解决方案1】:

我做过一次,但这有点 hack。请看下面的代码:

    final URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        final Class<URLClassLoader> sysclass = URLClassLoader.class;
        // TODO some kind of a hack. Need to invent better solution.
        final Method method = sysclass.getDeclaredMethod("addURL", new Class[] { URL.class });
        method.setAccessible(true);
        for (final File jar : jars) {
            method.invoke(sysloader, new URL[] { jar.toURI().toURL() });
        }

您需要将 ClassLoader.getSystemClassloader() 更改为您要使用的类加载器。您还必须检查这是否是 URLClassloader 的实例 我认为有更好的解决方案,但这对我有用

【讨论】:

  • 您似乎在 System 类加载器中添加 jars。如果有任何 jar 版本冲突,那么我的应用程序可能无法按预期工作。例如,jsonv1.jar 已经加载到 webapp CL(Application类加载器)然后如果我按照您的建议在 System CL 中加载 jsonv2.jar。那么我的应用程序可能会导致 NoSuchMethodError 或 NoClassDefFoundError ?
  • 是的,我使用了系统类加载器(我有一个非常简单的案例,并且确信某些 jar 只会加载一次),但是对于您,我建议使用不同的。我不是 100% 确定如果添加另一个包含相同类的 jar 会发生什么。我会说它将继续使用旧的。但这只是我的猜测,但很容易检查
【解决方案2】:

您需要实现自己的WebappClassLoaderBase,以便在context.xmlloader 的配置中定义。

实现你的WebappClassLoaderBase

最简单的方法是将WebappClassLoader扩展为next

package my.package;

public class MyWebappClassLoader extends WebappClassLoader {

    public MyWebappClassLoader() {
    }

    public MyWebappClassLoader(final ClassLoader parent) {
        super(parent);
    }

    @Override
    public void start() throws LifecycleException {
        String[] paths = {"/WEB-INF/ChildApp1/lib", "/WEB-INF/ChildApp2/lib"};
        // Iterate over all the non standard locations
        for (String path : paths) {
            // Get all the resources in the current location
            WebResource[] jars = resources.listResources(path);
            for (WebResource jar : jars) {
                // Check if the resource is a jar file
                if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                    // Add the jar file to the list of URL defined in the parent class
                    addURL(jar.getURL());
                }
            }
        }
        // Call start on the parent class
        super.start();
    }
}

部署你的WebappClassLoaderBase

  • 使用与您的 tomcat 版本相对应的 tomcat jar 构建您自己的WebappClassLoaderBase,该 tomcat 版本可从here 获得。
  • 从中创建一个罐子
  • 并将 jar 放入 tomcat/lib 以使其可从Common ClassLoader 获得

配置您的WebappClassLoaderBase

context.xml 中定义您的WebappClassLoaderBase

<Context>
    ...
    <Loader loaderClass="my.package.MyWebappClassLoader" />
</Context>

大功告成,现在你的 webapps 将能够从 /WEB-INF/ChildApp1/lib/WEB-INF/ChildApp2/lib 加载 jar 文件。


响应更新

由于您想做同样的事情,但只使用 war,您需要使用 hack 来动态添加您的 jar 文件。

您可以这样做:

实现ServletContextListener 以添加您的 jar 文件

为了在初始化上下文时动态添加您的jar 文件,您需要创建一个ServletContextListener,它将通过反射调用URLClassLoader#addURL(URL),这是一个丑陋的hack,但它可以工作。请注意,它之所以有效,是因为 Tomcat 中 webapp 的 ClassLoaderWebappClassLoader,它实际上是 URLClassLoader 的子类。

package my.package;

public class MyServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(final ServletContextEvent sce) {
        try {
            // Get the method URLClassLoader#addURL(URL)
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            // Make it accessible as the method is protected
            method.setAccessible(true);
            String[] paths = {"/WEB-INF/ChildApp1/lib", "/WEB-INF/ChildApp2/lib"};
            for (String path : paths) {
                File parent = new File(sce.getServletContext().getRealPath(path));
                File[] jars = parent.listFiles(
                    new FilenameFilter() {
                        @Override
                        public boolean accept(final File dir, final String name) {
                            return name.endsWith(".jar");
                        }
                    }
                );
                if (jars == null)
                    continue;
                for (File jar : jars) {
                    // Add the URL to the context CL which is a URLClassLoader 
                    // in case of Tomcat
                    method.invoke(
                        sce.getServletContext().getClassLoader(), 
                        jar.toURI().toURL()
                    );
                }
            }

        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public void contextDestroyed(final ServletContextEvent sce) {
    }
}

声明你的ServletContextListener

在您的 webapp 的 web.xml 中添加:

<listener>
    <listener-class>my.package.MyServletContextListener</listener-class>
</listener>

【讨论】:

  • 谢谢,我会测试并告诉你。第一个问题,为什么要将 WebappClassLoaderBase 保留在 tomcat 共享库(tomcat/lib)中,为什么 tomcat 不能从我的创建 WebappClassLoaderBase CL(应用程序类加载器) WEB-INF/lib....第二个,WebappClassLoaderBase CL会有WEB-INF/lib + /WEB-INF/ChildApp1/lib" + "/WEB-INF/ChildApp2/lib" jars ?
  • #1 因为加载器是全局的,并不特定于给定的 webapp,因此必须在公共类加载器中定义它才能在 tomcat 启动时使用 #2 是
  • 感谢 Nicolas,它成功了。所有外部位置和 WEB-INF/lib jar 都加载到 web-app 类加载器中。担心,这仅适用于 tomcat。我无法向客户提供一个罐子(用于共享 CL)和一个战争(应用程序)。我们可以以编程方式做同样的事情吗?....我的意思是在部署战争之后,在应用程序启动中,是否可以在单个 CL 中加载所有 jar?
  • 为什么不能给客户一罐一战?
  • 我们是 RAD 平台,我们可以在其中快速开发 webapps。就像用户将开发 webapps 并将其部署在我们的云环境中,或者他们可以导出应用程序(war's)以部署在其他一些服务器。如果我使用一个 jar + 一场战争,首先它将无法在其他 Web 服务器中运行(如谷歌应用服务器、Web 逻辑......)其次我需要教育所有用户(这是不可能的),第三最终我应该只提供战争 bzc,这就是人们对 webapps 可移植性的了解。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-03-14
  • 2020-06-08
  • 2014-12-29
  • 1970-01-01
  • 1970-01-01
  • 2011-11-07
  • 1970-01-01
相关资源
最近更新 更多