java multithreading classloader

java - ¿Desea ejecutar bibliotecas no seguras para subprocesos en paralelo? ¿Se puede hacer utilizando varios cargadores de clases?



multithreading classloader (7)

"No es factible endurecer la biblioteca", ¿pero es posible introducir una solución tan sangrienta como un cargador de clases personalizado?

DE ACUERDO. Soy el primero a quien no le gustan las respuestas que no son una respuesta a la pregunta original. Pero honestamente creo que parchear la biblioteca es mucho más fácil de hacer y mantener que introducir un cargador de clases personalizado.

El bloqueador es la clase org.verapdf.gf.model.impl.containers.StaticContainers cuyos campos static se pueden cambiar fácilmente para que funcionen por subproceso como se muestra a continuación. Esto afecta a otras seis clases.

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

Todavía puedes tener solo un PDFAParser por hilo. Pero la bifurcación tarda diez minutos en hacerlo y me funcionó en una prueba básica de humo de varios hilos. Probaría esto y me pondría en contacto con el autor original de la biblioteca. Tal vez él esté feliz de fusionarse y usted puede simplemente mantener una referencia de Maven a la biblioteca actualizada y mantenida.

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

Trabajo en un proyecto en el que usamos una biblioteca que no está garantizada como segura para subprocesos (y no lo es) y de un solo subproceso en un escenario de secuencias de Java 8, que funciona como se esperaba.

Nos gustaría utilizar flujos paralelos para obtener la escalabilidad de la fruta de bajo rendimiento.

Desafortunadamente, esto hace que la biblioteca falle, probablemente porque una instancia interfiere con las variables compartidas con la otra, por lo tanto necesitamos aislamiento.

Estaba considerando usar un cargador de clases por separado para cada instancia (posiblemente un subproceso local), lo que, según mi conocimiento, debería significar que, para todos los propósitos prácticos, obtengo el aislamiento necesario, pero no estoy familiarizado con la construcción deliberada de cargadores de clases para este fin.

¿Es este el enfoque correcto? ¿Cómo debo hacer esto para tener una calidad de producción adecuada?

Edición: se me pidió información adicional sobre la situación que desencadenó la pregunta para poder entenderla mejor. La pregunta sigue siendo sobre la situación general, no arreglar la biblioteca.

Tengo control total sobre el objeto creado por la biblioteca (que es https://github.com/veraPDF/ ) tal como lo detuvo

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

Uso del repositorio de proyectos para artefactos.

<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>

Por ahora no es factible endurecer la biblioteca.

EDITAR: Me pidieron que muestre el código. Nuestro adaptador de núcleo es aproximadamente:

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

que es una función que se asigna desde un InputStream (que proporciona un archivo PDF) a una matriz de bytes (que representa la salida del informe XML).

(Al ver el código, he notado que hay una llamada al inicializador en el constructor, lo que puede ser el culpable aquí en mi caso particular. Todavía me gustaría una solución al problema genérico.


Al aislar una biblioteca en un cargador de clases por subproceso, puede garantizar las propiedades de concurrencia de cualquier clase que sugiera. La única excepción son las bibliotecas que interactúan explícitamente con el cargador de clases bootstrap o el cargador de clases del sistema. Es posible inyectar clases en estos cargadores de clases, ya sea por reflexión o por la API de Instrumentation . Un ejemplo de tal funcionalidad sería el fabricante de simulacros en línea de Mockito que, sin embargo, no sufre una restricción de concurrencia por lo que sé.

Implementar un cargador de clases con este comportamiento no es tan difícil. La solución más sencilla sería incluir explícitamente los frascos necesarios en su proyecto, por ejemplo, como un recurso. De esta manera, podrías usar un URLClassLoader para cargar tus clases:

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

Al hacer referencia a null como el cargador de URLClassLoader de URLClassLoader (segundo argumento), garantiza que no hay clases compartidas fuera de las clases de arranque. Tenga en cuenta que no puede usar ninguna clase de este cargador de clases creado desde fuera de él. Sin embargo, si agrega un segundo jar que contiene una clase que activa su lógica, puede ofrecer un punto de entrada que sea accesible sin reflexión:

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

Simplemente agregue esta clase a su propio jar y provéala como un segundo elemento a la matriz de URL anterior. Tenga en cuenta que no puede hacer referencia a un tipo de biblioteca como valor de retorno, ya que este tipo no estará disponible para el consumidor que vive fuera del cargador de clases que utiliza el punto de entrada.

Al ThreadLocal creación del cargador de clases en un ThreadLocal , puede garantizar los cargadores de clases 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(); } }

Tenga en cuenta que los cargadores de clases son objetos caros y deben ser desreferenciados cuando ya no los necesite, incluso si un hilo continúa vivo. Además, para evitar fugas de archivos, debe cerrar un URLClassLoader previamente a la URLClassLoader de referencias.

