一个关于HttpClient的轮子

由于本文较长,需要耐住性子阅读,另外本文中涉及到的知识点较多,想要深入学习某知识点可以参考其他博客或官网资料。本文也非源码分析文章,示例中的源码大多是伪代码和剪辑过的代码示例,由于该轮子为公司内部使用所以源码不便公开,敬请谅解。造轮子不重要,重要的是掌握轮子原理,取其精华,去其糟粕。欢迎大家拍砖。

背景
目前部门内部接口调用基本都是基于Http的,并且部门内部也有封装好的HttpClient。即便如此,每次有新的项目也要写一些繁琐的请求代码,即使不写也是复制粘贴,体验不算太好。于是乎想造一个轮子使用,为什么说是造轮子呢?因为它的功能和SpringCloud的OpenFeign差不多,不过由于是自己项目使用,自然是没有OpenFeign功能强大。

原理
用过MyBatis的同学应该都知道Mapper,所以此次造轮子我借鉴(抄袭)了Spring-Mybatis的部分代码,而且也是先把它的代码大致过了一遍才开始动工,大概要掌握的知识有如下几点:

动态代理
Spring的FactoryBean
Spring的自定义Bean注册机制,包括扫描,自定义BeanDefinition等
自定义注解的使用
反射机制
轮子目标
实现一个基于动态代理的HttpClient,看一下代码基本就明白了。

造轮子之前
//日常编码方案(伪代码)
public class HttpUtil {
public Object post(String url){
HttpClient client = new HttpClient(url);
client.addHeader(“Content-Type”,“application/json”);
return client.send();
}
}
造轮子之后
//轮子方案
@HttpApi(“http://localhost:8080/”)
public interface UserService{

@HttpGet(“user/{id}”)
User getUserById(@Path(“id”) Long id);

@HttpPost(“user/register”)
boolean register(@Json User user);
}

//使用方法示例(伪代码)
//本地Controller或者其他服务类
public class UserController{
//注入
@Autowired
private UserService userService;

@GetMapping("/")
public User getUser(){
//发送Http请求,调用远程接口
return userService.getUserById(1L);
}
}
OK,那么到这里也就基本介绍了这个轮子的用途和大体实现方向了。如果看上述示例代码还是不太明白的话,没关系,继续往下看。

轮子雏形
理解FactoryBean
想要实现动态获取Bean那么这个接口至关重要,为什么呢?试想一下,当你定义了一个接口例如:

public interface UserService{
User getUserById(Long id);
}
那么我们势必要将该接口作为一个Bean注册到BeanFactory中,在《原理》那一段我们都知道使用动态代理创建实现类,那么如何优雅的将实现类作为Bean注册到BeanFactory中呢?此时FactoryBean接口就派上用场了。

/**

  • If a bean implements this
  • interface, it is used as a factory for an object to expose, not directly as a
  • bean instance that will be exposed itself
    */
    public interface FactoryBean {
    //获取真正的 bean 实例
    T getObject() throws Exception;
    // bean 类型
    Class<?> getObjectType();
    //是否单例
    boolean isSingleton();
    }

看英文注释就可以知道,当注册到BeanFactory中的类是FactoryBean的实现类时,暴露出来的真实的Bean其实是getObject()方法返回的bean实例,而不是FactoryBean本身。那么结合上文中的接口,我们简单定义一个UserServiceFactoryBean作为示范:

@Component
public class UserServiceFactoryBean implements FactoryBean {

@Override
public UserService getObject() throws Exception {
    //使用动态代理创建UserService的实现类
    UserService serviceByProxy = createUserServiceByProxy();
    return serviceByProxy;
}

@Override
public Class<?> getObjectType() {
    return UserService.class;
}

@Override
public boolean isSingleton() {
    return true;
}

}
是不是很简单,虽然是继承自FactoryBean,但是注入到服务类中的对象其实是由动态代理生成的UserService的实现类。当然作为示例这么实现自然很简单,但是作为一个轮子提供给开发者使用的话,上边这段代码其实并不是开发者手动去写的,因为开发者只负责定义接口即可,那么如何来自动生成FactoryBean的实现类呢?这个就涉及到自定义BeanDefinition了。

包扫描
还是以MyBatis为例,在Spring-MyBatis中,我们会使用@MapperScan注解来使应用程序启动的时候扫描指定包然后加载相应的Mapper。

@MapperScan(basePackages = {“com.lunzi.demo.mapper”})
这里要注意的是,在MapperScan注解的定义中有这么一行@Import({MapperScannerRegistrar.class}),这个类是何方神圣?它做了什么事情?其实从它的命名我们大概能猜出来,它是负责扫描包并且注册Mapper的一个工具类。

@Import({MapperScannerRegistrar.class})
public @interface MapperScan {

}
下面看一下这个类的定义:

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {}
到这里大概明白了,它继承了ImportBeanDefinitionRegistrar接口,并实现了registerBeanDefinitions方法。具体实现细节主要关注对被扫描之后的接口类做了什么处理。负责扫描的类是由SpringFramework提供的ClassPathBeanDefinitionScanner,有兴趣的同学可以去看看源码。扫描到了Mapper接口之后,我们看一下后续对这些接口做了什么处理。

主要查看:ClassPathMapperScanner.processBeanDefinitions方法

private void processBeanDefinitions(Set beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();

  // 注:mapper接口是我们实际要用的bean,但是注册到BeanFactory的是MapperFactoryBean
  // the mapper interface is the original class of the bean
  // but, the actual class of the bean is MapperFactoryBean
  definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
  //这里将beanClass设置为MapperFactoryBean 
  definition.setBeanClass(this.mapperFactoryBean.getClass());

  //...中间一些无关代码忽略
  
  //然后设置注入模式为 AUTOWIRE_BY_TYPE	
  definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}

}
那么Spring将BeanDefinition添加到Bean列表中,注册Bean的任务就完成了,为什么拿Spring-MyBatis中的代码做讲解呢?原理都是相通的,那么我们回归到正题,下面我们要做的事情就是仿照其实现。

