diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5b829a0c..3e82a383 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,17 +2,12 @@ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" - target-branch: "3.1.x" # oldest OSS supported branch + target-branch: "4.1.x" # oldest OSS supported branch schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" - target-branch: "4.0.x" # oldest OSS supported branch - schedule: - interval: "weekly" - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "4.1.x" + target-branch: "4.2.x" schedule: interval: "weekly" - package-ecosystem: "github-actions" @@ -20,28 +15,6 @@ updates: target-branch: "main" schedule: interval: "weekly" - - package-ecosystem: maven - directory: / - schedule: - interval: daily - target-branch: 3.1.x - ignore: - # only upgrade patch versions for maintenance branch - - dependency-name: "*" - update-types: - - version-update:semver-major - - version-update:semver-minor - - package-ecosystem: maven - directory: / - schedule: - interval: daily - target-branch: 4.0.x - ignore: - # only upgrade patch versions for maintenance branch - - dependency-name: "*" - update-types: - - version-update:semver-major - - version-update:semver-minor - package-ecosystem: maven directory: / schedule: @@ -53,6 +26,17 @@ updates: update-types: - version-update:semver-major - version-update:semver-minor + - package-ecosystem: maven + directory: / + schedule: + interval: daily + target-branch: 4.2.x + ignore: + # only upgrade patch versions for maintenance branch + - dependency-name: "*" + update-types: + - version-update:semver-major + - version-update:semver-minor - package-ecosystem: maven directory: / schedule: @@ -78,3 +62,8 @@ updates: directory: /docs schedule: interval: weekly + - package-ecosystem: npm + target-branch: 4.2.x + directory: /docs + schedule: + interval: weekly diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 9d5357d8..80b3cfb7 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,9 +5,9 @@ name: Build on: push: - branches: [ main, 4.1.x ] + branches: [ main, 4.2.x, 4.1.x ] pull_request: - branches: [ main, 4.1.x ] + branches: [ main, 4.2.x, 4.1.x ] jobs: build: diff --git a/docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc b/docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc index 7cf6e3cb..0c44db69 100755 --- a/docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-circuitbreaker.adoc @@ -85,9 +85,9 @@ The `Customizer` interface has a single method (called `customize`) that takes t For detailed information on how to customize a given implementation see the following documentation: -* link:../../../../spring-cloud-circuitbreaker/current/reference/html/spring-cloud-circuitbreaker.html#configuring-resilience4j-circuit-breakers[Resilience4J] +* link:../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html[Resilience4J] * link:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-docs/src/main/asciidoc/circuitbreaker-sentinel.adoc#circuit-breaker-spring-cloud-circuit-breaker-with-sentinel--configuring-sentinel-circuit-breakers[Sentinel] -* link:../../../../../spring-cloud-circuitbreaker/docs/current/reference/html/spring-cloud-circuitbreaker.html#configuring-spring-retry-circuit-breakers[Spring Retry] +* link:../../../../../spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-spring-retry.html[Spring Retry] Some `CircuitBreaker` implementations such as `Resilience4JCircuitBreaker` call `customize` method every time `CircuitBreaker#run` is called. It can be inefficient. In that case, you can use `CircuitBreaker#once` method. It is useful where calling `customize` many times doesn't make sense, diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 45941b98..e2abc13a 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -76,8 +76,8 @@ |spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone. |spring.cloud.refresh.additional-property-sources-to-retain | | Additional property sources to retain during a refresh. Typically only system property sources are retained. This property allows property sources, such as property sources created by EnvironmentPostProcessors to be retained as well. |spring.cloud.refresh.enabled | `+++true+++` | Enables autoconfiguration for the refresh scope and associated features. -|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional class names for beans to post process into refresh scope. -|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of class names for beans to never be refreshed or rebound. +|spring.cloud.refresh.extra-refreshable | `+++true+++` | Additional bean names or class names for beans to post process into refresh scope. +|spring.cloud.refresh.never-refreshable | `+++true+++` | Comma separated list of bean names or class names for beans to never be refreshed or rebound. |spring.cloud.refresh.on-restart.enabled | `+++true+++` | Enable refreshing context on start. |spring.cloud.service-registry.auto-registration.enabled | `+++true+++` | Whether service auto-registration is enabled. Defaults to true. |spring.cloud.service-registry.auto-registration.fail-fast | `+++false+++` | Whether startup fails if there is no AutoServiceRegistration. Defaults to false. diff --git a/docs/package.json b/docs/package.json index 567c1f3a..6c97a4cc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,7 @@ "@antora/atlas-extension": "1.0.0-alpha.2", "@antora/collector-extension": "1.0.1", "@asciidoctor/tabs": "1.0.0-beta.6", - "@springio/antora-extensions": "1.14.2", - "@springio/asciidoctor-extensions": "1.0.0-alpha.14" + "@springio/antora-extensions": "1.14.4", + "@springio/asciidoctor-extensions": "1.0.0-alpha.16" } } diff --git a/pom.xml b/pom.xml index 162991b7..770a92a7 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ commons 1.0.0 - 1.78.1 + 1.80 diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml index 80b4971e..ffb6747f 100644 --- a/spring-cloud-commons-dependencies/pom.xml +++ b/spring-cloud-commons-dependencies/pom.xml @@ -15,6 +15,7 @@ spring-cloud-commons-dependencies Spring Cloud Commons Dependencies + 4.12.0 @@ -53,6 +54,20 @@ spring-cloud-test-support ${project.version} + + + + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp.version} + diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index d246f36f..05a6dffe 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -176,6 +176,16 @@ httpclient5 true + + com.squareup.okhttp3 + okhttp + true + + + com.squareup.okhttp3 + logging-interceptor + true + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java index 57e26fc0..6f801218 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,7 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Venil Noronha * @author Olga Maciaszek-Sharma + * @author Yanming Zhou */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RefreshScope.class) @@ -209,6 +210,9 @@ public class RefreshAutoConfiguration { // Already refresh scoped return false; } + if (this.refreshables.contains(name)) { + return true; + } String type = definition.getBeanClassName(); if (!StringUtils.hasText(type) && registry instanceof BeanFactory) { Class cls = ((BeanFactory) registry).getType(name); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java index ca2e9d79..a2359c74 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ import org.springframework.util.StringUtils; * the @ConfigurationProperties bean. * * @author Dave Syer + * @author Yanming Zhou * @see RefreshScope for a deeper and optionally more focused refresh of bean components. * */ @@ -131,7 +132,7 @@ public class ConfigurationPropertiesRebinder // TODO: determine a more general approach to fix this. // see // https://github.com/spring-cloud/spring-cloud-commons/issues/571 - if (getNeverRefreshable().contains(bean.getClass().getName())) { + if (getNeverRefreshable().contains(bean.getClass().getName()) || getNeverRefreshable().contains(name)) { return false; // ignore } appContext.getAutowireCapableBeanFactory().destroyBean(bean); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java index 4a5d813e..aa61373d 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ConfigDataContextRefresher.java @@ -92,9 +92,6 @@ public class ConfigDataContextRefresher extends ContextRefresher postProcessor.postProcessEnvironment(environment, application); } - if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) { - environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE); - } MutablePropertySources target = getContext().getEnvironment().getPropertySources(); String targetName = null; for (PropertySource source : environment.getPropertySources()) { diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java index 4982b980..54991a87 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/ContextRefresher.java @@ -51,8 +51,6 @@ public abstract class ContextRefresher { protected final Log logger = LogFactory.getLog(getClass()); - protected static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs"; - protected static final String[] DEFAULT_PROPERTY_SOURCES = new String[] { // order matters, if cli args aren't first, things get messy CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, "defaultProperties" }; diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java index 5a30aadf..fe5fa4b9 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/context/refresh/LegacyContextRefresher.java @@ -41,6 +41,8 @@ import static org.springframework.cloud.util.PropertyUtils.BOOTSTRAP_ENABLED_PRO */ public class LegacyContextRefresher extends ContextRefresher { + private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs"; + @Deprecated public LegacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { super(context, scope); diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/env/EnvironmentUtils.java b/spring-cloud-context/src/main/java/org/springframework/cloud/env/EnvironmentUtils.java index c6943476..d2b0b3a6 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/env/EnvironmentUtils.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/env/EnvironmentUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.core.env.Environment; /** * @author Spencer Gibb */ +@Deprecated(since = "4.3.0", forRemoval = true) public final class EnvironmentUtils { private EnvironmentUtils() { diff --git a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 448ce8fe..ff535a76 100644 --- a/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-context/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -39,13 +39,13 @@ { "name": "spring.cloud.refresh.extra-refreshable", "type": "java.util.Set", - "description": "Additional class names for beans to post process into refresh scope.", + "description": "Additional bean names or class names for beans to post process into refresh scope.", "defaultValue": true }, { "name": "spring.cloud.refresh.never-refreshable", "type": "java.lang.String", - "description": "Comma separated list of class names for beans to never be refreshed or rebound.", + "description": "Comma separated list of bean names or class names for beans to never be refreshed or rebound.", "defaultValue": true }, { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java index 03d30c1f..4786f244 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/autoconfigure/RefreshAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Dave Syer * @author Olga Maciaszek-Sharma + * @author Yanming Zhou */ @ExtendWith(OutputCaptureExtension.class) class RefreshAutoConfigurationTests { @@ -79,7 +80,7 @@ class RefreshAutoConfigurationTests { } @Test - public void extraRefreshables() { + public void extraRefreshableWithClassName() { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, "sealedconfig.foo=bar", "spring.cloud.refresh.extra-refreshable:" + SealedConfigProps.class.getName())) { @@ -89,7 +90,17 @@ class RefreshAutoConfigurationTests { } @Test - void neverRefreshable() { + public void extraRefreshableWithBeanName() { + String beanName = "sealedconfig-" + SealedConfigProps.class.getName(); + try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, + "sealedconfig.foo=bar", "spring.cloud.refresh.extra-refreshable:" + beanName)) { + context.getBean(SealedConfigProps.class); + context.getBean(ContextRefresher.class).refresh(); + } + } + + @Test + void neverRefreshableWithClassName() { try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, "countingconfig.foo=bar", "spring.cloud.refresh.never-refreshable:" + CountingConfigProps.class.getName())) { @@ -99,6 +110,17 @@ class RefreshAutoConfigurationTests { } } + @Test + void neverRefreshableWithBeanName() { + String beanName = "countingconfig-" + CountingConfigProps.class.getName(); + try (ConfigurableApplicationContext context = getApplicationContext(WebApplicationType.NONE, Config.class, + "countingconfig.foo=bar", "spring.cloud.refresh.never-refreshable:" + beanName)) { + CountingConfigProps configProps = context.getBean(CountingConfigProps.class); + context.getBean(ContextRefresher.class).refresh(); + assertThat(configProps.count).as("config props was rebound when it should not have been").hasValue(1); + } + } + @Test void refreshScopeLifecylePresentByDefault() { new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RefreshAutoConfiguration.class)) diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/LoadBalancerTags.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/LoadBalancerTags.java index 42afc64f..7404573d 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/LoadBalancerTags.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/LoadBalancerTags.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,10 @@ import io.micrometer.core.instrument.Tags; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.CompletionContext; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.RequestData; import org.springframework.cloud.client.loadbalancer.RequestDataContext; +import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.client.loadbalancer.ResponseData; import org.springframework.util.StringUtils; @@ -55,7 +57,11 @@ class LoadBalancerTags { } Iterable buildSuccessRequestTags(CompletionContext completionContext) { - ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer(); + Response lbResponse = completionContext.getLoadBalancerResponse(); + if (lbResponse == null) { + return Tags.empty(); + } + ServiceInstance serviceInstance = lbResponse.getServer(); Tags tags = Tags.of(buildServiceInstanceTags(serviceInstance)); Object clientResponse = completionContext.getClientResponse(); if (clientResponse instanceof ResponseData responseData) { @@ -100,9 +106,9 @@ class LoadBalancerTags { } Iterable buildDiscardedRequestTags(CompletionContext completionContext) { - if (completionContext.getLoadBalancerRequest().getContext() instanceof RequestDataContext) { - RequestData requestData = ((RequestDataContext) completionContext.getLoadBalancerRequest().getContext()) - .getClientRequest(); + Request lbRequest = completionContext.getLoadBalancerRequest(); + if (lbRequest != null && lbRequest.getContext() instanceof RequestDataContext requestDataContext) { + RequestData requestData = requestDataContext.getClientRequest(); if (requestData != null) { return Tags.of(valueOrUnknown("method", requestData.getHttpMethod()), valueOrUnknown("uri", getPath(requestData)), valueOrUnknown("serviceId", getHost(requestData))); @@ -118,11 +124,15 @@ class LoadBalancerTags { } Iterable buildFailedRequestTags(CompletionContext completionContext) { - ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer(); + Response lbResponse = completionContext.getLoadBalancerResponse(); + if (lbResponse == null) { + return Tags.empty(); + } + ServiceInstance serviceInstance = lbResponse.getServer(); Tags tags = Tags.of(buildServiceInstanceTags(serviceInstance)).and(exception(completionContext.getThrowable())); - if (completionContext.getLoadBalancerRequest().getContext() instanceof RequestDataContext) { - RequestData requestData = ((RequestDataContext) completionContext.getLoadBalancerRequest().getContext()) - .getClientRequest(); + Request lbRequest = completionContext.getLoadBalancerRequest(); + if (lbRequest != null && lbRequest.getContext() instanceof RequestDataContext requestDataContext) { + RequestData requestData = requestDataContext.getClientRequest(); if (requestData != null) { return tags.and(Tags.of(valueOrUnknown("method", requestData.getHttpMethod()), valueOrUnknown("uri", getPath(requestData)))); diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java index 1d77caea..18fcd0d0 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,10 +85,10 @@ public class MicrometerStatsLoadBalancerLifecycle implements LoadBalancerLifecyc @Override public void onStartRequest(Request request, Response lbResponse) { - if (request.getContext() instanceof TimedRequestContext) { + if (request != null && request.getContext() instanceof TimedRequestContext) { ((TimedRequestContext) request.getContext()).setRequestStartTime(System.nanoTime()); } - if (!lbResponse.hasServer()) { + if (lbResponse == null || !lbResponse.hasServer()) { return; } ServiceInstance serviceInstance = lbResponse.getServer(); @@ -104,7 +104,11 @@ public class MicrometerStatsLoadBalancerLifecycle implements LoadBalancerLifecyc @Override public void onComplete(CompletionContext completionContext) { - ServiceInstance serviceInstance = completionContext.getLoadBalancerResponse().getServer(); + ServiceInstance serviceInstance = null; + Response loadBalancerResponse = completionContext.getLoadBalancerResponse(); + if (loadBalancerResponse != null) { + serviceInstance = loadBalancerResponse.getServer(); + } LoadBalancerProperties properties = serviceInstance != null ? loadBalancerFactory.getProperties(serviceInstance.getServiceId()) : loadBalancerFactory.getProperties(null); @@ -121,7 +125,11 @@ public class MicrometerStatsLoadBalancerLifecycle implements LoadBalancerLifecyc if (activeRequestsCounter != null) { activeRequestsCounter.decrementAndGet(); } - Object loadBalancerRequestContext = completionContext.getLoadBalancerRequest().getContext(); + Request lbRequest = completionContext.getLoadBalancerRequest(); + if (lbRequest == null) { + return; + } + Object loadBalancerRequestContext = lbRequest.getContext(); if (requestHasBeenTimed(loadBalancerRequestContext)) { if (CompletionContext.Status.FAILED.equals(completionContext.status())) { Timer.builder("loadbalancer.requests.failed") diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java index 27d360dd..c5d7bc5c 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ import org.springframework.http.HttpStatus; import org.springframework.util.MultiValueMapAdapter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.cloud.loadbalancer.stats.LoadBalancerTags.UNKNOWN; @@ -243,6 +244,26 @@ class MicrometerStatsLoadBalancerLifecycleTests { Tag.of("serviceInstance.port", "0"), Tag.of("status", "200"), Tag.of("uri", UNKNOWN)); } + @Test + void shouldHandleNullLoadBalancerResponse() { + RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), + new HttpHeaders(), new HashMap<>()); + Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); + assertThatCode(() -> { + statsLifecycle.onStartRequest(lbRequest, null); + statsLifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, null)); + }).doesNotThrowAnyException(); + } + + @Test + void shouldHandleNullLoadBalancerRequest() { + Response lbResponse = new EmptyResponse(); + assertThatCode(() -> { + statsLifecycle.onStartRequest(null, lbResponse); + statsLifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, null, lbResponse)); + }).doesNotThrowAnyException(); + } + private static class StatsTestContext { } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 8ccbaf06..3a65ad08 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -21,6 +21,8 @@ + +