tesla(特斯拉)服务化框架?一听就很高大上,有木有?
    tesla是什么?简单来说,tesla之于蘑菇街就好比dubbo之于阿里,当然tesla和dubbo
很相似,比如解决的问题域等,但是差异也有不少,比如支持异构系统等。不然我们也就直接借
用dubbo, 没必要耗费这么多人力物力来重复造轮子。
    为什么要tesla服务化框架呢?蘑菇街主站的代码是基于纯PHP轻框架kohana的,随着蘑菇
街不断发展,业务不断增多,服务爆炸式增长,不知不觉间主站代码越来越多,已然是个超级庞
然大物了,如今配置管理不简单,开发调试困难,服务间依赖关系也变得错综复杂,是时候需要
拆分应用进行服务化,以提高开发效率、调优性能、节省关键竞争资源了,tesla服务化框架便
应运而生了。我们选择了开源社区最流行的编程语言Java及C++来实现该框架,笔者这么说,看
官您应该懂的。
    前面已经提到蘑菇街主站代码是基于PHP,而我们的特斯拉服务化框架是基于JAVA,所以必
然存在一个异构系统调用的问题。那么服务化后异构系统怎么调用呢?
    简单举例说明一下,以商品评价服务中新增一条评价为例,假设我们需要对某个商品增加一
条评价,在PHP端先调用PHP的新增评价服务接口,tesla框架的PHP端代理(C++实现)会将该调用
封装成一个符合tesla的请求,然后序列化后经过路由传到Java端的商品评价服务上,Java端反
序列化这个请求,调用对应的新增评价服务方法然后将执行结果序列化后返回给PHP端。细心的看
官会发现新增评价服务会有两份,一份是Java端的具体方法实现及一份具有相同接口的PHP版映射,
作为服务提供方或开发人员需要实现Java,又要写PHP,这跟tesla服务化提高开发效率是相悖的。
另外,也不是每个开发人员都是全栈工程师,既会Java,又会PHP。而且程序猿总是喜欢偷懒的,
怎么可能愿意把时间和精力浪费在这么机械性的重复工作上呢?一方面为了减少服务提供方的开发
工作量,一方面也为了提高tesla服务化框架的易用性,急需一个Java代码转PHP代码的工具。而
笔者作为tesla小组中的打酱油冠军,很荣幸地接下了这个有意义的任务。
    首先来看下具体任务,把Java工程代码转成PHP代码,语言转换,看着好像很神奇的样子。
那这么神奇的任务,先来看下是否可行呢?众所周知,Java是面向对象语言,是强类型的;而PHP
也是面向对象语言,虽然是伪面向对象,是弱类型的。看上去有点接近,似乎可行,那么就来简单

对比下。

                                                               PHP与Java的类比较关系表

     

    
    由上表我们可以清楚看到PHP跟Java还是有很多共同点的,而且PHP中的绝大部分特征在Java
中都能够找到对应项,反之则不然。如果要将ava工程代码完全等价转化成PHP代码似乎是不可行的。
那么具体需求是什么呢?PHP类和Java类的差异化对我们的转化是否构成影响呢?于是笔者找到了第
一个接入tesla服务化的需求方@北斗大神,经过多次交流,得到如下需求的POJO类:
  1.     teslaSpace 即namespace,类的全路径名 tesla序列化时用
  2.     不需要include。得益于cohana框架,少了include各种依赖,世界清静不少
  3.     文件名全部小写 如:itemrate.php
  4.     类名、父类名、接口名驼峰式,比如:Model_Rate_Domain_ItemRate
  5.     属性
  6.     方法及参数名,方法体逻辑不要求实现,方法注释
  7.     非属性的set、get方法包装成tesla调用接口
  8.     每个类有一个共同的tesla的exec调用方法
  9.     tesla core中BaseEntity的类名为Model_Common_BaseEntity
  10.     枚举类型改成常量
  11.     类分类型保存,domain、service、constant三类
  12.     只转需要需要分类的三种Java类文件
    需求明确了,那么事情就好办多了。参照PHP与Java的类比较关系表,发现差异化很少,该POJO
类需要的信息,Java类都能够提供。如果我们能够解析出Java类的各个信息,那么生成PHP代码就是
砍瓜切菜的事。接下来任务就剩下如何解析Java类信息。
    由于不关心依赖关系,那么import的信息就直接忽略了。
    接下来获得类名。得益于标准Java工程,笔者知道Java工程类名和目录是有规律的,特别是基于
Spring框架的工程,只要扫描文件再加个正则匹配就能够拿到某工程下的所有Java文件了。假设某个
类路径为"/test/rate-api/src/main/java/com/mogujie/rate/domain/ItemRate.java",
很显然其类的全路径是"com.mogujie.rate.domain.ItemRate",通过正则表达式简单处理下其路
径字符串就能够得到我们需要的类名。
    接着继续获得类的其它信息,比如父类名。只要把这个字符串类名加载到类库得到Class,那么就
