javatomcatclassloaderservlet-container

Container-level Versioned Libraries Shared by WARs


In a Java servlet container (preferably Tomcat, but if this can be done in a different container then say so) I desire something which is theoretically possible. My question here is whether tools exist to support it, and if so what tools (or what names I should research further).

Here is my problem: in one servlet container I want to run a large number of different WAR files. They share some large common libraries (such as Spring). At first blush, I have two unacceptable alternatives:

  1. Include the large library (Spring, for example) in each WAR file. This is unacceptable because it will load a large number of copies of Spring, exhausting the memory on the server.

  2. Place the large library in the container classpath. Now all of the WAR files share one instance of the library (good). But this is unacceptable because I cannot upgrade the Spring version without upgrading ALL of the WAR files at once, and such a large change is difficult verging on impossible.

In theory, though, there is an alternative which could work:

  1. Put each version of the large library into the container-level classpath. Do some container level magic so that each WAR file declares which version it wishes to use and it will find that on its classpath.

The "magic" must be done at the container level (I think) because this can only be achieved by loading each version of the library with a different classloader, then adjusting what classloaders are visible to each WAR file.

So, have you ever heard of doing this? If so, how? Or tell me what it is called so I can research further.


Solution

  • Regarding Tomcat, for the 7th version you can use VirtualWebappLocader like so

    <Context>
        <Loader className="org.apache.catalina.loader.VirtualWebappLoader"
                virtualClasspath="/usr/shared/lib/spring-3/*.jar,/usr/shared/classes" />
    </Context>
    

    For the 8th version Pre- & Post- Resources should be used instead

    <Context>
        <Resources>
            <PostResources className="org.apache.catalina.webresources.DirResourceSet"
                           base="/usr/shared/lib/spring-3" webAppMount="/WEB-INF/lib" />
            <PostResources className="org.apache.catalina.webresources.DirResourceSet"
                           base="/usr/shared/classes" webAppMount="/WEB-INF/classes" />
        </Resources>
    </Context>
    

    Don't forget to put the corresponding context.xml into the META-INF of your webapp.

    For the jetty as well as other containers the same technique may be used. The only difference is in how to specify extra classpath elements for the webapp.


    UPDATE The samples above does not share the loaded classes, but the idea is the same - use custom classloader. Here is just the pretty ugly sample that also tries to prevent classloader leaks during undeployment.


    SharedWebappLoader

    package com.foo.bar;
    
    import org.apache.catalina.LifecycleException;
    import org.apache.catalina.loader.WebappLoader;
    
    public class SharedWebappLoader extends WebappLoader {
    
        private String pathID;
        private String pathConfig;
    
        static final ThreadLocal<ClassLoaderFactory> classLoaderFactory = new ThreadLocal<>();
    
        public SharedWebappLoader() {
            this(null);
        }
    
        public SharedWebappLoader(ClassLoader parent) {
            super(parent);
            setLoaderClass(SharedWebappClassLoader.class.getName());
        }
    
        public String getPathID() {
            return pathID;
        }
    
        public void setPathID(String pathID) {
            this.pathID = pathID;
        }
    
        public String getPathConfig() {
            return pathConfig;
        }
    
        public void setPathConfig(String pathConfig) {
            this.pathConfig = pathConfig;
        }
    
        @Override
        protected void startInternal() throws LifecycleException {
            classLoaderFactory.set(new ClassLoaderFactory(pathConfig, pathID));
            try {
                super.startInternal();
            } finally {
                classLoaderFactory.remove();
            }
        }
    
    }
    

    SharedWebappClassLoader

    package com.foo.bar;
    
    import org.apache.catalina.LifecycleException;
    import org.apache.catalina.loader.ResourceEntry;
    import org.apache.catalina.loader.WebappClassLoader;
    
    import java.net.URL;
    
    public class SharedWebappClassLoader extends WebappClassLoader {
    
        public SharedWebappClassLoader(ClassLoader parent) {
            super(SharedWebappLoader.classLoaderFactory.get().create(parent));
        }
    
        @Override
        protected ResourceEntry findResourceInternal(String name, String path) {
            ResourceEntry entry = super.findResourceInternal(name, path);
            if(entry == null) {
                URL url = parent.getResource(name);
                if (url == null) {
                    return null;
                }
    
                entry = new ResourceEntry();
                entry.source = url;
                entry.codeBase = entry.source;
            }
            return entry;
        }
    
        @Override
        public void stop() throws LifecycleException {
            ClassLoaderFactory.removeLoader(parent);
        }
    }
    

    ClassLoaderFactory

    package com.foo.bar;
    
    import java.io.BufferedInputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Properties;
    
    public class ClassLoaderFactory {
    
        private static final class ConfigKey {
            private final String pathConfig;
            private final String pathID;
            private ConfigKey(String pathConfig, String pathID) {
                this.pathConfig = pathConfig;
                this.pathID = pathID;
            }
            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
    
                ConfigKey configKey = (ConfigKey) o;
    
                if (pathConfig != null ? !pathConfig.equals(configKey.pathConfig) : configKey.pathConfig != null)
                    return false;
                if (pathID != null ? !pathID.equals(configKey.pathID) : configKey.pathID != null) return false;
    
                return true;
            }
    
            @Override
            public int hashCode() {
                int result = pathConfig != null ? pathConfig.hashCode() : 0;
                result = 31 * result + (pathID != null ? pathID.hashCode() : 0);
                return result;
            }
        }
    
        private static final Map<ConfigKey, ClassLoader> loaders = new HashMap<>();
        private static final Map<ClassLoader, ConfigKey> revLoaders = new HashMap<>();
        private static final Map<ClassLoader, Integer> usages = new HashMap<>();
    
        private final ConfigKey key;
    
        public ClassLoaderFactory(String pathConfig, String pathID) {
            this.key = new ConfigKey(pathConfig, pathID);
        }
    
        public ClassLoader create(ClassLoader parent) {
            synchronized (loaders) {
                ClassLoader loader = loaders.get(key);
                if(loader != null) {
                    Integer usageCount = usages.get(loader);
                    usages.put(loader, ++usageCount);
                    return loader;
                }
    
                Properties props = new Properties();
                try (InputStream is = new BufferedInputStream(new FileInputStream(key.pathConfig))) {
                    props.load(is);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
    
                String libsStr = props.getProperty(key.pathID);
                String[] libs = libsStr.split(File.pathSeparator);
                URL[] urls = new URL[libs.length];
                try {
                    for(int i = 0, len = libs.length; i < len; i++) {
                        urls[i] = new URL(libs[i]);
                    }
                } catch (MalformedURLException e) {
                    throw new RuntimeException(e);
                }
    
                loader = new URLClassLoader(urls, parent);
                loaders.put(key, loader);
                revLoaders.put(loader, key);
                usages.put(loader, 1);
    
                return loader;
            }
        }
    
        public static void removeLoader(ClassLoader parent) {
            synchronized (loaders) {
                Integer val = usages.get(parent);
                if(val > 1) {
                    usages.put(parent, --val);
                } else {
                    usages.remove(parent);
                    ConfigKey key = revLoaders.remove(parent);
                    loaders.remove(key);
                }
            }
        }
    
    }
    

    context.xml of the first app

    <Context>
        <Loader className="com.foo.bar.SharedWebappLoader"
                pathConfig="${catalina.base}/conf/shared.properties"
                pathID="commons_2_1"/>
    </Context>
    

    context.xml of the second app

    <Context>
        <Loader className="com.foo.bar.SharedWebappLoader"
                pathConfig="${catalina.base}/conf/shared.properties"
                pathID="commons_2_6"/>
    </Context>
    

    $TOMCAT_HOME/conf/shared.properties

    commons_2_1=file:/home/xxx/.m2/repository/commons-lang/commons-lang/2.1/commons-lang-2.1.jar
    commons_2_6=file:/home/xxx/.m2/repository/commons-lang/commons-lang/2.6/commons-lang-2.6.jar