2017-07-25 62 views
2

我們在工作中討論了靜態(從構建的JAR)到動態(從CLASS_PATH中的不同位置)加載Java庫的優缺點。是否可以爲Java創建自定義類加載器,以有條件地從JAR或CLASS_PATH加載類?

在討論中間,我突然想到:不管哪一方是正確的,也許有辦法讓你的蛋糕,吃它太:

  • 有一個默認的自定義類加載器的組織
  • 類加載器 - 加載特定庫時 - 檢查一個配置,對於每個庫(或app +庫的組合以獲得更細粒度),都包含一個標誌,用於確定庫是從JAR靜態加載還是動態加載來自CLASS_PATH
  • 這些應用程序都是使用JAR中的庫類構建的(de如果您不想動態加載新的庫版本,則使用該庫的「備份」版本)
  • 如果希望靜態加載庫(例如,因爲新版本與舊版本不兼容,或者只是爲了消除變更風險(如果有必要)),則將該庫的配置標誌設置爲true。
  • 如果您的變化風險爲零/低,您將該標誌設置爲false;並允許動態加載庫,從而允許您發佈由所有應用程序拾取的庫的新版本,而無需重新編譯和重新發布(顯然,應用程序將需要全部使用新庫進行測試只是一個錯誤方法的雷區)。

不管我的想法是好還是壞,我想知道的是,我的第二個項目符號點是否是即使與Java(比如1.8+)技術上是可行的,如果是的話,你會參與執行什麼它?

回答

2

是的,絕對。我將假設Java 8爲這個答案,因爲我還沒有熟悉Java 9的Module System的功能。

主要的想法很簡單 - 一個ClassLoaderloadClass(String, boolean)實現符合下面的「協議」:

  1. 如果類名稱參數指的是一個系統類,委託加載到你的父母裝載機和回報。否則繼續。
  2. 根據您的配置,名稱是否涉及應用程序類?如果是的話繼續執行2.1,否則執行3.
    1. 獲取應用程序「搜索路徑」(JAR文件本身的文件系統路徑,根據問題的要求)。
    2. 嘗試以實現特定的方式(通過例如針對搜索路徑條目的路徑解析)找到匹配名稱參數的搜索路徑下的類資源。如果這樣的資源存在,繼續執行步驟5,否則到3
  3. 每個已知庫尚未審查,通過優先級從高到低排序:
    1. 這個名字是指一類庫?如果這樣繼續;否則返回3.
    2. 獲取特定庫的搜索路徑。
    3. 嘗試在與name參數匹配的搜索路徑下查找類資源。如果成功,請繼續執行步驟5,否則返回步驟3.
  4. 如果在步驟2或3中未建立匹配的類資源,則引發異常。否則繼續。
  5. 檢索資源內容,根據需要委託給defineClassresolveClass,並返回新的類。


下面是這樣一個 ClassLoader的(相當醜陋)樣本實現。

假設:

  • 模塊應用
  • 模塊包含一組已知的類;換句話說,一個模塊「知道它的內容」。
  • 應用程序使用零個或多個庫。
  • 多個邏輯應用程序可能在同一個JVM上運行。
  • 一個Configuration通信,其ClassLoader
    • 「當前」 應用程序請求的類加載。
    • 應用程序和搜索路徑之間的映射。
    • 庫應用程序對和搜索路徑之間的映射。
  • 多個加載器可能會使用(持久表示)相同的配置。

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 be 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; 
       for (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) { 
      for (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; 
     } 
     finally { 
      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 to "reload" our own class 
       return self; 
      } 
      ret = findLoadedClass(name); 
      if (ret != null) { 
       // already loaded 
       return ret; 
      } 
      // unknown 
      ret = findClass(name); 
      if (resolve) { 
       resolveClass(ret); 
      } 
      return ret; 
     } 
    } 

    @Override 
    protected Class<?> findClass(String name) throws ClassNotFoundException { 
     // perform a search on the global classpath (obviously far from 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)) { 
       // probably a bootstrap classpath class - these are off limits to us 
       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: for (URL searchPathEntry : searchPath) { 
          for (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 an 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 the search path of any library "known to" app 
         for (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)); 
        } 
       } 
      } 
      finally { 
       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); 
    } 

} 

注:

  • 搜索路徑與加載程序註冊必須是有效類路徑( 「java.class.path」 的子集(子樹)屬性)。此外,不支持「胖JAR」。
  • 由於帖子長度限制,我無法包含用法示例。如果有要求,我會提供一個要點。
相關問題