2017-01-30 2 views
28

Я работаю над проектом, в котором мы используем библиотеку, которая не гарантирует потокобезопасность (и не является) и однопоточную в сценарии потоков Java 8, которая работает так, как ожидалось.Хотите запустить библиотеку non-threadsafe параллельно - можно ли это сделать с помощью нескольких загрузчиков классов?

Мы хотели бы использовать параллельные потоки, чтобы получить плотные плотные масштабируемые плоды.

К сожалению, это приводит к сбою библиотеки - скорее всего, потому что один экземпляр вмешивается в переменные, совместно используемые с другим экземпляром, поэтому нам нужна изоляция.

Я рассматривал возможность использования отдельного загрузчика классов для каждого экземпляра (возможно, локального потока), который, насколько мне известно, должен означать, что для всех практических целей я получаю необходимую изоляцию, но я не знаком с намеренно созданием загрузчиков классов для этой цели.

Это правильный подход? Как мне это сделать, чтобы иметь надлежащее качество продукции?


Редактировать: меня попросили получить дополнительную информацию о ситуации, вызвав вопрос, чтобы лучше понять его. Вопрос по-прежнему касается общей ситуации, а не исправления библиотеки.

У меня есть полный контроль над объектом, созданным с помощью библиотеки (которая https://github.com/veraPDF/), как затянуты в

<dependency> 
    <groupId>org.verapdf</groupId> 
    <artifactId>validation-model</artifactId> 
    <version>1.1.6</version> 
</dependency> 

с помощью репозитория Maven проекта для артефактов.

<repositories> 
    <repository> 
     <snapshots> 
      <enabled>true</enabled> 
     </snapshots> 
     <id>vera-dev</id> 
     <name>Vera development</name> 
     <url>http://artifactory.openpreservation.org/artifactory/vera-dev</url> 
    </repository> 
</repositories> 

На данный момент невозможно затвердеть библиотеку.


EDIT: меня попросили показать код. Наш основной адаптер примерно:

public class VeraPDFValidator implements Function<InputStream, byte[]> { 
    private String flavorId; 
    private Boolean prettyXml; 

    public VeraPDFValidator(String flavorId, Boolean prettyXml) { 
     this.flavorId = flavorId; 
     this.prettyXml = prettyXml; 
     VeraGreenfieldFoundryProvider.initialise(); 
    } 

    @Override 
    public byte[] apply(InputStream inputStream) { 
     try { 
      return apply0(inputStream); 
     } catch (RuntimeException e) { 
      throw e; 
     } catch (ModelParsingException | ValidationException | JAXBException | EncryptedPdfException e) { 
      throw new RuntimeException("invoking VeraPDF validation", e); 
     } 
    } 

    private byte[] apply0(InputStream inputStream) throws ModelParsingException, ValidationException, JAXBException, EncryptedPdfException { 
     PDFAFlavour flavour = PDFAFlavour.byFlavourId(flavorId); 
     PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false); 
     PDFAParser loader = Foundries.defaultInstance().createParser(inputStream, flavour); 
     ValidationResult result = validator.validate(loader); 

     // do in-memory generation of XML byte array - as we need to pass it to Fedora we need it to fit in memory anyway. 

     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     XmlSerialiser.toXml(result, baos, prettyXml, false); 
     final byte[] byteArray = baos.toByteArray(); 
     return byteArray; 
    } 
} 

, которая является функцией, которая отображает из InputStream (обеспечивая PDF-файл) в массив байтов (представляющий вывод отчета XML).

