因为本文较长,须要耐住性子阅读,另外本文中涉及到的知识点较多,想要深刻学习某知识点能够参考其余博客或官网资料。本文也非源码分析文章,示例中的源码大可能是伪代码和剪辑过的代码示例,因为该轮子为公司内部使用因此源码不便公开,敬请谅解。造轮子不重要,重要的是掌握轮子原理,取其精华,去其糟粕。欢迎你们拍砖。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的门面模式
可扩展点:
开发难点:
开发领悟:
其实写一个轮子不是为了写而写,而是要有实际开发痛点,轮子造出来以后是否可使用?是否哗众取宠华而不实?固然这些要通过实战的检验,好用很差用,开发说了算。如今已经接近尾声,但愿能给你们带来一些收获!拜了个拜。