Хотите параллельно запускать нетекающую библиотеку – можно ли это сделать с помощью нескольких загрузчиков classов?

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

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

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

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

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


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

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

 org.verapdf validation-model 1.1.6  

используя repository проекта maven для артефактов.

    true  vera-dev Vera development http://artifactory.openpreservation.org/artifactory/vera-dev   

Пока нелегко затвердеть библиотеку.


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

 public class VeraPDFValidator implements Function { 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).

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

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

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

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

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

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

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

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

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

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

Источник Jetty WebappClassLoader

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

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

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

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

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


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

Предположим, что точкой входа в мою библиотеку является class 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 ); } } 

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

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

 private static final ThreadLocal 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); } } ); 

Вы получите результат следующего типа, который показывает, что у нас нет конфликтов между streamами, и streamи должным образом повторно используют его соответствующее значение classа / поля от одного вызова 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 каждого streamа, обязательно примените этот подход, используя пул streamов с фиксированным количеством streamов, и количество streamов должно быть выбрано с умом, чтобы исключить ClassLoader памяти, поскольку ClassLoader не является бесплатным в течение так что вам нужно ограничить общее количество экземпляров в соответствии с размером вашей кучи.

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

 // 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(); } } ); 

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

https://github.com/kriegaex/ThreadSafeClassLoader

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

  de.scrum-master threadsafe-classloader 1.0-SNAPSHOT      true  ossrh Sonatype OSS Snapshots https://oss.sonatype.org/content/repositories/snapshots   

Класс ThreadSafeClassLoader :

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

 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 classes = new ArrayList<>(); public static ThreadLocal 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 newObject(ObjectConstructionRules rules) { rules.validate(classes); Class 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 classes) { if (implementingType == null) implementingType = targetType; if (!classes.contains(implementingType)) throw new IllegalArgumentException( "Class " + implementingType.getName() + " is not protected by this thread-safe classloader" ); } } } 

Я протестировал свою концепцию с помощью нескольких тестов на единицу и интеграции , среди которых один, показывающий, как воспроизводить и решать проблему veraPDF .

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

Класс VeraPDFValidator :

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

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

BTW, static boolean threadSafeMode существует только для переключения между старым (небезопасным) и новым (streamобезопасным) использованием 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 { public static boolean threadSafeMode = true; private static ThreadLocal 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 :

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

 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(); } } 

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

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

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

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

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

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

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

 class Unique extends ThreadLocal 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) type.newInstance()).call(); } } 

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

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

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

 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()); } } 

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

следующий

 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  { 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 processUnsafe(Object[][] p){ int i; BlockingQueue q; ArrayList results = new ArrayList(); try{ i = p.length; q = new ArrayBlockingQueue(i); ThreadPoolExecutor tpe = new ThreadPoolExecutor(i, i, 0, TimeUnit.NANOSECONDS, q); for(Object[] params : p) tpe.execute(new SafeRunnable(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  implements Runnable{ final URL[] paths; final private String unsafe; final private String method; final private Class[] args; final private Object[] processUs; final ArrayList result; SafeRunnable(String u, URL[] p, String m, Class[] a, Object[] params, ArrayList 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(); } } } } 

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

Дженерики нуждаются в полировке, но я тестировал их против этого classа (находится в другой банке)

 package stackoverflow4; public final class CrazyClass { static int i = 0; public int returnInt(){ System.out.println(i); return 8/++i; } } 

Это будет способ подключения из своего кода. Путь к загрузчику classов должен быть включен, поскольку он потерян при вызове 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(); } 

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

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

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

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

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

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

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

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

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

 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 каждого streamа. Но вилка занимает десять минут, чтобы сделать и работала для меня в базовом многопоточном тесте на дым. Я бы проверил это и связался с оригинальным автором библиотеки. Возможно, он счастлив слиться, и вы можете просто сохранить ссылку 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 document; private static ThreadLocal flavour; // TaggedPDF public static ThreadLocal roleMapHelper; //PBoxPDSeparation public static ThreadLocal>> separations; public static ThreadLocal> inconsistentSeparations; //ColorSpaceFactory public static ThreadLocal> cachedColorSpaces; public static ThreadLocal> fileSpecificationKeys; public static void clearAllContainers() { document = new ThreadLocal(); flavour = new ThreadLocal(); roleMapHelper = new ThreadLocal(); separations = new ThreadLocal>>(); separations.set(new HashMap>()); inconsistentSeparations = new ThreadLocal>(); inconsistentSeparations.set(new ArrayList()); cachedColorSpaces = new ThreadLocal>(); cachedColorSpaces.set(new HashMap()); fileSpecificationKeys = new ThreadLocal>(); fileSpecificationKeys.set(new HashSet()); } 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 roleMap) { StaticContainers.roleMapHelper.set(new TaggedPDFRoleMapHelper(roleMap, StaticContainers.flavour.get())); } }