@@ -122,6 +122,7 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
|
|||||||
* `Encoder` feignEncoder: `SpringEncoder`
|
* `Encoder` feignEncoder: `SpringEncoder`
|
||||||
* `Logger` feignLogger: `Slf4jLogger`
|
* `Logger` feignLogger: `Slf4jLogger`
|
||||||
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
|
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
|
||||||
|
* `CachingCapability` cachingCapability: If `@EnableCaching` annotation is used. Can be disabled via `feign.cache.enabled`.
|
||||||
* `Contract` feignContract: `SpringMvcContract`
|
* `Contract` feignContract: `SpringMvcContract`
|
||||||
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
|
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
|
||||||
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
|
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||||
@@ -141,7 +142,7 @@ Spring Cloud OpenFeign _does not_ provide the following beans by default for fei
|
|||||||
* `Collection<RequestInterceptor>`
|
* `Collection<RequestInterceptor>`
|
||||||
* `SetterFactory`
|
* `SetterFactory`
|
||||||
* `QueryMapEncoder`
|
* `QueryMapEncoder`
|
||||||
* `Capability` (`MicrometerCapability` is provided by default)
|
* `Capability` (`MicrometerCapability` and `CachingCapability` are provided by default)
|
||||||
|
|
||||||
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
||||||
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
||||||
@@ -602,6 +603,22 @@ public class FooConfiguration {
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
=== Feign Caching
|
||||||
|
|
||||||
|
If `@EnableCaching` annotation is used, a `CachingCapability` bean is created and registered so that your Feign client recognizes `@Cache*` annotations on its interface:
|
||||||
|
|
||||||
|
[source,java,indent=0]
|
||||||
|
----
|
||||||
|
public interface DemoClient {
|
||||||
|
|
||||||
|
@GetMapping("/demo/{filterParam}")
|
||||||
|
@Cacheable(cacheNames = "demo-cache", key = "#keyParam")
|
||||||
|
String demoEndpoint(String keyParam, @PathVariable String filterParam);
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
You can also disable the feature via property `feign.cache.enabled=false`.
|
||||||
|
|
||||||
=== Feign @QueryMap support
|
=== Feign @QueryMap support
|
||||||
|
|
||||||
The OpenFeign `@QueryMap` annotation provides support for POJOs to be used as
|
The OpenFeign `@QueryMap` annotation provides support for POJOs to be used as
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 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.Capability;
|
||||||
|
import feign.InvocationHandlerFactory;
|
||||||
|
|
||||||
|
import org.springframework.cache.interceptor.CacheInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows Spring's @Cache* annotations to be declared on the feign client's methods.
|
||||||
|
*
|
||||||
|
* @author Sam Kruglov
|
||||||
|
*/
|
||||||
|
public class CachingCapability implements Capability {
|
||||||
|
|
||||||
|
private final CacheInterceptor cacheInterceptor;
|
||||||
|
|
||||||
|
public CachingCapability(CacheInterceptor cacheInterceptor) {
|
||||||
|
this.cacheInterceptor = cacheInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
|
||||||
|
return new FeignCachingInvocationHandlerFactory(invocationHandlerFactory, cacheInterceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.Module;
|
import com.fasterxml.jackson.databind.Module;
|
||||||
|
import feign.Capability;
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Feign;
|
import feign.Feign;
|
||||||
import feign.RequestInterceptor;
|
import feign.RequestInterceptor;
|
||||||
@@ -50,6 +51,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.cache.interceptor.CacheInterceptor;
|
||||||
import org.springframework.cloud.client.actuator.HasFeatures;
|
import org.springframework.cloud.client.actuator.HasFeatures;
|
||||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||||
@@ -81,6 +83,7 @@ import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResour
|
|||||||
* @author Nguyen Ky Thanh
|
* @author Nguyen Ky Thanh
|
||||||
* @author Andrii Bohutskyi
|
* @author Andrii Bohutskyi
|
||||||
* @author Kwangyong Kim
|
* @author Kwangyong Kim
|
||||||
|
* @author Sam Kruglov
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(Feign.class)
|
@ConditionalOnClass(Feign.class)
|
||||||
@@ -105,6 +108,13 @@ public class FeignAutoConfiguration {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(value = "feign.cache.enabled", matchIfMissing = true)
|
||||||
|
@ConditionalOnBean(CacheInterceptor.class)
|
||||||
|
public Capability cachingCapability(CacheInterceptor cacheInterceptor) {
|
||||||
|
return new CachingCapability(cacheInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass({ Module.class, Page.class, Sort.class })
|
@ConditionalOnClass({ Module.class, Page.class, Sort.class })
|
||||||
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
||||||
|
|||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 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.AccessibleObject;
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import feign.InvocationHandlerFactory;
|
||||||
|
import feign.Target;
|
||||||
|
import org.aopalliance.intercept.MethodInvocation;
|
||||||
|
|
||||||
|
import org.springframework.cache.interceptor.CacheInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows Spring's @Cache* annotations to be declared on the feign client's methods.
|
||||||
|
*
|
||||||
|
* @author Sam Kruglov
|
||||||
|
*/
|
||||||
|
public class FeignCachingInvocationHandlerFactory implements InvocationHandlerFactory {
|
||||||
|
|
||||||
|
private final InvocationHandlerFactory delegateFactory;
|
||||||
|
private final CacheInterceptor cacheInterceptor;
|
||||||
|
|
||||||
|
public FeignCachingInvocationHandlerFactory(
|
||||||
|
InvocationHandlerFactory delegateFactory,
|
||||||
|
CacheInterceptor cacheInterceptor
|
||||||
|
) {
|
||||||
|
this.delegateFactory = delegateFactory;
|
||||||
|
this.cacheInterceptor = cacheInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
|
||||||
|
final InvocationHandler delegateHandler = delegateFactory.create(target, dispatch);
|
||||||
|
return (proxy, method, argsNullable) -> {
|
||||||
|
Object[] args = Optional.ofNullable(argsNullable).orElseGet(() -> new Object[0]);
|
||||||
|
return cacheInterceptor.invoke(new MethodInvocation() {
|
||||||
|
@Override
|
||||||
|
public Method getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] getArguments() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object proceed() throws Throwable {
|
||||||
|
return delegateHandler.invoke(proxy, method, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getThis() {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccessibleObject getStaticPart() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020-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.net.UnknownHostException;
|
||||||
|
|
||||||
|
import feign.Contract;
|
||||||
|
import feign.RequestLine;
|
||||||
|
import feign.RetryableException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
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.cache.CacheManager;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.cache.interceptor.SimpleKey;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sam Kruglov
|
||||||
|
*/
|
||||||
|
@SpringBootTest(classes = FeignClientCacheTests.TestConfiguration.class)
|
||||||
|
@DirtiesContext
|
||||||
|
public class FeignClientCacheTests {
|
||||||
|
|
||||||
|
private static final String CACHE_NAME = "foo-cache";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FooClient foo;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cacheExists(@Autowired CacheManager cacheManager) {
|
||||||
|
assertThat(cacheManager.getCache(CACHE_NAME)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void interceptedCallsReal() {
|
||||||
|
assertThatExceptionOfType(RetryableException.class)
|
||||||
|
.isThrownBy(foo::getWithCache)
|
||||||
|
.withRootCauseInstanceOf(UnknownHostException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonInterceptedCallsReal() {
|
||||||
|
assertThatExceptionOfType(RetryableException.class)
|
||||||
|
.isThrownBy(foo::getWithoutCache)
|
||||||
|
.withRootCauseInstanceOf(UnknownHostException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class givenCached {
|
||||||
|
|
||||||
|
String cachedValue = "cached";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@Autowired CacheManager cacheManager) {
|
||||||
|
cacheManager.getCache(CACHE_NAME).put(SimpleKey.EMPTY, cachedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void interceptedReturnsCached() {
|
||||||
|
assertThat(foo.getWithCache()).isSameAs(cachedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonInterceptedCallsReal() {
|
||||||
|
assertThatExceptionOfType(RetryableException.class)
|
||||||
|
.isThrownBy(foo::getWithoutCache)
|
||||||
|
.withRootCauseInstanceOf(UnknownHostException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableFeignClients(clients = FooClient.class)
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@EnableCaching
|
||||||
|
protected static class TestConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FeignClient(name = "foo", url = "http://foo", configuration = FooConfiguration.class)
|
||||||
|
interface FooClient {
|
||||||
|
|
||||||
|
@RequestLine("GET /with-cache")
|
||||||
|
@Cacheable(cacheNames = CACHE_NAME)
|
||||||
|
String getWithCache();
|
||||||
|
|
||||||
|
@RequestLine("GET /without-cache")
|
||||||
|
String getWithoutCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FooConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Contract feignContract() {
|
||||||
|
return new Contract.Default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user