Complemento Java sin dolor

En este artículo, me gustaría decirle cómo crear rápida y fácilmente un marco de aplicación Java con soporte para carga dinámica de complementos. El lector probablemente pensará de inmediato que este problema ya se ha resuelto durante mucho tiempo, y simplemente puede usar marcos ya preparados o escribir su cargador de clases, pero nada de esto será necesario en la solución que propongo:

  • No necesitamos bibliotecas o marcos especiales ( OSGi , Guice, etc.)
  • No utilizaremos el análisis de bytecode con ASM y bibliotecas similares.
  • No escribiremos nuestro cargador de clases.
  • No usaremos reflexiones y anotaciones.
  • No es necesario preocuparse por el classpath para encontrar complementos. No tocaremos el classpath en absoluto.
  • Además, no utilizaremos XML, YAML ni ningún otro lenguaje declarativo para describir puntos de extensión (puntos de extensión en complementos).

Sin embargo, todavía hay un requisito: dicha solución solo funcionará en Java 9 o superior. Porque se basará en módulos y servicios .

Entonces comencemos. Formulamos el problema más específicamente:
Debe implementar un marco de aplicación mínimo, que al inicio cargará los complementos del usuario desde la carpeta de plugins .

Es decir, la aplicación ensamblada debería verse así:

 plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar … 

Comencemos con el módulo core . Este módulo es el núcleo de nuestra aplicación, es decir, es nuestro marco.

Para aquellos que valoran el tiempo, el proyecto terminado está disponible en GitHub. Instrucciones de montaje.
Enlace

 git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core 

Cree los siguientes 4 archivos Java en el módulo:

 core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java 

El primer archivo, IService.java , es el archivo que describe nuestro punto de extensión. Otros complementos podrán entonces contribuir a este punto de expansión ("contribuir"). Este es el principio estándar para crear aplicaciones de complemento, que se denomina principio de inversión de dependencia (Inversión de dependencia). Este principio se basa en el hecho de que el núcleo no depende de clases específicas, sino de interfaces.

Le di al punto de extensión el nombre abstracto IService , ya que ahora estoy demostrando un concepto exclusivamente. En realidad, puede ser cualquier punto de extensión específico, por ejemplo, si está escribiendo un editor gráfico, puede ser el efecto del procesamiento de imágenes, por ejemplo, IEffectProvider , IEffectContribution o algo más, dependiendo de cómo prefiera nombrar los puntos de extensión. Al mismo tiempo, la aplicación en sí contendrá un conjunto básico de efectos, y los desarrolladores de terceros podrán escribir efectos adicionales más sofisticados y entregarlos en forma de complementos. El usuario solo necesita poner estos efectos en la carpeta de plugins y reiniciar la aplicación.

El archivo IService.java es el siguiente:

 public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } } 

Por lo tanto, IService es solo una interfaz que hace un trabajo abstracto doJob() (repito, los detalles no son importantes, en realidad será algo concreto).

También preste atención al segundo método getServices() . Este método devuelve todas las implementaciones de la interfaz IService que encontró en esta capa de módulo y sus padres. Hablaremos de esto con más detalle más adelante.

El segundo archivo, BasicService.java , es la implementación base de la interfaz IService . Siempre estará presente, incluso si no hay complementos en la aplicación. En otras palabras, el core no es solo el núcleo, sino también al mismo tiempo un complemento para sí mismo, que siempre se cargará. El archivo BasicService.java ve así:

 public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } } 

Para simplificar, doJob() simplemente imprime la cadena "Basic service" y eso es todo.

Por lo tanto, en este momento tenemos la siguiente imagen:



El tercer archivo, Main.java , es donde se implementa el método main() . Hay un poco de magia en él, para comprender cuál necesita saber qué es una capa de módulo.

Acerca de las capas del módulo


Cuando Java inicia la aplicación, todos los módulos de plataforma + módulos enumerados en el argumento --module-path (y también classpath , si los hay) caen en la llamada capa de Boot . En nuestro caso, si compilamos el módulo core.jar y ejecutamos java --module-path core.jar --module core desde la línea de comandos, entonces al menos los módulos java.base y core estarán en la capa de Boot :