Finalmente, para continuar usando la resolución de dependencia de Maven y para simplificar su código, puede crear un módulo de Maven separado donde defina su código de punto de entrada y declare las dependencias de la biblioteca de Maven. Al empacar, use el complemento de cortinas Maven para crear un frasco Uber que incluya todo lo que necesita. De esta manera, solo necesita proporcionar un solo jar a su URLClassLoader y no necesita asegurar todas las dependencias (transitivas) manualmente.


Creo que deberías intentar solucionar el problema antes de buscar una solución.

Siempre puede ejecutar su código en dos subprocesos, cargadores de clases, procesos, contenedores, máquinas virtuales o máquinas. Pero no son nada de lo ideal.

Vi dos DefaultInstance () desde el código. ¿La instancia es segura para hilos? Si no, ¿podemos tener dos instancias? ¿Es una fábrica o un singleton?

Segundo, ¿dónde ocurren los conflictos? Si se trataba de un problema de inicialización / caché, debería solucionarse un precalentamiento.

Por último, pero no menos importante, si la biblioteca era de código abierto, bifurque, arregle y extraiga la solicitud.


Encontré la pregunta interesante y creé una pequeña herramienta para ti:

https://github.com/kriegaex/ThreadSafeClassLoader

Actualmente no está disponible como versión oficial en Maven Central todavía, pero puede obtener una instantánea como esta:

<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>

Clase ThreadSafeClassLoader :

Utiliza JCL (Jar Class Loader) bajo el capó porque ya ofrece funciones de carga de clases, creación de instancias de objetos y generación de proxy analizadas en otras partes de este hilo. (¿Por qué reinventar la rueda?) Lo que agregué en la parte superior es una interfaz agradable para exactamente lo que necesitamos aquí:

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

Probé mi concepto con varias pruebas de unit e integration , entre ellas una que muestra cómo reproducir y resolver el problema veraPDF .

Ahora, así es como se ve tu código cuando usas mi cargador de clases especial:

Clase VeraPDFValidator :

Solo estamos agregando un static ThreadLocal<ThreadSafeClassLoader> a nuestra clase, diciéndole qué clases / bibliotecas deben incluirse en el nuevo cargador de clases (mencionar una clase por biblioteca es suficiente, luego mi herramienta identifica la biblioteca automáticamente).

Luego, a través de threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class)) creamos una instancia de nuestra clase auxiliar dentro del cargador de clases seguro de subprocesos y creamos un objeto proxy para que podamos llamarla desde afuera.

Por cierto, static boolean threadSafeMode solo existe para cambiar entre el uso antiguo (inseguro) y nuevo (seguro para subprocesos) de veraPDF para que el problema original sea reproducible para el caso de prueba de integración negativa.

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

Clase VeraPDFValidatorHelper :

En esta clase aislamos todos los accesos a la biblioteca rota. Nada especial aquí, solo el código copiado de la pregunta del OP. Todo lo que se hace aquí sucede dentro del cargador de clases seguro para subprocesos.

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


Esta respuesta se basa en mi comentario original de "complemento". Y comienza con un cargador de clases que se hereda solo de los cargadores de clases de arranque y extensiones.

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

Esta es la única clase que debe incluirse en la ruta de clases del usuario. Este cargador de clases de URL se hereda del padre de ClassLoader.getSystemClassLoader (). Solo incluye el cargador de arranque y la clase de extensiones. No tiene noción de la ruta de clase utilizada por el usuario.

Siguiente

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

y

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

son el plugin jar. No lambdas. Sólo un ejecutor de grupo de subprocesos. Cada hilo solo se agrega a una lista de resultados después de la ejecución.

Los genéricos necesitan pulido, pero he probado estos contra esta clase (reside en un frasco diferente)

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

Esta sería la forma de conectarse desde el código de uno. La ruta al cargador de clases debe incluirse porque se pierde con la llamada 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(); }

con más de 30 hilos y parece funcionar. El complemento utiliza un cargador de clases separado y cada uno de los subprocesos usa su propio cargador de clases. Después de abandonar el método todo está gc''ed.


Hemos enfrentado desafíos similares. Los problemas generalmente provenían de propiedades estáticas que se "compartían" involuntariamente entre los distintos subprocesos.

El uso de diferentes cargadores de clases funcionó para nosotros siempre y cuando pudiéramos garantizar que las propiedades estáticas se establecieron en clases cargadas por nuestro cargador de clases. Java puede tener algunas clases que proporcionan propiedades o métodos que no están aislados entre subprocesos o que no son seguros para subprocesos ('' System.setProperties() y Security.addProvider() están bien; cualquier documentación canónica sobre este tema es bienvenida por cierto).