(Видя код, я заметил, что есть призыв к инициализаторе в конструкторе, который может быть виновником здесь, в моем конкретном случае. Я до сих пор, как решение общей проблемы.

+4

Я не могу себе представить, как использование разных загрузчиков классов будет решать проблемы синхронизации. – rkosegi

+3

Обычно мы говорим о классах threadafe и/или методах. Библиотека потокобезопасности * довольно широка. Какая библиотека? Я бы определенно попытался найти альтернативы, прежде чем начинать рассматривать хакеры класса loadload. – Kayaman

+0

Этот фрукт не звучит так низко висит, если вам нужно спроектировать специальные загрузчики классов для каждого потока, когда создание резьбы не в ваших руках. – RealSkeptic

ответ

13

Мы столкнулись с подобными проблемами. Проблемы обычно возникали из-за статических свойств, которые стали неохотно «разделяться» между различными потоками.

Использование различных загрузчиков классов, работающих на нас, пока мы можем гарантировать, что статические свойства были фактически установлены на классы, загруженные нашим загрузчиком классов. Java может иметь несколько классов, которые предоставляют свойства или методы, которые не являются изолированными («System.setProperties() и Security.addProvider() в порядке - любая каноническая документация по этому вопросу приветствуется кстати).

Потенциально работоспособное и быстрое решение, которое по крайней мере может дать вам возможность протестировать эту теорию для вашей библиотеки, - использовать сервлет-движок, такой как Jetty или Tomcat.

Постройте несколько войн, которые содержат вашу библиотеку и запускают параллельные процессы (1 на войну).

При запуске кода внутри потока сервлета, WebappClassLoaders этих двигателей сначала загружает классы из загрузчика родительского класса (так же, как и в случае с движком), и если он не находит этот класс, пытается загрузить его из банки/классы, упакованные с войной.

С причалом вы можете программно развернуть войн в соответствии с вашим выбором, а затем теоретически масштабировать количество процессоров (войн) по мере необходимости.

Мы внедрили наш собственный загрузчик классов, расширив URLClassLoader и вдохнув вдохновение из Jetty Webapp ClassLoader. Это не такая трудная работа, как кажется.

Наш загрузчик классов делает абсолютно противоположное: он пытается загрузить класс из локальных банок в «пакет» сначала, а затем пытается получить их из загрузчика родительского класса. Это гарантирует, что библиотека, случайно загруженная родительским загрузчиком классов, никогда не рассматривается (сначала). Наш «пакет» на самом деле представляет собой банку, содержащую другие банки/библиотеки с настраиваемым файлом манифеста.

Проводка этого класса «код-загрузчик» «как есть» не имеет большого смысла (и создает несколько проблем с авторским правом). Если вы хотите изучить этот маршрут дальше, я могу попытаться придумать скелет.

Источник Jetty WebappClassLoader

+0

Если я правильно помню, у многих контейнеров сервлетов есть флаг для переключения «тот, кого загрузчик класса задает первым», - поведение спецификации сервлета. –

8

Ответ на самом деле зависит от того, что ваша библиотека опирается на:

  1. Если ваша библиотека полагается, по крайней мере, одну родную библиотеку, используя ClassLoader с, чтобы изолировать код вашей библиотеки выиграл» t, потому что согласно JNI Specification, не разрешается загружать одну и ту же родную библиотеку JNI в более чем один загрузчик классов, так что вы получите UnsatisfiedLinkError.
  2. Если вы используете библиотеку, по крайней мере, один внешний ресурс, который не предназначен для совместного использования, например, например, файл и который изменен вашей библиотекой, вы можете получить сложные ошибки и/или повреждение ресурса.

Предполагая, что вы не находитесь в перечисленных выше случаях, вообще говоря, если класс известен как небезопасный и не изменяет никаких статических полей, использование выделенного экземпляра этого класса для каждого вызова или для потока достаточно хорошо, поскольку экземпляр класса тогда больше не разделяется.

Здесь, как ваша библиотека, очевидно, полагается и модифицирует некоторые статические поля, которые не предназначены для совместного использования, вам действительно необходимо изолировать классы вашей библиотеки в выделенном ClassLoader и, конечно же, убедитесь, что ваши потоки не разделяют такой же ClassLoader.

Для этого можно просто создать URLClassLoader, к которому вы бы указать местоположение вашей библиотеки, как URL (с использованием URLClassLoader.newInstance(URL[] urls, ClassLoader parent)), то при отражении вы бы получить класс вашей библиотеки, соответствующей точке входа и вызвать ваш целевой метод , Чтобы избежать создания нового URLClassLoader при каждом вызове, вы можете рассмотреть возможность использования ThreadLocal для хранения URLClassLoader или Class или экземпляра Method, который будет использоваться для данного потока.


Так вот, как вы могли бы продолжить:

Допустим, что точка входа моей библиотеки является класс Foo, который выглядит следующим образом:

package com.company; 

public class Foo { 

    // A static field in which we store the name of the current thread 
    public static String threadName; 

    public void execute() { 
     // We print the value of the field before setting a value 
     System.out.printf(
      "%s: The value before %s%n", Thread.currentThread().getName(), threadName 
     ); 
     // We set a new value 
     threadName = Thread.currentThread().getName(); 
     // We print the value of the field after setting a value 
     System.out.printf(
      "%s: The value after %s%n", Thread.currentThread().getName(), threadName 
     ); 
    } 
} 

Этот класс явно не поточно а метод execute изменяет значение статического поля, которое не предназначено для изменения параллельными потоками, как ваш прецедент.

Предполагая, что для запуска моей библиотеки мне просто нужно создать экземпляр Foo и вызвать метод execute. Я мог бы хранить соответствующий Method в ThreadLocal, чтобы получить его отражение только один раз для каждого потока с помощью ThreadLocal.withInitial(Supplier<? extends S> supplier) в следующем:

private static final ThreadLocal<Method> TL = ThreadLocal.withInitial(
    () -> { 
     try { 
      // Create the instance of URLClassLoader using the context 
      // CL as parent CL to be able to retrieve the potential 
      // dependencies of your library assuming that they are 
      // thread safe otherwise you will need to provide their 
      // URL to isolate them too 
      URLClassLoader cl = URLClassLoader.newInstance(
       new URL[]{/* Here the URL of my library*/}, 
       Thread.currentThread().getContextClassLoader() 
      ); 
      // Get by reflection the class Foo 
      Class<?> myClass = cl.loadClass("com.company.Foo"); 
      // Get by reflection the method execute 
      return myClass.getMethod("execute"); 
     } catch (Exception e) { 
      // Here deal with the exceptions 
      throw new IllegalStateException(e); 
     } 
    } 
); 

И, наконец, давайте имитировать одновременное выполнение моей библиотеки:

// Launch 50 times concurrently my library 
IntStream.rangeClosed(1, 50).parallel().forEach(
    i -> { 
     try { 
      // Get the method instance from the ThreadLocal 
      Method myMethod = TL.get(); 
      // Create an instance of my class using the default constructor 
      Object myInstance = myMethod.getDeclaringClass().newInstance(); 
      // Invoke the method 
      myMethod.invoke(myInstance); 
     } catch (Exception e) { 
      // Here deal with the exceptions 
      throw new IllegalStateException(e); 
     } 
    } 
); 

Вы будете получить вывод следующего типа, который показывает, что у нас нет конфликтов между потоками, и потоки должным образом повторно используют его соответствующее значение класса/поля от одного вызова execute к другому:

ForkJoinPool.commonPool-worker-7: The value before null 
ForkJoinPool.commonPool-worker-7: The value after ForkJoinPool.commonPool-worker-7 
ForkJoinPool.commonPool-worker-7: The value before ForkJoinPool.commonPool-worker-7 
ForkJoinPool.commonPool-worker-7: The value after ForkJoinPool.commonPool-worker-7 
main: The value before null 
main: The value after main 
main: The value before main 
main: The value after main 
... 

Поскольку этот подход создает один ClassLoader для каждого потока, убедитесь, что применять этот подход с использованием пула потоков с фиксированным числом нитей и количеством нитей должны быть выбраны мудро, чтобы предотвратить переполнение памяти, потому что a ClassLoader не является свободным с точки зрения памяти, поэтому вам нужно ограничить общее количество экземпляров в соответствии с размером вашей кучи.

После того, как вы сделали с вашей библиотекой, вы должны Ыборкой ThreadLocal для каждого потока вашего пула потоков, чтобы предотвратить утечку памяти и сделать это здесь, как вы могли бы продолжить:

// The size of your the thread pool 
// Here as I used for my example the common pool, its size by default is 
// Runtime.getRuntime().availableProcessors() 
int poolSize = Runtime.getRuntime().availableProcessors(); 
// The cyclic barrier used to make sure that all the threads of the pool 
// will execute the code that will cleanup the ThreadLocal 
CyclicBarrier barrier = new CyclicBarrier(poolSize); 
// Launch one cleanup task per thread in the pool 
IntStream.rangeClosed(1, poolSize).parallel().forEach(
    i -> { 
     try { 
      // Wait for all other threads of the pool 
      // This is needed to fill up the thread pool in order to make sure 
      // that all threads will execute the cleanup code 
      barrier.await(); 
      // Close the URLClassLoader to prevent memory leaks 
      ((URLClassLoader) TL.get().getDeclaringClass().getClassLoader()).close(); 
     } catch (Exception e) { 
      // Here deal with the exceptions 
      throw new IllegalStateException(e); 
     } finally { 
      // Remove the URLClassLoader instance for this thread 
      TL.remove(); 
     } 
    } 
); 
+0

Должно быть проведено дополнительное расследование использования нетекающей библиотеки. Даже если ваш ответ кажется правильным, библиотека могла бы полагаться на https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#loadLibrary(java.lang.String) Насколько я знаю, загруженная собственная библиотека по-прежнему будет небезопасной, поскольку она загружается один раз в JVM, а не загрузчиком классов. –

+0

@ChristianKuetbach, насколько мне известно, нет никакого родного кода. –

+0

Проект github говорит Java как язык. Я просто хотел упомянуть родные DLL. Вы строите веб-сайт или что-то еще? В этом случае вы можете развернуть приложение с разными именами на один сервер. Самый веб-сервер, который я знаю, разделяет загрузчик классов для вас. Вы можете попытаться одновременно проверить огромные PDF-файлы, не переписывая код. –

3

Выделяя библиотеку на загрузчике классов в потоке вы можете гарантировать любые свойства параллелизма классов, как вы предлагаете. Единственным исключением являются библиотеки, которые явно взаимодействуют с загрузчиком класса загрузки или загрузчиком системного класса. Можно вводить классы в эти загрузчики классов либо рефлексией, либо API Instrumentation. Одним из примеров такой функциональности был бы встроенный макет Mockito, который, как мне известно, не страдает от ограничения параллелизма.

Реализация загрузчика классов с таким поведением не слишком сложна. Самым простым решением было бы явно включать в проект необходимые банки, например. как ресурс. Таким образом, вы могли бы использовать URLClassLoader для загрузки ваших классов:

URL url = getClass().getClassLoader().getResource("validation-model-1.1.6.jar"); 
ClassLoader classLoader = new URLClassLoader(new URL[] {url}, null); 

Ссылаясь null как супер загрузчик класса от URLClassLoader (второго аргумента), вы гарантируете, что не существуют никаких общих классов вне классов начальной загрузки. Обратите внимание, что вы не можете использовать какие-либо классы этого созданного загрузчика классов извне.Однако, если добавить второй сосуд, содержащий класс, который запускает логику, вы можете предложить точку входа, которая становится доступным без отражения:

class MyEntryPoint implements Callable<File> { 
    @Override public File call() { 
    // use library code. 
    } 
} 

Просто добавьте этот класс к своему банку и поставить его в качестве второго элемента к вышеуказанному массиву URL. Обратите внимание, что вы не можете ссылаться на тип библиотеки как возвращаемое значение, так как этот тип не будет доступен для потребителя, который живет за пределами загрузчика классов, который использует точку входа.

обертывания создания загрузчика классов в ThreadLocal, вы можете гарантировать класс погрузчики uniqunes:

class Unique extends ThreadLocal<ClassLoader> implements Closable { 
    @Override protected ClassLoader initialValue() { 
    URL validation = Unique.class.getClassLoader() 
          .getResource("validation-model-1.1.6.jar"); 
    URL entry = Unique.class.getClassLoader() 
          .getResource("my-entry.jar"); 
    return new URLClassLoader(new URL[] {validation, entry}, null); 
    } 

    @Override public void close() throws IOException { 
    get().close(); // If Java 7+, avoid handle leaks. 
    set(null); // Make class loader eligable for GC. 
    } 

    public File doSomethingLibrary() throws Exception { 
    Class<?> type = Class.forName("pkg.MyEntryPoint", false, get()); 
    return ((Callable<File>) type.newInstance()).call(); 
    } 
} 

Обратите внимание, что загрузчики классов дорогие объекты и не должны быть разыменовываетесь, когда вы больше не нуждаются в них, даже если поток продолжает жить. Кроме того, чтобы избежать утечек файлов, вы должны закрыть URLClassLoader до разыменования.

Наконец, чтобы продолжить использование разрешений зависимостей Maven и, чтобы упростить код, вы можете создать отдельный модуль Maven, в котором вы определяете свой код точки входа и объявляете свои зависимости от библиотеки Maven. При упаковке используйте Maven shade plugin to create an Uber jar, который включает в себя все, что вам нужно. Таким образом, вам нужно предоставить только одну банку для вашего URLClassLoader и не нужно обеспечивать все (переходные) зависимости вручную.

1

Я считаю, что вы должны попытаться устранить проблему, прежде чем искать обходной путь.

Вы всегда можете запустить код в двух потоках, загрузчиках классов, процессах, контейнерах, виртуальных машинах или машинах. Но они не идеальны.

Я видел два кода по умолчаниюInstance(). Делаете ли он потоки? Если нет, можем ли мы иметь два экземпляра? Это фабрика или синглтон?

Во-вторых, где происходят конфликты? Если речь идет о проблеме инициализации/кеширования, необходимо исправить предварительное нагревание.

Последнее, но не менее важное: если библиотека была с открытым исходным кодом, fork ее исправить и вытащить запрос.

+0

Благодарим вас за ответ. В настоящее время у меня нет времени на разветвление, установку и тестирование.Основная проблема не изменилась - как это сделать для данной, достаточно хорошо, но небезопасной библиотеки. –

3

Этот ответ основан на моем оригинальном комментарии к плагину. И он начинается с загрузчика классов, который наследуется только от загрузчиков классов загрузки и расширений.

package safeLoaderPackage; 

import java.net.URL; 
import java.net.URLClassLoader; 

public final class SafeClassLoader extends URLClassLoader{ 
    public SafeClassLoader(URL[] paths){ 
     super(paths, ClassLoader.getSystemClassLoader().getParent()); 
    } 
} 

Это единственный класс, который должен быть включен в путь пользователя. Этот загрузчик классов url наследуется от родителя класса ClassLoader.getSystemClassLoader(). Он просто включает загрузчик и загрузчик классов расширений. Он не имеет понятия пути к классу, используемого пользователем.

Следующая

package safeLoaderClasses; 

import java.net.URL; 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.concurrent.ArrayBlockingQueue; 
import java.util.concurrent.BlockingQueue; 
import java.util.concurrent.ThreadPoolExecutor; 
import java.util.concurrent.TimeUnit; 

public class SecureClassLoaderPlugin <R> { 

    private URL[] paths; 
    private Class[] args; 
    private String method; 
    private String unsafe; 

    public void setMethodData(final String u, final URL[] p, String m, Class[] a){ 
     method = m; 
     args = a; 
     paths = p; 
     unsafe = u; 
    } 

    public Collection<R> processUnsafe(Object[][] p){ 
     int i; 
     BlockingQueue<Runnable> q; 
     ArrayList<R> results = new ArrayList<R>(); 
     try{ 
      i = p.length; 
      q = new ArrayBlockingQueue<Runnable>(i); 
      ThreadPoolExecutor tpe = new ThreadPoolExecutor(i, i, 0, TimeUnit.NANOSECONDS, q); 
      for(Object[] params : p) 
       tpe.execute(new SafeRunnable<R>(unsafe, paths, method, args, params, results)); 
      while(tpe.getActiveCount() != 0){ 
       Thread.sleep(10); 
      } 
      for(R r: results){ 
       System.out.println(r); 
      } 
      tpe.shutdown(); 
     } 
     catch(Throwable t){ 

     } 
     finally{ 

     } 
     return results; 
    } 
} 

и

package safeLoaderClasses; 

import java.io.IOException; 
import java.lang.reflect.Method; 
import java.net.URL; 
import java.util.ArrayList; 

import safeLoaderInterface.SafeClassLoader; 

class SafeRunnable <R> implements Runnable{ 
    final URL[] paths; 
    final private String unsafe; 
    final private String method; 
    final private Class[] args; 
    final private Object[] processUs; 
    final ArrayList<R> result; 

    SafeRunnable(String u, URL[] p, String m, Class[] a, Object[] params, ArrayList<R> r){ 
     unsafe = u; 
     paths = p; 
     method = m; 
     args = a; 
     processUs = params; 
     result = r; 
    } 

    public void run() { 
     Class clazz; 
     Object instance; 
     Method m; 
     SafeClassLoader sl = null; 

     try{ 
      sl = new SafeClassLoader(paths); 
      System.out.println(sl); 

      clazz = sl.loadClass(unsafe); 
      m = clazz.getMethod(method, args); 
      instance = clazz.newInstance(); 
      synchronized(result){ 
       result.add((R) m.invoke(instance, processUs)); 
      } 
     } 
     catch(Throwable t){ 
      t.printStackTrace(); 
     } 
     finally{ 
      try { 
       sl.close(); 
      } catch (IOException e) { 
       e.printStackTrace(); 
      } 
     } 
    } 
} 

являются плагин банку. Нет лямбдов. Просто исполнитель потока. Каждый поток просто добавляет в список результатов после выполнения.

дженериков нужно полировать, но я проверил это против этого класса (находится в другом банке)

package stackoverflow4; 

public final class CrazyClass { 

    static int i = 0; 

    public int returnInt(){ 
     System.out.println(i); 
     return 8/++i; 
    } 
} 

Это будет способ подключения из своего кода. Путь к классу загрузчика должен быть включен, потому что теряется с GetParent() вызов

private void process(final String plugin, final String unsafe, final URL[] paths) throws Exception{ 
     Object[][] passUs = new Object[][] {{},{}, {},{}, {},{},{},{},{},{}}; 
     URL[] pathLoader = new URL[]{new File(new String(".../safeLoader.jar")).toURI().toURL(), 
       new File(new String(".../safeLoaderClasses.jar")).toURI().toURL()}; 
     //instantiate the loader 
     SafeClassLoader sl = new SafeClassLoader(pathLoader); 
     System.out.println(sl); 
     Class clazz = sl.loadClass("safeLoaderClasses.SecureClassLoaderPlugin"); 
     //Instance of the class that loads the unsafe jar and launches the thread pool executor 
     Object o = clazz.newInstance(); 
     //Look up the method that set ups the unsafe library 
     Method m = clazz.getMethod("setMethodData", 
       new Class[]{unsafe.getClass(), paths.getClass(), String.class, new Class[]{}.getClass()}); 
     //invoke it 
     m.invoke(o, new Object[]{unsafe,paths,"returnInt", new Class[]{}}); 
     //Look up the method that invokes the library 
     m = clazz.getMethod("processUnsafe", new Class[]{ passUs.getClass()}); 
     //invoke it 
     o = m.invoke(o, passUs); 
     //Close the loader 
     sl.close(); 
    } 

с до более 30 нитей и, кажется, работает. Плагин использует отдельный загрузчик классов, и каждый из потоков использует свой собственный загрузчик классов. После того, как вы покинули метод, все было сделано.

6

Я нашел вопрос interesing и создал небольшой инструмент для вас:

https://github.com/kriegaex/ThreadSafeClassLoader

В настоящее время он не доступен в качестве официального релиза на Maven Central, но вы можете получить снимок, как это:

<dependency> 
    <groupId>de.scrum-master</groupId> 
    <artifactId>threadsafe-classloader</artifactId> 
    <version>1.0-SNAPSHOT</version> 
</dependency> 

<!-- (...) --> 

<repositories> 
    <repository> 
    <snapshots> 
     <enabled>true</enabled> 
    </snapshots> 
    <id>ossrh</id> 
    <name>Sonatype OSS Snapshots</name> 
    <url>https://oss.sonatype.org/content/repositories/snapshots</url> 
    </repository> 
</repositories> 

Класс ThreadSafeClassLoader:

Использует JCL (Jar Class Loader) под капотом, поскольку он уже предлагает функции загрузки классов, создания объектов и создания прокси-сервера, обсуждаемые в других частях этого потока. (Почему вновь изобретать колесо?) То, что я добавил сверху хороший интерфейс именно то, что нам нужно здесь:

package de.scrum_master.thread_safe; 

import org.xeustechnologies.jcl.JarClassLoader; 
import org.xeustechnologies.jcl.JclObjectFactory; 
import org.xeustechnologies.jcl.JclUtils; 
import org.xeustechnologies.jcl.proxy.CglibProxyProvider; 
import org.xeustechnologies.jcl.proxy.ProxyProviderFactory; 

import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 

public class ThreadSafeClassLoader extends JarClassLoader { 
    private static final JclObjectFactory OBJECT_FACTORY = JclObjectFactory.getInstance(); 

    static { 
    ProxyProviderFactory.setDefaultProxyProvider(new CglibProxyProvider()); 
    } 

    private final List<Class> classes = new ArrayList<>(); 

    public static ThreadLocal<ThreadSafeClassLoader> create(Class... classes) { 
    return ThreadLocal.withInitial(
    () -> new ThreadSafeClassLoader(classes) 
    ); 
    } 

    private ThreadSafeClassLoader(Class... classes) { 
    super(); 
    this.classes.addAll(Arrays.asList(classes)); 
    for (Class clazz : classes) 
     add(clazz.getProtectionDomain().getCodeSource().getLocation()); 
    } 

    public <T> T newObject(ObjectConstructionRules rules) { 
    rules.validate(classes); 
    Class<T> castTo = rules.targetType; 
    return JclUtils.cast(createObject(rules), castTo, castTo.getClassLoader()); 
    } 

    private Object createObject(ObjectConstructionRules rules) { 
    String className = rules.implementingType.getName(); 
    String factoryMethod = rules.factoryMethod; 
    Object[] arguments = rules.arguments; 
    Class[] argumentTypes = rules.argumentTypes; 
    if (factoryMethod == null) { 
     if (argumentTypes == null) 
     return OBJECT_FACTORY.create(this, className, arguments); 
     else 
     return OBJECT_FACTORY.create(this, className, arguments, argumentTypes); 
    } else { 
     if (argumentTypes == null) 
     return OBJECT_FACTORY.create(this, className, factoryMethod, arguments); 
     else 
     return OBJECT_FACTORY.create(this, className, factoryMethod, arguments, argumentTypes); 
    } 
    } 

    public static class ObjectConstructionRules { 
    private Class targetType; 
    private Class implementingType; 
    private String factoryMethod; 
    private Object[] arguments; 
    private Class[] argumentTypes; 

    private ObjectConstructionRules(Class targetType) { 
     this.targetType = targetType; 
    } 

    public static ObjectConstructionRules forTargetType(Class targetType) { 
     return new ObjectConstructionRules(targetType); 
    } 

    public ObjectConstructionRules implementingType(Class implementingType) { 
     this.implementingType = implementingType; 
     return this; 
    } 

    public ObjectConstructionRules factoryMethod(String factoryMethod) { 
     this.factoryMethod = factoryMethod; 
     return this; 
    } 

    public ObjectConstructionRules arguments(Object... arguments) { 
     this.arguments = arguments; 
     return this; 
    } 

    public ObjectConstructionRules argumentTypes(Class... argumentTypes) { 
     this.argumentTypes = argumentTypes; 
     return this; 
    } 

    private void validate(List<Class> classes) { 
     if (implementingType == null) 
     implementingType = targetType; 
     if (!classes.contains(implementingType)) 
     throw new IllegalArgumentException(
      "Class " + implementingType.getName() + " is not protected by this thread-safe classloader" 
     ); 
    } 
    } 
} 

Я проверил свою концепцию с несколькими unit и integration испытаний, среди них один, показывающий, как reproduce and solve the veraPDF problem ,

Теперь это то, что ваш код выглядит при использовании моего специального загрузчика классов:

Класс VeraPDFValidator:

Мы просто добавить static ThreadLocal<ThreadSafeClassLoader> члена нашего класса, говоря это, который классы/библиотеки поставить в новый загрузчик классов (достаточно указать один класс на библиотеку, впоследствии мой инструмент автоматически идентифицирует библиотеку).

После этого через threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class)) мы создаем экземпляр нашего класса-помощника в потокобезопасном загрузчике классов и создаем для него прокси-объект, чтобы мы могли его вызвать извне.

