Spring Boot
Usa resilience4cata en una aplicación Spring Boot 4: el código generado protege los beans anotados, el
registro se configura desde application.yml y Actuator obtiene un indicador de salud, un endpoint y métricas.
El módulo resilience4cata-spring (disponible desde 0.4.0) es una autoconfiguración de Spring Boot. No añade
nada al camino de la llamada: no hay proxies, aspectos ni interceptores. Un bean protegido es la clase que
generó el procesador de anotaciones, así que una llamada cuesta lo mismo que sin Spring.
Requisitos: Spring Boot 4.1 o posterior (el módulo se compila contra 4.1.0 y se prueba en 4.1.1) y Java 21. Spring Boot 3 no está soportado.
Instalación
Añade el módulo y, como para cualquier uso de las anotaciones, el procesador de anotaciones
(instalación). El módulo trae resilience4cata-core y resilience4cata-config.
Gradle
dependencies {
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
annotationProcessor platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
implementation 'dev.resilience4cata:resilience4cata-spring'
compileOnly 'dev.resilience4cata:resilience4cata-annotations'
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'
implementation 'dev.resilience4cata:resilience4cata-micrometer' // optional: metrics
testImplementation 'dev.resilience4cata:resilience4cata-testkit' // optional: ManualClock
}
Gradle (Kotlin)
dependencies {
val r4cBom = platform("dev.resilience4cata:resilience4cata-bom:0.10.0")
implementation(r4cBom)
annotationProcessor(r4cBom)
implementation("dev.resilience4cata:resilience4cata-spring")
compileOnly("dev.resilience4cata:resilience4cata-annotations")
annotationProcessor("dev.resilience4cata:resilience4cata-processor")
implementation("dev.resilience4cata:resilience4cata-micrometer") // optional: metrics
testImplementation("dev.resilience4cata:resilience4cata-testkit") // optional: ManualClock
}
Maven
<!-- with resilience4cata-bom imported in <dependencyManagement> -->
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-spring</artifactId>
</dependency>
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-annotations</artifactId>
<scope>provided</scope>
</dependency>
<!-- in the maven-compiler-plugin configuration -->
<annotationProcessorPaths>
<path>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-processor</artifactId>
<version>0.10.0</version>
</path>
<!-- annotationProcessorPaths replaces discovery: list the other processors you use too -->
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
</annotationProcessorPaths>
Sin el procesador de anotaciones no se genera nada y los beans quedan sin protección: las anotaciones tienen retención
CLASS, así que nada en tiempo de ejecución puede notar que están.GET /actuator/resilience4catalista las instancias de políticas; una lista vacía significa que el procesador no se ejecutó.
Clases anotadas
Anota la clase misma. Es el estilo recomendado en Spring:
@Service
public class OrderService {
private final OrderRepository orders;
public OrderService(OrderRepository orders) {
this.orders = orders;
}
@Retry(maxAttempts = 3, delay = "100ms")
@CircuitBreaker(name = "orders")
public Order find(String id) {
return orders.load(id);
}
public Order findAgain(String id) {
return find(id); // a call on this: still retried
}
}
El procesador genera OrderService$Resilient extends OrderService, y la integración hace que Spring la
instancie dondequiera que una definición de bean nombre OrderService (escaneo de componentes, @Import,
registerBean). Así:
- el bean sigue siendo un
OrderService: se inyecta por su clase, sus interfaces,@Qualifier,List<…>, y conserva su nombre, su ámbito y@Primary; - las llamadas sobre
this(findAgainllamando afind) pasan por las políticas, a diferencia de Spring AOP y del propio@Retryablede Spring; - la inyección por constructor funciona como antes: los constructores generados reciben el
ResilienceRegistry(inyectado por tipo) seguido de los parámetros originales, con sus nombres y anotaciones (@Autowireden uno de varios constructores,@Qualifier,@Value; ver clases anotadas). La inyección por campo y por setter y@PostConstructse heredan.
Un método @Bean que devuelve new OrderService(...) produciría un bean sin protección, así que el arranque
falla con R4C-SPR-001: registra la clase en su lugar, o devuelve
new OrderService$Resilient(registry, ...) desde el método.
Proxies sobre la clase. La clase generada no es final, así que Spring aún puede poner un proxy basado en
clases alrededor (@Transactional, @Cacheable, @Async, beans con ámbito). El proxy queda fuera de las
políticas. Con transacciones eso supondría una sola transacción para todos los intentos, y cuando un intento
fallido la marca rollback-only, un intento posterior exitoso no puede hacer commit (UnexpectedRollbackException).
Por eso un método reintentado o con hedge que además es transaccional (@Transactional de Spring o de Jakarta, con
una propagación que se une a una transacción o la inicia) detiene el arranque con
R4C-SPR-004. Corrígelo declarando la política en una interfaz que implemente el bean
(el envoltorio de la interfaz queda fuera del proxy de transacciones: una transacción por intento), o moviendo
@Transactional a otro bean al que llame el método reintentado. Si lo que quieres es una sola transacción
compartida, pon resilience4cata.spring.allow-retry-inside-transaction=true. @CircuitBreaker, @Timeout,
@RateLimiter y @Bulkhead ejecutan la llamada una vez y no se comprueban. El override generado repite las
anotaciones del método que sobrescribe, así que un @Transactional de Jakarta a nivel de método (que Spring solo
lee en el método invocado) se aplica y se comprueba como el de Spring.
Interfaces anotadas
@Retry(maxAttempts = 3)
@Timeout("2s")
public interface StockClient {
int available(String sku);
}
@Component
public class HttpStockClient implements StockClient {
@Override
public int available(String sku) {
return 42; // an HTTP call in a real application
}
}
Un bean que implementa una interfaz anotada se reemplaza, una vez inicializado, por
Resilient.of(StockClient.class, bean, registry). Inyecta la interfaz:
@Component
public class StockReport {
private final StockClient stock; // the interface: the bean is StockClient$Resilient
public StockReport(StockClient stock) {
this.stock = stock;
}
public String line(String sku) {
return sku + ": " + stock.available(sku);
}
}
- El tipo del bean pasa a ser el envoltorio, que implementa solo
StockClient: inyectarHttpStockClientfalla, como con los proxies JDK, y el informe de arranque dice que inyectes la interfaz. - El envoltorio se aplica después de todos los demás post-procesadores, así que envuelve sus proxies: una llamada
reintentada a través de un proxy
@Transactionalobtiene una transacción nueva por intento. - Las llamadas sobre
thisdentro deHttpStockClientno están protegidas (el envoltorio está fuera del objeto). - Un bean que implementa dos interfaces anotadas, sin que una extienda a la otra, detiene el arranque con
R4C-SPR-003. Un bean que ya es una clase generada (un método@Beanque devuelve él mismoResilient.of(...)) se deja como está.
Configuración
Las políticas leen las propiedades de la aplicación bajo resilience4cata, con las claves de la
referencia de configuración en kebab-case o camelCase:
resilience4cata:
defaults:
retry:
delay: 50ms
circuit-breaker:
minimum-calls: 20
configs:
slow:
retry.delay: 500ms
instances:
orders:
circuit-breaker.failure-rate-threshold: 25
"[dev.resilience4cata.spring.guide.StockClient#available(java.lang.String)]":
timeout.value: 3s
- Los ids de instancia con algo más que letras, dígitos,
_y-(el derivado<tipo>#<método>(<tipos de parámetros>)) van entre corchetes, como Spring escribe las claves de mapas:"[com.acme.StockClient#available(java.lang.String)]"en YAML,resilience4cata.instances[com.acme.StockClient#available(java.lang.String)].timeout.value=3sen.properties. - Las listas (
retry-on,abort-on,record-on,ignore-on) pueden ser listas YAML o valores separados por comas. - Los placeholders (
${…}) se resuelven. - La precedencia es la de Spring: argumentos de línea de comandos y propiedades de prueba sobre propiedades
del sistema sobre variables de entorno sobre
application-<perfil>.ymlsobreapplication.yml. Las variables de entorno usan los nombresR4C_oRESILIENCE4CATA_deresilience4cata-config. Un ficheroresilience4cata.propertiesen el class path se sigue leyendo, por debajo de todos ellos. - Perfiles: los documentos de perfil de Spring (
application-prod.yml,spring.config.activate.on-profile) son la vía natural; las clavesprofiles.<nombre>.*propias de resilience4cata siguen funcionando (resilience4cata.profiles.active: ${spring.profiles.active:}las une). - La configuración se lee una vez, al arrancar, como en todo resilience4cata: no hay recarga.
Validación al arrancar. Una clave desconocida o un valor incorrecto detienen el arranque con todos los problemas a la vez, con el fichero y la línea donde se fijaron:
***************************
APPLICATION FAILED TO START
***************************
Description:
Invalid resilience4cata configuration (1 problem):
[R4C-CFG-001] unknown key 'resilience4cata.defaults.retry.max-atempts' (class path resource [application.yml] - 4:20). Did you mean 'resilience4cata.defaults.retry.max-attempts'? ...
Una vez creados los singletons, cada clave resilience4cata.instances.<id>.* debe corresponder a una instancia de
política, o el arranque falla con R4C-CFG-013. Con beans perezosos o pipelines
construidos más tarde, pon resilience4cata.spring.verify-instances=warn (el valor por defecto con
spring.main.lazy-initialization=true).
El módulo incluye metadatos de configuración de Spring Boot para las claves defaults y los interruptores de
abajo, que los IDE que los leen usan para completarlas y documentarlas.
Customizers y listeners
Un bean ResilienceRegistryCustomizer ajusta el builder del registro, por ejemplo una config con nombre en código:
@Bean
ResilienceRegistryCustomizer slowBackends() {
return builder -> builder.retry("slow", retry -> retry.maxAttempts(5).delay(Duration.ofSeconds(1)));
}
Los customizers se ejecutan en @Order, antes que las propiedades, así que ganan las propiedades:
configs.slow.retry.delay de arriba reemplaza el delay del customizer y conserva su maxAttempts. Una clave
defaults.<política>.* reemplaza el <política>Defaults(...) de un customizer para esa política.
Cada bean EventListener se registra para el tipo de evento de su firma genérica (un listener sin tipo recibe
todos los eventos):
@Bean
EventListener<CircuitBreakerEvent.StateTransition> breakerAlerts(Alerts alerts) {
return transition -> alerts.send(transition.instance() + " is now " + transition.to());
}
Los listeners se ejecutan en el hilo que causó el evento (eventos). Un bean listener no debe depender de un bean protegido: el registro necesita el listener, y el bean protegido necesita el registro.
Un bean Ticker (el ManualClock del testkit) pasa a ser el reloj del registro. Una aplicación que define su
propio bean ResilienceRegistry reemplaza al autoconfigurado (entonces no se aplican customizers ni beans
listener); el bean ResilienceConfig sigue disponible para construirlo con config.applyTo(builder).
Métricas, salud y el endpoint
- Métricas: con
resilience4cata-micrometeren el class path y un beanMeterRegistry(el de Spring Boot Actuator), los medidores de Micrometer se registran solos. Las llamadas exitosas no publican eventos, así que no cuestan nada. No hay observaciones por llamada (ObservationRegistry): una por llamada reservaría memoria y leería el reloj en cada éxito. - Salud: el indicador de salud
resilience4catainforma de cada circuit breaker (estado, tasas de fallos y de llamadas lentas, llamadas en la ventana). Sigue UP con breakers abiertos: un breaker abierto es la aplicación protegiéndose de una dependencia que falla, no un motivo para reiniciarla ni sacarla del balanceo. Lista los breakers que deben ponerlo DOWN (UNKNOWN mientras están semiabiertos) enresilience4cata.spring.health.down-when-open(*para todos). No forma parte de los grupos liveness ni readiness salvo que lo añadas. - Endpoint:
GET /actuator/resilience4catadevuelveregistry.snapshot(), yGET /actuator/resilience4cata/{instance}las políticas de un id de instancia. Es de solo lectura y no guarda historial de eventos. Exponlo como cualquier endpoint:management.endpoints.web.exposure.include=health,resilience4cata.
Interruptores
| Propiedad | Por defecto | Efecto |
|---|---|---|
resilience4cata.spring.wrap |
true |
Reemplaza los beans de interfaces anotadas por su envoltorio |
resilience4cata.spring.exclude |
vacío | Nombres de beans que dejar tal cual: sin subclase, sin envoltorio, sin comprobaciones |
resilience4cata.spring.allow-retry-inside-transaction |
false |
Permite @Retry/@Hedge en métodos transaccionales de clases anotadas (una transacción para todos los intentos) en lugar de fallar con R4C-SPR-004 |
resilience4cata.spring.verify-instances |
fail (warn con inicialización perezosa) |
fail, warn u off para claves de instancia que no corresponden a nada |
resilience4cata.spring.health.down-when-open |
vacío | Ids de breakers (o *) que ponen la salud en DOWN mientras están abiertos |
resilience4cata.spring.metrics.enabled |
true |
Registra los medidores de Micrometer |
management.health.resilience4cata.enabled |
true |
El indicador de salud |
Pruebas
Declara un bean ManualClock: las pausas de retry, las esperas de bulkhead y rate limiter y los timeouts lo
siguen, así que una prueba nunca duerme (guía de pruebas).
@TestConfiguration
static class TestClock {
@Bean
ManualClock clock() {
return new ManualClock(); // retry pauses, waits and timeouts follow it instead of the system clock
}
}
@Autowired
OrderService orders;
@Autowired
ManualClock clock;
@Autowired
OrderRepository repository; // fails the first loads it is told to
@Test
void retriesWithoutSleeping() {
repository.failures.set(2);
assertThat(orders.find("42")).isEqualTo(new Order("42"));
assertThat(clock.elapsed()).isPositive(); // the two pauses, taken on the manual clock
}
Dobles de prueba de beans protegidos:
@MockitoBeanreemplaza el bean por el mock sin envolver, sin las políticas, para una interfaz o una clase anotada.@MockitoSpyBeansobre una interfaz anotada también inyecta el spy sin envolver, así queverify(spy)funciona; sobre una clase anotada el spy conserva las políticas.- Un contexto de test compartido por varias clases de test comparte un registry, así que un breaker abierto sigue
abierto entre ellas: reinícialo con
registry.circuitBreaker(id).reset(), o usa@DirtiesContext.
AOT e imágenes nativas
Spring AOT (el paso de procesamiento de una imagen nativa, o de una aplicación JVM optimizada con AOT) funciona:
- el reemplazo de las clases anotadas ocurre mientras se procesan las definiciones de beans, así que el código
generado llama directamente a
new OrderService$Resilient(...), y el post-procesador que las reemplazó queda fuera del contexto optimizado; - los beans de interfaces se siguen envolviendo en tiempo de ejecución con
Resilient.of, que encuentraStockClient$Resilientpor nombre. El procesador de anotaciones escribe metadatos de alcanzabilidad de GraalVM para cada clase generada (META-INF/native-image/dev.resilience4cata.generated/…), así que no hacen falta hints de Spring en tiempo de ejecución; - las propiedades se leen en tiempo de ejecución, pero las condiciones se fijan al compilar, como en todo Spring
AOT:
resilience4cata.spring.metrics.enabled,management.health.resilience4cata.enabled, la exposición del endpoint, yresilience4cata.spring.excludepara las clases anotadas.
Spring Boot 4 necesita GraalVM 25 o posterior para imágenes nativas. El build del proyecto comprueba el procesamiento AOT en la JVM; su CI no construye imágenes nativas.
Límites
- Las interfaces anotadas cambian el tipo del bean al envoltorio: inyecta la interfaz. Las llamadas sobre
thisdentro de la implementación no están protegidas. Una interfaz anotada por bean. - Los métodos
@Beandeben devolver ellos mismos la clase generada; una clase@Configurationcompleta con métodos@Beanno puede anotarse (R4C-SPR-002). - Con clases anotadas, los proxies basados en clases como
@Transactionalquedan fuera de las políticas; un método transaccional reintentado o con hedge detiene el arranque (R4C-SPR-004) salvo que se permita. La comprobación lee los ficheros de clase, así que una imagen nativa la omite: se ejecuta en el build y las pruebas en la JVM. - Los métodos que el constructor llama sobre
thisse ejecutan antes de que se preparen las políticas (clases anotadas). - Sin observaciones por llamada, sin historial de eventos en el endpoint, sin recarga en caliente de la configuración.