在纯Spring环境中使用Feign来进行声明式HTTP调用
在很多小型系统中,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;
}
}
- 定义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>