PHP调用Java接口的两种技术方案对比:架构组与安居客方案详解
背景介绍

PHP 要调用 Java 提供的接口,就需要进行代码转化,然后使用 scf 来调用。当下存在两种技术方案,分别是架构组的方案以及安居客的方案。
架构组方案如下所示:
右上图呈现了进行代码转换需填写的信息,左上图展示了整个接口调用所历经的步骤,这些步骤依次总计有 8 步。
架构组这套方案是有缺陷的:
1.步骤繁琐,耗时,沟通成本高。
无法解析泛型,所以需要另外开发一个不带有泛型的接口,例如 XXXforPHP。
一个接口会牵涉到多个服务的调用,在进行本地调试的时候,没办法同时对线上服务和沙箱服务进行调试。
安居客方案如下:
右上图呈现了代码转化工具的使用方式,左上图呈现了实现接口调用的全部步骤,这些步骤一共有 7 步。
从架构组方案迭代到安居客方案,沟通成本有所减少。然而,这也存在一些缺陷:
1.步骤还是相对比较复杂,比较耗时。
2.遇到泛型需要去询问服务方是何种泛型,不能自动解析。
3.一个接口涉及多个服务调用,本地调试时无法同时调试线上和沙箱服务。
4.转换代码是一种离线模式,同时不能下载多个服务。
现在存在的问题有以下几点:
1.步骤太多,既耗时开发效率也低。
代码转换完成之后,需要自己把代码拷贝到项目里的指定目录。有许多同学因为在目录拷贝这个环节出现错误,从而导致接口无法调用。
3.泛型不能自动解析。
4.本地接口涉及多个服务时,不能同时调试线上和沙箱服务。
重构思想
从现有方案能够得知,当下 PHP 调用 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 中。
如果注解的情况是 SCFSerializable=true 并且 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 中。
解析枚举。若 isEnum 为真,那就是枚举文件。利用反射来获取字段信息,也就是 constant 信息,然后将其放入 enumMap 里,其中 key 是枚举类的包名,value 是各个常量值。
将 contract、entity、enum 这三个信息组合起来,放入 Json 中,然后将其作为结果返回,就如同下面这个图所示。
拿到从 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 值的计算,这个 hash 值必然是唯一的,凭借这个唯一的 hash 值能够确保顺序不会发生改变。生成方法时需考虑第二个问题,即序列化问题。我们来看右上方的图,第四个参数的类型为非基础类型,所以该类型序列化时对应的 typeID(也就是 hash 值)从这个类的包名获取。通过在这建立全局数组,在后续生成 typeID 时能获取该参数的包名信息。这个参数 $params 包含了入参信息。
entity数组解析流程图如下所示:
在生成实体的过程中,需要生成目录、命名空间和依赖。这些与 contract 的分析是相同的,所以不再详细说明。从右上图可以看出,实体的构造方法是对静态变量 TSPEC 进行赋值的过程。这个变量是一个数组,其中包含了各个字段的信息,如名字 var、顺序 orderID、是否为泛型 isGeneric 以及类型 type。对于复杂类型的 list 需生成其子元素的类型。在从服务器获取二进制流数据并进行反序列化时会用到该类型信息,只有类型相同才能正确解析出来。若此实体类有继承,还需生成该类的继承类。
enum数组解析如下图所示:
Enum 的解析相对简单。需要生成目录、命名空间以及 const 信息。const 信息可以通过遍历 enum 数组来获取。到这里,Java 代码到 PHP 代码的转化工作就完成了,并且生成在了指定的位置。我们不再需要手动拷贝代码,从而避免了因拷贝代码路径出错而导致的接口调用不通的问题。
第二步:解决泛型解析问题。
Java 同学知晓泛型所对应的具体类型,即他们开发的实体中的某一个,然而 PHP 同学并不知晓。不过,这个泛型序列化之后的 typeID 值也就是 hashcode 值,我们是能够获取到的。那么,我们对所有实体进行一次 hash 计算,接着进行反推,是否就能够得知泛型是哪个实体了呢?
优化 build.php 文件,将服务的实体和枚举信息进行收集,接着进行 hash 计算。我们目前拥有 scfv1 和 scfv3 这两种协议,由于不同协议对应的 typeID 存在差异,所以要针对这两种协议分别进行 hash 计算。对于 scfv1 协议而言,如果实体中存在 scheme 字段(998),那么就对该 scheme 字段进行 hash 计算。如果没有的话,就会对类名进行 hash 计算。对于 scfv3 协议,是对整个包名进行计算。有了这个 hash 文件,就能够解析泛型了。
第三步,要解决本地无法同时调试线上和沙箱服务的问题。为此需要增加沙箱配置文件,这样能方便开发者进行调试。该文件如下图所示:
服务名字需与沙箱 IP 准确对应。底层在获取服务信息时,若判断有此文件,拿到服务信息后就能直接使用沙箱服务。而对于不在此文件中的服务,则走线上。若将代码置于沙箱环境,可利用此文件代替灰度申请,省去申请操作。
第四步:将步骤由8步减少为3步。
下面这个图展示了 IHouseService 类的内容。可以观察到,实例化此类时需要传入服务名 serviceName,以及查找类 lookup。并且它们分别对应上图中的 hmc 和 HouseService。
无痕调用
集团方案进行了迭代,租房方案也有相应变化。原来调用步骤有八步,现在减少到了三步。
租房方案的实现代码的目录结构展示如下所示:
wscfcore 这个目录包含了所有与 scf 相关的文件。Hashdata 这个目录放置的是 hash 文件。package.lock 中存储着所有项目当前已下载的服务坐标信息,这些信息是以字符串的形式存储的。package 目录中存放的是所有下载的代码。Build 文件是整个代码的关键所在。它会依据坐标来拉取 Java 代码,接着把这些 Java 代码转化为 PHP 代码,并放置在指定的位置。service.xml 文件是用来存放我们需要下载的服务的 pom 坐标的,其采用 xml 格式,这样大家就可以很方便地直接将坐标信息进行复制粘贴。wscfcore 目录会被放置到 composer 中,以便大家进行下载和使用。
PHP 同学使用这套代码去调用某个服务的接口,在这个过程中,实现了 PHP 同学调用 Java 接口的方式与 Java 同学调用 Java 接口的方式相同,是一种没有痕迹的调用。