thrift是一种常用的微服务rpc通信协议。启动的thirft service,将其自己的服务地址、端口、私有tag(用于区分不同的标签,多人开发时候会有多个不同的服务)等信息注册到zookeeper中。thrift 调用时候,gateway会根据调用的service name,在zookeeper中找到与之对应的host、port等信息,然后进行rpc调用。
这个通用的thrift service调用工具,充当着gateway功能:与swagger2结合一起使用,可以方便的对thirft service中的方法进行调用。如图所示:利用swagger接口测试功能,对thrift service 进行调用测试。
尝试自己实现此系统,尝试的系统截图如下:
备注:这里可以在界面上增加一个private tag的输入框,用于表示将调用对应private tag的服务。【此private tag的最终作用:其实也就是在zookeeper中注册的节点上,有一个额外的字段tag而已,可以根据此private tag进行区分服务】
此web工具系统,有以下这些功能:
1、直接调用thrift service,根据输入得到对应的service调用的返回结果。
2、添加新的thrift idl 信息,idl name和版本号等;
3、更新已经添加的thrift idl的版本,比如目前要对自己开发的thrift idl(thrift service)进行接口自测,那么将thrift idl更新成自己开发的分支版本即可。
使用此web工具系统,有如下好处:
1、在进行开发自测时候,可以便捷的进行thrift 接口测试。如果没有这个web工具系统,那么就需要我们自己写很多意义不大的数据组装代码进行测试。
2、在线上环境,利用此web工具系统,可以方便的排查一些线上问题。甚至可以专门写一些只用于解决线上问题的thrift idl函数,而不提供给业务层gateway使用。如果没有这个web工具系统,由于线上网络与本地开发环境是隔离的,除非在docker中运行python thrift 调用(充当python gateway)才可以实现类似功能;而java thrift调用,还要打包成jar运行,很麻烦。
刚好thrift idl需要打包成jar,放于内部的maven公共仓库,才方便地被本地开发环境、测试/线上环境docker等引用到,以此maven公共仓库作为thrift jar来源。至于每次调用thrift idl的哪一个版本,要调用哪些thrift idl等信息,可以存放到数据库中,避免项目重启后这些信息丢失。
主要的实现思路如下:
1、从maven仓库,扫描下载指定thrift idl 的某版本jar包,下载到本地;
2、解压jar,判断是否是thrift service层,然后生成doc文档;
3、找到对应的service bean client,然后利用反射,调用service中的方法。
service bean的过程:
1、如何找到对应service的bean client:BeanDefinitionRegistryPostProcessor,把bean的定义动态注册到spring中。
2、将zookeeper中注册的不断变化的servcice ip以及端口信息,及时更新。
以上有关具体细节:
implements SmartLifecycle 在Spring加载和初始化所有bean后,接着执行一些任务或者启动需要的异步服务。
CloseableHttpClient 下载jar文件,并保存到本地。
使用dom4j,解析jar中的pom.xml 文件,获取其中的依赖lib信息。
调用 com.sun.tools.javadoc.Main.execute 执行javadoc。-doclet 指定自己的docLet类名;-encoding 指定源码文件的编码格式;
其他有关spring的关键点,具体如下。
以下全部是基于1.5.4.RELEASE 版本分析的。
compile('org.springframework.boot:spring-boot-starter-web:1.5.4.RELEASE')compile('org.springframework.boot:spring-boot-starter-test:1.5.4.RELEASE')compile("org.springframework.boot:spring-boot-configuration-processor:1.5.4.RELEASE")
关键的客户端代理工厂ThriftServiceClientProxyFactory
@Slf4j@Datapublic class ThriftServiceClientProxyFactory implements ApplicationContextAware, FactoryBean, InitializingBean, Closeable { /** 各种参数 * minIdle 最小空闲连接数 * maxIdle 最大空闲连接数 * maxActive 最大活跃连接数 * idleTime ms,default 3 min,链接空闲时间, -1,关闭空闲检测 * serverAddressProvider 服务地址 * thrift service类,如:xxservice.class * serviceName 服务名称 * loadbalance 负载均衡器名称 * tag 附加信息 * connectTimeout 连接超时时间 * socketTimeout thrift调用超时时间 * framedMaxSize 采用TFramedTransport的最大长度,thrift默认16384000 * isSync 是否同步调用,默认同步。 * autoRetry 是否自动重试 **/ private Object proxyClient; @Override public void afterPropertiesSet() throws Exception { //订阅service 在 zookeeper中的变化 // ...... InvocationHandler handler = /** 某某handler **/; proxyClient = Proxy.newProxyInstance(classLoader, thriftClientClass.getInterfaces(), handler); } @Override public Object getObject() throws Exception { return proxyClient; } @Override public Class> getObjectType() { // 返回thrift 的 $Iface // return thriftClass.getClassLoader().loadClass(thriftClass.getName() + "$Iface"); } @Override public boolean isSingleton() { return true; } @Override public void close() { // 释放各种连接 } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // applicationContext 赋值 }}
在普通的业务gateway中,本来是直接这样创建service client bean的:
public Object getThriftServiceProxy( Integer maxActive, String service, String serviceName, String loadbalance, Integer connectTimeout, Integer socketTimeout, String serviceTag) throws Exception { ThriftServiceClientProxyFactory proxyFactory = new ThriftServiceClientProxyFactory(); EnvTagRouter envTagRouter = new EnvTagRouter(); if (serviceTag != null && serviceTag.length() > 0) { envTagRouter.setBusinessTag(serviceTag); } else { envTagRouter.setBusinessTag(zkTag); } proxyFactory.setTagRouter(envTagRouter); proxyFactory.setMaxActive(maxActive); proxyFactory.setService(service); if (!TextUtils.isEmpty(serviceName)) { proxyFactory.setServiceName(serviceName); } proxyFactory.setLoadbalance(loadbalance); proxyFactory.setConnectTimeout(connectTimeout); proxyFactory.setSocketTimeout(socketTimeout); proxyFactory.setServerAddressProvider(this.serverAddressProvider); proxyFactory.afterPropertiesSet(); return proxyFactory.getObject(); }
在这里,借助客户端代理工厂ThriftServiceClientProxyFactory,通过反射实现了构建此对象。
一、BeanDefinitionRegistryPostProcessor
关于注册bean到容器 我们开发的类,如果想注册到spring容器,让spring来完成实例化,常用方式如下:
1. xml中通过bean节点来配置;
2. 使用@Service、@Controller、@Conponent等注解;其实,除了以上方式,spring还支持我们通过代码来将指定的类注册到spring容器中,也就是今天我们要实践的主要内容,接下来就从spring源码开始,先学习源码再动手实战;
还可以开发BeanDefinitionRegistryPostProcessor接口的实现类,通过代码注册bean的功能;
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(ThriftServiceClientProxyFactory.class);beanDefinitionBuilder.setScope("singleton");beanDefinitionBuilder.setDestroyMethodName("close");beanDefinitionBuilder.setLazyInit(false);RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();// targetType: Class类型, ABC_Service$ClientrootBeanDefinition.setTargetType(targetType);// 注册bean: 这里的bean name就是我们的thrift service的名字,注册 bean service clientbeanDefinitionRegistry.registerBeanDefinition(beanName, rootBeanDefinition);// 注意这里的骚操作!别看前面是ThriftServiceClientProxyFactory,实际却是 idl service的 ***$Client 类,比如Assert.assertEquals(targetType, defaultListableBeanFactory.getType(beanName));
这里,就是service client bean的创建。
RootBeanDefinition类 extends AbstractBeanDefinition抽象类/** * Return the current BeanDefinition object in its raw (unvalidated) form. * @see #getBeanDefinition() */public AbstractBeanDefinition getRawBeanDefinition() { return this.beanDefinition;}
在2.4.0版本中,下面的验证,是OK的。
compile('org.springframework.boot:spring-boot-starter-web:2.4.0')compile('org.springframework.boot:spring-boot-starter-test:2.4.0')compile("org.springframework.boot:spring-boot-configuration-processor:2.4.0")
import org.junit.Test;import org.springframework.beans.factory.FactoryBean;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.core.ResolvableType;import static org.junit.Assert.assertEquals;/** * @Author: *** * @Date: 2020/11/15 18:44 */public class Test2 { @Test public void testGenericsBasedInjectionWithBeanDefinitionTargetResolvableType() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); RootBeanDefinition bd = new RootBeanDefinition(); bd.setInstanceSupplier(TypedFactoryBean::new); bd.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class)); bd.setLazyInit(true); context.registerBeanDefinition("fb", bd); context.refresh(); assertEquals(String.class, context.getType("fb")); assertEquals(FactoryBean.class, context.getType("&fb")); } static class TypedFactoryBean implements FactoryBean<String> { public TypedFactoryBean() { throw new IllegalStateException(); } @Override public String getObject() { return ""; } @Override public Class> getObjectType() { return String.class; } @Override public boolean isSingleton() { return true; } }}
二、InstantiationAwareBeanPostProcessor
该接口除了具有父接口中的两个方法以外还自己额外定义了三个方法。所以该接口一共定义了5个方法,这5个方法的作用分别是:
方法 | 描述 |
postProcessBeforeInitialization | BeanPostProcessor接口中的方法,在Bean的自定义初始化方法之前执行 |
postProcessAfterInitialization | BeanPostProcessor接口中的方法 在Bean的自定义初始化方法执行完成之后执行 |
postProcessBeforeInstantiation | 自身方法,是最先执行的方法,它在目标对象实例化之前调用,该方法的返回值类型是Object,我们可以返回任何类型的值。由于这个时候目标对象还未实例化,所以这个返回值可以用来代替原本该生成的目标对象的实例(比如代理对象)。如果该方法的返回值代替原本该生成的目标对象,后续只有postProcessAfterInitialization方法会调用,其它方法不再调用;否则按照正常的流程走 |
postProcessAfterInstantiation | 在目标对象实例化之后调用,这个时候对象已经被实例化,但是该实例的属性还未被设置,都是null。因为它的返回值是决定要不要调用postProcessPropertyValues方法的其中一个因素(因为还有一个因素是mbd.getDependencyCheck());如果该方法返回false,并且不需要check,那么postProcessPropertyValues就会被忽略不执行;如果返回true,postProcessPropertyValues就会被执行 |
postProcessPropertyValues | 对属性值进行修改,如果postProcessAfterInstantiation方法返回false,该方法可能不会被调用。可以在该方法内对属性值进行修改 |
注意两个单词
单词 | 含义 |
Instantiation | 表示实例化,对象还未生成 |
Initialization | 表示初始化,对象已经生成 |
postProcessPropertyValues 方法中,对 ThriftServiceClientProxyFactory 对象一些小的赋值操作。
三、InitializingBean
@Overridepublic void afterPropertiesSet(){...}
InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
四、ApplicationContextAware
在我们的web程序中,用spring来管理各个实例(bean), 有时在程序中为了使用已被实例化的bean, 通常会用到这样的代码:
ApplicationContext appContext = new ClassPathXmlApplicationContext("applicationContext-common.xml");AbcService abcService = (AbcService)appContext.getBean("abcService");
但是这样就会存在一个问题:因为它会重新装载applicationContext-common.xml并实例化上下文bean
不用类似new ClassPathXmlApplicationContext()的方式,从已有的spring上下文取得已实例化的bean。通过ApplicationContextAware接口进行实现。
当一个类实现了这个接口(ApplicationContextAware)之后,这个类就可以方便获得ApplicationContext中的所有bean。
@Componentpublic class AppUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext arg0) throws BeansException { applicationContext = arg0; } public static Object getObject(String id) { Object object = null; object = applicationContext.getBean(id); return object; }}
五、FactoryBean
FactoryBean本质上还是一个Bean。
FactoryBean 通常是用来创建比较复杂的bean,一般的bean 直接用xml配置即可,但如果一个bean的创建过程中涉及到很多其他的bean 和复杂的逻辑,用xml配置比较困难,这时可以考虑用FactoryBean。
很多开源项目在集成Spring 时都使用到FactoryBean,比如 MyBatis3(https://github.com/mybatis/mybatis-3) 提供 mybatis-spring项目中的 org.mybatis.spring.SqlSessionFactoryBean:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml">property>bean>
现在一般通过注解的形式设置sqlSessionFactory。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> { private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);...public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { this.afterPropertiesSet(); } return this.sqlSessionFactory; }...}
六、Closeable
java.io.Closeable
重写close() 方法,释放各种连接等。还有连接池的关闭等。
连接池org.apache.commons.pool.impl.GenericKeyedObjectPool,使用commons-pool 的对象池,获取到调用service的client。
TTransport transport = new TFramedTransport(new TSocket("127.0.0.1", 8000, 10000));TProtocol protocol = new TBinaryProtocol(transport);
使用对象池进行管理。
七、服务的发布与注销
ZooKeeper在集群的各个节点之间缓存数据,Curator提供了三种类型的缓存方式:Path Cache,Node Cache 和Tree Cache。Path Cache用来监控一个ZNode的子节点. 当一个子节点增加, 更新,删除时, Path Cache会改变它的状态, 会包含最新的子节点, 子节点的数据和状态。
维护着一个 >> 结构的对象,private tag 是key,服务list 是value。共享这个对象,然后从中找出所要的服务。
八、AtomicReference
AtomicReference也是java.util.concurrent包下的类,跟AtomicInteger等是一样的,也是基于CAS无锁理论实现的,但是不同的是 AtomicReference 是操控多个属性的原子性的并发类。
九、动态创建swagger接口信息
Method scanDocumentationMethod = DocumentationPluginsBootstrapper.class.getDeclaredMethod("scanDocumentation", DocumentationContext.class);scanDocumentationMethod.setAccessible(true);scanDocumentationMethod.invoke(documentationPluginsBootstrapper, documentationContext);
总结:此web工具系统,在使用过程中,具有普遍性,不用再修改代码就可以调用所有的thrift serivce服务。具有通用性。
参考文档:
1、https://cloud.tencent.com/developer/article/1409273
2、https://www.jianshu.com/p/4c0723615a52
3、https://blog.csdn.net/boling_cavalry/article/details/82193692
4、https://blog.csdn.net/u014082714/article/details/81166648