La capa de Boot siempre está presente en cualquier aplicación Java, y esta es la configuración más pequeña posible. La mayoría de las aplicaciones existen en una sola capa de módulos. Sin embargo, en nuestro caso, queremos hacer una carga dinámica de complementos desde la carpeta de plugins . Podríamos obligar al usuario a corregir la línea de inicio de la aplicación para que él mismo agregue los complementos necesarios a --module-path , pero esta no será la mejor solución. Especialmente aquellas personas que no son programadores y no entienden por qué necesitan escalar en algún lugar y arreglar algo para algo tan simple.

Afortunadamente, hay una solución: Java le permite crear sus propias capas de módulos en tiempo de ejecución, que cargarán los módulos desde el lugar que necesitamos. Para nuestros propósitos, una nueva capa para complementos será suficiente, que tendrá una capa de Boot como padre (cualquier capa debe tener un padre):



El hecho de que la capa de complemento tenga la capa de Boot como principal significa que los módulos de la capa de complemento pueden hacer referencia a los módulos de la capa de Boot , pero no al revés.

Entonces, sabiendo ahora qué es una capa de módulo, finalmente puede ver el contenido del archivo Main.java :

 public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins"); //      plugins ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir); //  ModuleFinder      plugins       List<String> plugins = pluginsFinder .findAll() .stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toList()); //  ,      (   ) Configuration pluginsConfiguration = ModuleLayer .boot() .configuration() .resolve(pluginsFinder, ModuleFinder.of(), plugins); //      ModuleLayer layer = ModuleLayer .boot() .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader()); //     IService       Boot List<IService> services = IService.getServices(layer); for (IService service : services) { service.doJob(); } } } 

Si es la primera vez que mira este código, puede parecer muy complicado, pero es una sensación falsa debido a la gran cantidad de nuevas clases desconocidas. Si comprende un poco sobre el significado de las clases ModuleFinder , Configuration y ModuleLayer , entonces todo encaja. Y además, ¡solo hay unas pocas docenas de líneas! Esta es toda la lógica que se escribe una vez.

Descriptor del módulo


Hay un archivo más (cuarto) que no consideramos: module-info.java . Este es el archivo más corto que contiene la declaración de nuestro módulo y una descripción de los servicios (puntos de extensión):

 module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; } 

El significado de las líneas de este archivo debería ser obvio:

  • En primer lugar, el módulo exporta el paquete org.example.pluginapp.core que los complementos puedan heredar de la interfaz IService (de lo contrario, IService no sería accesible fuera del módulo core ).
  • En segundo lugar, anuncia que está utilizando el servicio IService .
  • En tercer lugar, dice que proporciona una implementación del servicio IService través de la clase BasicService .

Como la declaración del módulo está escrita en Java, obtenemos ventajas muy importantes: comprobaciones del compilador y garantías estáticas . Por ejemplo, si cometimos un error en el nombre de los tipos o indicamos un paquete inexistente, lo habríamos obtenido de inmediato. En el caso de algunos OSGi, no tendríamos ninguna comprobación en el momento de la compilación, ya que la declaración de los puntos de extensión se escribiría en XML.

Entonces, el marco está listo. Intentemos ejecutarlo:

 > java --module-path core.jar --module core Basic service 

Que paso

  1. Java intentó encontrar los módulos en la carpeta de plugins y no encontró ninguno.
  2. Se creó una capa vacía.
  3. ServiceLoader comenzó a buscar todas IService implementaciones de IService .
  4. En la capa vacía, no encontró ninguna implementación de servicio, ya que no hay módulos allí.
  5. Después de esta capa, continuó buscando en la capa principal (es decir, la capa de Boot ) y encontró una implementación de BasicService en el módulo core .
  6. Todas las implementaciones encontradas tenían el método doJob() llamado. Como solo se encontró una implementación, solo se imprimió el "Basic service" .