定义扫描注册类

public class HttpApiScannerRegistrar implements ImportBeanDefinitionRegistrar{

}
定义扫描注解

@Import(HttpApiScannerRegistrar.class)
public @interface HttpApiScan {

}
定义FactoryBean

这里要注意这个httpApiInterface,这玩意是生成代理类的接口,应用大量反射方法解析该接口类,下文详细分析,这里我们只要关注FactoryBean即可。

public class HttpApiFactoryBean implements FactoryBean,InitializingBean {
private Class httpApiInterface;

@Override
public T getObject() throws Exception {
    //下文讲述生成代理类的方法
    return ...;
}

}
写到这里我们就可以初步验证一下了,要不然会枯燥乏味,给你们点正向反馈。

@SpringBootApplication
//添加扫描注解
@HttpApiScan(basePackages = “com.lunzi.demo.api”)
public class HttpClientApiApplication {
public static void main(String[] args) {
SpringApplication.run(HttpClientApiApplication.class,args);
}
}
随便定义一个接口,里面的方法名无所谓的,毕竟暂时是个空壳子,用不上。不过这个接口要放在com.lunzi.demo.api包下,保证被扫描到。

public interface UserApiService {
Object test();
}
在随便写个controller

@RestController
@RequestMapping("/")
public class TestController {

@Autowired(required = false)
private UserApiService userApiService;

@GetMapping("test")
public Object[] getTestResult() {
    return userApiService.test();
}

}
别着急,这里还不能运行,毕竟FactoryBean的getObject方法还没有实现。下面该轮到动态代理上场了。

动态代理
java中的动态代理并不复杂,按照套路走就完事了,首先要定义一个实现InvocationHandler接口的类。

public class HttpApiProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//先写个默认实现
return “this is a proxy test”;
}
}
在定义一个代理工厂类,用于创建代理类,大家还记得httpApiInterface吗?创建代理类方法如下:

public T newInstance(HttpApiProxy httpApiProxy) {
return (T) Proxy.newProxyInstance(httpApiInterface.getClassLoader(), new Class[]{httpApiInterface}, httpApiProxy);
}
所以说知道FactoryBean的getObject方法怎么写了吧。

@Override
public T getObject() throws Exception {
//由于成品轮子代码封装较多。此处为伪代码用于展示具体原理
return new HttpProxyFactory().newInstance(new HttpApiProxy());
}
到此为止,我们可以运行DMEO程序了,截图如下:

代理类成功生成了,毋庸置疑,方法调用时也会返回 “this is a proxy test”;

组装配件
到此为止,我们实现了一个轮子外壳,它现在有什么作用呢?

根据注解扫描包自动注册FactoryBean
FactoryBean的getObject返回bean对象使用动态代理创建
在其他服务类中可注入
调用接口方法能够正常返回
下一步就要一步步实现轮子配件了,我们先回到接口代码,假如有一个用户服务:

//根据用户ID获取用户信息
GET http://api.demo.com/v1/user/{id}
//新注册一个用户
POST http://api.demo.com/v1/user/register
对应客户端接口如下:

public interface UserService{
User getUserById(Long id);
Boolean register(User user);
}
所以结合上文中的Http服务信息,我们发现接口还缺少如下信息:

Host信息
URL信息
参数类型信息
这里我先列举这三类,其实能够做的还有很多,后续我们升级轮子的时候在详细介绍。那么如何添加这些信息呢,那么就要用到注解功能了。首先添加Host信息:

@HttpApi(host = “http://api.demo.com”)
public interface UserService{
User getUserById(Long id);
Boolean register(User user);
}
是不是很简单呢?这里还要注意可扩展性,因为平时我们都会区分各种环境,开发,调试,测试,预发,生产环境,这里我们可以加上一个变量的功能,改造后如下:

@HttpApi(host = “${api.user.host}”)
public interface UserService{
User getUserById(Long id);
Boolean register(User user);
}
代码中的 api.user.host 只是一个示例,这里我们可以配置成任何变量,只要和配置文件中的对应即可。例如application-dev.yaml

api:
user:
host: http://api.demo.dev.com/
解决了Host问题,是不是要添加具体的URL了,还要考虑HttpMethod,由于大部分都不是正规的RestfulApi所以在轮子中我们暂时只考虑GET,POST方法。

@HttpApi(host = “${api.user.host}”)
public interface UserService{

@HttpGet("/v1/user/{id}")
User getUserById(Long id);

@HttpPost("/v1/user/register")
Boolean register(User user);
}
到这里解决了Host和Url的问题,那么还有一个参数问题,比如上述代码中的Get方法。用过SpringBoot的同学都知道 @PathVariable 注解,那么这里也类似。而且方法也支持QueryString参数,所以要加一些参数注解来区分各个参数的不同位置。那么接口继续改造:

@HttpApi(host = “${api.user.host}”)
public interface UserService{

//http://host/v1/user/123
@HttpGet("/v1/user/{id}")
User getUserById(@Path(“id”)Long id); //增加 @Path 注解标明此id参数对应着路径中的{id}

//http://host/v1/user/?id=123
@HttpGet("/v1/user/")
User getUserById(@Query(“id”)Long id); //增加 @Query 注解标明此id参数对应着路径中的?id=

@HttpPost("/v1/user/register")
Boolean register(User user);
}
看完Get方法,是不是Post方法你们也有思路了呢?比如我们要支持以下几种类型的参数

Content-Type=application/json (@Json)
Content-Type=application/x-www-form-urlencoded (@Form)
当然还有例如文件上传等,这里先不做演示。在丰富一下Post接口方法:

@HttpApi(host = “${api.user.host}”)
public interface UserService{
@HttpGet("/v1/user/{id}")
User getUserById(@Path(“id”)Long id);

@HttpPost("/v1/user/register")
Boolean register(@Json User user); //这里使用 @Json 和 @Form 区分参数类型
}
OK,到了这里接口定义告一段落,一个很简单粗糙的版本就出来了。不过罗马也不是一天建成的,慢慢来。现在稍作总结,轮子新增了以下几个小组件:

HttpApi 类注解:定义通用配置,例如Host,timeout等
HttpGet 方法注解:定义HttpMethod,URL
HttpPost 方法注解:定义HttpMethod,URL
Path 参数注解:定义参数类型为路径参数
Query 参数注解:定义参数类型为QueryString参数
Json 参数注解:定义参数类型为application/json
Form 参数注解:定义参数类型为application/x-www-form-urlencoded
组件解析
现在客户端的接口已经定义好了,剩下我们要做的就是去解析它,并且将解析结果存起来供后续使用。什么时候取做解析呢?在前文中我们定义了HttpApiFactoryBean,下面我们也实现InitializingBean接口,然后在 afterPropertiesSet 方法中去解析。

在Mybatis中有一个贯穿全文的配置类:Configuration,这里我们也参照该模式,新建一个Configuration配置类。里面大概有哪些东东呢?

HttpConfig 当前接口服务的基础配置,存储解析后的host,超时时间,其他全局可用的配置信息等
Map<String,Object> 存放每个方法对应的接口定义细节,由于一个接口存在多个方法,这里就用Map存储
HttpApiRegistry 它负责注册接口和提供接口的动态代理实现
OK,那么下一步我们就是要看看afterPropertiesSet方法做了什么事情。

@Override
public void afterPropertiesSet() throws Exception {
    configuration.addHttpApi(this.httpApiInterface);
}

在Configuration中,又调用了HttpApiRegistry的add方法:

public final void addHttpApi(Class<?> type) {
    this.httpApiRegistry.add(type);
}

这里可以看到关键参数是Class<?> type,对应我们的接口定义就是UserService.class。为什么要用Class呢?因为接下来我们要使用大量的反射方法去解析这个接口。

由于解析细节比较多,这里不再详细介绍,有兴趣的同学可以去看一下MyBatis解析Mapper的源码,我的灵感也是基于该源码做的实现。

这里我就跳过解析细节,给大家看一下解析的一个结果

knownHttpApis 保存了动态代理类缓存信息
httpApiStatements 对应着每个方法,从下图中可以看出包含HttpMethod,URL,参数,返回值等信息
methodParameters 是参数集合,每个参数包含参数名,参数类型,和一些其他Http的属性等

那么有了这些东西我们能干什么呢?我们回到HttpApiProxy 的 invoke 方法。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //....其他代码
  
    //先获取到唯一ID 例如:com.demo.api.UserService.getUserById
    String id = this.mapperInterface.getName() + "." + method.getName();
    //执行HTTP请求
    return HttpExecutors.execute(id,configuration,args);
}

