Tabla de Contenidos

01 - Fundamentos de Spring Modulith

Estos apuntes resumen los conceptos fundamentales de Spring Modulith 2.1.1. Spring Modulith ayuda a organizar una aplicación Spring Boot en módulos funcionales, definir cómo colaboran y verificar sus límites.

1. Módulos de aplicación

Un módulo de aplicación es una unidad funcional que puede contener:

La API ofrecida es la interfaz proporcionada (*provided interface*); las dependencias que el módulo necesita son sus interfaces requeridas (*required interfaces*).

Spring Modulith puede deducir el modelo de módulos del código y su organización en paquetes:

ApplicationModules modules = ApplicationModules.of(Application.class);
modules.forEach(System.out::println);

El tipo ApplicationModules representa en memoria los módulos detectados, sus componentes y la visibilidad observada.

2. Organización simple

Por defecto, el paquete que contiene la clase principal anotada con @SpringBootApplication es el paquete raíz. Cada paquete hijo directo se considera un módulo.

Si el paquete del módulo no tiene subpaquetes, se trata como módulo simple. Todos sus tipos públicos forman parte de la API del módulo; se pueden mantener tipos internos con visibilidad de paquete de Java.

com.example
├── Application.java
├── orders                 (módulo)
│   ├── OrderManagement.java
│   └── InternalPolicy.java
└── stock                  (módulo)
    └── StockManagement.java

3. Módulos con paquetes internos

Si un módulo contiene subpaquetes, Spring Modulith considera que el paquete raíz del módulo es su API. Los subpaquetes son internos y otros módulos no deben acceder a ellos.

com.example.orders             (API del módulo)
├── OrderManagement.java
└── internal                    (implementación interna)
    └── OrderPolicy.java

Los tipos internos pueden necesitar ser public para que otras clases del mismo módulo los usen. El compilador Java no impide por sí solo que otro módulo acceda a esos tipos públicos; la verificación de Spring Modulith permite detectar estas dependencias no permitidas.

4. Módulos anidados

Desde Spring Modulith 1.3, un módulo puede contener módulos anidados. Se declaran anotando el paquete que debe constituir el módulo anidado con @ApplicationModule.

com.example.inventory
├── InventoryManagement.java
└── nested                  (módulo anidado: @ApplicationModule)
    ├── NestedApi.java
    └── internal
        └── NestedInternal.java

Reglas de acceso principales:

5. Módulos abiertos

Un módulo puede declararse abierto con @ApplicationModule(type = Type.OPEN) en su package-info.java.

@org.springframework.modulith.ApplicationModule(
    type = Type.OPEN
)
package com.example.inventory;

En un módulo abierto, los tipos internos también pueden ser accedidos desde otros módulos y se incorporan a la interfaz sin nombre, salvo que se asignen a una interfaz con nombre.

Este modo facilita modularizar gradualmente una aplicación existente. En una aplicación diseñada modularmente desde el principio, abrir módulos suele indicar que los límites o la estructura de paquetes pueden mejorarse.

6. Declarar dependencias permitidas

Un módulo puede declarar explícitamente los otros módulos de los que se permite depender mediante allowedDependencies:

@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "orders"
)
package com.example.inventory;

En este ejemplo, el módulo inventory puede depender de orders y de tipos que no pertenezcan a ningún módulo. La verificación de la estructura comprueba que el código respeta las dependencias declaradas.

7. Interfaces con nombre

Por defecto, la API de un módulo con subpaquetes es su paquete raíz. Para exponer otro paquete concreto se declara una interfaz con nombre (*named interface*) mediante @NamedInterface en el package-info.java del paquete:

@org.springframework.modulith.NamedInterface("spi")
package com.example.orders.spi;

El paquete expuesto puede incluirse de forma explícita en las dependencias permitidas:

@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "orders :: spi"
)
package com.example.inventory;

El separador :: combina el nombre del módulo con el de su interfaz. También se puede permitir el acceso a todas las interfaces con nombre del módulo usando orders :: *.

8. Personalizar el modelo de módulos

La anotación @Modulithic de la clase principal permite configurar el modelo global:

Atributo Función
systemName Nombre legible de la aplicación en la documentación generada
sharedModules Módulos que se incluyen siempre en las pruebas de integración de módulos
additionalPackages Paquetes raíz adicionales en los que detectar módulos

8.1 Detección de módulos

La estrategia predeterminada busca módulos en paquetes hijos directos del paquete raíz. Se puede exigir que los módulos estén anotados con @ApplicationModule (o con la anotación @Module de jMolecules) mediante:

spring.modulith.detection-strategy=explicitly-annotated

Si ninguna estrategia integrada se ajusta a la aplicación, se puede implementar ApplicationModuleDetectionStrategy. Esta recibe el paquete raíz y devuelve los paquetes que deben considerarse módulos.

Los módulos también pueden aportar paquetes raíz externos mediante ApplicationModuleSourceFactory, registrada en META-INF/spring.factories. La estrategia de detección configurada se aplica a esos paquetes.

8.2 Detección de interfaces con nombre

Una estrategia ApplicationModuleDetectionStrategy también puede personalizar la detección de interfaces con nombre. Por ejemplo, se pueden exponer automáticamente los subpaquetes llamados api:

@Override
NamedInterfaces detectNamedInterfaces(
        JavaPackage basePackage, ApplicationModuleInformation information) {
    return NamedInterfaces.builder()
            .recursive()
            .matching("api")
            .build();
}

La API de construcción permite seleccionar o excluir paquetes. El paquete raíz del módulo sigue formando parte de su interfaz sin nombre.

Si la estrategia personalizada se necesita también en funcionalidades de ejecución, como inicializadores o funciones de producción, spring-modulith-core debe estar disponible como dependencia de compilación, no solo en las pruebas.

La estructura de paquetes expresa límites de módulos; las interfaces expuestas definen sus contratos. Spring Modulith permite inspeccionar y verificar que las dependencias respetan esos límites.

Referencia