From c2c53522fac812f563f1e0d2993d173dd6eb7a3b Mon Sep 17 00:00:00 2001 From: WonChul Heo Date: Mon, 4 Nov 2024 21:05:21 +0900 Subject: [PATCH] Support Spring RestClient as default TransportClientFactory (#4281) --- .../ROOT/pages/spring-cloud-netflix.adoc | 6 +- docs/modules/ROOT/partials/_configprops.adoc | 211 ++++++++-------- ...coveryClientOptionalArgsConfiguration.java | 106 ++++++-- ...ekaConfigServerBootstrapConfiguration.java | 63 ++++- .../eureka/http/EurekaHttpClientUtils.java | 129 ++++++++++ .../eureka/http/NotFoundHttpResponse.java | 67 +++++ ...RestClientDiscoveryClientOptionalArgs.java | 38 +++ .../http/RestClientEurekaHttpClient.java | 238 ++++++++++++++++++ .../RestClientTransportClientFactories.java | 58 +++++ .../RestClientTransportClientFactory.java | 86 +++++++ .../RestTemplateTransportClientFactory.java | 119 +-------- .../http/WebClientTransportClientFactory.java | 72 +----- ...itional-spring-configuration-metadata.json | 6 + ...rverBootstrapConfigurationClientTests.java | 131 ++++++++++ ...nfigurationRestClientIntegrationTests.java | 87 +++++++ ...BootstrapConfigurationRestClientTests.java | 62 +++++ ...nfigServerBootstrapConfigurationTests.java | 4 +- ...rBootstrapConfigurationWebClientTests.java | 4 +- ...ClientsOptionalArgsConfigurationTests.java | 40 ++- ...yClientOptionalArgsConfigurationTests.java | 8 +- .../http/EurekaServerMockApplication.java | 3 +- .../http/RestClientEurekaHttpClientTests.java | 68 +++++ ...RestClientTransportClientFactoryTests.java | 67 +++++ ...stTemplateTransportClientFactoryTests.java | 28 ++- 24 files changed, 1379 insertions(+), 322 deletions(-) create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/EurekaHttpClientUtils.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/NotFoundHttpResponse.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientDiscoveryClientOptionalArgs.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClient.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactories.java create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactory.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationClientTests.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientIntegrationTests.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientTests.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClientTests.java create mode 100644 spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactoryTests.java diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 0fd0485c8..336bba140 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -270,9 +270,11 @@ It is initialized in a `SmartLifecycle` (with `phase=0`), so the earliest you ca ==== Underlying HTTP clients -`EurekaClient` uses either `RestTemplate`, `WebClient` or `JerseyClient` under the hood. In order to use the `EurekaClient`, you need to have one of the supported HTTP clients on your classpath. +`EurekaClient` uses either `RestClient`, `RestTemplate`, `WebClient` or `JerseyClient` under the hood. In order to use the `EurekaClient`, you need to have one of the supported HTTP clients on your classpath. -To use `RestTemplate`, add `spring-boot-starter-web` to your dependencies. To use `WebClient`, add `spring-boot-starter-webflux` to your dependencies. If both `RestTemplate` and `WebClient` are on the classpath when `eureka.client.webclient.enabled` is set to `true`, `WebClient` is used. Otherwise, `RestTemplate` is used. +To use `RestTemplate` or `RestClient`, add `spring-boot-starter-web` to your dependencies. To use `WebClient`, add `spring-boot-starter-webflux` to your dependencies. If both `spring-boot-starter-web` and `spring-boot-starter-webflux` are included in the dependencies and the `eureka.client.webclient.enabled` flag is set to `true`, then `WebClient` will be used. If that's not the case and `eureka.client.restclient.enabled` is set to false, the `RestTemplate` will be used. Otherwise, the `RestClient` will be used. + +NOTE: Starting from 4.2.0, the default client has changed to `RestClient`. If you wish to use Jersey instead, you need to add the Jersey dependencies to your classpath. The following example shows the dependencies you need to add: diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 407b82262..d686cc8c4 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -36,7 +36,7 @@ |eureka.client.on-demand-update-status-change | `+++true+++` | If set to true, local status updates via ApplicationInfoManager will trigger on-demand (but rate limited) register/updates to remote eureka servers. |eureka.client.order | `+++0+++` | Order of the discovery client used by `CompositeDiscoveryClient` for sorting available clients. |eureka.client.prefer-same-zone-eureka | `+++true+++` | Indicates whether or not this instance should try to use the eureka server in the same zone for latency and/or other reason. Ideally eureka clients are configured to talk to servers in the same zone The changes are effective at runtime at the next registry fetch cycle as specified by registryFetchIntervalSeconds -|eureka.client.property-resolver | | +|eureka.client.property-resolver | | |eureka.client.proxy-host | | Gets the proxy host to eureka server if any. |eureka.client.proxy-password | | Gets the proxy password if any. |eureka.client.proxy-port | | Gets the proxy port to eureka server if any. @@ -46,21 +46,22 @@ |eureka.client.register-with-eureka | `+++true+++` | Indicates whether or not this instance should register its information with eureka server for discovery by others. In some cases, you do not want your instances to be discovered whereas you just want do discover other instances. |eureka.client.registry-fetch-interval-seconds | `+++30+++` | Indicates how often(in seconds) to fetch the registry information from the eureka server. |eureka.client.registry-refresh-single-vip-address | | Indicates whether the client is only interested in the registry information for a single VIP. -|eureka.client.rest-template-timeout.connect-request-timeout | `+++180000+++` | +|eureka.client.rest-template-timeout.connect-request-timeout | `+++180000+++` | |eureka.client.rest-template-timeout.connect-timeout | `+++180000+++` | Default values are set to 180000, in keeping with {@link RequestConfig} and {@link SocketConfig} defaults. -|eureka.client.rest-template-timeout.socket-timeout | `+++180000+++` | +|eureka.client.rest-template-timeout.socket-timeout | `+++180000+++` | |eureka.client.service-url | | Map of availability zone to list of fully qualified URLs to communicate with eureka server. Each value can be a single URL or a comma separated list of alternative locations. Typically the eureka server URLs carry protocol,host,port,context and version information if any. Example: https://ec2-256-156-243-129.compute-1.amazonaws.com:7001/eureka/ The changes are effective at runtime at the next service url refresh cycle as specified by eurekaServiceUrlPollIntervalSeconds. |eureka.client.should-enforce-registration-at-init | `+++false+++` | Indicates whether the client should enforce registration during initialization. Defaults to false. |eureka.client.should-unregister-on-shutdown | `+++true+++` | Indicates whether the client should explicitly unregister itself from the remote server on client shutdown. -|eureka.client.tls.enabled | | -|eureka.client.tls.key-password | | -|eureka.client.tls.key-store | | -|eureka.client.tls.key-store-password | | -|eureka.client.tls.key-store-type | | -|eureka.client.tls.trust-store | | -|eureka.client.tls.trust-store-password | | -|eureka.client.tls.trust-store-type | | +|eureka.client.tls.enabled | | +|eureka.client.tls.key-password | | +|eureka.client.tls.key-store | | +|eureka.client.tls.key-store-password | | +|eureka.client.tls.key-store-type | | +|eureka.client.tls.trust-store | | +|eureka.client.tls.trust-store-password | | +|eureka.client.tls.trust-store-type | | |eureka.client.use-dns-for-fetching-service-urls | `+++false+++` | Indicates whether the eureka client should use the DNS mechanism to fetch a list of eureka servers to talk to. When the DNS name is updated to have additional servers, that information is used immediately after the eureka client polls for that information as specified in eurekaServiceUrlPollIntervalSeconds. Alternatively, the service urls can be returned serviceUrls, but the users should implement their own mechanism to return the updated list in case of changes. The changes are effective at runtime. +|eureka.client.restclient.enabled | `+++false+++` | Enables the use of RestClient for Eureka HTTP Client. |eureka.client.webclient.enabled | `+++false+++` | Enables the use of WebClient for Eureka HTTP Client. |eureka.dashboard.enabled | `+++true+++` | Flag to enable the Eureka dashboard. Default true. |eureka.dashboard.path | `+++/+++` | The path to the Eureka dashboard (relative to the servlet path). Defaults to "/". @@ -71,8 +72,8 @@ |eureka.instance.appname | `+++unknown+++` | Get the name of the application to be registered with eureka. |eureka.instance.async-client-initialization | `+++false+++` | If true the EurekaClient will be initialized asynchronously when the InstanceRegistry bean is created. |eureka.instance.data-center-info | | Returns the data center this instance is deployed. This information is used to get some AWS specific instance information if the instance is deployed in AWS. -|eureka.instance.default-address-resolution-order | `+++[]+++` | -|eureka.instance.environment | | +|eureka.instance.default-address-resolution-order | `+++[]+++` | +|eureka.instance.environment | | |eureka.instance.health-check-url | | Gets the absolute health check page URL for this instance. The users can provide the healthCheckUrlPath if the health check page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence.

It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. |eureka.instance.health-check-url-path | | Gets the relative health check URL path for this instance. The health check page URL is then constructed out of the hostname and the type of communication - secure or unsecure as specified in securePort and nonSecurePort. It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. |eureka.instance.home-page-url | | Gets the absolute home page URL for this instance. The users can provide the homePageUrlPath if the home page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence. It is normally used for informational purposes for other services to use it as a landing page. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. @@ -91,7 +92,7 @@ |eureka.instance.non-secure-port-enabled | `+++true+++` | Indicates whether the non-secure port should be enabled for traffic or not. |eureka.instance.prefer-ip-address | `+++false+++` | Flag to say that, when guessing a hostname, the IP address of the server should be used in preference to the hostname reported by the OS. |eureka.instance.registry.default-open-for-traffic-count | `+++1+++` | Value used in determining when leases are cancelled, default to 1 for standalone. Should be set to 0 for peer replicated eurekas -|eureka.instance.registry.expected-number-of-clients-sending-renews | `+++1+++` | +|eureka.instance.registry.expected-number-of-clients-sending-renews | `+++1+++` | |eureka.instance.secure-health-check-url | | Gets the absolute secure health check page URL for this instance. The users can provide the secureHealthCheckUrl if the health check page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence.

It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. |eureka.instance.secure-port | `+++443+++` | Get the Secure port on which the instance should receive traffic. |eureka.instance.secure-port-enabled | `+++false+++` | Indicates whether the secure port should be enabled for traffic or not. @@ -99,83 +100,83 @@ |eureka.instance.status-page-url | | Gets the absolute status page URL path for this instance. The users can provide the statusPageUrlPath if the status page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence. It is normally used for informational purposes for other services to find about the status of this instance. Users can provide a simple HTML indicating what is the current status of the instance. |eureka.instance.status-page-url-path | | Gets the relative status page URL path for this instance. The status page URL is then constructed out of the hostName and the type of communication - secure or unsecure as specified in securePort and nonSecurePort. It is normally used for informational purposes for other services to find about the status of this instance. Users can provide a simple HTML indicating what is the current status of the instance. |eureka.instance.virtual-host-name | `+++unknown+++` | Gets the virtual host name defined for this instance. This is typically the way other instance would find this instance by using the virtual host name.Think of this as similar to the fully qualified domain name, that the users of your services will need to find this instance. -|eureka.server.a-s-g-cache-expiry-timeout-ms | `+++600000+++` | -|eureka.server.a-s-g-query-timeout-ms | `+++300+++` | -|eureka.server.a-s-g-update-interval-ms | `+++300000+++` | -|eureka.server.a-w-s-access-id | | -|eureka.server.a-w-s-secret-key | | -|eureka.server.batch-replication | `+++false+++` | -|eureka.server.binding-strategy | `+++eip+++` | -|eureka.server.delta-retention-timer-interval-in-ms | `+++30000+++` | -|eureka.server.disable-delta | `+++false+++` | -|eureka.server.disable-delta-for-remote-regions | `+++false+++` | -|eureka.server.disable-transparent-fallback-to-other-region | `+++false+++` | -|eureka.server.e-i-p-bind-rebind-retries | `+++3+++` | -|eureka.server.e-i-p-binding-retry-interval-ms | `+++300000+++` | -|eureka.server.e-i-p-binding-retry-interval-ms-when-unbound | `+++60000+++` | -|eureka.server.enable-replicated-request-compression | `+++false+++` | -|eureka.server.enable-self-preservation | `+++true+++` | -|eureka.server.eviction-interval-timer-in-ms | `+++0+++` | -|eureka.server.expected-client-renewal-interval-seconds | `+++30+++` | -|eureka.server.g-zip-content-from-remote-region | `+++true+++` | -|eureka.server.initial-capacity-of-response-cache | `+++1000+++` | -|eureka.server.json-codec-name | | -|eureka.server.list-auto-scaling-groups-role-name | `+++ListAutoScalingGroups+++` | -|eureka.server.log-identity-headers | `+++true+++` | -|eureka.server.max-elements-in-peer-replication-pool | `+++10000+++` | -|eureka.server.max-elements-in-status-replication-pool | `+++10000+++` | -|eureka.server.max-idle-thread-age-in-minutes-for-peer-replication | `+++15+++` | -|eureka.server.max-idle-thread-in-minutes-age-for-status-replication | `+++10+++` | -|eureka.server.max-threads-for-peer-replication | `+++20+++` | -|eureka.server.max-threads-for-status-replication | `+++1+++` | -|eureka.server.max-time-for-replication | `+++30000+++` | +|eureka.server.a-s-g-cache-expiry-timeout-ms | `+++600000+++` | +|eureka.server.a-s-g-query-timeout-ms | `+++300+++` | +|eureka.server.a-s-g-update-interval-ms | `+++300000+++` | +|eureka.server.a-w-s-access-id | | +|eureka.server.a-w-s-secret-key | | +|eureka.server.batch-replication | `+++false+++` | +|eureka.server.binding-strategy | `+++eip+++` | +|eureka.server.delta-retention-timer-interval-in-ms | `+++30000+++` | +|eureka.server.disable-delta | `+++false+++` | +|eureka.server.disable-delta-for-remote-regions | `+++false+++` | +|eureka.server.disable-transparent-fallback-to-other-region | `+++false+++` | +|eureka.server.e-i-p-bind-rebind-retries | `+++3+++` | +|eureka.server.e-i-p-binding-retry-interval-ms | `+++300000+++` | +|eureka.server.e-i-p-binding-retry-interval-ms-when-unbound | `+++60000+++` | +|eureka.server.enable-replicated-request-compression | `+++false+++` | +|eureka.server.enable-self-preservation | `+++true+++` | +|eureka.server.eviction-interval-timer-in-ms | `+++0+++` | +|eureka.server.expected-client-renewal-interval-seconds | `+++30+++` | +|eureka.server.g-zip-content-from-remote-region | `+++true+++` | +|eureka.server.initial-capacity-of-response-cache | `+++1000+++` | +|eureka.server.json-codec-name | | +|eureka.server.list-auto-scaling-groups-role-name | `+++ListAutoScalingGroups+++` | +|eureka.server.log-identity-headers | `+++true+++` | +|eureka.server.max-elements-in-peer-replication-pool | `+++10000+++` | +|eureka.server.max-elements-in-status-replication-pool | `+++10000+++` | +|eureka.server.max-idle-thread-age-in-minutes-for-peer-replication | `+++15+++` | +|eureka.server.max-idle-thread-in-minutes-age-for-status-replication | `+++10+++` | +|eureka.server.max-threads-for-peer-replication | `+++20+++` | +|eureka.server.max-threads-for-status-replication | `+++1+++` | +|eureka.server.max-time-for-replication | `+++30000+++` | |eureka.server.metrics.enabled | `+++false+++` | Indicates whether the metrics should be enabled for eureka instances. -|eureka.server.min-available-instances-for-peer-replication | `+++-1+++` | -|eureka.server.min-threads-for-peer-replication | `+++5+++` | -|eureka.server.min-threads-for-status-replication | `+++1+++` | -|eureka.server.my-url | | -|eureka.server.number-of-replication-retries | `+++5+++` | -|eureka.server.peer-eureka-nodes-update-interval-ms | `+++600000+++` | -|eureka.server.peer-eureka-status-refresh-time-interval-ms | `+++0+++` | -|eureka.server.peer-node-connect-timeout-ms | `+++200+++` | -|eureka.server.peer-node-connection-idle-timeout-seconds | `+++30+++` | -|eureka.server.peer-node-read-timeout-ms | `+++200+++` | -|eureka.server.peer-node-total-connections | `+++1000+++` | -|eureka.server.peer-node-total-connections-per-host | `+++500+++` | -|eureka.server.prime-aws-replica-connections | `+++true+++` | -|eureka.server.property-resolver | | -|eureka.server.rate-limiter-burst-size | `+++10+++` | -|eureka.server.rate-limiter-enabled | `+++false+++` | -|eureka.server.rate-limiter-full-fetch-average-rate | `+++100+++` | -|eureka.server.rate-limiter-privileged-clients | | -|eureka.server.rate-limiter-registry-fetch-average-rate | `+++500+++` | -|eureka.server.rate-limiter-throttle-standard-clients | `+++false+++` | -|eureka.server.registry-sync-retries | `+++0+++` | -|eureka.server.registry-sync-retry-wait-ms | `+++0+++` | -|eureka.server.remote-region-app-whitelist | | -|eureka.server.remote-region-connect-timeout-ms | `+++1000+++` | -|eureka.server.remote-region-connection-idle-timeout-seconds | `+++30+++` | -|eureka.server.remote-region-fetch-thread-pool-size | `+++20+++` | -|eureka.server.remote-region-read-timeout-ms | `+++1000+++` | -|eureka.server.remote-region-registry-fetch-interval | `+++30+++` | -|eureka.server.remote-region-total-connections | `+++1000+++` | -|eureka.server.remote-region-total-connections-per-host | `+++500+++` | -|eureka.server.remote-region-trust-store | | -|eureka.server.remote-region-trust-store-password | `+++changeit+++` | -|eureka.server.remote-region-urls | | -|eureka.server.remote-region-urls-with-name | | -|eureka.server.renewal-percent-threshold | `+++0.85+++` | -|eureka.server.renewal-threshold-update-interval-ms | `+++900000+++` | -|eureka.server.response-cache-auto-expiration-in-seconds | `+++180+++` | -|eureka.server.response-cache-update-interval-ms | `+++30000+++` | -|eureka.server.retention-time-in-m-s-in-delta-queue | `+++180000+++` | -|eureka.server.route53-bind-rebind-retries | `+++3+++` | -|eureka.server.route53-binding-retry-interval-ms | `+++300000+++` | -|eureka.server.route53-domain-t-t-l | `+++30+++` | -|eureka.server.sync-when-timestamp-differs | `+++true+++` | -|eureka.server.use-read-only-response-cache | `+++true+++` | -|eureka.server.wait-time-in-ms-when-sync-empty | `+++300000+++` | -|eureka.server.xml-codec-name | | +|eureka.server.min-available-instances-for-peer-replication | `+++-1+++` | +|eureka.server.min-threads-for-peer-replication | `+++5+++` | +|eureka.server.min-threads-for-status-replication | `+++1+++` | +|eureka.server.my-url | | +|eureka.server.number-of-replication-retries | `+++5+++` | +|eureka.server.peer-eureka-nodes-update-interval-ms | `+++600000+++` | +|eureka.server.peer-eureka-status-refresh-time-interval-ms | `+++0+++` | +|eureka.server.peer-node-connect-timeout-ms | `+++200+++` | +|eureka.server.peer-node-connection-idle-timeout-seconds | `+++30+++` | +|eureka.server.peer-node-read-timeout-ms | `+++200+++` | +|eureka.server.peer-node-total-connections | `+++1000+++` | +|eureka.server.peer-node-total-connections-per-host | `+++500+++` | +|eureka.server.prime-aws-replica-connections | `+++true+++` | +|eureka.server.property-resolver | | +|eureka.server.rate-limiter-burst-size | `+++10+++` | +|eureka.server.rate-limiter-enabled | `+++false+++` | +|eureka.server.rate-limiter-full-fetch-average-rate | `+++100+++` | +|eureka.server.rate-limiter-privileged-clients | | +|eureka.server.rate-limiter-registry-fetch-average-rate | `+++500+++` | +|eureka.server.rate-limiter-throttle-standard-clients | `+++false+++` | +|eureka.server.registry-sync-retries | `+++0+++` | +|eureka.server.registry-sync-retry-wait-ms | `+++0+++` | +|eureka.server.remote-region-app-whitelist | | +|eureka.server.remote-region-connect-timeout-ms | `+++1000+++` | +|eureka.server.remote-region-connection-idle-timeout-seconds | `+++30+++` | +|eureka.server.remote-region-fetch-thread-pool-size | `+++20+++` | +|eureka.server.remote-region-read-timeout-ms | `+++1000+++` | +|eureka.server.remote-region-registry-fetch-interval | `+++30+++` | +|eureka.server.remote-region-total-connections | `+++1000+++` | +|eureka.server.remote-region-total-connections-per-host | `+++500+++` | +|eureka.server.remote-region-trust-store | | +|eureka.server.remote-region-trust-store-password | `+++changeit+++` | +|eureka.server.remote-region-urls | | +|eureka.server.remote-region-urls-with-name | | +|eureka.server.renewal-percent-threshold | `+++0.85+++` | +|eureka.server.renewal-threshold-update-interval-ms | `+++900000+++` | +|eureka.server.response-cache-auto-expiration-in-seconds | `+++180+++` | +|eureka.server.response-cache-update-interval-ms | `+++30000+++` | +|eureka.server.retention-time-in-m-s-in-delta-queue | `+++180000+++` | +|eureka.server.route53-bind-rebind-retries | `+++3+++` | +|eureka.server.route53-binding-retry-interval-ms | `+++300000+++` | +|eureka.server.route53-domain-t-t-l | `+++30+++` | +|eureka.server.sync-when-timestamp-differs | `+++true+++` | +|eureka.server.use-read-only-response-cache | `+++true+++` | +|eureka.server.wait-time-in-ms-when-sync-empty | `+++300000+++` | +|eureka.server.xml-codec-name | | |spring.cloud.compatibility-verifier.compatible-boot-versions | `+++3.4.x+++` | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.4.x} |spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification. |spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. @@ -184,24 +185,24 @@ |spring.cloud.config.override-system-properties | `+++true+++` | Flag to indicate that the external properties should override system properties. Default true. |spring.cloud.decrypt-environment-post-processor.enabled | `+++true+++` | Enable the DecryptEnvironmentPostProcessor. |spring.cloud.discovery.client.composite-indicator.enabled | `+++true+++` | Enables discovery client composite health indicator. -|spring.cloud.discovery.client.health-indicator.enabled | `+++true+++` | -|spring.cloud.discovery.client.health-indicator.include-description | `+++false+++` | +|spring.cloud.discovery.client.health-indicator.enabled | `+++true+++` | +|spring.cloud.discovery.client.health-indicator.include-description | `+++false+++` | |spring.cloud.discovery.client.health-indicator.use-services-query | `+++true+++` | Whether or not the indicator should use {@link DiscoveryClient#getServices} to check its health. When set to {@code false} the indicator instead uses the lighter {@link DiscoveryClient#probe()}. This can be helpful in large deployments where the number of services returned makes the operation unnecessarily heavy. -|spring.cloud.discovery.client.simple.instances | | -|spring.cloud.discovery.client.simple.local.host | | -|spring.cloud.discovery.client.simple.local.instance-id | | -|spring.cloud.discovery.client.simple.local.metadata | | -|spring.cloud.discovery.client.simple.local.port | `+++0+++` | -|spring.cloud.discovery.client.simple.local.secure | `+++false+++` | -|spring.cloud.discovery.client.simple.local.service-id | | -|spring.cloud.discovery.client.simple.local.uri | | -|spring.cloud.discovery.client.simple.order | | +|spring.cloud.discovery.client.simple.instances | | +|spring.cloud.discovery.client.simple.local.host | | +|spring.cloud.discovery.client.simple.local.instance-id | | +|spring.cloud.discovery.client.simple.local.metadata | | +|spring.cloud.discovery.client.simple.local.port | `+++0+++` | +|spring.cloud.discovery.client.simple.local.secure | `+++false+++` | +|spring.cloud.discovery.client.simple.local.service-id | | +|spring.cloud.discovery.client.simple.local.uri | | +|spring.cloud.discovery.client.simple.order | | |spring.cloud.discovery.enabled | `+++true+++` | Enables discovery client health indicators. |spring.cloud.features.enabled | `+++true+++` | Enables the features endpoint. |spring.cloud.httpclientfactories.apache.enabled | `+++true+++` | Enables creation of Apache Http Client factory beans. |spring.cloud.httpclientfactories.ok.enabled | `+++true+++` | Enables creation of OK Http Client factory beans. -|spring.cloud.hypermedia.refresh.fixed-delay | `+++5000+++` | -|spring.cloud.hypermedia.refresh.initial-delay | `+++10000+++` | +|spring.cloud.hypermedia.refresh.fixed-delay | `+++5000+++` | +|spring.cloud.hypermedia.refresh.initial-delay | `+++10000+++` | |spring.cloud.inetutils.default-hostname | `+++localhost+++` | The default hostname. Used in case of errors. |spring.cloud.inetutils.default-ip-address | `+++127.0.0.1+++` | The default IP address. Used in case of errors. |spring.cloud.inetutils.ignored-interfaces | | List of Java regular expressions for network interfaces that will be ignored. @@ -213,7 +214,7 @@ |spring.cloud.loadbalancer.cache.enabled | `+++true+++` | Enables Spring Cloud LoadBalancer caching mechanism. |spring.cloud.loadbalancer.cache.ttl | `+++35s+++` | Time To Live - time counted from writing of the record, after which cache entries are expired, expressed as a {@link Duration}. The property {@link String} has to be in keeping with the appropriate syntax as specified in Spring Boot StringToDurationConverter. @see StringToDurationConverter.java |spring.cloud.loadbalancer.call-get-with-request-on-delegates | `+++true+++` | If this flag is set to {@code true}, {@code ServiceInstanceListSupplier#get(Request request)} method will be implemented to call {@code delegate.get(request)} in classes assignable from {@code DelegatingServiceInstanceListSupplier} that don't already implement that method, with the exclusion of {@code CachingServiceInstanceListSupplier} and {@code HealthCheckServiceInstanceListSupplier}, which should be placed in the instance supplier hierarchy directly after the supplier performing instance retrieval over the network, before any request-based filtering is done, {@code true} by default. -|spring.cloud.loadbalancer.clients | | +|spring.cloud.loadbalancer.clients | | |spring.cloud.loadbalancer.configurations | `+++default+++` | Enables a predefined LoadBalancer configuration. |spring.cloud.loadbalancer.eager-load.clients | | Names of the clients. |spring.cloud.loadbalancer.enabled | `+++true+++` | Enables Spring Cloud LoadBalancer. @@ -259,4 +260,4 @@ |spring.cloud.service-registry.auto-registration.register-management | `+++true+++` | Whether to register the management as a service. Defaults to true. |spring.cloud.util.enabled | `+++true+++` | Enables creation of Spring Cloud utility beans. -|=== \ No newline at end of file +|=== diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java index e62e618d1..a30458288 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java @@ -25,7 +25,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -42,6 +41,8 @@ import org.springframework.cloud.netflix.eureka.RestTemplateTimeoutProperties; import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.EurekaClientHttpRequestFactorySupplier; +import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs; +import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactories; import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactories; import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs; @@ -49,12 +50,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; /** * @author Daniel Lavoie * @author Armin Krezovic * @author Olga Maciaszek-Sharma + * @author Wonchul Heo */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(RestTemplateTimeoutProperties.class) @@ -70,14 +73,14 @@ public TlsProperties tlsProperties() { @Bean @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") - @Conditional(JerseyClientNotPresentOrNotEnabledCondition.class) + @Conditional(RestTemplateEnabledCondition.class) @ConditionalOnMissingBean(value = { AbstractDiscoveryClientOptionalArgs.class }, search = SearchStrategy.CURRENT) - @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, - havingValue = "false") public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs(TlsProperties tlsProperties, EurekaClientHttpRequestFactorySupplier eurekaClientHttpRequestFactorySupplier, ObjectProvider restTemplateBuilders) throws GeneralSecurityException, IOException { - logger.info("Eureka HTTP Client uses RestTemplate."); + if (logger.isInfoEnabled()) { + logger.info("Eureka HTTP Client uses RestTemplate."); + } RestTemplateDiscoveryClientOptionalArgs result = new RestTemplateDiscoveryClientOptionalArgs( eurekaClientHttpRequestFactorySupplier, restTemplateBuilders::getIfAvailable); setupTLS(result, tlsProperties); @@ -86,10 +89,8 @@ public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOption @Bean @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") - @Conditional(JerseyClientNotPresentOrNotEnabledCondition.class) + @Conditional(RestTemplateEnabledCondition.class) @ConditionalOnMissingBean(value = { TransportClientFactories.class }, search = SearchStrategy.CURRENT) - @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, - havingValue = "false") public RestTemplateTransportClientFactories restTemplateTransportClientFactories( RestTemplateDiscoveryClientOptionalArgs optionalArgs) { return new RestTemplateTransportClientFactories(optionalArgs); @@ -118,7 +119,9 @@ static class DiscoveryClientOptionalArgsTlsConfiguration { DiscoveryClientOptionalArgsTlsConfiguration(TlsProperties tlsProperties, AbstractDiscoveryClientOptionalArgs optionalArgs) throws GeneralSecurityException, IOException { - logger.info("Eureka HTTP Client uses Jersey"); + if (logger.isInfoEnabled()) { + logger.info("Eureka HTTP Client uses Jersey"); + } setupTLS(optionalArgs, tlsProperties); } @@ -129,16 +132,15 @@ static class DiscoveryClientOptionalArgsTlsConfiguration { @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", havingValue = "true") protected static class WebClientConfiguration { - @Autowired - private TlsProperties tlsProperties; - @Bean @ConditionalOnMissingBean( value = { AbstractDiscoveryClientOptionalArgs.class, RestTemplateDiscoveryClientOptionalArgs.class }, search = SearchStrategy.CURRENT) - public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs( + public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs(TlsProperties tlsProperties, ObjectProvider builder) throws GeneralSecurityException, IOException { - logger.info("Eureka HTTP Client uses WebClient."); + if (logger.isInfoEnabled()) { + logger.info("Eureka HTTP Client uses WebClient."); + } WebClientDiscoveryClientOptionalArgs result = new WebClientDiscoveryClientOptionalArgs( builder::getIfAvailable); setupTLS(result, tlsProperties); @@ -168,6 +170,33 @@ public WebClientNotFoundConfiguration() { } + @ConditionalOnClass(name = "org.springframework.web.client.RestClient") + @Conditional(RestClientEnabledCondition.class) + protected static class RestClientConfiguration { + + @Bean + @ConditionalOnMissingBean(value = { AbstractDiscoveryClientOptionalArgs.class }, + search = SearchStrategy.CURRENT) + public RestClientDiscoveryClientOptionalArgs restClientDiscoveryClientOptionalArgs(TlsProperties tlsProperties, + ObjectProvider builder) throws GeneralSecurityException, IOException { + if (logger.isInfoEnabled()) { + logger.info("Eureka HTTP Client uses RestClient."); + } + RestClientDiscoveryClientOptionalArgs result = new RestClientDiscoveryClientOptionalArgs( + builder::getIfAvailable); + setupTLS(result, tlsProperties); + return result; + } + + @Bean + @ConditionalOnMissingBean(value = TransportClientFactories.class, search = SearchStrategy.CURRENT) + public RestClientTransportClientFactories restClientTransportClientFactories( + ObjectProvider builder) { + return new RestClientTransportClientFactories(builder::getIfAvailable); + } + + } + static class JerseyClientPresentAndEnabledCondition extends AllNestedConditions { JerseyClientPresentAndEnabledCondition() { @@ -204,4 +233,53 @@ static class OnJerseyClientDisabled { } + static class RestTemplateEnabledCondition extends AllNestedConditions { + + RestTemplateEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @Conditional(JerseyClientNotPresentOrNotEnabledCondition.class) + static class OnJerseyClientNotPresentOrNotEnabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, + havingValue = "false") + static class OnWebClientDisabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "restclient.enabled", havingValue = "false") + static class OnRestClientDisabled { + + } + + } + + static class RestClientEnabledCondition extends AllNestedConditions { + + RestClientEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @Conditional(JerseyClientNotPresentOrNotEnabledCondition.class) + static class OnJerseyClientNotPresentOrNotEnabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, + havingValue = "false") + static class OnWebClientDisabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "restclient.enabled", matchIfMissing = true, + havingValue = "true") + static class OnRestClientDisabled { + + } + + } + } diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java index 32f54e527..24df0e986 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -37,6 +38,8 @@ import org.springframework.cloud.netflix.eureka.RestTemplateTimeoutProperties; import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.EurekaClientHttpRequestFactorySupplier; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.RestClientTransportClientFactory; import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory; import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient; @@ -46,6 +49,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.lang.Nullable; +import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; /** @@ -54,6 +58,7 @@ * * @author Dave Syer * @author Armin Krezovic + * @author Wonchul Heo */ @ConditionalOnClass(ConfigServicePropertySourceLocator.class) @Conditional(EurekaConfigServerBootstrapConfiguration.EurekaConfigServerBootstrapCondition.class) @@ -69,8 +74,7 @@ public EurekaClientConfigBean eurekaClientConfigBean() { @Bean @ConditionalOnMissingBean(EurekaHttpClient.class) - @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, - havingValue = "false") + @Conditional(RestTemplateEnabledCondition.class) public RestTemplateEurekaHttpClient configDiscoveryRestTemplateEurekaHttpClient(EurekaClientConfigBean config, Environment env, @Nullable TlsProperties properties, EurekaClientHttpRequestFactorySupplier eurekaClientHttpRequestFactorySupplier, @@ -109,6 +113,61 @@ public WebClientEurekaHttpClient configDiscoveryWebClientEurekaHttpClient(Eureka } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "org.springframework.web.client.RestClient") + @Conditional(RestClientEnabledCondition.class) + @ImportAutoConfiguration(RestClientAutoConfiguration.class) + protected static class RestClientConfiguration { + + @Bean + @ConditionalOnMissingBean(EurekaHttpClient.class) + public RestClientEurekaHttpClient configDiscoveryRestClientEurekaHttpClient(EurekaClientConfigBean config, + ObjectProvider builder, Environment env) { + return (RestClientEurekaHttpClient) new RestClientTransportClientFactory(builder::getIfAvailable) + .newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, env)); + } + + } + + static class RestTemplateEnabledCondition extends AllNestedConditions { + + RestTemplateEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, + havingValue = "false") + static class OnWebClientDisabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "restclient.enabled", havingValue = "false") + static class OnRestClientDisabled { + + } + + } + + static class RestClientEnabledCondition extends AllNestedConditions { + + RestClientEnabledCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled", matchIfMissing = true, + havingValue = "false") + static class OnWebClientDisabled { + + } + + @ConditionalOnProperty(prefix = "eureka.client", name = "restclient.enabled", matchIfMissing = true, + havingValue = "true") + static class OnRestClientDisabled { + + } + + } + static class EurekaConfigServerBootstrapCondition extends AllNestedConditions { EurekaConfigServerBootstrapCondition() { diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/EurekaHttpClientUtils.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/EurekaHttpClientUtils.java new file mode 100644 index 000000000..a45213c30 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/EurekaHttpClientUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.net.URI; +import java.net.URISyntaxException; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.converters.jackson.mixin.ApplicationsJsonMixIn; +import com.netflix.discovery.converters.jackson.mixin.InstanceInfoJsonMixIn; +import com.netflix.discovery.converters.jackson.serializer.InstanceInfoJsonBeanSerializer; +import com.netflix.discovery.shared.Applications; +import com.netflix.discovery.shared.transport.EurekaHttpClient; + +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.lang.Nullable; + +/** + * Utility class for dealing with {@link EurekaHttpClient}. + * + * @author Daniel Lavoie + * @author Wonchul Heo + * @since 4.2.0 + */ +final class EurekaHttpClientUtils { + + private EurekaHttpClientUtils() { + throw new AssertionError("Must not instantiate constant utility class"); + } + + /** + * Provides the serialization configurations required by the Eureka Server. JSON + * content exchanged with eureka requires a root node matching the entity being + * serialized or deserialized. Achieved with + * {@link SerializationFeature#WRAP_ROOT_VALUE} and + * {@link DeserializationFeature#UNWRAP_ROOT_VALUE}. + * {@link PropertyNamingStrategies.SnakeCaseStrategy} is applied to the underlying + * {@link ObjectMapper}. + * @return a {@link MappingJackson2HttpMessageConverter} object + */ + static MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { + final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper()); + return converter; + } + + /** + * Provides the serialization configurations required by the Eureka Server. JSON + * content exchanged with eureka requires a root node matching the entity being + * serialized or deserialized. Achieved with + * {@link SerializationFeature#WRAP_ROOT_VALUE} and + * {@link DeserializationFeature#UNWRAP_ROOT_VALUE}. + * {@link PropertyNamingStrategies.SnakeCaseStrategy} is applied to the underlying + * {@link ObjectMapper}. + * @return a {@link ObjectMapper} object + */ + static ObjectMapper objectMapper() { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + + final SimpleModule jsonModule = new SimpleModule(); + jsonModule.setSerializerModifier(createJsonSerializerModifier()); + objectMapper.registerModule(jsonModule); + + objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true); + objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); + objectMapper.addMixIn(Applications.class, ApplicationsJsonMixIn.class); + objectMapper.addMixIn(InstanceInfo.class, InstanceInfoJsonMixIn.class); + + return objectMapper; + } + + private static BeanSerializerModifier createJsonSerializerModifier() { + return new BeanSerializerModifier() { + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, + JsonSerializer serializer) { + if (beanDesc.getBeanClass().isAssignableFrom(InstanceInfo.class)) { + return new InstanceInfoJsonBeanSerializer((BeanSerializerBase) serializer, false); + } + return serializer; + } + }; + } + + @Nullable + static UserInfo extractUserInfo(String serviceUrl) { + try { + final URI serviceURI = new URI(serviceUrl); + if (serviceURI.getUserInfo() != null) { + final String[] credentials = serviceURI.getUserInfo().split(":"); + if (credentials.length == 2) { + return new UserInfo(credentials[0], credentials[1]); + } + } + } + catch (URISyntaxException ignore) { + } + return null; + } + + record UserInfo(String username, String password) { + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/NotFoundHttpResponse.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/NotFoundHttpResponse.java new file mode 100644 index 000000000..4d650e8fc --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/NotFoundHttpResponse.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Response that ignores body, specifically for 404 errors. + * + * @author Spencer Gibb + * @author Wonchul Heo + * @since 4.2.0 + */ +class NotFoundHttpResponse implements ClientHttpResponse { + + private final ClientHttpResponse response; + + NotFoundHttpResponse(ClientHttpResponse response) { + this.response = response; + } + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return response.getStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return response.getStatusText(); + } + + @Override + public void close() { + response.close(); + } + + @Override + public InputStream getBody() throws IOException { + // ignore body on 404 for heartbeat, see gh-4145 + return null; + } + + @Override + public HttpHeaders getHeaders() { + return response.getHeaders(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientDiscoveryClientOptionalArgs.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientDiscoveryClientOptionalArgs.java new file mode 100644 index 000000000..93f94a18d --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientDiscoveryClientOptionalArgs.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.util.function.Supplier; + +import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; +import jakarta.ws.rs.client.ClientRequestFilter; + +import org.springframework.web.client.RestClient; + +/** + * {@link RestClient} implementation of DiscoveryClientOptionalArgs that supports + * supplying {@link ClientRequestFilter}. + * + * @author Wonchul Heo + * @since 4.2.0 + */ +public class RestClientDiscoveryClientOptionalArgs extends AbstractDiscoveryClientOptionalArgs { + + public RestClientDiscoveryClientOptionalArgs(Supplier builder) { + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClient.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClient.java new file mode 100644 index 000000000..b446b3246 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClient.java @@ -0,0 +1,238 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.InstanceStatus; +import com.netflix.discovery.shared.Application; +import com.netflix.discovery.shared.Applications; +import com.netflix.discovery.shared.transport.EurekaHttpClient; +import com.netflix.discovery.shared.transport.EurekaHttpResponse; +import com.netflix.discovery.shared.transport.EurekaHttpResponse.EurekaHttpResponseBuilder; +import com.netflix.discovery.util.StringUtil; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriBuilder; + +import static com.netflix.discovery.shared.transport.EurekaHttpResponse.anEurekaHttpResponse; + +/** + * {@link RestClient} implementation of {@link EurekaHttpClient}. + * + * @author Wonchul Heo + * @since 4.2.0 + */ +public class RestClientEurekaHttpClient implements EurekaHttpClient { + + private final RestClient restClient; + + public RestClientEurekaHttpClient(RestClient restClient) { + this.restClient = restClient; + } + + @Override + public EurekaHttpResponse register(InstanceInfo info) { + final ResponseEntity response = restClient.post() + .uri(builder -> builder.pathSegment("apps", info.getAppName()).build()) + .body(info) + .header(HttpHeaders.ACCEPT_ENCODING, "gzip") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toBodilessEntity(); + return eurekaHttpResponse(response); + } + + @Override + public EurekaHttpResponse cancel(String appName, String id) { + final ResponseEntity response = restClient.delete() + .uri(builder -> builder.pathSegment("apps", appName, id).build()) + .retrieve() + .toBodilessEntity(); + return eurekaHttpResponse(response); + } + + @Override + public EurekaHttpResponse sendHeartBeat(String appName, String id, InstanceInfo info, + InstanceStatus overriddenStatus) { + final Function uriFunction = builder -> builder.pathSegment("apps", appName, id) + .queryParam("status", info.getStatus().toString()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()) + .queryParamIfPresent("overriddenstatus", Optional.ofNullable(overriddenStatus).map(InstanceStatus::name)) + .build(); + + final ResponseEntity response = restClient.put() + .uri(uriFunction) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toEntity(InstanceInfo.class); + + final EurekaHttpResponseBuilder builder = anEurekaHttpResponse(statusCodeValueOf(response), + InstanceInfo.class) + .headers(headersOf(response)); + + final InstanceInfo entity = response.getBody(); + if (entity != null) { + builder.entity(entity); + } + + return builder.build(); + } + + @Override + public EurekaHttpResponse statusUpdate(String appName, String id, InstanceStatus newStatus, + InstanceInfo info) { + final Function uriFunction = builder -> builder.pathSegment("apps", appName, id, "status") + .queryParam("value", newStatus.name()) + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()) + .build(); + + final ResponseEntity response = restClient.put() + .uri(uriFunction) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toBodilessEntity(); + return eurekaHttpResponse(response); + } + + @Override + public EurekaHttpResponse deleteStatusOverride(String appName, String id, InstanceInfo info) { + final Function uriFunction = builder -> builder.pathSegment("apps", appName, id, "status") + .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString()) + .build(); + + final ResponseEntity response = restClient.delete() + .uri(uriFunction) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toBodilessEntity(); + return eurekaHttpResponse(response); + } + + @Override + public EurekaHttpResponse getApplications(String... regions) { + return getApplicationsInternal("/apps/", regions); + } + + @Override + public EurekaHttpResponse getDelta(String... regions) { + return getApplicationsInternal("/apps/delta", regions); + } + + @Override + public EurekaHttpResponse getVip(String vipAddress, String... regions) { + return getApplicationsInternal("/vips/" + vipAddress, regions); + } + + @Override + public EurekaHttpResponse getSecureVip(String secureVipAddress, String... regions) { + return getApplicationsInternal("/svips/" + secureVipAddress, regions); + } + + @Override + public EurekaHttpResponse getApplication(String appName) { + + final ResponseEntity response = restClient.get() + .uri("/apps/" + appName) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toEntity(Application.class); + + final int statusCode = statusCodeValueOf(response); + final Application body = response.getBody(); + + final Application application = statusCode == HttpStatus.OK.value() && body != null ? body : null; + + return anEurekaHttpResponse(statusCode, application).headers(headersOf(response)).build(); + } + + @Override + public EurekaHttpResponse getInstance(String appName, String id) { + return getInstanceInternal("/apps/" + appName + '/' + id); + } + + @Override + public EurekaHttpResponse getInstance(String id) { + return getInstanceInternal("/instances/" + id); + } + + @Override + public void shutdown() { + } + + public RestClient getRestClient() { + return restClient; + } + + private EurekaHttpResponse getApplicationsInternal(String urlPath, String[] regions) { + final Function uriFunction = builder -> builder + .queryParamIfPresent("regions", + Optional.ofNullable(regions).filter(it -> it.length > 0).map(StringUtil::join)) + .build(); + + final ResponseEntity response = restClient.get() + .uri(urlPath, uriFunction) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toEntity(Applications.class); + + final int statusCode = statusCodeValueOf(response); + final Applications body = response.getBody(); + + return anEurekaHttpResponse(statusCode, statusCode == HttpStatus.OK.value() && body != null ? body : null) + .headers(headersOf(response)) + .build(); + } + + private EurekaHttpResponse getInstanceInternal(String urlPath) { + final ResponseEntity response = restClient.get() + .uri(urlPath) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .retrieve() + .toEntity(InstanceInfo.class); + + final int statusCode = statusCodeValueOf(response); + final InstanceInfo body = response.getBody(); + + return anEurekaHttpResponse(statusCode, statusCode == HttpStatus.OK.value() && body != null ? body : null) + .headers(headersOf(response)) + .build(); + } + + private static Map headersOf(ResponseEntity response) { + return response.getHeaders().toSingleValueMap(); + } + + private static int statusCodeValueOf(ResponseEntity response) { + return response.getStatusCode().value(); + } + + private static EurekaHttpResponse eurekaHttpResponse(ResponseEntity response) { + return anEurekaHttpResponse(statusCodeValueOf(response)).headers(headersOf(response)).build(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactories.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactories.java new file mode 100644 index 000000000..d1596bf25 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactories.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.discovery.shared.transport.TransportClientFactory; +import com.netflix.discovery.shared.transport.jersey.TransportClientFactories; + +import org.springframework.web.client.RestClient; + +/** + * @author Wonchul Heo + * @since 4.2.0 + */ +public class RestClientTransportClientFactories implements TransportClientFactories { + + private final Supplier builder; + + public RestClientTransportClientFactories(Supplier builder) { + this.builder = builder; + } + + @Override + public TransportClientFactory newTransportClientFactory(EurekaClientConfig clientConfig, + Collection additionalFilters, InstanceInfo myInstanceInfo) { + return new RestClientTransportClientFactory(builder); + } + + @Override + public TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig, + final Collection additionalFilters, final InstanceInfo myInstanceInfo, + final Optional sslContext, final Optional hostnameVerifier) { + return new RestClientTransportClientFactory(builder); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactory.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactory.java new file mode 100644 index 000000000..9fac73a11 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactory.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import java.util.function.Supplier; + +import com.netflix.discovery.shared.resolver.EurekaEndpoint; +import com.netflix.discovery.shared.transport.EurekaHttpClient; +import com.netflix.discovery.shared.transport.TransportClientFactory; + +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.extractUserInfo; +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.mappingJacksonHttpMessageConverter; + +/** + * Provides the custom {@link RestClient} required by the + * {@link RestClientEurekaHttpClient}. Relies on Jackson for serialization and + * deserialization. + * + * @author Wonchul Heo + * @since 4.2.0 + */ +public class RestClientTransportClientFactory implements TransportClientFactory { + + private final Supplier builderSupplier; + + public RestClientTransportClientFactory(Supplier builderSupplier) { + this.builderSupplier = builderSupplier; + } + + @Override + public EurekaHttpClient newClient(EurekaEndpoint endpoint) { + // we want a copy to modify. Don't change the original + final RestClient.Builder builder = builderSupplier.get().clone(); + setUrl(builder, endpoint.getServiceUrl()); + builder.messageConverters(converters -> converters.add(0, mappingJacksonHttpMessageConverter())); + + builder.defaultStatusHandler(HttpStatusCode::isError, (req, res) -> { + }); + + builder.requestInterceptor((request, body, execution) -> { + final ClientHttpResponse response = execution.execute(request, body); + if (!response.getStatusCode().equals(HttpStatus.NOT_FOUND)) { + return response; + } + return new NotFoundHttpResponse(response); + }); + + return new RestClientEurekaHttpClient(builder.build()); + } + + private static void setUrl(RestClient.Builder builder, String serviceUrl) { + final String url = UriComponentsBuilder.fromUriString(serviceUrl).userInfo(null).toUriString(); + + final EurekaHttpClientUtils.UserInfo userInfo = extractUserInfo(serviceUrl); + if (userInfo != null) { + builder.requestInterceptor(new BasicAuthenticationInterceptor(userInfo.username(), userInfo.password())); + } + builder.baseUrl(url); + } + + @Override + public void shutdown() { + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactory.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactory.java index c30ef9a5f..f554781e1 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactory.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactory.java @@ -16,31 +16,12 @@ package org.springframework.cloud.netflix.eureka.http; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Optional; import java.util.function.Supplier; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.SerializationConfig; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; -import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.converters.jackson.mixin.ApplicationsJsonMixIn; -import com.netflix.discovery.converters.jackson.mixin.InstanceInfoJsonMixIn; -import com.netflix.discovery.converters.jackson.serializer.InstanceInfoJsonBeanSerializer; -import com.netflix.discovery.shared.Applications; import com.netflix.discovery.shared.resolver.EurekaEndpoint; import com.netflix.discovery.shared.transport.EurekaHttpClient; import com.netflix.discovery.shared.transport.TransportClientFactory; @@ -48,17 +29,18 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.cloud.configuration.TlsProperties; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.BasicAuthenticationInterceptor; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.extractUserInfo; +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.mappingJacksonHttpMessageConverter; + /** * Provides the custom {@link RestTemplate} required by the * {@link RestTemplateEurekaHttpClient}. Relies on Jackson for serialization and @@ -66,6 +48,7 @@ * * @author Daniel Lavoie * @author Armin Krezovic + * @author Wonchul Heo */ public class RestTemplateTransportClientFactory implements TransportClientFactory { @@ -149,18 +132,10 @@ private RestTemplate restTemplate(String serviceUrl) { restTemplate = new RestTemplate(requestFactory); } - try { - URI serviceURI = new URI(serviceUrl); - if (serviceURI.getUserInfo() != null) { - String[] credentials = serviceURI.getUserInfo().split(":"); - if (credentials.length == 2) { - restTemplate.getInterceptors() - .add(new BasicAuthenticationInterceptor(credentials[0], credentials[1])); - } - } - } - catch (URISyntaxException ignore) { - + final EurekaHttpClientUtils.UserInfo userInfo = extractUserInfo(serviceUrl); + if (userInfo != null) { + restTemplate.getInterceptors() + .add(new BasicAuthenticationInterceptor(userInfo.username(), userInfo.password())); } restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); @@ -177,88 +152,10 @@ private RestTemplate restTemplate(String serviceUrl) { return restTemplate; } - /** - * Provides the serialization configurations required by the Eureka Server. JSON - * content exchanged with eureka requires a root node matching the entity being - * serialized or deserialized. Achived with - * {@link SerializationFeature#WRAP_ROOT_VALUE} and - * {@link DeserializationFeature#UNWRAP_ROOT_VALUE}. - * {@link PropertyNamingStrategies.SnakeCaseStrategy} is applied to the underlying - * {@link ObjectMapper}. - * @return a {@link MappingJackson2HttpMessageConverter} object - */ - public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - converter.setObjectMapper(new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)); - - SimpleModule jsonModule = new SimpleModule(); - jsonModule.setSerializerModifier(createJsonSerializerModifier()); - converter.getObjectMapper().registerModule(jsonModule); - - converter.getObjectMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, true); - converter.getObjectMapper().configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); - converter.getObjectMapper().addMixIn(Applications.class, ApplicationsJsonMixIn.class); - converter.getObjectMapper().addMixIn(InstanceInfo.class, InstanceInfoJsonMixIn.class); - - return converter; - } - - public static BeanSerializerModifier createJsonSerializerModifier() { - return new BeanSerializerModifier() { - @Override - public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, - JsonSerializer serializer) { - if (beanDesc.getBeanClass().isAssignableFrom(InstanceInfo.class)) { - return new InstanceInfoJsonBeanSerializer((BeanSerializerBase) serializer, false); - } - return serializer; - } - }; - } - @Override public void shutdown() { } - /** - * Response that ignores body, specifically for 404 errors. - */ - private static class NotFoundHttpResponse implements ClientHttpResponse { - - private final ClientHttpResponse response; - - NotFoundHttpResponse(ClientHttpResponse response) { - this.response = response; - } - - @Override - public HttpStatusCode getStatusCode() throws IOException { - return response.getStatusCode(); - } - - @Override - public String getStatusText() throws IOException { - return response.getStatusText(); - } - - @Override - public void close() { - response.close(); - } - - @Override - public InputStream getBody() throws IOException { - // ignore body on 404 for heartbeat, see gh-4145 - return null; - } - - @Override - public HttpHeaders getHeaders() { - return response.getHeaders(); - } - - } - class ErrorHandler extends DefaultResponseErrorHandler { @Override diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientTransportClientFactory.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientTransportClientFactory.java index 180b0a276..db7362290 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientTransportClientFactory.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/WebClientTransportClientFactory.java @@ -16,25 +16,9 @@ package org.springframework.cloud.netflix.eureka.http; -import java.net.URI; -import java.net.URISyntaxException; import java.util.function.Supplier; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.SerializationConfig; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; -import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase; -import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.converters.jackson.mixin.ApplicationsJsonMixIn; -import com.netflix.discovery.converters.jackson.mixin.InstanceInfoJsonMixIn; -import com.netflix.discovery.converters.jackson.serializer.InstanceInfoJsonBeanSerializer; -import com.netflix.discovery.shared.Applications; import com.netflix.discovery.shared.resolver.EurekaEndpoint; import com.netflix.discovery.shared.transport.EurekaHttpClient; import com.netflix.discovery.shared.transport.TransportClientFactory; @@ -52,6 +36,9 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.extractUserInfo; +import static org.springframework.cloud.netflix.eureka.http.EurekaHttpClientUtils.objectMapper; + /** * Provides the custom {@link WebClient.Builder} required by the * {@link WebClientEurekaHttpClient}. Relies on Jackson for serialization and @@ -60,6 +47,7 @@ * @author Daniel Lavoie * @author Haytham Mohamed * @author Armin Krezovic + * @author Wonchul Heo */ public class WebClientTransportClientFactory implements TransportClientFactory { @@ -82,33 +70,13 @@ public EurekaHttpClient newClient(EurekaEndpoint endpoint) { private WebClient.Builder setUrl(WebClient.Builder builder, String serviceUrl) { String url = UriComponentsBuilder.fromUriString(serviceUrl).userInfo(null).toUriString(); - try { - URI serviceURI = new URI(serviceUrl); - if (serviceURI.getUserInfo() != null) { - String[] credentials = serviceURI.getUserInfo().split(":"); - if (credentials.length == 2) { - builder.filter(ExchangeFilterFunctions.basicAuthentication(credentials[0], credentials[1])); - } - } - } - catch (URISyntaxException ignore) { + final EurekaHttpClientUtils.UserInfo userInfo = extractUserInfo(serviceUrl); + if (userInfo != null) { + builder.filter(ExchangeFilterFunctions.basicAuthentication(userInfo.username(), userInfo.password())); } return builder.baseUrl(url); } - private static BeanSerializerModifier createJsonSerializerModifier() { - return new BeanSerializerModifier() { - @Override - public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDesc, - JsonSerializer serializer) { - if (beanDesc.getBeanClass().isAssignableFrom(InstanceInfo.class)) { - return new InstanceInfoJsonBeanSerializer((BeanSerializerBase) serializer, false); - } - return serializer; - } - }; - } - private void setCodecs(WebClient.Builder builder) { ObjectMapper objectMapper = objectMapper(); builder.codecs(configurer -> { @@ -119,32 +87,6 @@ private void setCodecs(WebClient.Builder builder) { }); } - /** - * Provides the serialization configurations required by the Eureka Server. JSON - * content exchanged with eureka requires a root node matching the entity being - * serialized or deserialized. Achieved with - * {@link SerializationFeature#WRAP_ROOT_VALUE} and - * {@link DeserializationFeature#UNWRAP_ROOT_VALUE}. - * {@link PropertyNamingStrategies.SnakeCaseStrategy} is applied to the underlying - * {@link ObjectMapper}. - * @return a {@link ObjectMapper} object - */ - private ObjectMapper objectMapper() { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - - SimpleModule jsonModule = new SimpleModule(); - jsonModule.setSerializerModifier(createJsonSerializerModifier()); - objectMapper.registerModule(jsonModule); - - objectMapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true); - objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); - objectMapper.addMixIn(Applications.class, ApplicationsJsonMixIn.class); - objectMapper.addMixIn(InstanceInfo.class, InstanceInfoJsonMixIn.class); - - return objectMapper; - } - // Skip over 4xx http errors private ExchangeFilterFunction http4XxErrorExchangeFilterFunction() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { diff --git a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ee8cfa643..a04fc748d 100644 --- a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -12,6 +12,12 @@ "description": "Determines whether the EurekaClient instance can be refreshed or not(If disabled none of the Eureka client properties will be refreshable).", "type": "java.lang.Boolean" }, + { + "defaultValue": false, + "name": "eureka.client.restclient.enabled", + "description": "Enables the use of RestClient for Eureka HTTP Client.", + "type": "java.lang.Boolean" + }, { "defaultValue": false, "name": "eureka.client.webclient.enabled", diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationClientTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationClientTests.java new file mode 100644 index 000000000..e0fefd5f5 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationClientTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2013-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.config; + +import com.netflix.discovery.shared.transport.EurekaHttpClient; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient; +import org.springframework.cloud.test.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Wonchul Heo + */ +class EurekaConfigServerBootstrapConfigurationClientTests { + + @Test + void properBeansCreatedWhenRestTemplateEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.restclient.enabled=false") + .run(context -> { + assertThat(context).hasSingleBean(RestTemplateEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(RestClientEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class); + }); + } + + @Test + void properBeansCreatedWhenRestClientEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.restclient.enabled=true") + .run(context -> { + assertThat(context).hasSingleBean(RestClientEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class); + }); + } + + @Test + void properBeansCreatedWhenWebClientEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.webclient.enabled=true") + .run(context -> { + final EurekaHttpClient bean = context.getBean(EurekaHttpClient.class); + System.out.println(bean); + assertThat(context).hasSingleBean(WebClientEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(RestClientEurekaHttpClient.class); + }); + } + + @Nested + @ClassPathExclusions({ "spring-webflux-*" }) + static class NoWebFlux { + + @Test + void properBeansCreatedWhenRestTemplateEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.restclient.enabled=false") + .run(context -> { + assertThat(context).hasSingleBean(RestTemplateEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(RestClientEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class); + }); + } + + @Test + void properBeansCreatedWhenRestClientEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.restclient.enabled=true") + .run(context -> { + assertThat(context).hasSingleBean(RestClientEurekaHttpClient.class); + assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class); + }); + } + + @Test + void properBeansCreatedWhenWebClientEnabledThenFailed() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true") + .withPropertyValues("eureka.client.enabled=true") + .withPropertyValues("eureka.client.webclient.enabled=true") + .run(context -> { + assertThatThrownBy(() -> context.getBean(WebClientEurekaHttpClient.class)) + .hasRootCauseInstanceOf(NoSuchBeanDefinitionException.class) + .hasRootCauseMessage( + "No qualifying bean of type 'com.netflix.discovery.shared.transport.EurekaHttpClient' available: " + + "expected at least 1 bean which qualifies as autowire candidate. " + + "Dependency annotations: {}"); + }); + } + + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientIntegrationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientIntegrationTests.java new file mode 100644 index 000000000..8e5bfa67e --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Wonchul Heo + */ +@SpringBootTest( + properties = { "spring.cloud.config.discovery.enabled=true", "eureka.client.enabled=true", + "spring.config.use-legacy-processing=true", "eureka.client.restclient.enabled=true" }, + webEnvironment = RANDOM_PORT) +class EurekaConfigServerBootstrapConfigurationRestClientIntegrationTests { + + @LocalServerPort + private int port; + + @Autowired + private RestClientEurekaHttpClient eurekaHttpClient; + + @Test + void restClientRespectsCodecProperties() { + RestClient restClient = eurekaHttpClient.getRestClient(); + ResponseEntity response = restClient.get() + .uri("http://localhost:" + port) + .retrieve() + .toEntity(String.class); + + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("rest-client"); + } + + @SpringBootConfiguration(proxyBeanMethods = false) + @EnableAutoConfiguration + @RestController + static class RestClientController { + + @GetMapping("/") + public String hello() { + return "rest-client"; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .csrf(AbstractHttpConfigurer::disable) + .build(); + } + + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientTests.java new file mode 100644 index 000000000..1f588afa2 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationRestClientTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Wonchul Heo + */ +class EurekaConfigServerBootstrapConfigurationRestClientTests { + + @Test + void properBeansCreatedWhenEnabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true", "eureka.client.enabled=true", + "eureka.client.restclient.enabled=true") + .run(context -> { + assertThat(context).hasSingleBean(EurekaClientConfigBean.class); + assertThat(context).hasSingleBean(RestClientEurekaHttpClient.class); + assertThat(context).hasSingleBean(ConfigServerInstanceProvider.Function.class); + }); + } + + @Test + void properBeansCreatedWhenEnabledRestClientDisabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EurekaConfigServerBootstrapConfiguration.class)) + .withPropertyValues("spring.cloud.config.discovery.enabled=true", "eureka.client.enabled=true", + "eureka.client.restclient.enabled=false") + .run(context -> { + assertThat(context).hasSingleBean(EurekaClientConfigBean.class); + assertThat(context).doesNotHaveBean(RestClientEurekaHttpClient.class); + assertThat(context).hasSingleBean(RestTemplateEurekaHttpClient.class); + assertThat(context).hasSingleBean(ConfigServerInstanceProvider.Function.class); + }); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationTests.java index 1c6a2cf9e..e0f3cae48 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationTests.java @@ -39,7 +39,7 @@ import org.springframework.cloud.config.client.ConfigServerInstanceProvider; import org.springframework.cloud.netflix.eureka.CloudEurekaClient; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; -import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; import org.springframework.cloud.test.ClassPathExclusions; import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.context.annotation.Bean; @@ -205,7 +205,7 @@ public void eurekaConfigServerInstanceProviderCalledWithVipAddress() { private void assertEurekaBeansPresent(AssertableApplicationContext context) { assertThat(context).hasSingleBean(EurekaClientConfigBean.class); - assertThat(context).hasSingleBean(RestTemplateEurekaHttpClient.class); + assertThat(context).hasSingleBean(RestClientEurekaHttpClient.class); assertThat(context).hasSingleBean(ConfigServerInstanceProvider.Function.class); } diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationWebClientTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationWebClientTests.java index a34b3e8f6..4c20039fb 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationWebClientTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfigurationWebClientTests.java @@ -22,7 +22,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; -import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; +import org.springframework.cloud.netflix.eureka.http.RestClientEurekaHttpClient; import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient; import static org.assertj.core.api.Assertions.assertThat; @@ -53,7 +53,7 @@ void properBeansCreatedWhenEnabledWebClientDisabled() { .run(context -> { assertThat(context).hasSingleBean(EurekaClientConfigBean.class); assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class); - assertThat(context).hasSingleBean(RestTemplateEurekaHttpClient.class); + assertThat(context).hasSingleBean(RestClientEurekaHttpClient.class); assertThat(context).hasSingleBean(ConfigServerInstanceProvider.Function.class); }); } diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaHttpClientsOptionalArgsConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaHttpClientsOptionalArgsConfigurationTests.java index 93aa5e8fb..587c6b690 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaHttpClientsOptionalArgsConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaHttpClientsOptionalArgsConfigurationTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs; import org.springframework.cloud.netflix.eureka.sample.EurekaSampleApplication; @@ -32,6 +33,7 @@ /** * @author Daniel Lavoie + * @author Wonchul Heo */ @RunWith(ModifiedClassPathRunner.class) @ClassPathExclusions({ "jersey-client-*", "jersey-core-*", "jersey-apache-client4-*" }) @@ -39,12 +41,25 @@ public class EurekaHttpClientsOptionalArgsConfigurationTests { @Test - public void contextLoadsWithRestTemplate() { + public void contextLoadsWithRestTemplateWhenWebClientDisabled() { new WebApplicationContextRunner().withUserConfiguration(EurekaSampleApplication.class) .withPropertyValues("eureka.client.webclient.enabled=false") + .withPropertyValues("eureka.client.restclient.enabled=false") .run(context -> { assertThat(context).hasSingleBean(RestTemplateDiscoveryClientOptionalArgs.class); assertThat(context).doesNotHaveBean(WebClientDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(RestClientDiscoveryClientOptionalArgs.class); + }); + } + + @Test + public void contextLoadsWithRestTemplateWhenRestClientDisabled() { + new WebApplicationContextRunner().withUserConfiguration(EurekaSampleApplication.class) + .withPropertyValues("eureka.client.restclient.enabled=false") + .run(context -> { + assertThat(context).hasSingleBean(RestTemplateDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(WebClientDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(RestClientDiscoveryClientOptionalArgs.class); }); } @@ -55,15 +70,30 @@ public void contextLoadsWithWebClient() { .run(context -> { assertThat(context).doesNotHaveBean(RestTemplateDiscoveryClientOptionalArgs.class); assertThat(context).hasSingleBean(WebClientDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(RestClientDiscoveryClientOptionalArgs.class); + }); + } + + @Test + public void contextLoadsWithRestClient() { + new WebApplicationContextRunner().withUserConfiguration(EurekaSampleApplication.class) + .withPropertyValues("eureka.client.restclient.enabled=true") + .run(context -> { + assertThat(context).doesNotHaveBean(RestTemplateDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(WebClientDiscoveryClientOptionalArgs.class); + assertThat(context).hasSingleBean(RestClientDiscoveryClientOptionalArgs.class); }); } @Test public void contextLoadsWithRestTemplateAsDefault() { - new WebApplicationContextRunner().withUserConfiguration(EurekaSampleApplication.class).run(context -> { - assertThat(context).hasSingleBean(RestTemplateDiscoveryClientOptionalArgs.class); - assertThat(context).doesNotHaveBean(WebClientDiscoveryClientOptionalArgs.class); - }); + new WebApplicationContextRunner().withUserConfiguration(EurekaSampleApplication.class) + .withPropertyValues("eureka.client.restclient.enabled=false") + .run(context -> { + assertThat(context).hasSingleBean(RestTemplateDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(WebClientDiscoveryClientOptionalArgs.class); + assertThat(context).doesNotHaveBean(RestClientDiscoveryClientOptionalArgs.class); + }); } } diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/JerseyClientOptionalArgsConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/JerseyClientOptionalArgsConfigurationTests.java index 1c502ddea..624905cf8 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/JerseyClientOptionalArgsConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/JerseyClientOptionalArgsConfigurationTests.java @@ -21,7 +21,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; +import org.springframework.cloud.netflix.eureka.http.RestClientDiscoveryClientOptionalArgs; import static org.assertj.core.api.Assertions.assertThat; @@ -34,7 +34,7 @@ public class JerseyClientOptionalArgsConfigurationTests { @SuppressWarnings("OptionalGetWithoutIsPresent") @Test - void shouldCreateRestTemplateDiscoveryClientOptionalArgsWhenJerseyClientDisabled() { + void shouldCreateRestClientDiscoveryClientOptionalArgsWhenJerseyClientDisabled() { new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DiscoveryClientOptionalArgsConfiguration.class)) .withPropertyValues("eureka.client.jersey.enabled=false") @@ -44,8 +44,8 @@ void shouldCreateRestTemplateDiscoveryClientOptionalArgsWhenJerseyClientDisabled .values() .stream() .findFirst() - .get()).isInstanceOf(RestTemplateDiscoveryClientOptionalArgs.class); - assertThat(context).hasSingleBean(RestTemplateDiscoveryClientOptionalArgs.class); + .get()).isInstanceOf(RestClientDiscoveryClientOptionalArgs.class); + assertThat(context).hasSingleBean(RestClientDiscoveryClientOptionalArgs.class); }); } diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/EurekaServerMockApplication.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/EurekaServerMockApplication.java index 1baa8d31d..69568aeef 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/EurekaServerMockApplication.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/EurekaServerMockApplication.java @@ -58,6 +58,7 @@ * Mocked Eureka Server. * * @author Daniel Lavoie + * @author Wonchul Heo */ @Configuration(proxyBeanMethods = false) @RestController @@ -112,7 +113,7 @@ public class EurekaServerMockApplication { */ @Bean public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { - return new RestTemplateTransportClientFactory().mappingJacksonHttpMessageConverter(); + return EurekaHttpClientUtils.mappingJacksonHttpMessageConverter(); } @ResponseStatus(HttpStatus.OK) diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClientTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClientTests.java new file mode 100644 index 000000000..dfac061ed --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientEurekaHttpClientTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider; +import com.netflix.discovery.shared.resolver.DefaultEndpoint; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.client.RestClient; + +/** + * @author Wonchul Heo + */ +@SpringBootTest(classes = EurekaServerMockApplication.class, + properties = { "debug=true", "security.basic.enabled=true", "eureka.client.fetch-registry=false", + "eureka.client.register-with-eureka=false", "logging.level.org.springframework=INFO" }, + webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +class RestClientEurekaHttpClientTests extends AbstractEurekaHttpClientTests { + + @Autowired + private InetUtils inetUtils; + + @Value("http://${security.user.name}:${security.user.password}@localhost:${local.server.port}/eureka") + private String serviceUrl; + + @BeforeEach + void setup() { + eurekaHttpClient = new RestClientTransportClientFactory(RestClient::builder) + .newClient(new DefaultEndpoint(serviceUrl)); + + EurekaInstanceConfigBean config = new EurekaInstanceConfigBean(inetUtils); + + String appname = "customapp"; + config.setIpAddress("127.0.0.1"); + config.setHostname("localhost"); + config.setAppname(appname); + config.setVirtualHostName(appname); + config.setSecureVirtualHostName(appname); + config.setNonSecurePort(4444); + config.setSecurePort(8443); + config.setInstanceId("127.0.0.1:customapp:4444"); + + info = new EurekaConfigBasedInstanceInfoProvider(config).get(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactoryTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactoryTests.java new file mode 100644 index 000000000..0c4269475 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestClientTransportClientFactoryTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka.http; + +import com.netflix.discovery.shared.resolver.DefaultEndpoint; +import com.netflix.discovery.shared.transport.EurekaHttpClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Wonchul Heo + */ +class RestClientTransportClientFactoryTests { + + private RestClientTransportClientFactory transportClientFactory; + + @BeforeEach + void setup() { + transportClientFactory = new RestClientTransportClientFactory(RestClient::builder); + } + + @Test + void withoutUserInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestClientEurekaHttpClient.class); + } + + @Test + void invalidUserInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://test@localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestClientEurekaHttpClient.class); + } + + @Test + void userInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://test:test@localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestClientEurekaHttpClient.class); + } + + @AfterEach + void shutdown() { + transportClientFactory.shutdown(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactoryTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactoryTests.java index 9241e7657..37aa572c3 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactoryTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/http/RestTemplateTransportClientFactoryTests.java @@ -17,40 +17,50 @@ package org.springframework.cloud.netflix.eureka.http; import com.netflix.discovery.shared.resolver.DefaultEndpoint; +import com.netflix.discovery.shared.transport.EurekaHttpClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Daniel Lavoie + * @author Wonchul Heo */ class RestTemplateTransportClientFactoryTests { - private RestTemplateTransportClientFactory transportClientFatory; + private RestTemplateTransportClientFactory transportClientFactory; @BeforeEach void setup() { - transportClientFatory = new RestTemplateTransportClientFactory(); + transportClientFactory = new RestTemplateTransportClientFactory(); } @Test - void testWithoutUserInfo() { - transportClientFatory.newClient(new DefaultEndpoint("http://localhost:8761")); + void withoutUserInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestTemplateEurekaHttpClient.class); } @Test - void testInvalidUserInfo() { - transportClientFatory.newClient(new DefaultEndpoint("http://test@localhost:8761")); + void invalidUserInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://test@localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestTemplateEurekaHttpClient.class); } @Test - void testUserInfo() { - transportClientFatory.newClient(new DefaultEndpoint("http://test:test@localhost:8761")); + void userInfo() { + EurekaHttpClient eurekaHttpClient = transportClientFactory + .newClient(new DefaultEndpoint("http://test:test@localhost:8761")); + assertThat(eurekaHttpClient).isInstanceOf(RestTemplateEurekaHttpClient.class); } @AfterEach void shutdown() { - transportClientFatory.shutdown(); + transportClientFactory.shutdown(); } }