1 背景介绍
PHP调用Java提供的接口,需要代码转化,使用scf调用。 目前有两种技术方案: 架构组方案和安居客方案。
架构组方案如下所示:
右上图展示了要转换代码需要填写的信息,左上图展示了整个接口调用所需要的步骤,依次总共需要8步。
架构组这套方案是有缺陷的:
1.步骤繁琐,耗时,沟通成本高。
2.无法解析泛型,需另开发一个不含有泛型的接口,比如XXXforPHP。
3.一个接口涉及多个服务调用,本地调试时无法同时调试线上和沙箱服务。
安居客方案如下:
右上图展示了代码转化工具的使用方法,左上图展示了实现接口调用的整个步骤,共需7步。
从架构组方案迭代到安居客方案,沟通成本相对减少,但是也是有其缺陷的:
1.步骤还是相对比较复杂,比较耗时。
2.遇到泛型需要去询问服务方是何种泛型,不能自动解析。
3.一个接口涉及多个服务调用,本地调试时无法同时调试线上和沙箱服务。
4.转换代码是一种离线模式,同时不能下载多个服务。
现在存在的问题有以下几点:
1.步骤太多,既耗时开发效率也低。
2.代码转换完以后需要自己将代码拷贝到项目中的指定目录(有很多同学因为目录拷贝错误导致接口调不通)。
3.泛型不能自动解析。
4.本地接口涉及多个服务时,不能同时调试线上和沙箱服务。
2 重构思想
从现有方案可以看出,现在PHP调用Java接口步骤是很繁琐的,开发效率偏低。 而且还有两个问题: 泛型无法解析、不便本地调试。 所以要进行重构来解决这些问题。 重构要围绕下面三个方向进行:
3 流程解析
围绕现有问题进行了重构:
具体实现流程如下。
第一步:实现代码工程化管理。
- 开发PHPbase服务解决Java代码下载问题。
- 解析pom坐标信息。根据坐标拼接要下载jar包的URL,通过copyURLToFile将URL的信息下载到指定目录下,也就是将服务的jar包下载下来。
- 解析下载的jar包。循环读取jar包信息,碰到pom.xml文件读取依赖信息,并下载。有的依赖version写在了父pom的dependency内,需要通过父pom读取,还要兼顾version写在属性中的情况。父pom的依赖信息也要下载下来。获取到所有的jar包,放到一个URL数组里,然后通过URLClassLoader解析,碰到以.class结尾的文件进行判断是contract?entity?enum? 流程图如下:
- 解析contract。对于注解中有serverContract的就是contract文件。通过反射拿到类中含有的方法名字,以及入参和出参。参数的类型包括一些基本类型string、int、bool等还会有一些复杂类型map、list、set等,还可能是泛型。对于复杂类型map、list等类型会继续分析子元素的类型信息。假如是map类型,那它的子元素信息就是key_type:XXX,value_type:XXX。如果子元素中key_type是个object,那么它的key_class就是这个object的包名。同样,如果子元素的value_type是个object,那么它的value_class就是这个object的包名。对于复杂类型list、set等也是如此分析。如果是泛型,它的elem_type赋值为object,elem_class赋值为java.lang.object。将方法名字拼接一个hash值(方法名+入参+出参)再拼接一个字符串params代表入参,以这个字段为key,以入参的类型信息为value放到变量paramVar中。同样方法名字拼接一个hash值(方法名+入参+出参)再拼接一个字符串return代表出参,以这个字段为key,以出参的类型信息为value也放到变量paramVar中。入参和出参的具体信息可以看下图。最后以contract类的包名信息为key,paramVar为value放入变量contractMap中。
- 解析entity。如果注解SCFSerializable=true and SCFMember=true,就是一个entity文件。通过SCFMember获取到字段的信息:var、orderID、generic,将这些信息放入变量TSPEC中,key是orderId的值,value就是这些字段的信息。 对于字段类型分析同contract中类型分析是一致的,在此不赘述了。如果这个实体有继承关系,要将继承信息也放入变量TSPEC中。key是999,value是继承信息。如果注解SCFSerializable.name不为空,将这个值也放入变量TSPEC中。key是998,value是注解SCFSerializable.name的值。最后以entity类的包名信息为key,TSPCE为value放入entityMap中。
- 解析enum。如果isEnum=true的就是枚举文件。通过反射获取字段信息也就是constant信息,放入enumMap中,key是枚举类的包名,value是各个常量值。
- 返回结果。将contract、entity、enum这三个信息组装在一起放入Json中作为结果返回,如下图所示也就是最下面的这个图。
- 开发build.php来实现Java代码到PHP代码的转化,解决现在代码需要人工拷贝的问题。流程图如下:
从PHPbase拿到解析坐标后的数据,也就是那个JSON数据,反解析获取到三个包含contract、entity、enum的数组。
下面三个图展示了三个数组的部分具体内容:
第一个图是截取的contract数组中的部分信息。key是一个完整的包名,value就是这个类中的所有函数信息。我们看下第一个方法,key是由方法名字+hash值+params字符串组成。value就是入参的类型信息,可以看到有string,有object。类型是object 的就有这个对象对应的class信息。params是入参,带有return的就是出参信息。
第二个图是截取的entity数组中的部分信息。我们看到key是一个完整的包名,value是实体的字段信息。比如第一个字段排序是第一个,不是泛型,类型是list,子元素类型是object,elem_class就是这个object的包名信息。key为998对应的value就是Java中注解SCFSerializer.name的值
第三个图是截取的enum数组中的部分信息。key是完整包名,value就是各个常量的值。
数组信息分析完,看下具体解析过程。contract数组解析流程图如下所示:
拿到contract数组循环遍历,对于类文件的目录我们用的是包名+固定路径“Libs/wscfcore/package”,为了防止和项目中已有的代码相区别,我们将代码放到了libs/wscfcore/package下。命名空间也是包名信息+刚才那个固定路径,大家可以看下右上方这个图。对于类文件的依赖,也是包括两部分,一个是固定的依赖 “use libswscfcoreclient”,这个是因为类中的构造方法用到了scf client的实例化。依赖的另一部分就是参数中用到的引用。生成方法的时候要考虑两个问题,一个是重名问题,一个是序列化问题。
当方法重名的时候我们用0,1来进行区分,比如getInfo0,getInfo1。这里面有一个坑,就是要考虑下每次生成这个方法时的顺序问题,因为方法重名里面的参数个数是不一样的,所以名字绝对不能变换顺序。我们为了防止发生顺序变更问题,在Java代码生成的时候用到了一个hash值。就是将方法名、入参、出参做了一个hash值,这个肯定是唯一的,通过这个来保证顺序不变。
生成方法的时候还要考虑第二个问题那就是序列化的问题,我们看下右上方这个图,对于第四个参数的类型是非基础类型,所以这个类型序列化时对应的typeID也就是hash值就从这个类的包名来获取,通过在这建立全局数组,在后边生成typeID的时候获取这个参数的包名信息。$params这个参数里面包含了lookup,methodName,以及入参信息。
entity数组解析流程图如下所示:
生成实体的过程中也要生成目录,命名空间,依赖。这些同contract的分析是一样的,就不赘述了。实体的构造方法我们看右上图可以看出是对静态变量TSPEC赋值的过程。这个变量是一个数组,里面包含了各个字段的信息,名字var,顺序orderID,是不是泛型isGeneric,以及类型type。对于复杂类型list也要生成它的子元素的类型。这个类型信息在从服务器拿到二进制流数据进行反序列化的时候要用到,类型必须和其相同才能正确的解析出来。如果这个实体类有继承,那么也要生成这个类的继承类。
enum数组解析如下图所示:
Enum的解析就简单些了。要生成目录,命名空间,const信息。const信息遍历enum数组即可。到这Java代码到PHP代码的转化就完成了,并生成在了指定位置。我们再也不用手动拷贝代码了。避免了因为拷贝代码路径出错导致的接口调不通问题。
- 引入一个service.xml文件来管理服务版本信息,所有要下载的服务都写在这里,采用XML格式的文件,开发同学直接将坐标信息复制到这里即可,方便开发同学使用。
第二步:解决泛型解析问题。
Java同学是知道泛型对应的具体是何种类型,就是他们开发的实体中的某一个,不过PHP同学是不知道的。但是这个泛型序列化以后的typeID值也就是hashcode值我们可以拿到,那么我们将所有实体做一个hash计算,然后我们反推是不是就可以知道泛型是哪个实体了呢?
所以优化build.php文件,收集服务的实体和枚举信息,进行hash计算。我们现在有scfv1、scfv3两种协议,不同协议对应的typeID是不同的,所以针对这两种协议进行hash计算。对于scfv1协议,若实体中有scheme字段(998),那么对scheme字段进行。若没有,则对类名进行hash计算。而对于scfv3协议是对整个包名进行计算。有了这个hash文件就可以解析泛型了。
第三步:解决本地无法同时调试线上和沙箱服务的问题。增加沙箱配置文件,方便开发者进行调试。文件如下图所示:
服务名字和沙箱IP要准确对应上。底层获取服务信息时,判断有此文件,拿到服务的信息就可以直接走沙箱服务了。对于不存在于此文件的服务是走线上的。如果将代码放到沙箱环境,灰度的申请就可以用这个文件代替了,省去了申请的操作。
第四步:将步骤由8步减少为3步。
- 将Java的pom坐标信息放入service.xml文件中。
- 执行build文件,响应信息如下图所示。从service.xml中获取坐标信息,检查是否已经下载过该服务。若没有下载过,掉PHPbase拉取Java代码,然后转化为PHP代码,并放到项目中的指定位置。
- 接口调用。运用反射机制实例化调用类,然后再调用类中的方法。
下面这个图是IHouseService类的内容,可以看到实例化此类要传入服务名serviceName,查找类lookup,分别对应上图中的hmc、HouseService
4.无痕调用
从集团方案迭代到租房方案,调用步骤由原来的八步减少到了现在的三步。
租房方案的实现代码的目录结构展示如下所示:
wscfcore这个目录是包括了所有scf相关的文件。Hashdata这个目录里面放的是hash文件。package.lock里面存储了所有项目中目前已经下载的服务坐标信息,存的是字符串。package目录里面存的是所有下载的代码。Build文件是整个代码的核心,根据坐标拉取Java代码然后再转化为PHP代码放在指定位置。service.xml文件是存放我们要下载的服务的pom坐标,采用xml格式方便大家直接将坐标信息复制粘贴即可。wscfcore目录会放到composer中供大家下载使用。
PHP同学用这套代码,调用某个服务的接口时,实现了PHP同学调Java接口就像Java同学调Java接口是一样的,是一种无痕式的调用。