BTW, static boolean threadSafeMode существует только для переключения между старым (небезопасным) и новым (потокобезопасным) использованием veraPDF, чтобы сделать исходную проблему воспроизводимой для теста отрицательной интеграции.

package de.scrum_master.app; 

import de.scrum_master.thread_safe.ThreadSafeClassLoader; 
import org.verapdf.core.*; 
import org.verapdf.pdfa.*; 

import javax.xml.bind.JAXBException; 
import java.io.InputStream; 
import java.lang.reflect.InvocationTargetException; 
import java.util.function.Function; 

import static de.scrum_master.thread_safe.ThreadSafeClassLoader.ObjectConstructionRules.forTargetType; 

public class VeraPDFValidator implements Function<InputStream, byte[]> { 
    public static boolean threadSafeMode = true; 

    private static ThreadLocal<ThreadSafeClassLoader> threadSafeClassLoader = 
    ThreadSafeClassLoader.create(   // Add one class per artifact for thread-safe classloader: 
     VeraPDFValidatorHelper.class,   // - our own helper class 
     PDFAParser.class,      // - veraPDF core 
     VeraGreenfieldFoundryProvider.class // - veraPDF validation-model 
    ); 

    private String flavorId; 
    private Boolean prettyXml; 

    public VeraPDFValidator(String flavorId, Boolean prettyXml) 
    throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 
    this.flavorId = flavorId; 
    this.prettyXml = prettyXml; 
    } 

    @Override 
    public byte[] apply(InputStream inputStream) { 
    try { 
     VeraPDFValidatorHelper validatorHelper = threadSafeMode 
     ? threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class)) 
     : new VeraPDFValidatorHelper(); 
     return validatorHelper.validatePDF(inputStream, flavorId, prettyXml); 
    } catch (ModelParsingException | ValidationException | JAXBException | EncryptedPdfException e) { 
     throw new RuntimeException("invoking veraPDF validation", e); 
    } 
    } 
} 

