From 3788b6823d3f2fbc65d5bb6d997a76828c397bb4 Mon Sep 17 00:00:00 2001 From: HDPark95 Date: Mon, 3 Aug 2026 22:59:16 +0900 Subject: [PATCH] Use binary class name to resolve per-class HTTP interface fallbacks CircuitBreakerRequestValueProcessor stored the declaring class under its canonical name, while fallbacks are registered per service using Class#getName() (the binary name). For a nested @HttpExchange interface these two forms differ (Outer.Inner vs Outer$Inner), so the per-class fallback was never matched and the invocation fell through to the default fallback or NoFallbackAvailableException. Store the binary name so the declaring-class lookup key matches the registration key. Top-level interfaces are unaffected because their canonical and binary names are identical. Signed-off-by: HDPark95 --- .../CircuitBreakerRequestValueProcessor.java | 2 +- .../CircuitBreakerAdapterDecoratorTests.java | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerRequestValueProcessor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerRequestValueProcessor.java index dd0039da2..d93d226f6 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerRequestValueProcessor.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerRequestValueProcessor.java @@ -72,7 +72,7 @@ public void process(Method method, MethodParameter[] parameters, @Nullable Objec builder.addAttribute(PARAMETER_TYPES_ATTRIBUTE_NAME, method.getParameterTypes()); builder.addAttribute(ARGUMENTS_ATTRIBUTE_NAME, arguments); builder.addAttribute(RETURN_TYPE_ATTRIBUTE_NAME, method.getReturnType()); - builder.addAttribute(DECLARING_CLASS_ATTRIBUTE_NAME, method.getDeclaringClass().getCanonicalName()); + builder.addAttribute(DECLARING_CLASS_ATTRIBUTE_NAME, method.getDeclaringClass().getName()); } } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerAdapterDecoratorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerAdapterDecoratorTests.java index 4a92b3c11..cdb82a510 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerAdapterDecoratorTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/circuitbreaker/httpservice/CircuitBreakerAdapterDecoratorTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.client.circuitbreaker.httpservice; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -24,6 +25,8 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; import org.springframework.web.service.invoker.HttpExchangeAdapter; import org.springframework.web.service.invoker.HttpRequestValues; @@ -128,4 +131,34 @@ void shouldThrowExceptionWhenNoFallbackAvailable() { .isThrownBy(() -> fallbackHandler.apply(new RuntimeException("test"))); } + // Fallback classes are registered per service under Class#getName() (binary name), + // so the declaring-class attribute written by the processor must use the same form. + // For a nested @HttpExchange interface getName() ("Outer$Inner") differs from + // getCanonicalName() ("Outer.Inner"), which used to leave the per-class fallback + // unresolved and fall through to NoFallbackAvailableException. + @Test + void shouldResolvePerClassFallbackForNestedServiceInterface() throws NoSuchMethodException { + Method method = NestedTestService.class.getMethod("test", String.class, Integer.class); + HttpRequestValues.Builder builder = HttpRequestValues.builder(); + new CircuitBreakerRequestValueProcessor().process(method, new MethodParameter[0], + new Object[] { "testDescription", 5 }, builder); + builder.setHttpMethod(HttpMethod.GET); + builder.setUriTemplate("/test"); + Map attributes = builder.build().getAttributes(); + CircuitBreakerAdapterDecorator nestedDecorator = new CircuitBreakerAdapterDecorator(adapter, circuitBreaker, + Map.of(NestedTestService.class.getName(), Fallbacks.class)); + when(httpRequestValues.getAttributes()).thenReturn(attributes); + Function fallbackHandler = nestedDecorator.createFallbackHandler(httpRequestValues); + + Object fallback = fallbackHandler.apply(new RuntimeException("test")); + + assertThat(fallback).isEqualTo("testDescription: 5"); + } + + interface NestedTestService { + + String test(String description, Integer value); + + } + }