因爲本文較長,須要耐住性子閱讀,另外本文中涉及到的知識點較多,想要深刻學習某知識點能夠參考其餘博客或官網資料。本文也非源碼分析文章,示例中的源碼大可能是僞代碼和剪輯過的代碼示例,因爲該輪子爲公司內部使用因此源碼不便公開,敬請諒解。造輪子不重要,重要的是掌握輪子原理,取其精華,去其糟粕。歡迎你們拍磚。java
目前部門內部接口調用基本都是基於Http的,而且部門內部也有封裝好的HttpClient。即使如此,每次有新的項目也要寫一些繁瑣的請求代碼,即便不寫也是複製粘貼,體驗不算太好。因而乎想造一個輪子使用,爲何說是造輪子呢?由於它的功能和SpringCloud的OpenFeign差很少,不過因爲是本身項目使用,天然是沒有OpenFeign功能強大。json
用過MyBatis的同窗應該都知道Mapper,因此這次造輪子我借鑑(抄襲)了Spring-Mybatis的部分代碼,並且也是先把它的代碼大體過了一遍纔開始動工,大概要掌握的知識有以下幾點:api
實現一個基於動態代理的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
想要實現動態獲取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";
到此爲止,咱們實現了一個輪子外殼,它如今有什麼做用呢?
下一步就要一步步實現輪子配件了,咱們先回到接口代碼,假若有一個用戶服務:
//根據用戶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信息:
@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方法大家也有思路了呢?好比咱們要支持如下幾種類型的參數
固然還有例如文件上傳等,這裏先不作演示。在豐富一下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,到了這裏接口定義告一段落,一個很簡單粗糙的版本就出來了。不過羅馬也不是一天建成的,慢慢來。如今稍做總結,輪子新增瞭如下幾個小組件:
如今客戶端的接口已經定義好了,剩下咱們要作的就是去解析它,而且將解析結果存起來供後續使用。何時取作解析呢?在前文中咱們定義了HttpApiFactoryBean,下面咱們也實現InitializingBean接口,而後在 afterPropertiesSet 方法中去解析。
在Mybatis中有一個貫穿全文的配置類:Configuration,這裏咱們也參照該模式,新建一個Configuration配置類。裏面大概有哪些東東呢?
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的源碼,個人靈感也是基於該源碼作的實現。
這裏我就跳過解析細節,給你們看一下解析的一個結果
那麼有了這些東西咱們能幹什麼呢?咱們回到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的門面模式
可擴展點:
開發難點:
開發領悟:
其實寫一個輪子不是爲了寫而寫,而是要有實際開發痛點,輪子造出來以後是否可使用?是否譁衆取寵華而不實?固然這些要通過實戰的檢驗,好用很差用,開發說了算。如今已經接近尾聲,但願能給你們帶來一些收穫!拜了個拜。