Класс VeraPDFValidatorHelper:

В этом классе выделит весь доступ к разбитой библиотеке. Здесь ничего особенного, просто код скопирован с вопроса OP. Все это делается внутри потокобезопасного загрузчика классов.

package de.scrum_master.app; 

import org.verapdf.core.*; 
import org.verapdf.pdfa.*; 
import org.verapdf.pdfa.flavours.PDFAFlavour; 
import org.verapdf.pdfa.results.ValidationResult; 

import javax.xml.bind.JAXBException; 
import java.io.ByteArrayOutputStream; 
import java.io.InputStream; 

public class VeraPDFValidatorHelper { 
    public byte[] validatePDF(InputStream inputStream, String flavorId, Boolean prettyXml) 
    throws ModelParsingException, ValidationException, JAXBException, EncryptedPdfException 
    { 
    VeraGreenfieldFoundryProvider.initialise(); 
    PDFAFlavour flavour = PDFAFlavour.byFlavourId(flavorId); 
    PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false); 
    PDFAParser loader = Foundries.defaultInstance().createParser(inputStream, flavour); 
    ValidationResult result = validator.validate(loader); 

    ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
    XmlSerialiser.toXml(result, baos, prettyXml, false); 
    return baos.toByteArray(); 
    } 
} 
+0

