Java – container level versioned library shared by war
In a java servlet container (preferably tomcat, but if this can be done in another container, then say so) I want something that works in theory My question is whether there are tools to support it and which tools exist (or which names I should study further)
This is my problem: in a servlet container, I want to run a large number of different war files They share some large public libraries (such as spring) At first glance, I have two unacceptable choices:
>Include large libraries (such as spring) in each war file This is not acceptable because it will load a large number of spring copies and run out of memory on the server. > Place large libraries in the container classpath Now all war files share a library instance (good) But this is unacceptable because I can't upgrade the spring version without upgrading all war files, and such a big change is difficult to achieve
But in theory, there is an alternative:
>Put each version of a large library into a container - level classpath Do some container level magic so that each war file declares which version it wants to use, and it will find it in its classpath
"Magic" must be done at the container level (I think), because this can only be achieved by loading each version of the library with different class loaders, and then adjusting the class loader that is visible in every WAR file.
So, have you ever heard of this? What if so? Or tell me what it's called so I can study it further
Solution
For tomcat, for version 7, you can use virtual web app locator like this
<Context> <Loader className="org.apache.catalina.loader.VirtualWebappLoader" virtualClasspath="/usr/shared/lib/spring-3/*.jar,/usr/shared/classes" /> </Context>
For version 8, you should use pre - & post - resources
<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 meta - inf of webapp
For the jetty and other containers can use the same technology The only difference is how to specify additional classpath elements for webapp
Update the above example does not share the loaded classes, but the idea is the same - use a custom class loader This is a very ugly sample that also attempts 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); } } } }
The context of the first application xml
<Context> <Loader className="com.foo.bar.SharedWebappLoader" pathConfig="${catalina.base}/conf/shared.properties" pathID="commons_2_1"/> </Context>
Context of the second application xml
<Context> <Loader className="com.foo.bar.SharedWebappLoader" pathConfig="${catalina.base}/conf/shared.properties" pathID="commons_2_6"/> </Context>
$TOMCAT_ Home / conf directory / 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