前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >开源一款http客户端

开源一款http客户端

作者头像
叔牙
发布2024-12-30 12:14:55
发布2024-12-30 12:14:55
8000
代码可运行
举报
运行总次数:0
代码可运行

微信公众号:PersistentCoder关注可了解更多的教程。问题或建议,请公众号留言。

一、背景介绍

java项目中难免会遇到向外部发送http请求的场景,比如常见的支付通道接入、开源平台的接口调用、三方合作平台的接口调用等,大多都需要在当前业务中编写相应代码调用外部的http接口,目前比较常用的适用于java语言的http客户端有以下几种:

  • Java Native: java自带的原生http客户端,不需要引入过多外部依赖,通过简单编程也能实现http请求的发送。
  • Spring RestTemplate: spring框架自带的http客户端,只要引入了springboot框架,基本不在需要再引入其他依赖,通过注入RestTemplate类型的bean就能完成http接口调用。
  • httpclient:apache开源的http客户端,提供了连接池和异步非阻塞能力,也是比较常用的http客户端库。
  • okhttp: okhttp也是一个比较常用的http客户端,提供了内置高效连接池、压缩和缓存能力,以其高性能、简洁易用的 API、强大的特性和良好的社区支持,成为了很多开发者在构建 HTTP 客户端时的首选。

那么有没有一种工具,能够集成常用的http客户端能力,用户通过简单的引入配置就能实现外部http接口调用呢?

想要成为这样一个工具需要具备以下特性:

  1. 通过引入依赖完成基本的准备工作,通过starter引入,项目必须得依赖都准备完毕。
  2. 通过注解开启http客户端能力,starter更具用户的注解做相关配置实例化。
  3. 用户根据需要,自行选择使用底层的哪种http客户端,比如是springboot项目,已经内置了RestTemplate,那么直接选择RestTemplate则会收敛项目依赖复杂度。
  4. 用户通过定义接口添加注解的方式,使用此工具的http客户端能力,具体接口的实例化和http调用能力的组装,由注解背后的能力完成。

简单来说,就是要满足简单易用,并且可自定义,将请求的调用能力封装起来,让开发者更多关注于业务相关的开发工作。

二、使用方式

基于以上的诉求,以及实际研发工作中的需要,本人基于常用的http客户端编写了一个集中化的http客户端封装,组件叫做facade-http-edi-starter,作为一个自定义spring-boot-starter提供复用,并且jar包已经发布到sonatype仓库,公网环境直接通过maven中央仓库引用即可“食用”。

facade-http-edi-starter提供了以下特性和能力:

  • 封装集成java原生http客户端、httpclient、RestTemplate和okhttp等客户端能力
  • 用户可根据需要选择使用哪一种客户端能力
  • 通过注解开启facade-http-edi-starter能力
  • 用户可通过预定义注解来定义发送http请求的接口,并且可定义请求类型、请求头、入参等
  • 通过facade-http-edi-starter定义的http接口调用默认返回string类型的响应,同时支持用户自定义响应结果解析工具,用来返回用户自定义的java对象
  • 调用同一个域名的不同接口,在定义接口调用的时候,可以抽象host提供复用能力
  • 用户可根据自己选择的底层http客户端类型,引入对应的依赖,比如如果选择RestTemplate,那么就不用引入httpclient和okhttp,本身starter中的pom依赖定义成provided

空口无凭,接下来我们就通过在项目中真实使用来介绍facade-http-edi-starter的使用方式。

1.引入pom依赖

引入facade-http-edi-starter依赖,当前最新版本是1.0.3.RELEASE

代码语言:javascript
代码运行次数:0
复制
<dependency>
    <groupId>io.github.scorpioaeolus</groupId>
    <artifactId>facade-http-edi-starter</artifactId>
    <version>1.0.3.RELEASE</version>
</dependency>
2.开启http客户端能力

在springboot项目启动类上添加EnableEdiApiScan注解来开启http客户端能力,并且需要指定扫描的包路径。

代码语言:javascript
代码运行次数:0
复制
@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。

3.定义http接口调用实现

在EnableEdiApiScan注解指定scanBasePackages包路径定义接口,并通过注解来定义http接口调用:

代码语言:javascript
代码运行次数:0
复制
@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,如下代码提供相同的能力:

代码语言:javascript
代码运行次数:0
复制
@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客户端默认返回的字符串数据会通过传入的转换工具转成对应的对象并返回。

4.调用http客户端

也调用方定义相关属性配置,并且可以直接注入前边定义的接口,然后发起http请求调用:

代码语言:javascript
代码运行次数:0
复制
@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:

代码语言:javascript
代码运行次数:0
复制
t -> JSONObject.parseObject(t,ExchangeRate.class)

用来做响应结果的转换。

5.测试调用

启动项目发起请求调用:

从服务日志中可以看到是使用RestTemplate发起了http请求调用:

这样业务项目就特别简单的方式集成了facade-http-edi-starter并实现了http请求调用,是不是很丝滑。

三、实现原理

1.启用能力

核心注解EnableEdiApiScan,用于启用facade-http-edi-starter能力:

代码语言:javascript
代码运行次数:0
复制
@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是能力实现的核心组件。

2.扫描http能力接口

EdiApiRegistrar用于扫描scanBasePackages路径下所有加了EdiApi注解的接口并添加BeanDefinition到容器中:

代码语言:javascript
代码运行次数:0
复制
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);
  }
}
3.注入http客户端相关配置

EdiConfigurationSelector用于根据用户选择的http客户端类型注入相关的辅助配置:

代码语言:javascript
代码运行次数:0
复制
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);
        }
    }
}
4.用户自定义接口注入http客户端能力

在前边步骤中,会将@EdiApi注解的接口扫描转换成EdiServiceFactoryBean类型的BeanDefinition:

代码语言:javascript
代码运行次数:0
复制
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获取注入真正的实例:

代码语言:javascript
代码运行次数:0
复制
@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上项目,或者私信作者沟通。

5.http客户端封装实现

对于http客户端封装实现,我们选择RestTemplate简单做下介绍。

代码语言:javascript
代码运行次数:0
复制
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:

代码语言:javascript
代码运行次数:0
复制
<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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景介绍
  • 二、使用方式
    • 1.引入pom依赖
    • 2.开启http客户端能力
    • 3.定义http接口调用实现
    • 4.调用http客户端
    • 5.测试调用
  • 三、实现原理
    • 1.启用能力
    • 2.扫描http能力接口
    • 3.注入http客户端相关配置
    • 4.用户自定义接口注入http客户端能力
    • 5.http客户端封装实现
  • 四、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档