Снимки теперь доступны в репозитории снимков Sonatype OSS в качестве подготовки к более поздней версии на Maven Central. См. Обновленный ответ. – kriegaex

1

«Это неосуществимо затвердевать библиотеку», но это возможно, чтобы ввести такой кровавый обходной путь, как пользовательский класс загрузчика?

OK. Я первый, кто не любит ответы, которые не являются ответом на исходный вопрос. Но я искренне верю, что исправление библиотеки намного проще и удобнее, чем введение пользовательского загрузчика классов.

Блокатор класса org.verapdf.gf.model.impl.containers.StaticContainers, который может быть легко изменен для работы в потоке, как показано ниже. Это влияет шесть классов

org.verapdf.gf.model.GFModelParser 
org.verapdf.gf.model.factory.colors.ColorSpaceFactory 
org.verapdf.gf.model.impl.cos.GFCosFileSpecification 
org.verapdf.gf.model.impl.external.GFEmbeddedFile 
org.verapdf.gf.model.impl.pd.colors.GFPDSeparation 
org.verapdf.gf.model.tools.FileSpecificationKeysHelper 

Вы все еще можете иметь только один PDFAParser на поток. Но the fork занимает десять минут, чтобы сделать и работал для меня в основном многопоточном тесте на дым. Я бы проверил это и связался с оригинальным автором библиотеки. Возможно, он счастлив слиться, и вы можете просто сохранить ссылку Maven на обновленную и сохраненную библиотеку.

