美文网首页我的微服务
基于Feign的局部请求拦截

基于Feign的局部请求拦截

作者: AmosZhu | 来源:发表于2020-02-21 11:00 被阅读0次

    由于项目的要求,不能对所有基于Feign的进行拦截,需要对不同的Feign请求进行不同的拦截,经过资料的收集整理以及SpringCloud中对于Feign的集成的源码的阅读,解决了针对Feign请求的局部拦截

    本项目中SpringCloud的版本是Camden.SR6版本

    背景说明

    在既有的项目上进行二次开发,服务A需要请求服务B同时需要将服务A中请求的消息头相关信息传送给服务B,但是由于既有项目中的相关设计,不支持feign请求的全局拦截,只能针对服务A请求服务B的feign请求进行拦截,所以开发了如下的方法;

    这里说明下,之所以采用Feign是由于Feign添加支持负载均衡,这点尤为重要。

    思路说明

    既然当前SpringCloud的版本不支持Feign请求的拦截,那么只能自己开发拦截的方法来拦截Feign请求了,整理资料有如下两种思路:

    1. Feign内部也是使用Ribbon来完成支持负载均衡的,所以抛开Feign,直接使用Ribbon也是可以的;

    为了扩展方便,可以采用扫描自定义注解和AOP拦截的方式,然后通过前置方法将消息头相关内容存储到请求中

    这个思路简单易用,而且方法都是自己开发的,出现问题,定位和修改都是很容易的,但是这种方法也相当于重新开发了一种新的功能,工作量和代码量肯定是不小的,

    1. 还是使用Feign,既然SpringCloud当前版本不支持,那么就利用原生的Feign来自己封装;

    SpringCloud的@FeignClient也是基于原生的Feign的基础上进行封装的,所以我们也可以开发新的封装,使之支持目前的需求,对Feign的请求进行局部拦截

    如果想进行新的封装,我们可以借鉴SPringCloud对Feign的封装方法,这里我们可以参考 FeignClient源码深度解析这篇文章,说的很详细,在这里感谢大佬的分享。

    代码实现

    废话不多说,为了让代码改动量小,并且利用Feign的特性:(一个接口就可以访问其他的项目),我们选择第二种方法来实现

    在这里对于SpringCloud支持Feign的封装思路就显得比较重要了,不过在这之前,我们可以使用原生的Feign来支持请求的拦截

    首先是依赖的支持

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.2.6.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-ribbon</artifactId>
            <version>8.18.0</version>
        </dependency>
    
    1. 首先我们定义一个接口,该接口配置Feign访问其他项目的路径
        /**
         * @author: amos
         * @Description: 访问其他业务的请求
         * @date: 2019/12/23 0023 下午 17:39
         * @Version: V1.0
         */
        public interface BizClient {
            @RequestMapping(value = "/biz/list", method = RequestMethod.POST)
            Result list(@RequestBody BizDTO dto);
        }
    

    注意:该接口上没有添加 @FeignClient 注解,因为目前项目是支持SpringCloud的Feign使用方式的,如果添加了注解,就会直接走SpringCloud的Feign请求方式

    1. 原生的Feign使用方式
        /**
         * 
         * @author: amos
         * @Description: 基于原生的Feign请求来获取请求访问对象     
         * @date: 2020/2/19 0019 下午 16:09
         * @Version: V1.0
         */
        @Configuration
        public class BasicFeignBuilderConfig {
        
            public Client client;
        
            private HttpMessageConverter jsonConverter;
        
            private ObjectFactory<HttpMessageConverters> converter;
            private static final String CLINET_URL = "http://APPLICATION-NAME";
            /**
             * 初始化client
             */
            @PostConstruct
            public void initClient() {
                this.client = RibbonClient.create();
                this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
                this.converter = () -> new HttpMessageConverters(jsonConverter);
            }
             /**
             * 利用Feign来获取接口访问对象
             *
             * @param clazz
             * @param <T>
             * @return
             */
            public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
                T t = Feign.builder()
                        .encoder(new SpringEncoder(converter))
                        .client(client)
                        .decoder(new SpringDecoder(converter))
                        .contract(new SpringMvcContract())
                        .requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
                        .target(clazz, CLINET_URL);
                return t;
            }   
            /**
             * 将BizClient注册到SpringContext的上下文中
             *
             * @return
             */
            @Bean("bizClient")
            public BizClient bizClient() {
                return this.feignBuilderRequestInterceptor(BizClient.class);
            }
        }
    

    至此使用原生的Feign结束,但是一测试就报错

    java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: APPLICATION-NAME

    从网上查询资料,并进行了相关的依赖和配置项 都没有生效

    1. 阅读源码,了解SpringCloud支持Feign的原理

    上面的办法既然不可行,主要的问题是Ribbon识别不了我们的实例名,也就是代码中Client有问题,但是SpringCloud的Feign却是可以支持的,所以这里的关键就是SpringCloud中的Feign是怎么支持的Ribbon的,然后将他支持的方式移到我们目前代码,解决由于Ribbon造成的负载均衡的问题就可以了。

    为此,我们需要阅读SpringCloud支持Feign方面的相关的源码,源码的阅读可以参考上面的博客链接,说明的非常详细,下面我们主要分析下源码中的代理工厂的代码;

    FeignClientFactoryBean这个类就是FeignClient的代理工厂类,我们看下工厂类的入口getObject()方法:

        @Override
        public Object getObject() throws Exception {
            // 从Spring的ApplicationContext中获取FeignContext
            FeignContext context = applicationContext.getBean(FeignContext.class);
            // 利用构造器来构造Feign的对象
            Feign.Builder builder = feign(context);
            .....
        }
    

    这里我们主要看下构造器中是怎么获取Client对象的,我们需要知道他是怎么处理支持负载均衡的,我们追踪到对应的代码:

        protected <T> T getOptional(FeignContext context, Class<T> type) {
            return context.getInstance(this.name, type);
        }
        
        protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
                HardCodedTarget<T> target) {
            // 这里就是我们需要的client对象 
            // 通过上面的代码我们知道 FeignContext 中获取对应的Bean
            Client client = getOptional(context, Client.class);
            if (client != null) {
                builder.client(client);
                Targeter targeter = get(context, Targeter.class);
                return targeter.target(this, builder, context, target);
            }
    
            throw new IllegalStateException(
                    "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
        }
    

    至此,我们知道了Client对象主要来源于FeignContext中,而FeignContext是来源于ApplicationContext中,到这里就非常请求了,我们需要从ApplicationContext中获取FeignContext,然后再从FeignContext中获取Client对象。

    所以我们需要改造上面 BasicFeignBuilderConfig的代码:

        /**
         * 
         * @author: amos
         * @Description: 基于原生的Feign请求来获取请求访问对象     
         * @date: 2020/2/19 0019 下午 16:09
         * @Version: V1.0
         */
        @Configuration
        public class BasicFeignBuilderConfig implements ApplicationContextAware{
        
            public Client client;
        
            private HttpMessageConverter jsonConverter;
        
            private ObjectFactory<HttpMessageConverters> converter;
            
            private static final String CLINET_URL = "http://APPLICATION-NAME";
            
            private ApplicationContext applicationContext;
    
            @Override
            public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
                this.applicationContext = applicationContext;
            }
            /**
             * 初始化client
             */
            @PostConstruct
            public void initClient() {
                this.client =  FeignContext context = applicationContext.getBean(FeignContext.class);
                this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
                this.converter = () -> new HttpMessageConverters(jsonConverter);
            }
             /**
             * 利用Feign来获取接口访问对象
             *
             * @param clazz
             * @param <T>
             * @return
             */
            public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
                T t = Feign.builder()
                        .encoder(new SpringEncoder(converter))
                        .client(client)
                        .decoder(new SpringDecoder(converter))
                        .contract(new SpringMvcContract())
                        .requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
                        .target(clazz, CLINET_URL);
                return t;
            }   
            /**
             * 将BizClient注册到SpringContext的上下文中
             *
             * @return
             */
            @Bean("bizClient")
            public BizClient bizClient() {
                return this.feignBuilderRequestInterceptor(BizClient.class);
            }
        }
    

    上面的代码主要是 类实现ApplicationContextAware接口来获取 ApplicationContext对象,然后从ApplicationContext对象中获取FeignContext对象,再获取到我们需要的Client对象即可;

    至此已经完全结束了,我们可以在业务代码中直接注入 BizClient 直接调用对应得方法了。

        @Autowired
        BizClient bizClient
        
        public Result list(BizDTO dto){
            return bizClient.list(dto);
        }
    

    上面的代码还可以再进行封装,如果有多个BizClient的业务请求,可以通过自定义注解来实现系统在启动的时候,扫描自定义的注解,然后同样利用代理工厂的方法生成实例对象,然后注入到Spring的ApplicationContext中,方便业务直接拿来使用。

    逻辑和Spring支持Feign的逻辑是一样的,主要依赖ImportBeanDefinitionRegistrarResourceLoaderAwareBeanClassLoaderAware三个类。

    我会在下一篇博文中基于该方法来说明,如何实现系统启动将自定义注解的bean注入到Spring的ApplicationContext

    相关文章

      网友评论

        本文标题:基于Feign的局部请求拦截

        本文链接:https://www.haomeiwen.com/subject/gezqqhtx.html