美文网首页编程篇
在纯Spring环境中使用Feign来进行声明式HTTP调用

在纯Spring环境中使用Feign来进行声明式HTTP调用

作者: 一帅 | 来源:发表于2017-12-20 15:59 被阅读0次

    在很多小型系统中,HTTP调用是系统间通信最为重要的手段之一。但是HTTP调用对于开发者而言又极其的繁琐,有没有更优雅的方式来拯救HTTP调用呢?没错,就是Feign

    Feign之前的HTTP调用

    在Feign之前,我们在进行HTTP调用的时候,更多的是选择使用原生的Apache HTTP Client 库来调用。所以在项目中会存在诸如HtppUtil之类的公共方法(有的时候会有好多这种httputil的类,而且会有好多静态方法,搞得调用方一脸懵逼)。比如

    public static Map<String, Object> sendPostRequest(String reqURL, String sendData, boolean isEncoder,
                String encodeCharset, String decodeCharset)
        {
            Map<String, Object> rs = new HashMap<String, Object>();
            String responseContent = null;
            long responseLength = 0; // 响应长度
            HttpClient httpClient = new DefaultHttpClient();
    
            HttpPost httpPost = new HttpPost(reqURL);
            // httpPost.setHeader(HTTP.CONTENT_TYPE,
            // "application/x-www-form-urlencoded; charset=UTF-8");
            httpPost.setHeader(HTTP.CONTENT_TYPE, "application/x-www-form-urlencoded");
            try
            {
                if (isEncoder)
                {
                    List<NameValuePair> formParams = new ArrayList<NameValuePair>();
                    for (String str : sendData.split("&"))
                    {
                        formParams.add(new BasicNameValuePair(str.substring(0, str.indexOf("=")), str.substring(str
                                .indexOf("=") + 1)));
                    }
                    httpPost.setEntity(new StringEntity(URLEncodedUtils.format(formParams, encodeCharset == null ? "UTF-8"
                            : encodeCharset)));
                }
                else
                {
                    httpPost.setEntity(new StringEntity(sendData));
                }
    
                HttpResponse response = httpClient.execute(httpPost);
                HttpEntity entity = response.getEntity();
                if (null != entity)
                {
                    responseLength = entity.getContentLength();
                    responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
                    EntityUtils.consume(entity);
                }
                rs.put(RS_STATUS, response.getStatusLine().getStatusCode());
                rs.put(RS_LENGTH, responseLength);
                rs.put(RS_CONTENT, responseContent);
                LOGGER.debug("请求URL:{}", reqURL);
                LOGGER.debug("请求响应:\n{}\n{}", response.getStatusLine(), responseContent);
                return rs;
            }
            catch (Exception e)
            {
                LOGGER.error("与[" + reqURL + "]通信过程中发生异常,堆栈信息如下", e);
            }
            finally
            {
                httpClient.getConnectionManager().shutdown();
            }
            return null;
        }
    

    如果系统间调用非常少的话,那么这种方式用起来其实也没多麻烦。但是如果有很多系统,那么系统间的交互就会很多,那么这种HTTP调用就会非常多,那么你就会发现其实很多的调用方做了很多重复的事情。比如调用方一般是这么调用的。

            Map<String, Object> resMap = HttpUtil.sendPostRequest("url", JsonUtil.toJSONString(resMap));
            String content = (String) resMap.get("content");
            JSONObject resJson = JSONObject.parseObject(content);
            if (JsonUtil.CODE_SUCCESS.equals(resJson.getString("code")))
            {
                // Do Something
                return resultMap;
            }
            else
            {
                return null;
            }
    

    这么做对调用方而言,又几个需要重复做的事情

    • 如果是POST请求,那么POST请求的序列化需要调用方来做
    • 如果是GET请求,那么如果在URL中有参数,需要调用方手动拼凑
    • HTTP请求的返回结果需要调用方来序列化
    • 如果是POST请求,那么还需要区分请求是application/x-www-form-urlencoded形式的还是application/json格式的

    针对以上的问题,其实都是可以通过优化HttpUtil中的提供的方法来实现的。但是也会导致方法签名很复杂,而且会同时存在各种重载的静态方法,搞得调用者该一脸懵逼。

    那么到底有没有一种方式来屏蔽这种底层的通信协议呢?

    mybatis的mapper调用方法给我们的启示

    用过mybatis的mapper调用方法的同学就知道,那种调用方式上的酣畅淋漓。这你不言而喻啊。为什么呢,因为你需要写接口以及遵守规约的sqlmapper文件,然后实现类都不需要写,就可以使用mybatis了。
    就类似于下面这样

    public interface MineMapper
    {
        public List<EmStdomReturn> queryReturnRecords(@Param("orderId")Long orderId);
    }
    

    然后在mapper文件中写上对应的id为queryReturnRecords的select语句就好了。调用方就可以直接注入MineMapper来使用了。

    那么HTTP调用能不能也像这样,只需要声明接口,然后就可以直接调用了呢?

    SpringClound中的Feign-client

    其实如果你使用SpringBoot或者SpringClound的话,那么其实已经实现了,而且注解都是使用的SpringMVC中的注解,压根不需要什么学习成本,真是开发者的福音啊。那就先来看一下吧

    @FeignClient(name = "ea")  //  [A]
    public interface AdvertGroupRemoteService {
    
        @RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET) // [B]
        AdvertGroupVO findByGroupId(@PathVariable("groupId") Integer adGroupId) // [C]
    
        @RequestMapping(value = "/group/{groupId}", method = RequestMethod.PUT)
        void update(@PathVariable("groupId") Integer groupId, @RequestParam("groupName") String groupName)
    
    
    • A: @FeignClient用于通知Feign组件对该接口进行代理(不需要编写接口实现),使用者可直接通过@Autowired注入。

    • B: @RequestMapping表示在调用该方法时需要向/group/{groupId}发送GET请求。

    • C: @PathVariable与SpringMVC中对应注解含义相同。
      具体可以参照这篇文章使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务

    但是这个只能在SpringBoot或者SpringClound中使用,对于还没有将应用迁移到SpringBoot或者SpringClound的开发者而言,就只能眼馋了。那么有没办法在纯Spring环境中使用呢?

    在纯Spring环境中使用Feign

    首先需要在maven依赖中加上以下依赖

    <dependency>
        <groupId>com.netflix.feign</groupId>
        <artifactId>feign-core</artifactId>
        <version>8.18.0</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.netflix.feign</groupId>
        <artifactId>feign-jackson</artifactId>
        <version>8.18.0</version>
    </dependency>
    <dependency>
        <groupId>com.netflix.feign</groupId>
        <artifactId>feign-httpclient</artifactId>
        <version>8.18.0</version>
    </dependency>
    

    具体的Feign的API使用的话可以参考下面几篇文章
    Feign真正正确的使用方法
    Feign更正确的使用方法--结合ribbon
    Feign真正正确的使用方法

    但是这几篇文章都没有讲到将Feign整合到Spring环境中。下面我们就来讲Feign整合到Spring环境中。

    我们先来看一下我们最终所要达到的效果

    • service
    // 用HttpApi注解标注的要使用HTTP的纯接口
    
    @HttpApi
    public interface RemoteService
    {
    
        @Headers(
        {
            "Content-Type: application/json", "Accept: application/json"
        })
        @RequestLine("POST http://172.31.3.206:6020/emapi/std_mendian/mine/orderDetail.do")
        ApiResponse<OrderVo> orderDetail(MineOrderQueryParam param);
    
        @RequestLine("GET /emapi/std_mendian/handop/syncOrders.do?tenantId={tenantId}&syncType={syncType}")
        ApiResponse<Object> syncOrders(@Param("tenantId")
        Long tenantId, @Param("syncType")
        String syncType);
    }
    
    
    • controller
    @Controller
    @RequestMapping("/test")
    public class TestController
    {
        @Resource
        private RemoteService remoteService;
    
        @RequestMapping("/getOrderVo")
        @ResponseBody
        public ApiResponse<OrderVo> getOrderVo()
        {
            MineOrderQueryParam param = new MineOrderQueryParam();
            param.setEmOrderType("PSI");
            param.setTenantId(8958085892090750662L);
            param.setOrderId(6978373559115607797L);
            return remoteService.orderDetail(param);
        }
    
        @RequestMapping("/getSyncRes")
        @ResponseBody
        public ApiResponse<Object> getSyncRes()
        {
            return remoteService.syncOrders(6164376664484637551L, "sync_all");
        }
    
    }
    

    可以看到我们最终要达到的效果,就是在注入由@HttpApi标注的接口的时候像注入普通的service一样简单好用。

    那么到底怎么实现呢?其实我们看一下mybatis的实现方法就有灵感了。

    如果使用mybatis的mapper调用方式的话,需要在在spring文件中作如下配置

        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="xxx"/>
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        </bean>
    

    那么MapperScannerConfigurer到底是个什么东西呢,点进去看一下就一目了然了。

    public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
      /**
       * {@inheritDoc}
       * 
       * @since 1.0.2
       */
      public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        if (this.processPropertyPlaceHolders) {
          processPropertyPlaceHolders();
        }
    
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.registerFilters();
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
      }
    .....
    }
    

    源码面前,了无秘密

    其实就是在Spring的BeanDefinitionRegistry(bean注册器) 加入我们要扫描的目录下的mapper接口。然后Spring就会实例化这些接口。但是实际上还有一个问题,一个接口是怎么实例化的呢。很简单,就是动态代理。


    企业微信截图_15137561542320.png

    那MapperFactoryBean又是个什么东东呢。我们一层层追踪下去,就会发现实际最后就是JDK提供的动态代理


    企业微信截图_15137561542320.png

    那么至此我们就发现mybatis的mapper调用方法总计起来就是几个关键词

    1.BeanDefinitionRegistryPostProcessor 2.扫描classpath下的mapper接口的候选者 3.动态代理

    Spring整合Feign的具体实现

    • 1 定义HTTP注解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface HttpApi
    {
    }
    
    • 2 定义Feign的FactoryBean
    public class HttpFeignFactory implements FactoryBean<Object>
    {
    
        private Builder httpbuilder;
    
        private String baseUrl;
    
        private Class<?> serviceClass;
    
        @Override
        public Object getObject() throws Exception
        {
                    // mybatis中的动态代理其实在feign提供的API中已经实现了
            return httpbuilder.target(serviceClass, baseUrl);
        }
    
        @Override
        public Class<?> getObjectType()
        {
            return serviceClass;
        }
    
        @Override
        public boolean isSingleton()
        {
            return true;
        }
    
        public Builder getHttpbuilder()
        {
            return httpbuilder;
        }
    
        public void setHttpbuilder(Builder httpbuilder)
        {
            this.httpbuilder = httpbuilder;
        }
    
        public Class<?> getServiceClass()
        {
            return serviceClass;
        }
    
        public void setServiceClass(Class<?> serviceClass)
        {
            this.serviceClass = serviceClass;
        }
    
        public String getBaseUrl()
        {
            return baseUrl;
        }
    
        public void setBaseUrl(String baseUrl)
        {
            this.baseUrl = baseUrl;
        }
    }
    
      1. 定义BeanDefinitionRegistryPostProcessor
    public class HttpApiRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor
    {
        private String basePackage;
    
        private String baseUrl;
    
        // 这里可根据实际情况来配置。或者使用参数来控制具体的配置,这里简单起见,直接写死http配置
        private Builder httpbuilder = Feign.builder().encoder(new JacksonEncoder()).decoder(new JacksonDecoder())
                .client(new ApacheHttpClient()).options(new Options(1000, 3500))
                .retryer(new Retryer.Default(5000, 5000, 3));
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
        {
            // do nothing
        }
    
        protected String buildDefaultBeanName(Class<?> clazz)
        {
            String shortClassName = ClassUtils.getShortName(clazz.getName());
            return Introspector.decapitalize(shortClassName);
        }
    
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
        {
            Reflections reflections = new Reflections(basePackage);
    
            // 被HttpApi表示的
            Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(HttpApi.class);
            for (Class<?> serviceClass : annotated)
            {
                // 添加
                if (serviceClass.isInterface())
                {
                    for (Annotation annotation : serviceClass.getAnnotations())
                    {
                        if (annotation instanceof HttpApi)
                        {// 自定义注解HttpApi,都需要通过HttpFeignFactory创建bean
                            RootBeanDefinition beanDefinition = new RootBeanDefinition();
                            beanDefinition.setBeanClass(HttpFeignFactory.class);
                            beanDefinition.setLazyInit(true);
                            beanDefinition.getPropertyValues().addPropertyValue("httpbuilder", httpbuilder);
                            beanDefinition.getPropertyValues().addPropertyValue("serviceClass", serviceClass);
                            beanDefinition.getPropertyValues().addPropertyValue("baseUrl", baseUrl);
                            String beanName = this.buildDefaultBeanName(serviceClass);
                            registry.registerBeanDefinition(beanName, beanDefinition);
                        }
                    }
                }
            }
        }
    
        public String getBasePackage()
        {
            return basePackage;
        }
    
        public void setBasePackage(String basePackage)
        {
            this.basePackage = basePackage;
        }
    
        public String getBaseUrl()
        {
            return baseUrl;
        }
    
        public void setBaseUrl(String baseUrl)
        {
            this.baseUrl = baseUrl;
        }
    

    注意这里使用了reflections框架,需要加上这个maven依赖

    <dependency>
        <groupId>org.reflections</groupId>
        <artifactId>reflections</artifactId>
        <version>0.9.10</version>
    </dependency>
    

    这样的话,我们只需要在Spring配置文件上加上这样一句配置就搞定了

        
        <bean class="feign.spring.HttpApiRegistryPostProcessor">
            <property name="basePackage" value="xxx.yyy.zzz"/>
            <property name="baseUrl" value="http://localhost:6020/"/>
        </bean>
    
    

    相关文章

      网友评论

        本文标题:在纯Spring环境中使用Feign来进行声明式HTTP调用

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