Clases anotadas
Anota una clase no final en lugar de una interfaz, y el procesador genera una subclase cuyos overrides aplican las políticas, así que las llamadas que el objeto se hace a sí mismo también quedan protegidas.
Anota la clase
public class StockService {
private final Warehouse warehouse;
public StockService(Warehouse warehouse) {
this.warehouse = warehouse;
}
@Retry(maxAttempts = 3, delay = "50ms")
public int stock(String sku) {
return warehouse.count(sku);
}
public int total(List<String> skus) {
int total = 0;
for (String sku : skus) {
total += stock(sku); // self-invocation: goes through stock()'s retry
}
return total;
}
}
El procesador escribe StockService$Resilient extends StockService. Solo sobrescribe los métodos que tienen políticas (aquí stock), y cada uno termina con super.stock(...). total no se sobrescribe, pero su llamada a stock pasa por el override, porque this.stock(...) despacha a él.
Créala
Resilient.create recibe la clase, el registry y los argumentos de uno de los constructores de la clase:
StockService stock = Resilient.create(StockService.class, registry, warehouse);
int total = stock.total(List.of("a", "b")); // each stock(sku) call is retried
Por cada constructor no privado de la clase, la subclase generada tiene uno que recibe primero un ResilienceRegistry y después los mismos parámetros. Llamarlo directamente hace lo mismo que Resilient.create, con verificación del compilador:
StockService stock = new StockService$Resilient(registry, warehouse); // checked by the compiler
Resilient.create elige el constructor como lo haría el compilador, las coincidencias exactas antes que los varargs. Si ningún constructor encaja, falla con R4C-RT-009; si encaja más de uno (por ejemplo, al pasar null a constructores sobrecargados), con R4C-RT-010. Los dos mensajes sugieren la llamada new …$Resilient(registry, …) que conviene usar en su lugar.
Constructores e inyección de dependencias
Cada constructor replicado conserva los nombres de parámetro del original y copia sus anotaciones y las de cada parámetro, así que un contenedor de inyección de dependencias que construye StockService$Resilient ve los mismos metadatos que en StockService: @Autowired o @Inject en uno de varios constructores, @Qualifier("…"), @Named("…") o @Value("${…}") en un parámetro.
public class ReportService {
@Autowired
public ReportService(@Qualifier("reportsDb") DataSource dataSource, @Value("${reports.limit}") int limit) { … }
…
}
public class ReportService$Resilient extends ReportService {
@org.springframework.beans.factory.annotation.Autowired
public ReportService$Resilient(dev.resilience4cata.ResilienceRegistry registry,
@org.springframework.beans.factory.annotation.Qualifier("reportsDb") javax.sql.DataSource dataSource,
@org.springframework.beans.factory.annotation.Value("${reports.limit}") int limit) {
super(dataSource, limit);
…
}
}
- Solo se copian las anotaciones que quedan en los class files (retención
CLASSoRUNTIME); las de retenciónSOURCE, como las de Lombok, no. Las anotaciones de uso de tipo (@Nullablede JSpecify) tampoco se repiten. - El parámetro
ResilienceRegistryno lleva anotación: los contenedores lo inyectan por tipo, así que la aplicación necesita exactamente un beanResilienceRegistry. - Si un parámetro del constructor de la clase ya se llama
registry, el generado se llamar4c$registry. Los parámetros llamadosjavaodevse renombran ar4c$p<índice>, porque esos nombres ocultarían los paquetes que usa el constructor generado. @ConstructorPropertieslista un nombre por parámetro, así que la copia generada empieza por el del registry.- Los nombres de parámetro son los del código fuente, así que un contenedor que empareja beans por nombre de parámetro (Spring, compilando con
-parameters) ve los mismos nombres en las dos clases.
Los overrides hacen lo mismo con los métodos: cada uno repite las anotaciones del método que sobrescribe y de sus parámetros, con las mismas reglas, así que las anotaciones que los frameworks leen solo en el método invocado (el @Transactional de Jakarta en Spring, los interceptor bindings de CDI o Micronaut, @Deprecated) no se pierden. No se repiten las anotaciones propias de resilience4cata ni los bundles que las llevan, ni las anotaciones a nivel de tipo de la clase (las @Inherited llegan a la subclase de todos modos), salvo con la opción del compilador -Ar4c.inject=micronaut, que las repite y añade @Replaces(C.class) para que Micronaut cree C$Resilient en lugar de C (guía de Micronaut).
Un contenedor que construye beans a partir de constructores @Inject por su cuenta (Micronaut lo hace) también ve el @Inject reflejado, así que puede crear tanto C como C$Resilient. La integración con Micronaut lo reporta (R4C-MN-004); con otros contenedores, asegúrate de que solo se registre C$Resilient.
¿Interfaz o clase?
| Interfaz anotada | Clase anotada | |
|---|---|---|
| Se crea con | Resilient.of(Api.class, impl, registry) |
Resilient.create(Impl.class, registry, args...) |
| La implementación | Cualquiera: escrita a mano, generada, una lambda | Debe ser una clase no final tuya |
this.other() dentro de la implementación |
No se intercepta | Se intercepta |
| Anotaciones que cuentan | Solo las de la interfaz | Las de la clase, sus superclases y sus interfaces |
| Costo | Código generado, una lectura de campo para llegar al delegate | Código generado que llama a super directamente |
Measured En AWS Graviton3 (
c7g.xlarge), un retry cuyo primer intento tiene éxito cuesta 1.0 ns en el estilo de clase contra 1.2 ns en el estilo de interfaz; con un reintento, 5.6 ns contra 6.5 ns. Benchmarks
Una clase se procesa solo si ella misma declara una anotación de resiliencia: en la clase, en uno de sus métodos o a través de un bundle. Una clase que solo implementa una interfaz anotada no recibe subclase; envuélvela con Resilient.of(Api.class, impl, registry), que aplica las políticas de la interfaz y no intercepta la auto-invocación.
Anotaciones en la clase, sus superclases y sus interfaces
El estilo de clase combina, atributo por atributo, todas las anotaciones que se aplican a un método. De menor a mayor precedencia:
- las anotaciones a nivel de tipo de la interfaz,
- las anotaciones del método de la interfaz,
- las anotaciones a nivel de tipo de las superclases que declaran el método, de arriba hacia abajo,
- las anotaciones a nivel de tipo de la propia clase,
- las anotaciones del método en la clase (un override en una subclase gana sobre el método de la superclase).
@Retry(delay = "10ms", jitter = Jitter.NONE)
public interface OrderApi {
@Retry(maxAttempts = 2)
String status(String id);
@Retry(maxAttempts = 3)
String quote(String id);
}
public class OrderService implements OrderApi {
final AtomicInteger calls = new AtomicInteger();
@Override
@Retry(maxAttempts = 4) // wins over the interface method's 2; the delay still comes from OrderApi
public String status(String id) {
calls.incrementAndGet();
throw new IllegalStateException("orders down");
}
@Override // not annotated here: OrderApi's three attempts apply
public String quote(String id) {
calls.incrementAndGet();
throw new IllegalStateException("quotes down");
}
@Fallback(method = "cachedPrice")
public String price(String id) {
throw new IllegalStateException("pricing down");
}
protected String cachedPrice(String id, IllegalStateException e) { // not private: the subclass calls it
return "cached price of " + id;
}
}
Resilient.create(OrderService.class, registry) le da a status 4 intentos y a quote 3, los dos con el delay de 10 ms de la interfaz, y a price su fallback. El mismo objeto envuelto con Resilient.of(OrderApi.class, ...) vería solo las anotaciones de OrderApi: 2 intentos para status.
Una anotación a nivel de tipo en la clase se aplica a todos los métodos que la subclase puede sobrescribir, incluidos los heredados, salvo:
- los métodos final, que se omiten;
toString,equals,hashCodey los demás métodos deObject, que solo toman las anotaciones escritas en el propio método.
Los ids de instancia usan la clase anotada: com.acme.OrderService#status(java.lang.String).
Restricciones
Todos estos casos son errores de compilación, con un código y la corrección en la referencia de errores:
| Qué | Código |
|---|---|
La clase es final |
R4C-SUB-001 |
La clase es sealed |
R4C-SUB-002 |
La clase es abstract |
R4C-SUB-003 |
La clase es private, o está anidada en una clase privada |
R4C-SUB-004 |
| La clase es interna (anidada no estática) | R4C-SUB-005 |
Todos los constructores son private |
R4C-SUB-006 |
Un método final está anotado a nivel de método (en la clase, una superclase o una interfaz) |
R4C-SUB-007 |
| Un método anotado es package-private en una superclase de otro paquete | R4C-SUB-008 |
Un método static o private está anotado |
R4C-APT-002 |
| El tipo es un enum o un record | R4C-APT-001 |
Además:
- Los métodos fallback hermanos se llaman sobre
this, así que no pueden serprivate(ni package-private en una superclase de otro paquete).protectedfunciona. - Los métodos que el constructor llama sobre
thisse ejecutan antes de que las políticas estén configuradas, porque los campos de la subclase se asignan después de quesuper(...)retorna. No anotes métodos que los constructores llaman. - La subclase generada no es
final, así que los frameworks pueden crear proxies de ella (CGLIB para@Transactional, proxies con scope). Sus overrides llaman a las políticas; una subclase tuya que los vuelva a sobrescribir se las salta. Extiende tu propia clase, no la generada. Resilient.createrechaza una interfaz (R4C-RT-007) y un tipo que no puede tener una subclase generada (R4C-RT-008).
Con Lombok
Si Lombok genera el constructor o los métodos de una clase anotada, Lombok debe ejecutarse antes que resilience4cata: decláralo primero en el processor path (instalación).
Métodos async
Los métodos de una clase anotada pueden devolver CompletionStage<T> o CompletableFuture<T>, con las mismas reglas que en las interfaces (métodos async).