一個關於HttpClient的輪子

因爲本文較長,須要耐住性子閱讀,另外本文中涉及到的知識點較多,想要深刻學習某知識點能夠參考其餘博客或官網資料。本文也非源碼分析文章,示例中的源碼大可能是僞代碼和剪輯過的代碼示例,因爲該輪子爲公司內部使用因此源碼不便公開,敬請諒解。造輪子不重要,重要的是掌握輪子原理,取其精華,去其糟粕。歡迎你們拍磚。java

背景

目前部門內部接口調用基本都是基於Http的,而且部門內部也有封裝好的HttpClient。即使如此,每次有新的項目也要寫一些繁瑣的請求代碼,即便不寫也是複製粘貼,體驗不算太好。因而乎想造一個輪子使用,爲何說是造輪子呢?由於它的功能和SpringCloud的OpenFeign差很少,不過因爲是本身項目使用,天然是沒有OpenFeign功能強大。json

原理

用過MyBatis的同窗應該都知道Mapper,因此這次造輪子我借鑑(抄襲)了Spring-Mybatis的部分代碼,並且也是先把它的代碼大體過了一遍纔開始動工,大概要掌握的知識有以下幾點:api

  • 動態代理
  • 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,那麼到這裏也就基本介紹了這個輪子的用途和大致實現方向了。若是看上述示例代碼仍是不太明白的話,不要緊,繼續往下看。app

輪子雛形

理解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<T> {
  //獲取真正的 bean 實例
	T getObject() throws Exception;
  // bean 類型
	Class<?> getObjectType();
  //是否單例
	boolean isSingleton();
}

看英文註釋就能夠知道,當註冊到BeanFactory中的類是FactoryBean的實現類時,暴露出來的真實的Bean實際上是getObject()方法返回的bean實例,而不是FactoryBean自己。那麼結合上文中的接口,咱們簡單定義一個UserServiceFactoryBean做爲示範:ide

@Component
public class UserServiceFactoryBean implements FactoryBean<UserService> {
    
    @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<BeanDefinitionHolder> 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<T> implements FactoryBean<T>,InitializingBean {
        private Class<T> 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<T> 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<T> 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生態要深刻理解和學習

開發領悟:

  • 不會不懂的地方就看開源框架源碼,你會發現新世界

其實寫一個輪子不是爲了寫而寫,而是要有實際開發痛點,輪子造出來以後是否可使用?是否譁衆取寵華而不實?固然這些要通過實戰的檢驗,好用很差用,開發說了算。如今已經接近尾聲,但願能給你們帶來一些收穫!拜了個拜。

相關文章
相關標籤/搜索