编程篇

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

2017-12-20  本文已影响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;
        }

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

针对以上的问题,其实都是可以通过优化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)

但是这个只能在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环境中。

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

// 用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
@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的具体实现

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface HttpApi
{
}
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;
    }
}
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>

上一篇下一篇

猜你喜欢

热点阅读