Hi,
sorry this is not bug, but probably question.
We use org.springframework.cloud.openfeign.FeignClient together with springboot 2 framework. Some time ago we have migrated from old feign framework based on springboot 1.x.
What I found that is that for each interface annotated with @FeignClient with defined same custom configuration, all beans defined there are prototypes. Im not sure if it worked same way also in old version.
Example of configuration class
public class ClientConfiguration {
private static final String LOGIN_URL = "login-url";
private static final String USER_NAME = "username";
private static final String PSWD = "password";
@Bean
public ErrorDecoder errorDecoder() {
return new DummyErrorDecoder();
}
@Bean
public Retryer retryer() {
return new DummyRetryer();
}
@Bean
@ConditionalOnProperty({LOGIN_URL, USER_NAME, PSWD})
public RequestInterceptor authFeignRequestInterceptor(ApplicationContext ctx) {
return new DummyRequestInterceptor();
}
}
Example of feign clients:
@FeignClient(contextId = "ApiClient1", name = "manager", url = "${manager.url}", configuration = ClientConfiguration.class)
public interface ApiClient1 extends Api1 {
}
@FeignClient(contextId = "ApiClient2", name = "manager", url = "${manager.url}", configuration = ClientConfiguration.class)
public interface ApiClient2 extends Api2 {
}
When I run application ApiClient1 and ApiClient2 have own set of beans defined in ClientConfiguration. What I want to achieve at least is to have only one instance of RequestInterceptor, because it keeps current authorization token with synchronized method to fetch new one.
Is possible to set up what I need. I dont want to public interceptor as component, because I use also another feign client with different interceptors.
Can somebody help me how to do it? thanks brpalo
Comment From: cbezmen
Hi @metalpalo,
I don't know I understand you clearly but please check the code below,
package com.example.metalpalo;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@Slf4j
@EnableFeignClients
public class MetalpaloApplication {
@Autowired
private TestClient testClient;
@Autowired
private TestClientSecond testClientSecond;
public static void main(String[] args) {
SpringApplication.run(MetalpaloApplication.class, args);
}
@Bean
public ApplicationRunner test() {
return args -> {
User user = new User();
user.setName("Can");
User test = testClient.test(user);
User testSecond = testClientSecond.test(user);
log.info("{} {}", test, testSecond);
};
}
@FeignClient(name = "test", url = "http://localhost:8080", configuration = CoreFeignConfiguration.class)
public interface TestClient {
@PostMapping(value = "/user")
User test(@RequestBody User test);
}
@FeignClient(name = "testSecond", url = "http://localhost:8080", configuration = CoreFeignConfiguration.class)
public interface TestClientSecond {
@PostMapping(value = "/user")
User test(@RequestBody User test);
}
class CoreFeignConfiguration {
@Bean
RequestInterceptor authFeignRequestInterceptor(DummyRequestInterceptor dummyRequestInterceptor) {
log.info("{}", dummyRequestInterceptor.toString());
return dummyRequestInterceptor;
}
}
@Component
static class DummyRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("test", "test");
}
}
@RestController
@Slf4j
public static class TestController {
@PostMapping(value = "/user")
public User test(@RequestBody User test) {
log.info(String.valueOf(test));
return test;
}
}
@Data
public static class User {
private String name;
}
}
The log will be like:
2021-06-23 20:54:35.603 INFO 3913 --- [ main] c.e.metalpalo.MetalpaloApplication : com.example.metalpalo.MetalpaloApplication$DummyRequestInterceptor@45cec376
2021-06-23 20:54:35.679 INFO 3913 --- [ main] c.e.metalpalo.MetalpaloApplication : com.example.metalpalo.MetalpaloApplication$DummyRequestInterceptor@45cec376
2021-06-23 20:54:35.710 INFO 3913 --- [ main] c.e.metalpalo.MetalpaloApplication : com.example.metalpalo.MetalpaloApplication$DummyRequestInterceptor@45cec376
2021-06-23 20:54:36.139 INFO 3913 --- [nio-8080-exec-1] .e.m.MetalpaloApplication$TestController : MetalpaloApplication.User(name=Can)
2021-06-23 20:54:36.158 INFO 3913 --- [nio-8080-exec-2] .e.m.MetalpaloApplication$TestController : MetalpaloApplication.User(name=Can)
2021-06-23 20:54:36.160 INFO 3913 --- [ main] c.e.metalpalo.MetalpaloApplication : MetalpaloApplication.User(name=Can) MetalpaloApplication.User(name=Can)
Comment From: metalpalo
hi cbezmen,
I think you understand me.
Solution you suggested is to define DummyRequestInterceptor as singleton bean in class annotated with @Configuration or @SpringBootApplication(what it is same).
But in this case DummyRequestInterceptor is scanned by spring and availaible also for another feign clients in application.
I have another default FeignClientConfiguration for remaining feign clients without customer configuration class. They use OAuth2 authorization.
@Configuration
public class FeignClientConfiguration {
@Bean
RequestInterceptor authFeignRequestInterceptor(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), oAuth2ProtectedResourceDetails);
}
}
Example of another client with this default configuration:
@FeignClient(name = "testThird", url = "http://localhost:9090")
public interface TestClientThird {
@PostMapping(value = "/user")
User test(@RequestBody User test);
}
I know that name authFeignRequestInterceptor in custom configuration must be same as default one with OAuth2. Otherwise both interceptors are added to feign interceptors list for ApiClient1 and ApiClient2 from my first message. I this case I'm not sure if both types of clients will use correct interceptors.
I would like to have simplest solution as possible without some excluding in component scan or something else
thanks brpalo
Comment From: cbezmen
@metalpalo Please remove @Configuration annotation from FeignClientConfiguration.
If you add @Configuration annotation, it will act as default Feign configuration class and add configured beans to all feign clients.
Comment From: metalpalo
@cbezmen but I want to keep FeignClientConfiguration as is it, all remaining feign clients should use singleton components from there. Removing @Configuration and putting this class on all remaining feign client will cause identical problem as I have now, but generally.
Comment From: metalpalo
maybe I will use this stupid solution. implement spring managed bean something like this:
@Component
public class DummyRequestInterceptorSingletonFactory {
private DummyRequestInterceptor interceptor;
@Autowired
public DummyRequestInterceptorSingletonFactory() {
interceptor = new DummyRequestInterceptor();
}
public RequestInterceptor getObject() {
return interceptor;
}
}
and then modify your suggested example like this?
public class CoreFeignConfiguration {
@Bean
RequestInterceptor authFeignRequestInterceptor(DummyRequestInterceptorSingletonFactory factory) {
return factory.getObject;
}
}
it seems very simple, we will see thanks brpalo
Comment From: OlgaMaciaszek
@metalpalo Does the workaround work for you?
Comment From: gklp
@metalpalo you need to provide yet another feign builder in here for separated clients. I mean, you should not define as a bean. If you do that. The auto configuration catches it and injects it globally into application context for all feign clients or feign interfaces.
It's running example : repo link
IMHO, your way is correct but it has missing parts.
public class FooConfiguration
{
public RequestInterceptor interceptor()
{
return requestTemplate -> System.out.println("Foo fired!");
}
@Bean
public Feign.Builder fooBuilder()
{
return Feign.builder().requestInterceptor(interceptor()).client(new Default(null, null));
}
}
other configuration
public class BarConfiguration
{
public RequestInterceptor interceptor()
{
return new RequestInterceptor()
{
@Override
public void apply(RequestTemplate requestTemplate)
{
System.out.println("Bar fired!");
}
};
}
@Bean
public Feign.Builder barBuilder()
{
return Feign.builder().requestInterceptor(interceptor());
}
}
When the clients are run with different configurations. Every client run their own interceptor.
Look at that; https://cloud.spring.io/spring-cloud-openfeign/reference/html/. The section is starting :
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 collision of these configuration beans.
I've tried it on Greenwich by the way. It works perfectly. But 2020.0.3 version is not working correctly. Was the behavior of configuration changed? I don't know. It might be a bug. @OlgaMaciaszek
Comment From: metalpalo
@metalpalo Does the workaround work for you?
If you've meant what I suggested, that means the singleton factory injection, then yes it works
Comment From: metalpalo
@metalpalo you need to provide yet another feign builder in here for separated clients. I mean, you should not define as a bean. If you do that. The auto configuration catches it and injects it globally into application context for all feign clients or feign interfaces.
I don't understand what you meant. The class CoreFeignConfiguration is not annotated by @Configuration so it is not visible globally for all feigns.
Comment From: gklp
@metalpalo you need to provide yet another feign builder in here for separated clients. I mean, you should not define as a bean. If you do that. The auto configuration catches it and injects it globally into application context for all feign clients or feign interfaces.
I don't understand what you meant. The class CoreFeignConfiguration is not annotated by @configuration so it is not visible globally for all feigns.
authFeignRequestInterceptor is annotated by @Bean, so where is the bean on the application context, if it is not visible globally?
Comment From: metalpalo
@metalpalo you need to provide yet another feign builder in here for separated clients. I mean, you should not define as a bean. If you do that. The auto configuration catches it and injects it globally into application context for all feign clients or feign interfaces.
I don't understand what you meant. The class CoreFeignConfiguration is not annotated by @configuration so it is not visible globally for all feigns.
authFeignRequestInterceptor is annotated by @bean, so where is the bean on the application context, if it is not visible globally?
This bean is visible in child context for concrete feign client I think. Please correct me if Im wrong.
The feign configuration classes I don't generally annotate with @Configuration. That means that global application context doesnt dispose of beans defined there. But for some feign clients(without defined custom clientConfiguration) I used default spring configuration class(annotated with @Configuration) and there I define global beans which are visible globally. But what I keep in them mind is that names of interceptors is important, If bean in custom client configuration class is forexample authFeignRequestInterceptor and in the spring configuration class(globally) is defaultAuthFeignRequestInterceptor then both interceptors are joined to feign with defined custom configuration. So I keep same bean name for global and custom interceptors and then second one wins. Maybe something changed in feign construction last 2-3 years and this rule is not required anymore. Please correct me if Im wrong
Comment From: gklp
I guess that the behavior of feign construction was changed. Because I could not do that your situation in older version. I had to use separated FeignBuilder. If your solution is working as expected, it is good news for me.
@Bean
public Feign.Builder barBuilder() {
return Feign.builder().requestInterceptor(interceptor()); // interceptor is not bean in here to prevent globally injection.
}
Comment From: OlgaMaciaszek
Closing the issue as a working workaround is in place and it's not high priority at the moment.