package org.verapdf.gf.model.impl.containers; 

import org.verapdf.as.ASAtom; 
import org.verapdf.cos.COSKey; 
import org.verapdf.gf.model.impl.pd.colors.GFPDSeparation; 
import org.verapdf.gf.model.impl.pd.util.TaggedPDFRoleMapHelper; 
import org.verapdf.model.pdlayer.PDColorSpace; 
import org.verapdf.pd.PDDocument; 
import org.verapdf.pdfa.flavours.PDFAFlavour; 

import java.util.*; 

public class StaticContainers { 

    private static ThreadLocal<PDDocument> document; 
    private static ThreadLocal<PDFAFlavour> flavour; 

    // TaggedPDF 
    public static ThreadLocal<TaggedPDFRoleMapHelper> roleMapHelper; 

    //PBoxPDSeparation 
    public static ThreadLocal<Map<String, List<GFPDSeparation>>> separations; 
    public static ThreadLocal<List<String>> inconsistentSeparations; 

    //ColorSpaceFactory 
    public static ThreadLocal<Map<String, PDColorSpace>> cachedColorSpaces; 

    public static ThreadLocal<Set<COSKey>> fileSpecificationKeys; 

    public static void clearAllContainers() { 
     document = new ThreadLocal<PDDocument>(); 
     flavour = new ThreadLocal<PDFAFlavour>(); 
     roleMapHelper = new ThreadLocal<TaggedPDFRoleMapHelper>(); 
     separations = new ThreadLocal<Map<String, List<GFPDSeparation>>>(); 
     separations.set(new HashMap<String,List<GFPDSeparation>>()); 
     inconsistentSeparations = new ThreadLocal<List<String>>(); 
     inconsistentSeparations.set(new ArrayList<String>()); 
     cachedColorSpaces = new ThreadLocal<Map<String, PDColorSpace>>(); 
     cachedColorSpaces.set(new HashMap<String,PDColorSpace>()); 
     fileSpecificationKeys = new ThreadLocal<Set<COSKey>>(); 
     fileSpecificationKeys.set(new HashSet<COSKey>()); 
    } 

