Spring-cloud-netflix: Feign client with custom Configuration (custom RequestInterceptor) picking up another client Configuration (RequestInterceptor)

Created on 28 Feb 2018  ·  7Comments  ·  Source: spring-cloud/spring-cloud-netflix

Hi,

This seems to be a bug, given that this is suppose to be supported as of #288.

I am currently using Spring Boot 2.0.0.RC2 with Spring Cloud Finchley.M7.

I have two Feign clients, each with a custom Feign Configuration class adding a custom RequestInterceptor. One RequestInterceptor adds a header for auth and another is an expensive one because it does all the OAuth2 Authorization Code flow.

@FeignClient(name = "gAuthifyApiClient", url = "{gAuthifyUrl}", configuration = GAuthifyApiClient.FeignClientConfiguration.class, decode404 = true)
public interface GAuthifyApiClient {
...
    @Configuration("gAuthifyFeignClientConfiguration")
    class FeignClientConfiguration {
        @Bean
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor(@Value("${gauthify.api.key}") String apiKey) {
            return new BasicAuthRequestInterceptor("", apiKey);
        }
    }
...
}
@FeignClient(name = "profileIdApiClient", url = "{profileIdUrl}", configuration = ProfileIdApiClient.FeignClientConfiguration.class, decode404 = true)
public interface ProfileIdApiClient {
...
    @Configuration("profileIdFeignClientConfiguration")
    class FeignClientConfiguration {

        @Bean
        public BearerHeaderAuthRequestInterceptor bearerHeaderAuthRequestInterceptor() {
            return new BearerHeaderAuthRequestInterceptor();
        }
    }
...
    class BearerHeaderAuthRequestInterceptor implements RequestInterceptor {
       //Expensive OAuth2 flow logic
    }
...
}

The problem is that the client that uses the former (header auth) is picking up both interceptors. Hence, when using the first client, both interceptors are triggered.

I did some debugging and it seems it has to do with the hierarchical nature of the FactoryBean used to search for RequestInterceptors for the named factory bean.

More specifically...

FeignClientFactoryBean.configureUsingConfiguration(...) {
...
   Map<String, RequestInterceptor> requestInterceptors = **context.getInstances**(this.name, RequestInterceptor.class);
...
}
NamedContextFactory.**getInstances**(String name, Class<T> type) {
    AnnotationConfigApplicationContext context = getContext(name);
    if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) {
        return **BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);**
    }
    return null;
}
**BeanFactoryUtils.beansOfTypeIncludingAncestors(ListableBeanFactory lbf, Class<T> type)**
            throws BeansException {
        Assert.notNull(lbf, "ListableBeanFactory must not be null");
        Map<String, T> result = new LinkedHashMap<>(4);
        result.putAll(lbf.getBeansOfType(type));
        **if (lbf instanceof HierarchicalBeanFactory)** {
            HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf;
            if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) {
                Map<String, T> parentResult = beansOfTypeIncludingAncestors(
                        (ListableBeanFactory) hbf.getParentBeanFactory(), type);
                parentResult.forEach((beanName, beanType) -> {
                    if (!result.containsKey(beanName) && !hbf.containsLocalBean(beanName)) {
                        result.put(beanName, beanType);
                    }
                });
            }
        }
        return result;
    }

Any ideas? Is this already supported in the specified version?

Thanks in advance!

Most helpful comment

Can you try removing the @Configuration annotations? They are not necessary. http://cloud.spring.io/spring-cloud-static/Edgware.SR2/single/spring-cloud.html#spring-cloud-feign-overriding-defaults

Also as an FYI the Feign code has been moved to its own top level project, so future Feign specific issues should be opened in https://github.com/spring-cloud/spring-cloud-openfeign

All 7 comments

Can you try removing the @Configuration annotations? They are not necessary. http://cloud.spring.io/spring-cloud-static/Edgware.SR2/single/spring-cloud.html#spring-cloud-feign-overriding-defaults

Also as an FYI the Feign code has been moved to its own top level project, so future Feign specific issues should be opened in https://github.com/spring-cloud/spring-cloud-openfeign

Thanks @ryanjbaxter. I was just writing a comment about I fixed the issue by removing @Configuration from Feign configuration classes.

Thanks anyway for you promptly reply and for pointing out about where to open issues for this component.

This issue can be deleted or closed.

@ryanjbaxter BTW, looking at the documentation you outlined, it clearly states:

Creating a bean of one of those type and placing it in a @FeignClient configuration (such as FooConfiguration above) allows you to override each one of the beans described. Example:

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}

The documentation seems to be wrong. That is probably the cause that made me configure it that way.

I re-read it and there is a note about annotating with @Configuration.

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.

You were right, I didn't take that into account.

Thanks!

Hello, I am using SpringCloud Finchley.RELEASE with SpringBoot 2.0.3.RELEASE.
I want to custom my Feign Client and add custom RequestInterceptor(ReportExportInterceptor ) . I have read you talk above, and my Feign Client Configuration Class was not annotated with @Configuration. However the interceptor
was not triggered.
When the ReportExportInterceptor annotated with @Configuration , its work.When I add this code , its not work.
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,value = ReportExportClientService.ReportExportInterceptor.class)}) public class HolderSaasStoreAggregationMerchantApplication {}

So I am not sure what`s wrong with my code. Could you give me some advice? Thank you!

this is my Feign Client code.

`@FeignClient(name = "holder-saas-store-report", configuration = ReportExportClientService.ReportExportInterceptor.class)
public interface ReportExportClientService {

@PostMapping("file/export/{exportType}")
ExportRespDTO export(@PathVariable("exportType") Integer exportType);


/**
 * 拦截器为了传递request不确定的content内容
 */
class ReportExportInterceptor {

    @Bean("reportInterceptor")
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                        .getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                Enumeration<String> headerNames = request.getHeaderNames();
                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        String name = headerNames.nextElement();
                        String values = request.getHeader(name);
                        template.header(name, values);
                    }
                }
                String paramString = getParamString(request);
                template.body(paramString);
            }

            public String getParamString(HttpServletRequest request) {
                String param = null;
                try {
                    BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
                    StringBuilder responseStrBuilder = new StringBuilder();
                    String inputStr;
                    while ((inputStr = streamReader.readLine()) != null) {
                        responseStrBuilder.append(inputStr);
                    }
                    JSONObject jsonObject = JSONObject.parseObject(responseStrBuilder.toString());
                    if (jsonObject != null) {
                        param = jsonObject.toJSONString();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return param;
            }
        };
    }

}

}
`

Hi.

I can tell you what it worked for me...

My Application

@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Notice that SpringBootApplication contains a ComponentScan.excludeFilters.

Feign client

@FeignClient(name = "acmeApiClient", url = "${acme.api.url}", configuration = AcmeApiClient.Configuration.class)
public interface AcmeApiClient {

    @GetMapping("/users")
    ResponseEntity getUsers();

    class Configuration {
        @Bean
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor(@Value("${acme.api.key}") String apiKey) {
            return new BasicAuthRequestInterceptor("", apiKey);
        }
    }

I would try removing the @ComponentScan.excludeFilters and adding both annotations, @SpringBootApplication and @EnableFeignClients.

As a side note, I would suggest renaming ReportExportInterceptor to Configuration, as it is a configuration class that configures an RequestInterceptor.

Hope it helps. Please let me know how it went.

Bests

Thank for you reply!
I create a new test project,do as you say.It worked!!!
But I have a great confuse that why it didn`t work on my another project.Any Environmental problems?
Do you have any ideas?

Was this page helpful?
0 / 5 - 0 ratings