classloader - Can a custom class loader be created for Java to conditionally load a class from JAR or from CLASS_PATH? -
we had discussion @ work pros , cons of static (from built jar) vs dynamic (from separate location in class_path) loading of java libraries.
in middle of discussion, occurred me: regardless of side right, maybe there's way have cake , eat too:
- have default custom class loader organization
- class loader - when loading specific library - checks configuration that, every library (or app+library combo more granularity), contains flag determines if library loaded statically jar or dynamically class_path
- the apps built library classes in jars (default "backup" version of library use if don't want dynamically load new library version)
- if wish library loaded statically (e.g. because new version incompatible older one, or eliminate change risk if warranted), set configuration flag library true.
- if instead change risk zero/low, set flag false; , allow library loaded dynamically, allowing release new versions of library picked apps without re-compilation , re-release (obviously, apps need tested new library minefield of bugs approach).
whether idea or bad, what know is, whether second bullet point technically feasible java (say 1.8+), , if so, involved in implementing it?
yes, absolutely. assume java 8 answer , have yet familiarize myself capabilities of java 9's module system.
the main idea straightforward--a classloader
loadclass(string, boolean)
implementation adheres following "protocol":
- if class name argument refers system class, delegate loading parent loader , return. otherwise proceed.
- does name, according configuration, refer application class? if proceed step 2.1, otherwise 3.
- get application "search path" (the filesystem path of jar file according requirements in question).
- attempt find, in implementation-specific manner (by means of e.g. path resolution against search path's entries), class resource below search path matches name argument. if such resource exists, proceed step 5, otherwise 3.
- for each known library not yet examined, sorted descending priority:
- does name refer class of library? if continue; otherwise return 3.
- get search path particular library.
- attempt find class resource under search path matches name argument. if successful, proceed step 5, otherwise return step 3.
- if no matching class resource established in step 2 or 3, raise exception. otherwise continue.
- retrieve content of resource, delegate
defineclass
,resolveclass
appropriate, , return new class.
below (pretty ugly) sample implementation of such
classloader
. assumptions:
- a module application or library.
- a module comprises known set of classes; in other words, module "is aware of contents".
- an application uses 0 or more libraries.
- multiple logical applications may running on same jvm.
- a
configuration
communicatesclassloader
:- the "current" application requesting class loading.
- mappings between applications , search paths.
- mappings between library-application pairs , search paths.
- multiple loaders may use (the persisted representation of) same configuration.
package com.example.q45313762; import java.io.bufferedinputstream; import java.io.bytearrayoutputstream; import java.io.ioexception; import java.io.inputstream; import java.net.urisyntaxexception; import java.net.url; import java.net.urlclassloader; import java.nio.file.files; import java.nio.file.paths; import java.security.codesource; import java.security.protectiondomain; import java.security.cert.certificate; import java.util.arrays; import java.util.collections; import java.util.enumeration; import java.util.iterator; import java.util.linkedhashmap; import java.util.linkedhashset; import java.util.map; import java.util.objects; import java.util.set; import java.util.concurrent.locks.lock; import java.util.concurrent.locks.readwritelock; import java.util.concurrent.locks.reentrantreadwritelock; import java.util.function.bifunction; import java.util.function.predicate; import java.util.function.supplier; public final class configurableclasspathclassloader extends urlclassloader { public interface configuration { interface module { string getname(); string getversion(); boolean includes(string resourcename); } interface library extends module {} interface application extends module {} enum loadingmode { static, dynamic; } application getcurrentapplication(); iterable<url> getlibrarysearchpath(library lib, loadingmode mode, application app); iterable<url> getapplicationsearchpath(application app); iterable<library> getapplicationlibraries(application app); } public static final class simplestaticconfiguration implements configuration { private static abstract class simplemodule implements module { private final string name, version; private final predicate<string> resourcenamematcher; private simplemodule(string name, string version, predicate<string> resourcenamematcher) { requirenonenull(name, version, resourcenamematcher); name = name.trim(); version = version.trim(); if (name.isempty() || version.isempty()) { throw new illegalargumentexception("arguments must not empty."); } this.name = name; this.version = version; this.resourcenamematcher = resourcenamematcher; } @override public string getname() { return name; } @override public string getversion() { return version; } @override public boolean includes(string resourcename) { if (resourcename == null) { return false; } return resourcenamematcher.test(resourcename); } @override public final int hashcode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashcode()); result = prime * result + ((resourcenamematcher == null) ? 0 : resourcenamematcher.hashcode()); result = prime * result + ((version == null) ? 0 : version.hashcode()); return result; } @override public final boolean equals(object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof simplemodule)) { return false; } simplemodule other = (simplemodule) obj; if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } if (resourcenamematcher == null) { if (other.resourcenamematcher != null) { return false; } } else if (!resourcenamematcher.equals(other.resourcenamematcher)) { return false; } if (version == null) { if (other.version != null) { return false; } } else if (!version.equals(other.version)) { return false; } return true; } } public static final class simplelibrary extends simplemodule implements library { public simplelibrary(string name, string version, predicate<string> resourcenamematcher) { super(name, version, resourcenamematcher); } } public static final class simpleapplication extends simplemodule implements application { public simpleapplication(string name, string version, predicate<string> resourcenamematcher) { super(name, version, resourcenamematcher); } } private static final class moduleregistry { private static abstract class key { private final module module; private key(module module) { requirenonenull(module); requirenonenull(module.getname(), module.getversion()); this.module = module; } private module getmodule() { return module; } } private static final class librarykey extends key { private final loadingmode mode; private final application app; private librarykey(library lib, loadingmode mode, application app) { super(lib); requirenonenull(mode); requirenonenull(app); this.mode = mode; this.app = app; } private library getlibrary() { return (library) super.getmodule(); } private loadingmode getloadingmode() { return mode; } private application getapplication() { return app; } @override public int hashcode() { final int prime = 31; int result = 1; library lib = getlibrary(); result = prime * result + ((lib == null) ? 0 : lib.hashcode()); result = prime * result + ((mode == null) ? 0 : mode.hashcode()); result = prime * result + ((app == null) ? 0 : app.hashcode()); return result; } @override public boolean equals(object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof librarykey)) { return false; } librarykey other = (librarykey) obj; library thislib = getlibrary(), otherslib = other.getlibrary(); if (thislib == null) { if (otherslib != null) { return false; } } else if (!thislib.equals(otherslib)) { return false; } if (mode != other.mode) { return false; } if (app == null) { if (other.app != null) { return false; } } else if (!app.equals(other.app)) { return false; } return true; } } private static final class applicationkey extends key { private applicationkey(application app) { super(app); } private application getapplication() { return (application) super.getmodule(); } @override public int hashcode() { final int prime = 31; int result = 1; application app = getapplication(); result = prime * result + ((app == null) ? 0 : app.hashcode()); return result; } @override public boolean equals(object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof applicationkey)) { return false; } applicationkey other = (applicationkey) obj; application thisapp = getapplication(), othersapp = other.getapplication(); if (thisapp == null) { if (othersapp != null) { return false; } } else if (!thisapp.equals(othersapp)) { return false; } return true; } } private static final class value { private final set<url> searchpath; private value(url... searchpath) { requirenonenull((object) searchpath); if (searchpath == null || searchpath.length == 0) { this.searchpath = empty_search_path; } else { this.searchpath = new linkedhashset<>(arrays.aslist(searchpath)); iterator<url> itr = this.searchpath.iterator(); while (itr.hasnext()) { url searchpathentry = itr.next(); string proto = searchpathentry.getprotocol(); if ("file".equals(proto) || "jar".equals(proto)) { continue; } itr.remove(); } verify(); } } private set<url> getsearchpath() { verify(); return (searchpath == empty_search_path) ? searchpath : collections.unmodifiableset(searchpath); } private void verify() { iterator<url> itr = searchpath.iterator(); while (itr.hasnext()) { try { if (!files.exists(paths.get(itr.next().touri()))) { itr.remove(); } } catch (illegalargumentexception | urisyntaxexception | securityexception e) { itr.remove(); } } } @override public int hashcode() { final int prime = 31; int result = 1; result = prime * result + ((searchpath == null) ? 0 : searchpath.hashcode()); return result; } @override public boolean equals(object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof value)) { return false; } value other = (value) obj; if (searchpath == null) { if (other.searchpath != null) { return false; } } else if (!searchpath.equals(other.searchpath)) { return false; } return true; } } private final map<key, value> m = new linkedhashmap<>(); private supplier<application> appprovider; private moduleregistry() { } private moduleregistry(moduleregistry mr) { m.putall(mr.m); appprovider = mr.appprovider; } private void putlibraryentry(library lib, loadingmode mode, application app, url... searchpath) { m.put(new librarykey(lib, mode, app), new value(searchpath)); } private void putapplicationentry(application app, url... searchpath) { m.put(new applicationkey(app), new value(searchpath)); } private set<library> getlibraries(application app) { set<library> ret = null; (key k : m.keyset()) { if (!(k instanceof librarykey)) { continue; } librarykey lk = (librarykey) k; if (lk.getapplication().equals(app)) { if (ret == null) { ret = new linkedhashset<>(); } ret.add(lk.getlibrary()); } } if (ret == null) { ret = no_libs; } return ret; } private set<url> getlibrarysearchpath(library lib, loadingmode mode, application app) { set<url> ret = empty_search_path; value v = m.get(new librarykey(lib, mode, app)); if (mode == loadingmode.dynamic && (v == null || v.getsearchpath().isempty())) { v = m.get(new librarykey(lib, loadingmode.static, app)); } if (v != null) { ret = v.getsearchpath(); } return ret; } private set<url> getapplicationsearchpath(application app) { set<url> ret = empty_search_path; value v = m.get(new applicationkey(app)); if (v != null) { ret = v.getsearchpath(); } return ret; } private supplier<application> getapplicationprovider() { return appprovider; } private void setapplicationprovider(supplier<application> appprovider) { requirenonenull(appprovider); requirenonenull(appprovider.get()); this.appprovider = appprovider; } private void clear() { m.clear(); } } public static final class builder { private final moduleregistry registry = new moduleregistry(); private builder() { } public synchronized builder withlibrary(library lib, loadingmode mode, application app, url... searchpath) { registry.putlibraryentry(lib, mode, app, searchpath); return this; } public synchronized builder withapplication(application app, url... searchpath) { registry.putapplicationentry(app, searchpath); return this; } public synchronized builder withapplicationprovider(supplier<application> appprovider) { registry.setapplicationprovider(appprovider); return this; } public synchronized simplestaticconfiguration build() { simplestaticconfiguration ret = new simplestaticconfiguration(this); registry.clear(); return ret; } public synchronized builder reset() { registry.clear(); return this; } } public static final set<url> empty_search_path = collections.emptyset(); private static final set<library> no_libs = collections.emptyset(); public static builder newbuilder() { return new builder(); } private final moduleregistry registry; private simplestaticconfiguration(builder b) { registry = new moduleregistry(b.registry); } @override public application getcurrentapplication() { return registry.getapplicationprovider().get(); } @override public iterable<url> getlibrarysearchpath(library lib, loadingmode mode, application app) { return registry.getlibrarysearchpath(lib, mode, app); } @override public iterable<url> getapplicationsearchpath(application app) { return registry.getapplicationsearchpath(app); } @override public iterable<library> getapplicationlibraries(application app) { return registry.getlibraries(app); } } private static final string java_home_prop = system.getproperty("java.home"); private static void requirenonenull(object... args) { if (args != null) { (object o : args) { objects.requirenonnull(o); } } } private final lock readlock, writelock; private configuration cfg; { readwritelock rwl = new reentrantreadwritelock(false); readlock = rwl.readlock(); writelock = rwl.writelock(); } public configurableclasspathclassloader(configuration cfg, classloader parent) { super(new url[0], parent); setconfiguration(cfg); } public void setconfiguration(configuration cfg) { requirenonenull(cfg); try { writelock.lock(); this.cfg = cfg; } { writelock.unlock(); } } @override protected class<?> loadclass(string name, boolean resolve) throws classnotfoundexception { if (name == null) { throw new classnotfoundexception(name); } synchronized (getclassloadinglock(name)) { class<?> ret; class<?> self = getclass(); if (self.getname().equals(name)) { // no need "reload" our own class return self; } ret = findloadedclass(name); if (ret != null) { // loaded return ret; } // unknown ret = findclass(name); if (resolve) { resolveclass(ret); } return ret; } } @override protected class<?> findclass(string name) throws classnotfoundexception { // perform search on global classpath (obviously far ideal) enumeration<url> allmatches; string modifiedname = name.replace(".", "/").concat(".class"); try { allmatches = getresources(modifiedname); } catch (ioexception ioe) { throw new classnotfoundexception(name); } set<url> filteredmatches = new linkedhashset<>(); while (allmatches.hasmoreelements()) { url match = allmatches.nextelement(); if (match.getpath().replacefirst("file:", "").startswith(java_home_prop)) { // bootstrap classpath class - these off limits return getparent().loadclass(name); } // candidate match filteredmatches.add(match); } if (!filteredmatches.isempty()) { try { readlock.lock(); bifunction<configuration.module, iterable<url>, url[]> matcher = (module, searchpath) -> { url[] ret = null; if (module.includes(name)) { outer: (url searchpathentry : searchpath) { (url filteredmatch : filteredmatches) { if (filteredmatch != null && filteredmatch.getpath().replacefirst("file:", "") .startswith(searchpathentry.getpath())) { ret = new url[] { filteredmatch, searchpathentry }; break outer; } } } } return ret; }; configuration.application app = cfg.getcurrentapplication(); url matchedclassresource = null, matchingsearchpath = null; if (app != null) { // try application search path match url[] tmp = matcher.apply(app, cfg.getapplicationsearchpath(app)); if (tmp != null) { matchedclassresource = tmp[0]; matchingsearchpath = tmp[1]; } else { // try matching against search path of library "known to" app (configuration.library lib : cfg.getapplicationlibraries(app)) { tmp = matcher.apply(lib, cfg.getlibrarysearchpath(lib, configuration.loadingmode.dynamic, app)); if (tmp != null) { matchedclassresource = tmp[0]; matchingsearchpath = tmp[1]; break; } } } if (matchedclassresource != null) { // matched - load byte[] classdata = readclassdata(matchedclassresource); return defineclass(name, classdata, 0, classdata.length, constructclassdomain(matchingsearchpath)); } } } { readlock.unlock(); } } throw new classnotfoundexception(name); } private byte[] readclassdata(url classresource) { try (inputstream in = new bufferedinputstream(classresource.openstream()); bytearrayoutputstream out = new bytearrayoutputstream()) { while (in.available() > 0) { out.write(in.read()); } return out.tobytearray(); } catch (ioexception ioe) { throw new runtimeexception(ioe); } } private protectiondomain constructclassdomain(url codesourcelocation) { codesource cs = new codesource(codesourcelocation, (certificate[]) null); return new protectiondomain(cs, getpermissions(cs), this, null); } }
notes:
- search paths registered loader must subsets (subtrees) of effective classpath ("java.class.path" property). furthermore, "fat jars" not supported.
- i not include usage example due post length constraints. provide 1 via gist if requested.
Comments
Post a Comment