可以方便获得该类的信息了。笔者知道为了追求效率,tesla在调用方法时没有采用反射机制,而是采
用了字节码的方式,笔者不关心这个,笔者关心的是如果获得Java类的类名及其它各种信息。字节码太
复杂,笔者头脑简单,短期内搞不定,但是反射么,网上参考资料挺多的,虽然性能差点,但是无碍啊。

很快,万能的谷歌就告诉笔者如何获得一个类的类名:

    String pkgName = "com.mogujie.rate.domain.ItemRate"; // 前面我们得到的全路径类名
    Class clazz = Class.forName(pkgName); 
    Class clazz = Thread.currentThread().getContextClassLoader().loadClass(pkgName); 

当然前提是运行时,该类已经加载过,不然会抛出类加载失败的异常。由于需要转得Java工程中
的类往往是未被加载的,需要在转换前加载一下,那么怎么加载呢?
    很快查到了可以使用URLClassLoader来加载jar包,这就很简单了,只要先把Java工程编译
打成jar包,加载即可,一切似乎都变得那么简单。那么父类名就是clazz.getSuperclass();
接口集合clazz.getGenericInterfaces();自身属性集合clazz.getDeclaredFields();
自身方法集合clazz.getDeclaredMethods();构造函数集合clazz.getDeclaredConstructors();
到目前为止,似乎需要的信息都已经获得了。接下来就是如何将这些信息转化成PHP类了。很顺利地,12个
需求点有9个很轻松的就完成了,但是第6点的方法参数只能得到参数类型,不能得到形参名,幸好PHP是弱
类型,不关心参数类型,只关心参数,所有只能猥琐地用param[seqno]来解决;遗憾的是得不到方法体的
信息;而第10点却是个痛点,Java中Enum也是按类来处理,对于一个Java新手来说不知道如何区分普通类
和枚举类的区别,笔者就纳闷了,为什么接口也是按类处理,但有接口方法判断是否是接口类,那么怎么就
枚举类不加个枚举类判断的接口方法呢?
    鉴于时限,且剩余几点也非必要,在一期中暂时先搁置了。回头来分析下,当前方案有哪些弊端呢?
首先,我们需要先编译该工程,如果编译失败则不能继续下面的转化工作,受限制比较多。而且还有两个未
能解决的痛点。在PM樱木的建议下,决定采用扫描文件的方式来逐行将Java代码翻译成PHP代码,如此能够
摆脱类依赖的问题。但是问题是怎么来匹配获得需要所需的各种信息呢?
    笔者开始尝试了解各种解析Java类的知识,也尝试使用正则表达式来粗鲁的匹配需要的信息,怎奈能力
不济,一时搞不定。正在焦头烂额不知所措之际,架构师桃片建议试试AST。
    AST是啥?Java抽象语法树,通过AST语法树解析器ASTParser可以解析Java文件。了解到这里,笔者
