Compare commits

..
15 Commits
50 changed files with 1610 additions and 247 deletions
+2
View File
@@ -28,6 +28,8 @@ image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a648
:project-full-name: Spring Cloud OpenFeign
:all: {asterisk}{asterisk}
:core_path: {project-root}/spring-cloud-openfeign-core
This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration
and binding to the Spring Environment and other Spring programming model idioms.
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
</parent>
<artifactId>spring-cloud-openfeign-docs</artifactId>
<packaging>jar</packaging>
+2
View File
@@ -14,3 +14,5 @@
:sc-ext: java
:project-full-name: Spring Cloud OpenFeign
:all: {asterisk}{asterisk}
:core_path: {project-root}/spring-cloud-openfeign-core
+1 -1
View File
@@ -1,6 +1,7 @@
|===
|Name | Default | Description
|feign.circuitbreaker.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.
|feign.client.config | |
|feign.client.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`.
|feign.client.default-config | `default` |
@@ -19,7 +20,6 @@
|feign.httpclient.max-connections-per-route | `50` |
|feign.httpclient.time-to-live | `900` |
|feign.httpclient.time-to-live-unit | |
|feign.hystrix.enabled | `false` | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
|===
@@ -92,8 +92,6 @@ In this case the client is composed from the components already in `FeignClients
NOTE: `FooConfiguration` does not need to be annotated with `@Configuration`. However, if it is, then take care to exclude it from any `@ComponentScan` that would otherwise include this configuration as it will become the default source for `feign.Decoder`, `feign.Encoder`, `feign.Contract`, etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any `@ComponentScan` or `@SpringBootApplication`, or it can be explicitly excluded in `@ComponentScan`.
NOTE: The `serviceId` attribute is now deprecated in favor of the `name` attribute.
NOTE: Using `contextId` attribute of the `@FeignClient` annotation in addition to changing the name of
the `ApplicationContext` ensemble, it will override the alias of the client name
and it will be used as part of the name of the configuration bean created for that client.
@@ -116,7 +114,7 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
* `Encoder` feignEncoder: `SpringEncoder`
* `Logger` feignLogger: `Slf4jLogger`
* `Contract` feignContract: `SpringMvcContract`
* `Feign.Builder` feignBuilder: `HystrixFeign.Builder`
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
* `Client` feignClient: if Spring Cloud LoadBalancer is in the classpath, `FeignBlockingLoadBalancerClient` is used.
If none of them is in the classpath, the default feign client is used.
@@ -208,26 +206,6 @@ If we create both `@Configuration` bean and configuration properties, configurat
It will override `@Configuration` values. But if you want to change the priority to `@Configuration`,
you can change `feign.client.default-to-properties` to `false`.
NOTE: If you need to use `ThreadLocal` bound variables in your `RequestInterceptor`s you will need to either set the
thread isolation strategy for Hystrix to `SEMAPHORE` or disable Hystrix in Feign.
application.yml
[source,yaml]
----
# To disable Hystrix in Feign
feign:
hystrix:
enabled: false
# To set thread isolation to SEMAPHORE
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
----
If we want to create multiple feign clients with the same name or url
so that they would point to the same server but each with a different custom configuration then
we have to use `contextId` attribute of the `@FeignClient` in order to avoid name
@@ -332,12 +310,12 @@ the default Feign native annotations.
You can also use the `Builder`to configure FeignClient not to inherit beans from the parent context.
You can do this by overriding calling `inheritParentContext(false)` on the `Builder`.
[[spring-cloud-feign-hystrix]]
=== Feign Hystrix Support
[[spring-cloud-feign-circuitbreaker]]
=== Feign Spring Cloud CircuitBreaker Support
If Hystrix is on the classpath and `feign.hystrix.enabled=true`, Feign will wrap all methods with a circuit breaker. Returning a `com.netflix.hystrix.HystrixCommand` is also available. This lets you use reactive patterns (with a call to `.toObservable()` or `.observe()` or asynchronous use (with a call to `.queue()`).
If Spring Cloud CircuitBreaker is on the classpath and `feign.circuitbreaker.enabled=true`, Feign will wrap all methods with a circuit breaker.
To disable Hystrix support on a per-client basis create a vanilla `Feign.Builder` with the "prototype" scope, e.g.:
To disable Spring Cloud CircuitBreaker support on a per-client basis create a vanilla `Feign.Builder` with the "prototype" scope, e.g.:
[source,java,indent=0]
----
@@ -351,60 +329,28 @@ public class FooConfiguration {
}
----
WARNING: Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped
all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in
favor for an opt-in approach.
The circuit breaker name follows this pattern `<feignClientName>_<calledMethod>`. When calling a `@FeignClient` with name `foo` and the called interface method is `bar` then the circuit breaker name will be `foo_bar`.
[[spring-cloud-feign-hystrix-fallback]]
=== Feign Hystrix Fallbacks
[[spring-cloud-feign-circuitbreaker-fallback]]
=== Feign Spring Cloud CircuitBreaker Fallbacks
Hystrix supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
[source,java,indent=0]
----
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
static class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CirciutBreakerTests.java[tags=client_with_fallback, indent=0]
----
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
[source,java,indent=0]
----
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
@Override
public HystrixClient create(Throwable cause) {
return new HystrixClient() {
@Override
public Hello iFailSometimes() {
return new Hello("fallback; reason was: " + cause.getMessage());
}
};
}
}
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CirciutBreakerTests.java[tags=client_with_fallback_factory, indent=0]
----
WARNING: There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return `com.netflix.hystrix.HystrixCommand` and `rx.Observable`.
=== Feign and `@Primary`
When using Feign with Hystrix fallbacks, there are multiple beans in the `ApplicationContext` of the same type. This will cause `@Autowired` to not work because there isn't exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as `@Primary`, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the `primary` attribute of `@FeignClient` to false.
When using Feign with Spring Cloud CircuitBreaker fallbacks, there are multiple beans in the `ApplicationContext` of the same type. This will cause `@Autowired` to not work because there isn't exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as `@Primary`, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the `primary` attribute of `@FeignClient` to false.
[source,java,indent=0]
----
+3 -3
View File
@@ -4,14 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<packaging>pom</packaging>
<name>Spring Cloud OpenFeign</name>
<description>Spring Cloud OpenFeign</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<relativePath/>
</parent>
<scm>
@@ -26,7 +26,7 @@
<properties>
<main.basedir>${basedir}</main.basedir>
<jackson.version>2.11.3</jackson.version>
<spring-cloud-commons.version>3.0.0-M6</spring-cloud-commons.version>
<spring-cloud-commons.version>3.0.0</spring-cloud-commons.version>
<!-- Plugin versions -->
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
+6 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-openfeign-core</artifactId>
@@ -119,6 +119,11 @@
<artifactId>okhttp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
@@ -0,0 +1,87 @@
/*
* Copyright 2013-2020 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.openfeign;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static feign.Util.checkNotNull;
/**
* Used to control the fallback given its cause.
*
* Ex.
*
* <pre>
* {@code
* // This instance will be invoked if there are errors of any kind.
* FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> {
* if (cause instanceof FeignException && ((FeignException) cause).status() == 403) {
* return Collections.emptyList();
* } else {
* return Arrays.asList("yogi");
* }
* };
*
* GitHub github = FeignCircuitBreaker.builder()
* ...
* .target(GitHub.class, "https://api.github.com", fallbackFactory);
* }
* </pre>
*
* @param <T> the feign interface type
*/
public interface FallbackFactory<T> {
/**
* Returns an instance of the fallback appropriate for the given cause.
* @param cause cause of an exception.
* @return fallback
*/
T create(Throwable cause);
final class Default<T> implements FallbackFactory<T> {
final Log logger;
final T constant;
public Default(T constant) {
this(constant, LogFactory.getLog(Default.class));
}
Default(T constant, Log logger) {
this.constant = checkNotNull(constant, "fallback");
this.logger = checkNotNull(logger, "logger");
}
@Override
public T create(Throwable cause) {
if (logger.isTraceEnabled()) {
logger.trace("fallback due to: " + cause.getMessage(), cause);
}
return constant;
}
@Override
public String toString() {
return constant.toString();
}
}
}
@@ -27,6 +27,7 @@ import javax.annotation.PreDestroy;
import feign.Client;
import feign.Feign;
import feign.RequestInterceptor;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
@@ -39,20 +40,27 @@ import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
/**
* @author Spencer Gibb
@@ -82,6 +90,7 @@ public class FeignAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(FeignCircuitBreakerDisabledConditions.class)
protected static class DefaultFeignTargeterConfiguration {
@Bean
@@ -92,6 +101,26 @@ public class FeignAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CircuitBreaker.class)
@ConditionalOnProperty("feign.circuitbreaker.enabled")
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
@Bean
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
public Targeter defaultFeignTargeter() {
return new DefaultTargeter();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(CircuitBreakerFactory.class)
public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory) {
return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
}
}
// the following configuration is for alternate feign clients if
// SC loadbalancer is not on the class path.
// see corresponding configurations in FeignRibbonClientAutoConfiguration
@@ -209,4 +238,19 @@ public class FeignAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OAuth2ClientContext.class)
@ConditionalOnProperty("feign.oauth2.enabled")
protected static class Oauth2FeignConfiguration {
@Bean
@ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class)
@ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class })
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
OAuth2ProtectedResourceDetails resource) {
return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource);
}
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2013-2020 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.openfeign;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
/**
* Allows Feign interfaces to work with {@link CircuitBreaker}.
*
* @author Marcin Grzejszczak
* @since 3.0.0
*/
public final class FeignCircuitBreaker {
private FeignCircuitBreaker() {
throw new IllegalStateException("Don't instantiate a utility class");
}
/**
* @return builder for Feign CircuitBreaker integration
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for Feign CircuitBreaker integration.
*/
public static final class Builder extends Feign.Builder {
private CircuitBreakerFactory circuitBreakerFactory;
private String feignClientName;
Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
return this;
}
Builder feignClientName(String feignClientName) {
this.feignClientName = feignClientName;
return this;
}
public <T> T target(Target<T> target, T fallback) {
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
}
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
return build(fallbackFactory).newInstance(target);
}
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
return new FeignCircuitBreakerInvocationHandler(circuitBreakerFactory, feignClientName, target,
dispatch, nullableFallbackFactory);
}
});
return super.build();
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2013-2020 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.openfeign;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
class FeignCircuitBreakerDisabledConditions extends AnyNestedCondition {
FeignCircuitBreakerDisabledConditions() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnMissingClass("org.springframework.cloud.client.circuitbreaker.CircuitBreaker")
static class CircuitBreakerClassMissing {
}
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "false", matchIfMissing = true)
static class CircuitBreakerDisabled {
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2013-2020 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.openfeign;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import feign.InvocationHandlerFactory;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import static feign.Util.checkNotNull;
class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final CircuitBreakerFactory factory;
private final String feignClientName;
private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
private final FallbackFactory<?> nullableFallbackFactory;
private final Map<Method, Method> fallbackMethodMap;
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory) {
this.factory = factory;
this.feignClientName = feignClientName;
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.nullableFallbackFactory = nullableFallbackFactory;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
// early exit if the invoked method is from java.lang.Object
// code is the same as ReflectiveFeign.FeignInvocationHandler
if ("equals".equals(method.getName())) {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
}
catch (IllegalArgumentException e) {
return false;
}
}
else if ("hashCode".equals(method.getName())) {
return hashCode();
}
else if ("toString".equals(method.getName())) {
return toString();
}
String circuitName = this.feignClientName + "_" + method.getName();
CircuitBreaker circuitBreaker = this.factory.create(circuitName);
Supplier<Object> supplier = asSupplier(method, args);
if (this.nullableFallbackFactory != null) {
Function<Throwable, Object> fallbackFunction = throwable -> {
Object fallback = this.nullableFallbackFactory.create(throwable);
try {
return this.fallbackMethodMap.get(method).invoke(fallback, args);
}
catch (Exception e) {
throw new IllegalStateException(e);
}
};
return circuitBreaker.run(supplier, fallbackFunction);
}
return circuitBreaker.run(supplier);
}
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
return () -> {
try {
return this.dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
/**
* If the method param of InvocationHandler.invoke is not accessible, i.e in a
* package-private interface, the fallback call will cause of access restrictions. But
* methods in dispatch are copied methods. So setting access to dispatch method
* doesn't take effect to the method in InvocationHandler.invoke. Use map to store a
* copy of method to invoke the fallback to bypass this and reducing the count of
* reflection calls.
* @return cached methods map for fallback invoking
*/
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
Map<Method, Method> result = new LinkedHashMap<Method, Method>();
for (Method method : dispatch.keySet()) {
method.setAccessible(true);
result.put(method, method);
}
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FeignCircuitBreakerInvocationHandler) {
FeignCircuitBreakerInvocationHandler other = (FeignCircuitBreakerInvocationHandler) obj;
return this.target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return this.target.hashCode();
}
@Override
public String toString() {
return this.target.toString();
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2013-2020 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.openfeign;
import feign.Feign;
import feign.Target;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.util.StringUtils;
@SuppressWarnings("unchecked")
class FeignCircuitBreakerTargeter implements Targeter {
private final CircuitBreakerFactory circuitBreakerFactory;
FeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory) {
this.circuitBreakerFactory = circuitBreakerFactory;
}
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
if (!(feign instanceof FeignCircuitBreaker.Builder)) {
return feign.target(target);
}
FeignCircuitBreaker.Builder builder = (FeignCircuitBreaker.Builder) feign;
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() : factory.getContextId();
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(name, context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
}
return builder(name, builder).target(target);
}
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
return builder(feignClientName, builder).target(target, fallbackFactory);
}
private <T> T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget<T> target,
FeignCircuitBreaker.Builder builder, Class<?> fallback) {
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
return builder(feignClientName, builder).target(target, fallbackInstance);
}
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
Class<?> beanType, Class<T> targetType) {
Object fallbackInstance = context.getInstance(feignClientName, beanType);
if (fallbackInstance == null) {
throw new IllegalStateException(
String.format("No " + fallbackMechanism + " instance of type %s found for feign client %s",
beanType, feignClientName));
}
if (!targetType.isAssignableFrom(beanType)) {
throw new IllegalStateException(String.format("Incompatible " + fallbackMechanism
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
beanType, targetType, feignClientName));
}
return (T) fallbackInstance;
}
private FeignCircuitBreaker.Builder builder(String feignClientName, FeignCircuitBreaker.Builder builder) {
return builder.circuitBreakerFactory(this.circuitBreakerFactory).feignClientName(feignClientName);
}
}
@@ -49,14 +49,6 @@ public @interface FeignClient {
@AliasFor("name")
String value() default "";
/**
* The service id with optional protocol prefix. Synonym for {@link #value() value}.
* @deprecated use {@link #name() name} instead
* @return the service id with optional protocol prefix
*/
@Deprecated
String serviceId() default "";
/**
* This will be used as the bean name instead of name if present, but will not be used
* as a service id.
@@ -108,7 +100,7 @@ public @interface FeignClient {
* factory must produce instances of fallback classes that implement the interface
* annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
*
* @see feign.hystrix.FallbackFactory for details.
* @see FallbackFactory for details.
* @return fallback factory for the specified Feign client interface
*/
Class<?> fallbackFactory() default void.class;
@@ -33,11 +33,15 @@ import feign.optionals.OptionalDecoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
import org.springframework.cloud.openfeign.support.PageJacksonModule;
@@ -48,6 +52,7 @@ import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.convert.ConversionService;
@@ -131,13 +136,6 @@ public class FeignClientsConfiguration {
return Retryer.NEVER_RETRY;
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
@@ -185,4 +183,39 @@ public class FeignClientsConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(FeignCircuitBreakerDisabledConditions.class)
protected static class DefaultFeignBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CircuitBreaker.class)
@ConditionalOnProperty("feign.circuitbreaker.enabled")
protected static class CircuitBreakerPresentFeignBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean({ Feign.Builder.class, CircuitBreakerFactory.class })
public Feign.Builder defaultFeignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnBean(CircuitBreakerFactory.class)
public Feign.Builder circuitBreakerFeignBuilder() {
return FeignCircuitBreaker.builder();
}
}
}
@@ -25,7 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@@ -30,18 +30,18 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.HttpRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
/**
@@ -78,12 +78,12 @@ public class FeignBlockingLoadBalancerClient implements Client {
String serviceId = originalUri.getHost();
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
String hint = getHint(serviceId);
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
new DefaultRequestContext(request, hint));
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
new RequestDataContext(buildRequestData(request), hint));
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
@@ -93,15 +93,15 @@ public class FeignBlockingLoadBalancerClient implements Client {
if (LOG.isWarnEnabled()) {
LOG.warn(message);
}
supportedLifecycleProcessors.forEach(
lifecycle -> lifecycle.onComplete(new CompletionContext<ClientHttpResponse, ServiceInstance>(
CompletionContext.Status.DISCARD, lbResponse)));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
.body(message, StandardCharsets.UTF_8).build();
}
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = buildRequest(request, reconstructedUrl);
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbResponse,
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
supportedLifecycleProcessors);
}
@@ -27,7 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
import org.springframework.context.annotation.Bean;
@@ -17,6 +17,9 @@
package org.springframework.cloud.openfeign.loadbalancer;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import feign.Client;
@@ -26,6 +29,11 @@ import feign.Response;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
/**
* @author Olga Maciaszek-Sharma
@@ -40,29 +48,47 @@ final class LoadBalancerUtils {
}
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced) throws IOException {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, lbResponse));
try {
Response response = feignClient.execute(feignRequest, options);
if (loadBalanced) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS, lbResponse, response)));
supportedLifecycleProcessors.forEach(
lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
lbRequest, lbResponse, buildResponseData(response))));
}
return response;
}
catch (Exception exception) {
if (loadBalanced) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbResponse)));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbRequest, lbResponse)));
}
throw exception;
}
}
static ResponseData buildResponseData(Response response) {
HttpHeaders responseHeaders = new HttpHeaders();
response.headers().forEach((key, value) -> responseHeaders.put(key, new ArrayList<>(value)));
return new ResponseData(HttpStatus.resolve(response.status()), responseHeaders, null,
buildRequestData(response.request()));
}
static RequestData buildRequestData(Request request) {
HttpHeaders requestHeaders = new HttpHeaders();
request.headers().forEach((key, value) -> requestHeaders.put(key, new ArrayList<>(value)));
return new RequestData(HttpMethod.resolve(request.httpMethod().name()), URI.create(request.url()),
requestHeaders, null, new HashMap<>());
}
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) throws IOException {
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbResponse,
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbRequest, lbResponse,
supportedLifecycleProcessors, true);
}
@@ -26,7 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
import org.springframework.context.annotation.Bean;
@@ -35,7 +35,6 @@ import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.HttpRequestContext;
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
@@ -44,14 +43,14 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.retry.RetryListener;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.NoBackOffPolicy;
@@ -59,7 +58,7 @@ import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
/**
* A {@link Client} implementation that provides Spring Retry support for requests
@@ -107,7 +106,10 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
HttpRequestContext.class, ClientHttpResponse.class, ServiceInstance.class);
RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
String hint = getHint(serviceId);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
new RetryableRequestContext(null, buildRequestData(request), hint));
// On retries the policy will choose the server and set it in the context
// and extract the server and update the request being made
if (context instanceof LoadBalancedRetryContext) {
@@ -119,9 +121,7 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
+ "Reattempting service instance selection");
}
ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
String hint = getHint(serviceId);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
new RetryableRequestContext(previousServiceInstance, request, hint));
lbRequest.getContext().setPreviousServiceInstance(previousServiceInstance);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
retrievedServiceInstance = loadBalancerClient.choose(serviceId, lbRequest);
if (LOG.isDebugEnabled()) {
@@ -136,9 +136,9 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
}
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
retrievedServiceInstance);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<ClientHttpResponse, ServiceInstance>(CompletionContext.Status.DISCARD,
lbResponse)));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>(
CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
feignRequest = request;
}
else {
@@ -153,8 +153,9 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
}
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
retrievedServiceInstance);
Response response = executeWithLoadBalancerLifecycleProcessing(delegate, options, feignRequest, lbResponse,
supportedLifecycleProcessors, retrievedServiceInstance != null);
Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(delegate, options,
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors,
retrievedServiceInstance != null);
int responseStatus = response.status();
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
if (LOG.isDebugEnabled()) {
@@ -0,0 +1,182 @@
/*
* Copyright 2015-2019 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.openfeign.security;
import java.util.Arrays;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
/**
* Pre-defined custom RequestInterceptor for Feign Requests. It uses the
* {@link OAuth2ClientContext OAuth2ClientContext} provided from the environment and
* construct a new header on the request before it is made by Feign.
*
* @author Joao Pedro Evangelista
* @author Tim Ysewyn
* @since 3.0.0
*/
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
/**
* The name of the token.
*/
public static final String BEARER = "Bearer";
/**
* The name of the header.
*/
public static final String AUTHORIZATION = "Authorization";
private final OAuth2ClientContext oAuth2ClientContext;
private final OAuth2ProtectedResourceDetails resource;
private final String tokenType;
private final String header;
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider>asList(
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
/**
* Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
* within Authorization header.
* @param oAuth2ClientContext provided context
* @param resource type of resource to be accessed
*/
public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
OAuth2ProtectedResourceDetails resource) {
this(oAuth2ClientContext, resource, BEARER, AUTHORIZATION);
}
/**
* Fully customizable constructor for changing token type and header name, in cases of
* Bearer and Authorization is not the default such as "bearer", "authorization".
* @param oAuth2ClientContext current oAuth2 Context
* @param resource type of resource to be accessed
* @param tokenType type of token e.g. "token", "Bearer"
* @param header name of the header e.g. "Authorization", "authorization"
*/
public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
OAuth2ProtectedResourceDetails resource, String tokenType, String header) {
this.oAuth2ClientContext = oAuth2ClientContext;
this.resource = resource;
this.tokenType = tokenType;
this.header = header;
}
/**
* Create a template with the header of provided name and extracted extract.
*
* @see RequestInterceptor#apply(RequestTemplate)
*/
@Override
public void apply(RequestTemplate template) {
template.header(header); // Clears out the header, no "clear" method available.
template.header(header, extract(tokenType));
}
/**
* Extracts the token extract id the access token exists or returning an empty extract
* if there is no one on the context it may occasionally causes Unauthorized response
* since the token extract is empty.
* @param tokenType type name of token
* @return token value from context if it exists otherwise empty String
*/
protected String extract(String tokenType) {
OAuth2AccessToken accessToken = getToken();
return String.format("%s %s", tokenType, accessToken.getValue());
}
/**
* Extract the access token within the request or try to acquire a new one by
* delegating it to {@link #acquireAccessToken()}.
* @return valid token
*/
public OAuth2AccessToken getToken() {
OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken();
}
catch (UserRedirectRequiredException e) {
oAuth2ClientContext.setAccessToken(null);
String stateKey = e.getStateKey();
if (stateKey != null) {
Object stateToPreserve = e.getStateToPreserve();
if (stateToPreserve == null) {
stateToPreserve = "NONE";
}
oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve);
}
throw e;
}
}
return accessToken;
}
/**
* Try to acquire the token using a access token provider.
* @return valid access token
* @throws UserRedirectRequiredException in case the user needs to be redirected to an
* approval page or login page
*/
protected OAuth2AccessToken acquireAccessToken() throws UserRedirectRequiredException {
AccessTokenRequest tokenRequest = oAuth2ClientContext.getAccessTokenRequest();
if (tokenRequest == null) {
throw new AccessTokenRequiredException(
"Cannot find valid context on request for resource '" + resource.getId() + "'.", resource);
}
String stateKey = tokenRequest.getStateKey();
if (stateKey != null) {
tokenRequest.setPreservedState(oAuth2ClientContext.removePreservedState(stateKey));
}
OAuth2AccessToken existingToken = oAuth2ClientContext.getAccessToken();
if (existingToken != null) {
oAuth2ClientContext.setAccessToken(existingToken);
}
OAuth2AccessToken obtainableAccessToken;
obtainableAccessToken = accessTokenProvider.obtainAccessToken(resource, tokenRequest);
if (obtainableAccessToken == null || obtainableAccessToken.getValue() == null) {
throw new IllegalStateException(
" Access token provider returned a null token, which is illegal according to the contract.");
}
oAuth2ClientContext.setAccessToken(obtainableAccessToken);
return obtainableAccessToken;
}
public void setAccessTokenProvider(AccessTokenProvider accessTokenProvider) {
this.accessTokenProvider = accessTokenProvider;
}
}
@@ -401,25 +401,6 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
return false;
}
/**
* @deprecated Not used internally anymore. Will be removed in the future.
*/
@Deprecated
public static class ConvertingExpander implements Param.Expander {
private final ConversionService conversionService;
public ConvertingExpander(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public String expand(Object value) {
return conversionService.convert(value, String.class);
}
}
private static class ConvertingExpanderFactory {
private final ConversionService conversionService;
@@ -3,9 +3,9 @@
],
"properties": [
{
"name": "feign.hystrix.enabled",
"name": "feign.circuitbreaker.enabled",
"type": "java.lang.Boolean",
"description": "If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.",
"description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.",
"defaultValue": "false"
},
{
@@ -29,7 +29,7 @@ import org.junit.Test;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
@@ -48,7 +48,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Spencer Gibb
*/
@SpringBootTest(classes = FeignHttpClientUrlTests.TestConfig.class, webEnvironment = DEFINED_PORT,
value = { "spring.application.name=feignclienturltest", "feign.hystrix.enabled=false",
value = { "spring.application.name=feignclienturltest", "feign.circuitbreaker.enabled=false",
"feign.okhttp.enabled=false", "spring.cloud.loadbalancer.retry.enabled=false" })
@DirtiesContext
class FeignHttpClientUrlTests {
@@ -0,0 +1,309 @@
/*
* Copyright 2013-2020 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.openfeign.circuitbreaker;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CirciutBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = {
"spring.application.name=springcircuittest", "spring.jmx.enabled=false", "feign.circuitbreaker.enabled=true" })
@DirtiesContext
public class CirciutBreakerTests {
@Autowired
MyCircuitBreaker myCircuitBreaker;
@Autowired
TestClient testClient;
@Autowired
TestClientWithFactory testClientWithFactory;
@LocalServerPort
private int port = 0;
@BeforeAll
public static void beforeClass() {
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
}
@AfterAll
public static void afterClass() {
System.clearProperty("server.port");
}
@Before
public void setup() {
this.myCircuitBreaker.clear();
}
@Test
public void testSimpleTypeWithFallback() {
Hello hello = testClient.getHello();
assertThat(hello).as("hello was null").isNotNull();
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
}
@Test
public void test404WithFallback() {
assertThat(testClient.getException()).isEqualTo("Fixed response");
}
@Test
public void testSimpleTypeWithFallbackFactory() {
Hello hello = testClientWithFactory.getHello();
assertThat(hello).as("hello was null").isNotNull();
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
}
@Test
public void test404WithFallbackFactory() {
assertThat(testClientWithFactory.getException()).isEqualTo("Fixed response");
}
// tag::client_with_fallback[]
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class Fallback implements TestClient {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
// end::client_with_fallback[]
// tag::client_with_fallback_factory[]
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {
@Override
public FallbackWithFactory create(Throwable cause) {
return new FallbackWithFactory();
}
}
static class FallbackWithFactory implements TestClientWithFactory {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
// end::client_with_fallback_factory[]
public static class Hello {
private String message;
public Hello() {
}
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Hello that = (Hello) o;
return Objects.equals(this.message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(this.message);
}
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@RestController
@EnableFeignClients(clients = { TestClient.class, TestClientWithFactory.class })
@Import(NoSecurityConfiguration.class)
protected static class Application implements TestClient {
static final Log log = LogFactory.getLog(Application.class);
@Bean
MyCircuitBreaker myCircuitBreaker() {
return new MyCircuitBreaker();
}
@Bean
CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) {
return new CircuitBreakerFactory() {
@Override
public CircuitBreaker create(String id) {
log.info("Creating a circuit breaker with id [" + id + "]");
return myCircuitBreaker;
}
@Override
protected ConfigBuilder configBuilder(String id) {
return Object::new;
}
@Override
public void configureDefault(Function defaultConfiguration) {
}
};
}
@Override
public Hello getHello() {
return new Hello("hello world 1");
}
@Override
public String getException() {
throw new IllegalStateException("BOOM!");
}
@Bean
Fallback fallback() {
return new Fallback();
}
@Bean
TestFallbackFactory testFallbackFactory() {
return new TestFallbackFactory();
}
}
static class MyCircuitBreaker implements CircuitBreaker {
AtomicBoolean runWasCalled = new AtomicBoolean();
@Override
public <T> T run(Supplier<T> toRun) {
this.runWasCalled.set(true);
return toRun.get();
}
@Override
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
try {
return run(toRun);
}
catch (Throwable throwable) {
return fallback.apply(throwable);
}
}
public void clear() {
this.runWasCalled.set(false);
}
}
}
@@ -25,8 +25,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
@@ -34,7 +36,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
@@ -86,8 +87,9 @@ public class FeignAcceptEncodingTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -25,8 +25,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
@@ -34,7 +36,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -48,9 +49,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Jakub Narloch
*/
@SpringBootTest(classes = FeignContentEncodingTests.Application.class, webEnvironment = RANDOM_PORT,
value = { "feign.compression.request.enabled=true",
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
"ribbon.OkToRetryOnAllOperations=false" })
value = { "feign.compression.request.enabled=true", "ribbon.OkToRetryOnAllOperations=false" })
@RunWith(SpringJUnit4ClassRunner.class)
public class FeignContentEncodingTests {
@@ -89,8 +88,9 @@ public class FeignContentEncodingTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -26,8 +26,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
@@ -36,7 +38,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -55,9 +56,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Charlie Mordant.
*/
@SpringBootTest(classes = FeignPageableEncodingTests.Application.class, webEnvironment = RANDOM_PORT,
value = { "feign.compression.request.enabled=true",
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
"ribbon.OkToRetryOnAllOperations=false" })
value = { "feign.compression.request.enabled=true", "ribbon.OkToRetryOnAllOperations=false" })
@RunWith(SpringJUnit4ClassRunner.class)
public class FeignPageableEncodingTests {
@@ -105,8 +104,9 @@ public class FeignPageableEncodingTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -19,13 +19,14 @@ package org.springframework.cloud.openfeign.hateoas.app;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
/**
* Test HATEOAS application.
@@ -49,8 +50,9 @@ class LocalHalClientConfiguration {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -71,7 +71,7 @@ public class FeignClientValidationTests {
@EnableFeignClients(clients = NameAndServiceIdConfiguration.Client.class)
protected static class NameAndServiceIdConfiguration {
@FeignClient(name = "bar", serviceId = "foo")
@FeignClient(name = "bar")
interface Client {
@RequestMapping(method = RequestMethod.GET, value = "/")
@@ -37,9 +37,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
@@ -134,6 +135,7 @@ class FeignBlockingLoadBalancerClientTests {
@Test
void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException {
Request request = testRequest();
when(delegate.execute(any(), any())).thenReturn(Response.builder().status(200).request(request).build());
Request.Options options = new Request.Options();
String url = "http://127.0.0.1/path";
ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false);
@@ -148,16 +150,21 @@ class FeignBlockingLoadBalancerClientTests {
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
.thenReturn(loadBalancerLifecycleBeans);
Object actualResult = feignBlockingLoadBalancerClient.execute(request, options);
feignBlockingLoadBalancerClient.execute(request, options);
Collection<org.springframework.cloud.client.loadbalancer.Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
Collection<org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartLog().values();
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
Collection<org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> lifecycleLogStartedRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartRequestLog().values();
Collection<CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
assertThat(lifecycleLogRequests)
.extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint())
assertThat(lifecycleLogRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult);
assertThat(lifecycleLogStartedRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests)
.extracting(completionContext -> completionContext.getClientResponse().getHttpStatus())
.contains(HttpStatus.OK);
}
private Request testRequest() {
@@ -176,30 +183,43 @@ class FeignBlockingLoadBalancerClientTests {
}
protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
protected static class TestLoadBalancerLifecycle
implements LoadBalancerLifecycle<RequestDataContext, ResponseData, ServiceInstance> {
final ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> startLog = new ConcurrentHashMap<>();
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> startLog = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> startRequestLog = new ConcurrentHashMap<>();
final Map<String, CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> completeLog = new ConcurrentHashMap<>();
@Override
public void onStart(org.springframework.cloud.client.loadbalancer.Request<Object> request) {
public void onStart(org.springframework.cloud.client.loadbalancer.Request<RequestDataContext> request) {
startLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(CompletionContext<Object, ServiceInstance> completionContext) {
public void onStartRequest(org.springframework.cloud.client.loadbalancer.Request<RequestDataContext> request,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse) {
startRequestLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(CompletionContext<ResponseData, ServiceInstance, RequestDataContext> completionContext) {
completeLog.put(getName() + UUID.randomUUID(), completionContext);
}
ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> getStartLog() {
Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> getStartLog() {
return startLog;
}
ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> getCompleteLog() {
Map<String, CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> getCompleteLog() {
return completeLog;
}
Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> getStartRequestLog() {
return startRequestLog;
}
protected String getName() {
return this.getClass().getSimpleName();
}
@@ -38,14 +38,16 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryPolicy;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import static org.assertj.core.api.Assertions.assertThat;
@@ -192,16 +194,21 @@ class RetryableFeignBlockingLoadBalancerClientTests {
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
.thenReturn(loadBalancerLifecycleBeans);
Object actualResult = feignBlockingLoadBalancerClient.execute(request, options);
feignBlockingLoadBalancerClient.execute(request, options);
Collection<org.springframework.cloud.client.loadbalancer.Request<Object>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
Collection<org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartLog().values();
Collection<CompletionContext<Object, ServiceInstance>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
Collection<org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> lifecycleLogStartedRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("loadBalancerLifecycle")).getStartRequestLog().values();
Collection<CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
assertThat(lifecycleLogRequests)
.extracting(lbRequest -> ((DefaultRequestContext) lbRequest.getContext()).getHint())
assertThat(lifecycleLogRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests).extracting(CompletionContext::getClientResponse).contains(actualResult);
assertThat(lifecycleLogStartedRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
.contains(callbackTestHint);
assertThat(anotherLifecycleLogRequests)
.extracting(completionContext -> completionContext.getClientResponse().getHttpStatus())
.contains(HttpStatus.OK);
}
private Request testRequest() {
@@ -220,30 +227,45 @@ class RetryableFeignBlockingLoadBalancerClientTests {
}
protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle<Object, Object, ServiceInstance> {
protected static class TestLoadBalancerLifecycle
implements LoadBalancerLifecycle<RetryableRequestContext, ResponseData, ServiceInstance> {
final ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> startLog = new ConcurrentHashMap<>();
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> startLog = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> completeLog = new ConcurrentHashMap<>();
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> startRequestLog = new ConcurrentHashMap<>();
final Map<String, CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> completeLog = new ConcurrentHashMap<>();
@Override
public void onStart(org.springframework.cloud.client.loadbalancer.Request<Object> request) {
public void onStart(org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext> request) {
startLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(CompletionContext<Object, ServiceInstance> completionContext) {
public void onStartRequest(
org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext> request,
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse) {
startRequestLog.put(getName() + UUID.randomUUID(), request);
}
@Override
public void onComplete(
CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext> completionContext) {
completeLog.put(getName() + UUID.randomUUID(), completionContext);
}
ConcurrentHashMap<String, org.springframework.cloud.client.loadbalancer.Request<Object>> getStartLog() {
Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> getStartLog() {
return startLog;
}
ConcurrentHashMap<String, CompletionContext<Object, ServiceInstance>> getCompleteLog() {
Map<String, CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> getCompleteLog() {
return completeLog;
}
Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> getStartRequestLog() {
return startRequestLog;
}
protected String getName() {
return this.getClass().getSimpleName();
}
@@ -251,7 +273,7 @@ class RetryableFeignBlockingLoadBalancerClientTests {
}
protected static class AnotherLoadBalancerLifecycle
extends FeignBlockingLoadBalancerClientTests.TestLoadBalancerLifecycle {
extends RetryableFeignBlockingLoadBalancerClientTests.TestLoadBalancerLifecycle {
@Override
protected String getName() {
@@ -0,0 +1,65 @@
/*
* Copyright 2015-2015 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.openfeign.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
/**
* Mocks the access token provider
*
* @author Mihhail Verhovtsov
*/
public class MockAccessTokenProvider implements AccessTokenProvider {
private OAuth2AccessToken token;
public MockAccessTokenProvider(OAuth2AccessToken token) {
this.token = token;
}
@Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
AccessTokenRequest accessTokenRequest)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException {
return token;
}
@Override
public boolean supportsResource(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
return true;
}
@Override
public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
OAuth2RefreshToken oAuth2RefreshToken, AccessTokenRequest accessTokenRequest)
throws UserRedirectRequiredException {
return null;
}
@Override
public boolean supportsRefresh(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
return false;
}
}
@@ -0,0 +1,79 @@
/*
* Copyright 2015-2015 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.openfeign.security;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
/**
* Mocks the OAuth2 access token
*
* @author Mihhail Verhovtsov
*/
public class MockOAuth2AccessToken implements OAuth2AccessToken {
private String value;
public MockOAuth2AccessToken(String value) {
this.value = value;
}
@Override
public Map<String, Object> getAdditionalInformation() {
return null;
}
@Override
public Set<String> getScope() {
return null;
}
@Override
public OAuth2RefreshToken getRefreshToken() {
return null;
}
@Override
public String getTokenType() {
return null;
}
@Override
public boolean isExpired() {
return false;
}
@Override
public Date getExpiration() {
return null;
}
@Override
public int getExpiresIn() {
return 0;
}
@Override
public String getValue() {
return value;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2015-2015 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.openfeign.security;
import java.util.HashMap;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
/**
* Mocks the current client context
*
* @author João Pedro Evangelista
*/
final class MockOAuth2ClientContext implements OAuth2ClientContext {
private final String value;
MockOAuth2ClientContext(String value) {
this.value = value;
}
@Override
public OAuth2AccessToken getAccessToken() {
return new DefaultOAuth2AccessToken(value);
}
@Override
public void setAccessToken(OAuth2AccessToken accessToken) {
}
@Override
public AccessTokenRequest getAccessTokenRequest() {
DefaultAccessTokenRequest tokenRequest = new DefaultAccessTokenRequest(new HashMap<String, String[]>());
tokenRequest.setExistingToken(new DefaultOAuth2AccessToken(value));
return tokenRequest;
}
@Override
public void setPreservedState(String stateKey, Object preservedState) {
}
@Override
public Object removePreservedState(String stateKey) {
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2015-2019 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.openfeign.security;
import java.util.Collection;
import java.util.Map;
import feign.Request.HttpMethod;
import feign.RequestTemplate;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author João Pedro Evangelista
* @author Tim Ysewyn
*/
public class OAuth2FeignRequestInterceptorTests {
private OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor;
private RequestTemplate requestTemplate;
@Before
public void setUp() {
oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new MockOAuth2ClientContext("Fancy"),
new BaseOAuth2ProtectedResourceDetails());
requestTemplate = new RequestTemplate().method(HttpMethod.GET);
}
@Test
public void applyAuthorizationHeader() {
oAuth2FeignRequestInterceptor.apply(requestTemplate);
Map<String, Collection<String>> headers = requestTemplate.headers();
Assert.assertTrue("RequestTemplate must have a Authorization header", headers.containsKey("Authorization"));
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"),
contains("Bearer Fancy"));
}
@Test(expected = OAuth2AccessDeniedException.class)
public void tryToAcquireToken() {
oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
new BaseOAuth2ProtectedResourceDetails());
OAuth2AccessToken oAuth2AccessToken = oAuth2FeignRequestInterceptor.getToken();
Assert.assertTrue(oAuth2AccessToken.getValue() + " Must be null", oAuth2AccessToken.getValue() == null);
}
@Test
public void configureAccessTokenProvider() {
OAuth2AccessToken mockedToken = new MockOAuth2AccessToken("MOCKED_TOKEN");
oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(mockedToken));
Assert.assertEquals("Should return same mocked token instance", mockedToken,
oAuth2FeignRequestInterceptor.acquireAccessToken());
}
@Test
public void applyAuthorizationHeaderOnlyOnce() {
OAuth2ClientContext oAuth2ClientContext = mock(OAuth2ClientContext.class);
when(oAuth2ClientContext.getAccessToken()).thenReturn(new MockOAuth2AccessToken("MOCKED_TOKEN"));
OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(
oAuth2ClientContext, new BaseOAuth2ProtectedResourceDetails());
oAuth2FeignRequestInterceptor.apply(requestTemplate);
// First idempotent call failed, retry mechanism kicks in, and token has expired
// in the meantime
OAuth2AccessToken expiredAccessToken = mock(OAuth2AccessToken.class);
when(expiredAccessToken.isExpired()).thenReturn(true);
when(oAuth2ClientContext.getAccessToken()).thenReturn(expiredAccessToken);
AccessTokenRequest accessTokenRequest = mock(AccessTokenRequest.class);
when(oAuth2ClientContext.getAccessTokenRequest()).thenReturn(accessTokenRequest);
OAuth2AccessToken newToken = new MockOAuth2AccessToken("Fancy");
oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(newToken));
oAuth2FeignRequestInterceptor.apply(requestTemplate);
Map<String, Collection<String>> headers = requestTemplate.headers();
Assert.assertTrue("RequestTemplate must have a Authorization header", headers.containsKey("Authorization"));
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"), hasSize(1));
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"),
contains("Bearer Fancy"));
}
}
@@ -16,47 +16,41 @@
package org.springframework.cloud.openfeign.support;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.CollectionFormat;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest(classes = PageableSupportTest.Config.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@SpringBootTest(classes = PageableSupportTest.Config.class, webEnvironment = RANDOM_PORT)
public class PageableSupportTest {
@Autowired
private PageableFeignClient feignClient;
@BeforeAll
public static void beforeClass() {
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
}
@AfterAll
public static void afterClass() {
System.clearProperty("server.port");
}
@Test
void shouldProperlyFormatPageable() {
String direction = feignClient.performRequest(PageRequest.of(1, 10, Sort.by(Sort.Order.desc("property"))));
@@ -64,7 +58,7 @@ public class PageableSupportTest {
assertThat(direction).isEqualTo("DESC");
}
@FeignClient(name = "pageable", url = "http://localhost:${server.port}/")
@FeignClient("pageable")
protected interface PageableFeignClient {
@CollectionFormat(feign.CollectionFormat.CSV)
@@ -79,6 +73,7 @@ public class PageableSupportTest {
@RestController
@EnableFeignClients(clients = PageableFeignClient.class)
@Import(NoSecurityConfiguration.class)
@LoadBalancerClient(name = "pageable", configuration = LocalClientConfiguration.class)
protected static class Config {
@GetMapping(path = "/page")
@@ -88,4 +83,19 @@ public class PageableSupportTest {
}
// Load balancer with fixed server list for "local" pointing to localhost
@Configuration(proxyBeanMethods = false)
static class LocalClientConfiguration {
@LocalServerPort
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("pageable",
new DefaultServiceInstance("pageable-1", "pageable", "localhost", port, false));
}
}
}
@@ -108,7 +108,7 @@ class ApacheHttpClientConfigurationTests {
@EnableFeignClients(clients = { ApacheHttpClientConfigurationTestApp.FooClient.class })
static class ApacheHttpClientConfigurationTestApp {
@FeignClient(name = "foo", serviceId = "foo")
@FeignClient(name = "foo")
interface FooClient {
}
@@ -26,14 +26,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -155,8 +156,9 @@ public class FeignClientNotPrimaryTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
@@ -36,7 +38,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
@@ -54,7 +55,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest(classes = FeignHttpClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=feignclienttest", "feign.hystrix.enabled=false",
value = { "spring.application.name=feignclienttest", "feign.circuitbreaker.enabled=false",
"feign.okhttp.enabled=false", "spring.cloud.loadbalancer.retry.enabled=false" })
@DirtiesContext
class FeignHttpClientTests {
@@ -243,8 +244,9 @@ class FeignHttpClientTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
@@ -36,7 +38,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.bind.annotation.PathVariable;
@@ -53,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Olga Maciaszek-Sharma
*/
@SpringBootTest(classes = FeignOkHttpTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=feignclienttest", "feign.hystrix.enabled=false",
value = { "spring.application.name=feignclienttest", "feign.circuitbreaker.enabled=false",
"feign.httpclient.enabled=false", "feign.okhttp.enabled=true",
"spring.cloud.httpclientfactories.ok.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false" })
@DirtiesContext
@@ -245,8 +246,9 @@ class FeignOkHttpTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -26,15 +26,16 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.GetMapping;
@@ -50,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = IterableParameterTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=iterableparametertest",
"logging.level.org.springframework.cloud.openfeign.valid=DEBUG", "feign.httpclient.enabled=false",
"feign.okhttp.enabled=false", "feign.hystrix.enabled=false" })
"feign.okhttp.enabled=false", "feign.circuitbreaker.enabled=false" })
@DirtiesContext
public class IterableParameterTests {
@@ -94,8 +95,9 @@ public class IterableParameterTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -49,9 +49,11 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
@@ -62,7 +64,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.annotation.DateTimeFormat;
@@ -96,7 +97,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@SpringBootTest(classes = ValidFeignClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=feignclienttest",
"logging.level.org.springframework.cloud.openfeign.valid=DEBUG", "feign.httpclient.enabled=false",
"feign.okhttp.enabled=false", "feign.hystrix.enabled=true",
"feign.okhttp.enabled=false", "feign.circuitbreaker.enabled=true",
"spring.cloud.loadbalancer.retry.enabled=false" })
@DirtiesContext
class ValidFeignClientTests {
@@ -808,8 +809,9 @@ class ValidFeignClientTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -23,15 +23,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.cloud.openfeign.testclients.TestClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -84,8 +85,9 @@ public class FeignClientEnvVarTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("localapp",
new DefaultServiceInstance("localapp-1", "localapp", "localhost", port, false));
}
}
@@ -25,15 +25,16 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -118,8 +119,9 @@ public class FeignClientScanningTests {
private int port = 0;
@Bean
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
return ServiceInstanceListSuppliers.from("local",
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
}
}
@@ -15,7 +15,6 @@ eureka:
fetchRegistry: false
#error:
# path: /myerror
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3001
ReadTimeout: 60001
@@ -30,8 +29,6 @@ badClients:
endpoints:
health:
sensitive: false
hystrix:
shareSecurityContext: true
feignClient:
localappName: localapp
methodLevelRequestMappingPath: /hello2
+8 -7
View File
@@ -6,20 +6,26 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<packaging>pom</packaging>
<name>spring-cloud-openfeign-dependencies</name>
<description>Spring Cloud OpenFeign Dependencies</description>
<properties>
<feign.version>10.10.1</feign.version>
<feign-form.version>3.8.0</feign-form.version>
<spring-security-oauth2-autoconfigure.version>2.1.2.RELEASE</spring-security-oauth2-autoconfigure.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring-security-oauth2-autoconfigure.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
@@ -50,11 +56,6 @@
<artifactId>feign-httpclient</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.0-M6</version>
<version>3.0.0</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-starter-openfeign</artifactId>