Escribir un complemento


Habiendo escrito el núcleo de nuestra aplicación, ahora es el momento de intentar escribir complementos para ella. Escribamos dos complementos plugin1 y plugin2 : deje que la primera imprima "Service 1" , la segunda - "Service 2" . Para hacer esto, debe proporcionar dos implementaciones más de IService en plugin1 y plugin2 respectivamente:



Cree el primer complemento con dos archivos:

 plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java 

Archivo Service1.java :

 public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } } 

Archivo module-info.java :

 module plugin1 { requires core; provides IService with Service1; } 

Tenga en cuenta que plugin1 depende del core . Este es el principio de inversión de dependencia que mencioné anteriormente: el núcleo no depende de complementos, sino viceversa.

El segundo complemento es completamente similar al primero, por lo que no lo daré aquí.

Ahora recopilemos los complementos, colóquelos en la carpeta de plugins y ejecute la aplicación:

 > java --module-path core.jar --module core Service 1 Service 2 Basic service 

¡Hurra, los complementos fueron recogidos! ¿Cómo sucedió esto?

  1. Java encontró dos módulos en la carpeta de plugins .
  2. Se creó una capa con dos módulos plugins1 y plugins2 .
  3. ServiceLoader comenzó a buscar todas IService implementaciones de IService .
  4. En la capa de complemento, encontró dos implementaciones del servicio IService .
  5. Después de eso, continuó buscando en la capa principal (es decir, la capa de Boot ) y encontró una implementación de BasicService en el módulo core .
  6. Todas las implementaciones encontradas tenían el método doJob() llamado.

Tenga en cuenta que es precisamente porque la búsqueda de proveedores de servicios comienza con las capas secundarias y luego pasa a las capas principales, luego "Service 2" imprimen primero "Service 1" y "Service 2" y luego "Basic Service" . Si desea que los servicios se ordenen para que los servicios básicos vayan primero, y luego los complementos, puede ajustar el método IService.getServices() agregando la clasificación allí (es posible que deba agregar el int getOrdering() a la interfaz IService ).

Resumen


Entonces, mostré cómo puede organizar rápida y eficientemente una aplicación Java de complemento que tiene las siguientes propiedades:

  • Simplicidad: para los puntos de extensión y sus enlaces, solo se utilizan las características básicas de Java (interfaces, clases y ServiceLoader ), sin marcos, reflejos, anotaciones y cargadores de clases.
  • Declarabilidad: los puntos de extensión se describen en los descriptores del módulo. Simplemente mire module-info.java y comprenda qué puntos de extensión existen y qué complementos contribuyen a estos puntos.
  • Garantías estáticas: en caso de errores en los descriptores del módulo, el programa no se compilará. Además, como beneficio adicional, si usa IntelliJ IDEA, recibirá advertencias adicionales (por ejemplo, si olvidó usar uses y usar ServiceLoader.load() )
  • Seguridad: el sistema Java modular comprueba al inicio que la configuración de los módulos es correcta y se niega a ejecutar el programa en caso de errores.

Repito, solo mostré la idea. En una aplicación de complemento real, habría decenas a cientos de módulos y cientos a miles de puntos de extensión.

Decidí plantear este tema porque durante los últimos 7 años he estado escribiendo una aplicación modular usando Eclipse RCP, en la que el notorio OSGi se usa como un sistema de complemento, y los descriptores de los complementos están escritos en XML. Tenemos más de un centenar de complementos y todavía estamos en Java 8. Pero incluso si actualizamos a una nueva versión de Java, es poco probable que usemos módulos de Java, ya que están muy vinculados a OSGi.

Pero si está escribiendo una aplicación de complemento desde cero, entonces los módulos Java son una de las posibles opciones para su implementación. Recuerde que los módulos son solo una herramienta, no un objetivo.

Brevemente sobre mi


He estado programando durante más de 10 años (8 de ellos en Java), respondo a StackOverflow y ejecuto mi propio canal en Telegram dedicado a Java.

Source: https://habr.com/ru/post/479478/


All Articles