这里要注意,如果接口定义的是重载方法,比如getUserById(Long id). getUserById(Long id1,Long id2);

很抱歉,直接扔给你一个异常,告诉你不允许这么定义,否则id就冲突了!就是这么简单粗暴。

HttpExecutors.execute(id,configuration,args) 方法流程图如下:

之所以后边的HttpClient实现没有详细介绍,因为这里的选择有很多,例如okhttp,httpClient,java原生的httpConnection等。

轮子跑起来
接口定义

package com.demo.api;

import com.xiaoju.manhattan.common.base.entity.BaseResponse;
import com.xiaoju.manhattan.http.client.annotation.*;

import java.util.List;

@HttpApi(value = “${host}”,
connectTimeout = 2000,
readTimeout = 2000,
retryTime = 3,
interceptor = “userApiServiceInterceptor”,
exceptionHandler = “userApiServiceErrorHandler”)
public interface UserApiService {

/**
 * 根据用户ID获取用户信息
 */
@HttpGet("/api/user/{id}")
BaseResponse getByUserId(@Path("id") Long id);

}
客户端

@RestController
@RequestMapping("/")
public class TestController {

@Autowired(required = false)
private UserApiService userApiService;

@GetMapping("user")
public BaseResponse<User> getUserById() {
    Long id = System.currentTimeMillis();
    return userApiService.getByUserId(id);
}

}

模拟用户Http服务接口

@RestController
@RequestMapping("/api/user")
public class DemoController {

@GetMapping("{id}")
public BaseResponse getUserById(@PathVariable(“id”) Long id) throws Exception{
User user = new User();
user.setName(“轮子”);
user.setId(id);
user.setAddress(“博客模拟地址”);
return BaseResponse.build(user);
}
}
正常调用
{
“data”: {
“id”: 1586752061978,
“name”: “轮子”,
“address”: “博客模拟地址”
},
“errorCode”: 0,
“errorMsg”: “ok”,
“success”: true
}
拦截器示例
@Component(value = “userApiServiceInterceptor”)
public class UserApiServiceInterceptor implements HttpApiInterceptor {

@Override
public Object beforeExecute(RequestContext requestContext) {
    //添加通用签名请求头
    String signature = "1234567890";
    requestContext.addHeader("signature", signature);
    //添加通用参数
    requestContext.addParameter("from","blog");
    
    return null;
}

@Override
public Object afterExecute(RequestContext requestContext) {
    
    return null;
}

}

服务端改造

@GetMapping("{id}")
public BaseResponse getUserById(HttpServletRequest request, @PathVariable(“id”) Long id) throws Exception {
User user = new User();
user.setName(“轮子”);
user.setId(id);
user.setAddress(“博客模拟地址:” + request.getHeader(“signature”) + “|” + request.getParameter(“from”));
return BaseResponse.build(user);
}
调用结果:

{
“data”: {
“id”: 1586752450283,
“name”: “轮子”,
“address”: “博客模拟地址:1234567890|blog”
},
“errorCode”: 0,
“errorMsg”: “ok”,
“success”: true
}
错误处理器与拦截器原理相同,不在演示。

总结
从想法抛出到具体实现大概用了几天的时间,这个轮子到底能不能在项目中跑还是个未知数,不过我还是保持乐观态度,毕竟大量借鉴了MyBatis的源码实现,嘿嘿。

当然还有一些不足之处:

类结构设计还需要改进,还有较大的优化空间,向大师们学习

不支持文件上传(如何支持?你知道怎么做了吗?)

不支持 HttpPut,HttpDelete (加一些扩展,很容易)

不支持切换底层HttpClient实现逻辑,如果能根据当前引用包动态加载就好了,类似Slf4j的门面模式

可扩展点:

HttpGet可以加入缓存机制
拦截器可以丰富功能
异步请求支持
开发难点:

由于高度的抽象和大量的泛型使用,需要对反射原理掌握的更加深入一些
对Spring生态要深入理解和学习
深圳网站建设www.sz886.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值