看到了曙光。所以开始了解AST和JavaParser,具体原理就不细说了,网上资料很多,感兴趣的自己可以去
了解下。在互联网行业是以结果为导向的,很快就知道原来eclipse和谷歌都有一套JavaParser,两套方案
都实现了下,但是根据匹配度、易扩展性和模块化等,笔者最后选择了谷歌的JavaParser来实现Java代码转
PHP代码。现在来分享下谷歌JavaParser如何解析Java文件。
    要使用很简单,只需在pom.xml文件加入依赖即可:
    <dependency>
           <groupId>com.google.code.javaparser</groupId>
           <artifactId>javaparser</artifactId>
           <version>1.0.8</version>
    </dependency>
    解析时,首先先获得某个文件的编译单元,获得方法如下:
    FileInputStream fis = new FileInputStream(file);
    CompilationUnit cu = JavaParser.parse(fis, "UTF-8");
    然后创建继承了VoidVisitorAdapter<Object>的自定义的Visitor重载其visit函数即可。
    比如需要获得Package信息,那么可以新建PackageVisitor,并重载visit函数如下: 
    @Override
    public void visit(PackageDeclaration n, Object arg) {
        packageName = n.getName().toString();
    }
    同样地,要获得其类名那么可以实现ClazzOrInterfazzVisitor,并重载visit函数如下:
    @Override
    public void visit(ClassOrInterfaceDeclaration n, Object arg) {
        isInterfazz = n.isInterface();
        setClazz(n.getName().toString());
        if (n.getJavaDoc() != null) {
            comments = n.getJavaDoc().toString();
        }
        if (n.getExtends() != null) {
            parents = new ArrayList<String>();
            Iterator<ClassOrInterfaceType> itr = n.getExtends().iterator();
            while (itr.hasNext()) {
                parents.add(itr.next().getName().toString());
            }
        }
        if (n.getImplements() != null) {
            interfazzs = new ArrayList<String>();
            Iterator<ClassOrInterfaceType> itr = n.getImplements().iterator();
            while (itr.hasNext()) {
                interfazzs.add(itr.next().getName().toString());
            }
        }
    }
    以此类推,实现MethodVisitor,并重载visit函数如下:
    @Override
    public void visit(MethodDeclaration n, Object arg) {
        method = new Method();
        method.setName(n.getName().toString());


        if (n.getParameters() != null) {
            Parameter obj = null;
            com.mogujie.tesla.j2psmith.entity.Parameter parameter = null;
            com.mogujie.tesla.j2psmith.entity.Parameter[] parameters = new com.mogujie.tesla.j2psmith.entity.Parameter[n
                    .getParameters().size()];
            List<Parameter> parametersSource = n.getParameters();
            Iterator<Parameter> itr = parametersSource.iterator();
            int i = 0;
            while (itr.hasNext()) {
                obj = itr.next();
                parameter = new com.mogujie.tesla.j2psmith.entity.Parameter();
                parameter.setName(obj.getId().getName());
                parameter.setType(obj.getType().toString());
                parameters[i++] = parameter;
            }
            method.setParameters(parameters);
        } else {
            method.setParameters(new com.mogujie.tesla.j2psmith.entity.Parameter[0]);
        }


        if (n.getJavaDoc() != null) {
            String comments = n.getJavaDoc().toString();
            method.setComments(comments);
        }


        if (n.getModifiers() > 0) {
            method.setModifiers(n.getModifiers());
        }
        if (n.getBody() != null) {
            method.setBody(n.getBody().toString());
        }
        this.addMethod(method);


    }
    实现FieldVisitor,并重载visit函数如下:  
    @Override
    public void visit(FieldDeclaration n, Object arg) {
        List<VariableDeclarator> variables = n.getVariables();
        if (variables != null && variables.size() > 0) {
            Iterator<VariableDeclarator> itr = variables.iterator();
            Parameter parameter = null;
            VariableDeclarator variableDeclarator = null;
            while (itr.hasNext()) {
                variableDeclarator = itr.next();
                parameter = new Parameter();
                parameter.setName(variableDeclarator.getId().toString());
                if (variableDeclarator.getInit() != null) {
                    parameter.setValue(variableDeclarator.getInit().toString());
                }
                if (n.getJavaDoc() != null) {
                    parameter.setComments(n.getJavaDoc().toString());
                }


                fields.add(parameter);
            }
        }
    }
    最重要的是,它可以很方便的访问Enum类,只要实现EnumVisitor,并重载visit函数如下: 
    @Override
    public void visit(EnumDeclaration n, Object arg) {
        isEnum = true;
        clazz = n.getName();
        if (n.getJavaDoc() != null) {
            comments = n.getJavaDoc().toString();
        }
        if (n.getEntries() != null) {
            Iterator<EnumConstantDeclaration> itr = n.getEntries().iterator();
            EnumConstantDeclaration ecd = null;
            Parameter parameter = null;
            while (itr.hasNext()) {
                ecd = itr.next();
                parameter = new Parameter();
                parameter.setName(ecd.getName());
                parameter.setValue(ecd.getArgs().get(0).toString());
                this.addParameter(parameter);
            }
        }
    }
    不仅如此,细心的看官应该看到了,在MethodVisitor中,我们拿到了方法体及注释。然后可以根据正则("\\{\\s+(this\\.)?([a-zA-Z0-9_]+)\\s+\\=\\s+([a-zA-Z0-9_]+)\\s*\\;\\s+\\}")来判断某方法是否是属性set方法,可以根据正则("\\{\\s+(return){1}\\s+([a-zA-Z0-9_]+)\\s*\\;\\s+\\}")来判断某个方法是否是属性get方法。除此之外,在我们的需求中,均是接口方法。
    因为使用了谷歌ASTParser,Java类解析变得异常简单,所以生成的PHP文件也更加符合预期的要求,总算把之前的几个痛点解决了。
    但事情总是不如意更加多,随着JDK的演进,Java新语法的支持,而谷歌JavaParser开源版并未及时跟进,比如泛型前导的语法不支持等,随着后期服务化业务的不断接入,应该会有更多地问题冒出了。而笔者则只能硬着头皮不断优化这个转码小工具了。
    好了,此次分享就到这里,水平有限,希望有经验或者建议的看官多提意见或建议,尽情期待tesla小组带来更多分享。


更多推荐

码农拾遗之Java代码转PHP代码