Una solución rápida y potencialmente viable, que al menos puede darle la oportunidad de probar esta teoría para su biblioteca, es usar un motor de servlet como Jetty o Tomcat.

Construye unas cuantas guerras que contienen tu biblioteca e inicia procesos en paralelo (1 por guerra).

Cuando se ejecuta el código dentro de un subproceso de servlet, los WebappClassLoaders de estos motores intentan cargar primero las clases desde el cargador de clases principal (igual que el motor) y si no encuentra la clase, intenta cargarlo desde los archivos / clases empaquetados. con la guerra.

Con Jetty puede implementar guerras programáticas en el contexto de su elección y luego escalar teóricamente el número de procesadores (guerras) según sea necesario.

Hemos implementado nuestro propio cargador de clases al extender URLClassLoader y nos hemos inspirado en el Jetty Webapp ClassLoader. No es un trabajo tan duro como parece.

Nuestro cargador de clases hace exactamente lo contrario: primero intenta cargar una clase desde los archivos jars locales al ''paquete'', luego intenta obtenerlos del cargador de clases principal. Esto garantiza que una biblioteca cargada accidentalmente por el cargador de clases principal nunca se considera (primero). Nuestro ''paquete'' es en realidad un jar que contiene otros archivos / bibliotecas con un archivo de manifiesto personalizado.

Publicar este código de cargador de clase "como está" no tendría mucho sentido (y crearía algunos problemas de derechos de autor). Si quieres explorar más esa ruta, puedo intentar crear un esqueleto.

Fuente del Jetty WebappClassLoader


La respuesta realmente depende de en qué confíe su biblioteca:

  1. Si su biblioteca se basa en al menos una biblioteca nativa, el uso de ClassLoader s para aislar el código de su biblioteca no ayudará porque de acuerdo con la Especificación JNI , no está permitido cargar la misma biblioteca nativa JNI en más de un cargador de clases de tal manera que terminaría con un UnsatisfiedLinkError .
  2. Si su biblioteca depende de al menos un recurso externo que no debe compartirse como, por ejemplo, un archivo y que es modificado por su biblioteca, podría terminar con errores complejos y / o la corrupción del recurso.

Suponiendo que no se encuentra en los casos mencionados anteriormente, en general si una clase se conoce como no segura para subprocesos y no modifica ningún campo estático, usar una instancia dedicada de esta clase por llamada o por hilo es lo suficientemente bueno como la instancia de clase entonces ya no se comparte

Aquí, como su biblioteca obviamente se basa y modifica algunos campos estáticos que no deben compartirse, debe aislar las clases de su biblioteca en un ClassLoader dedicado y, por supuesto, asegurarse de que sus subprocesos no compartan el mismo ClassLoader .

Para esto, simplemente puede crear un URLClassLoader al que proporcionaría la ubicación de su biblioteca como URL (utilizando URLClassLoader.newInstance(URL[] urls, ClassLoader parent) ), luego, por reflexión, recuperará la clase de su biblioteca correspondiente al Punto de entrada e invoca tu método de destino. Para evitar la creación de un nuevo URLClassLoader en cada llamada, puede considerar confiar en un ThreadLocal para almacenar el URLClassLoader o la Class o la instancia del Method que se utilizará para un subproceso determinado.

Así que aquí es cómo podría proceder:

Digamos que el punto de entrada de mi biblioteca es la clase Foo que se ve así:

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

Claramente, esta clase no es segura para subprocesos y la execute del método modifica el valor de un campo estático que no está destinado a ser modificado por subprocesos concurrentes como su caso de uso.

Suponiendo que para iniciar mi biblioteca simplemente necesito crear una instancia de Foo e invocar el método de execute . Podría almacenar el Method correspondiente en un ThreadLocal para recuperarlo por reflexión solo una vez por hilo usando ThreadLocal.withInitial(Supplier<? extends S> supplier) como sigue:

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

Y finalmente simulemos una ejecución concurrente de mi biblioteca:

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

Obtendrá una salida del siguiente tipo que muestra que no tenemos conflictos entre los subprocesos y los subprocesos reutilizan correctamente el valor de su clase / campo correspondiente de una llamada a otra:

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 ...

Dado que este enfoque creará un ClassLoader por subproceso, asegúrese de aplicar este enfoque utilizando un grupo de subprocesos con un número fijo de subprocesos y el número de subprocesos debe elegirse con prudencia para evitar quedarse sin memoria porque un ClassLoader no es gratis en términos de huella de memoria, por lo que debe limitar la cantidad total de instancias de acuerdo con el tamaño de su montón.

Una vez que haya terminado con su biblioteca, debe limpiar el ThreadLocal de cada subproceso de su grupo de subprocesos para evitar pérdidas de memoria y para hacerlo, puede hacer lo siguiente:

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