微信公众号:PersistentCoder关注可了解更多的教程。问题或建议,请公众号留言。
java项目中难免会遇到向外部发送http请求的场景,比如常见的支付通道接入、开源平台的接口调用、三方合作平台的接口调用等,大多都需要在当前业务中编写相应代码调用外部的http接口,目前比较常用的适用于java语言的http客户端有以下几种:
那么有没有一种工具,能够集成常用的http客户端能力,用户通过简单的引入配置就能实现外部http接口调用呢?
想要成为这样一个工具需要具备以下特性:
简单来说,就是要满足简单易用,并且可自定义,将请求的调用能力封装起来,让开发者更多关注于业务相关的开发工作。
基于以上的诉求,以及实际研发工作中的需要,本人基于常用的http客户端编写了一个集中化的http客户端封装,组件叫做facade-http-edi-starter,作为一个自定义spring-boot-starter提供复用,并且jar包已经发布到sonatype仓库,公网环境直接通过maven中央仓库引用即可“食用”。
facade-http-edi-starter提供了以下特性和能力:
空口无凭,接下来我们就通过在项目中真实使用来介绍facade-http-edi-starter的使用方式。
引入facade-http-edi-starter依赖,当前最新版本是1.0.3.RELEASE
<dependency>
<groupId>io.github.scorpioaeolus</groupId>
<artifactId>facade-http-edi-starter</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
在springboot项目启动类上添加EnableEdiApiScan注解来开启http客户端能力,并且需要指定扫描的包路径。
@SpringBootApplication
@EnableEdiApiScan(scanBasePackages = "com.facade.edi.samples.demo.proxy",clientType = EnableEdiApiScan.ClientType.REST_TEMPLATE)
public class DemoApplication
{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在EnableEdiApiScan中可以通过clientType来指定底层http客户端类型,如果不指定则使用默认类型REST_TEMPLATE。
在EnableEdiApiScan注解指定scanBasePackages包路径定义接口,并通过注解来定义http接口调用:
@EdiApi(hostKey = "currencyapi.host")
public interface RateApi {
@GET("/v3/latest")
ExchangeRate getRateV2(@Query("base_currency") String baseCurrency
, @Query("currencies") String currencies
, @Header("Content-Type") String contentType
, @Header("apiKey") String apiKey
, @ResponseConvert ClientResponseConverter<ExchangeRate> converter);
}
上述同一个host如果定义了多个接口调用,则可以结合EdiApi注解将host做抽象通用定义,则该接口定义的多个http调用共享相同host,当然如果只有一个http接口调用也可以http调用定义方法中通过Host注解来定义host,如下代码提供相同的能力:
@EdiApi
public interface RateApi {
@GET("/v3/latest")
ExchangeRate getRateV2(@Query("base_currency") String baseCurrency
, @Query("currencies") String currencies
, @Header("Content-Type") String contentType
, @Header("apiKey") String apiKey
, @Host("host") String host
, @ResponseConvert ClientResponseConverter<ExchangeRate> converter);
}
另外入参中定义了@ResponseConvert指定响应结果转换工具,http客户端默认返回的字符串数据会通过传入的转换工具转成对应的对象并返回。
也调用方定义相关属性配置,并且可以直接注入前边定义的接口,然后发起http请求调用:
@Value("${currencyapi.apiKey:}")
private String apiKey;
@Resource
RateApi rateApi;
@GetMapping("/rate/v2")
public String getRate() {
ExchangeRate result = rateApi.getRateV2("AED", "INR", "application/json", this.apiKey, t -> JSONObject.parseObject(t,ExchangeRate.class));
return JSONObject.toJSONString(result);
}
http接口调用中通过lambda表达式定义了一个ClientResponseConverter:
t -> JSONObject.parseObject(t,ExchangeRate.class)
用来做响应结果的转换。
启动项目发起请求调用:
从服务日志中可以看到是使用RestTemplate发起了http请求调用:
这样业务项目就特别简单的方式集成了facade-http-edi-starter并实现了http请求调用,是不是很丝滑。
核心注解EnableEdiApiScan,用于启用facade-http-edi-starter能力:
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@Import({EdiApiRegistrar.class, EdiConfigurationSelector.class})
public @interface EnableEdiApiScan {
/**
* 接口扫描路径
* @return String[]
*/
String[] scanBasePackages();
/**
* 请求客户端类型
*
* @return ClientType
*/
ClientType clientType() default ClientType.REST_TEMPLATE;
//...省略...
}
scanBasePackages定义扫描的包路径,clientType定义底层使用的http客户端类型,默认是RestTemplate。
EdiApiRegistrar和EdiConfigurationSelector是能力实现的核心组件。
EdiApiRegistrar用于扫描scanBasePackages路径下所有加了EdiApi注解的接口并添加BeanDefinition到容器中:
public class EdiApiRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
annotationMetadata.getAnnotationAttributes(EnableEdiApiScan.class.getName()));
assert attributes != null;
String[] basePackages = attributes.getStringArray("scanBasePackages");
if(basePackages.length == 0) {
throw new RuntimeException("开启EDI需要指定扫描路径");
}
AdviceMode mode = (AdviceMode) attributes.get("mode");
EdiApiScanner scanner = new EdiApiScanner(registry,mode);
scanner.scan(basePackages);
}
}
EdiConfigurationSelector用于根据用户选择的http客户端类型注入相关的辅助配置:
public class EdiConfigurationSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(EnableEdiApiScan.class.getName(), false));
EnableEdiApiScan.ClientType clientType = attributes.getEnum("clientType");
if(clientType == EnableEdiApiScan.ClientType.REST_TEMPLATE) {
return new String[] {
//AutoProxyRegistrar.class.getName(),
EdiClientConfig.class.getName(),
EdiRestTemplateConfig.class.getName()
};
} else if(clientType == EnableEdiApiScan.ClientType.OK_HTTP) {
return new String[] {
//AutoProxyRegistrar.class.getName(),
EdiClientConfig.class.getName(),
EdiOkHttpConfig.class.getName()
};
} else if(clientType == EnableEdiApiScan.ClientType.HTTP_CLIENT) {
return new String[] {
//AutoProxyRegistrar.class.getName(),
EdiClientConfig.class.getName(),
EdiHttpClientConfig.class.getName()
};
} else if(clientType == EnableEdiApiScan.ClientType.NATIVE) {
return new String[] {
EdiClientConfig.class.getName(),
EdiNativeClientConfig.class.getName()
};
} else {
throw new UnsupportedOperationException("Unknown clientType: " + clientType);
}
}
}
在前边步骤中,会将@EdiApi注解的接口扫描转换成EdiServiceFactoryBean类型的BeanDefinition:
public static BeanDefinitionHolder createSpiFactoryBeanBeanDefinitionHolder(Class<?> spiClass, AdviceMode adviceMode) {
GenericBeanDefinition beanDef = new GenericBeanDefinition();
beanDef.setBeanClass(EdiServiceFactoryBean.class);
beanDef.getConstructorArgumentValues().addGenericArgumentValue(spiClass);
beanDef.getConstructorArgumentValues().addGenericArgumentValue(adviceMode);
String beanName = generateSpiFactoryBeanName(spiClass);
return new BeanDefinitionHolder(beanDef, beanName);
}
而EdiServiceFactoryBean是一个FactoryBean,真正注入的时候会通过getObject获取注入真正的实例:
@Override
public T getObject() throws Exception {
if (targetClass == null) {
throw new NullPointerException("class is null");
}
if(AdviceMode.ASPECTJ == this.adviceMode) {
return this.ediApiProxyFactory.newCglibInstance(targetClass);
}
return this.ediApiProxyFactory.newInstance(targetClass);
}
然后通过newInstance实例化用户自定义的接口实例时,会将http客户端以及http请求调用能力嵌入进入,由于篇幅原来这里针对源码不展开细讲,感兴趣可翻看github上项目,或者私信作者沟通。
对于http客户端封装实现,我们选择RestTemplate简单做下介绍。
public class RestTemplateInvokeHttpFacade extends AbstractInvokeHttpFacade {
private RestTemplate restTemplate;
public RestTemplateInvokeHttpFacade(RestTemplate restTemplate) {
super();
this.restTemplate = restTemplate;
}
@Override
public HttpApiResponse invoke(HttpApiRequest request) {
log.info("RestTemplateInvokeHttpFacade.invoke request={}",request);
HttpApiResponse resp = new HttpApiResponse();
HttpHeaders headers = this.processRequestHeader(request);
HttpEntity<?> httpEntity = this.processRequestBody(headers,request);
URI uri = this.buildUri(request);
try {
ResponseEntity<String> response = this.restTemplate.exchange(uri
, Objects.requireNonNull(HttpMethod.resolve(request.getHttpMethod()))
, httpEntity
, String.class);
resp.setHttpCode(response.getStatusCodeValue());
resp.setResponse(response.getBody());
resp.setHttpErrorMsg(response.getStatusCode().getReasonPhrase());
} catch (RestClientException e) {
log.error("RestTemplateInvokeHttpFacade.invokeResponse http invoke error;url={}",request.getUrl(), e);
resp.setHttpCode(500);
resp.setHttpErrorMsg(e.getMessage());
}
return resp;
}
}
看到这里应该比较明了了,其实就是解析用户通过@EdiApi定义的接口相关参数,转换组装成RestTemplate请求请发送,然后把结果返回,其实就是把原来需要用户自己做的那部分编码工作做了封装。
通篇来看,其实并不是开源了一款http客户端,而是集成了市面上比较流行、适用度最广泛的http客户端,然后做了抽象和封装,让开发者能够更简单便捷地使用http客户端工具和编写http接口调用,让业务开发人员更多的关注业务开发,不用把时间浪费在编写基础工具上。
当然关于工具这回事,很多时候都是为了自己方便,然后发现具有通用性和普遍性,方便自己、方便他人,这也是每一个程序员的追求,至于好用不好用,用完再说,如果有好的建议,也必定会虚心采纳做改进。
如果想直接使用,直接从maven中央仓库引用最新版jar:
<dependency>
<groupId>io.github.scorpioaeolus</groupId>
<artifactId>facade-http-edi-starter</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
如果想翻阅或者研究下源码,github仓库地址:
https://github.com/ScorpioAeolus/facade-http-edi
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!