    public static PDDocument getDocument() { 
     return document.get(); 
    } 

    public static void setDocument(PDDocument document) { 
     StaticContainers.document.set(document); 
    } 

    public static PDFAFlavour getFlavour() { 
     return flavour.get(); 
    } 

    public static void setFlavour(PDFAFlavour flavour) { 
     StaticContainers.flavour.set(flavour); 
     if (roleMapHelper.get() != null) { 
      roleMapHelper.get().setFlavour(flavour); 
     } 
    } 

    public static TaggedPDFRoleMapHelper getRoleMapHelper() { 
     return roleMapHelper.get(); 
    } 

    public static void setRoleMapHelper(Map<ASAtom, ASAtom> roleMap) { 
     StaticContainers.roleMapHelper.set(new TaggedPDFRoleMapHelper(roleMap, StaticContainers.flavour.get())); 
    } 
} 
+1

Это просто неправда, что нам нравится обходной путь лучше, чем упрочение библиотеки. Последнее всегда лучше, если это возможно. Но OP явно задал вопрос о возможности изоляции через classloader в принципе. Вот почему мы попытались ответить на вопрос. И BTW, не каждая библиотека имеет открытый исходный код, и не каждый поддерживающий вверх приемник принимает запросы на тягу или даже жалобы. Таким образом, вопрос, а также предлагаемые решения (или обходные пути, «кровавые» или нет) действительны. – kriegaex

+1

Спасибо за выполнение работы - я уверен, что команда разработчиков будет счастлива за ваш патч. Я хотел бы упомянуть, что ваш выбор слов несчастлив, хотя проблема, вызвавшая вопрос, является фиксируемой, идея заключалась в том, чтобы прояснить, был ли это способ обойти проблему в целом (для ситуаций, которые не так легко устранить). –