我们在项目中遇到一个需求,输入为json字符串,如:

{
  "title": "lrtest",
  "alg": "LogisticRegression",
  "params": {
      "inputTable": "xxx.lrtest3",
      "labelColumn": "label",
      "featureColumns":  [
        {"columnName":"f0","columnType":"Double"},
        ..
        {"columnName":"f5","columnType":"Double"}
      ],
      "elasticNetParam": 1.0,
      "fitIntercept": true,
      "maxIter": 10,
..
      "fraction": 0.8,  
  }
}

输出为该机器学习算法执行的scala代码,如:

..
    val lor = new LogisticRegression()
      .setFeaturesCol("features")
      .setLabelCol("indexedLabel")
      .setRegParam(params.regParam)
      .setElasticNetParam(params.elasticNetParam)
      .setMaxIter(params.maxIter)
      .setTol(params.tol)
      .setFitIntercept(params.fitIntercept)
..

本质上需要的是一个json到scala的编译器。最早的时候使用纯java编程,解决方法是先写好模板化的代码,其中的参数如MaxIter这种在模板代码中写为${maxIter},然后根据json串解析到的参数,通过String.replace()模板代码以生成最终代码。过程如下:

模板代码:

val lr = new LogisticRegression()
  .setFeaturesCol("features")
  .setLabelCol("label")
  .setWeightCol("weight")
  .setElasticNetParam({elasticNetParam})
  .setFitIntercept({fitIntercept})
  .setMaxIter({maxIter})
  .setRegParam({regParam})
  .setStandardization({standardization})
  .setThreshold({threshold})
  .setTol({tol})

replace的过程就不贴了,简单来说就是先从文件中将模板代码读取出来,然后将占位符replace实际json串的参数,脑补下好了,代码删掉了,懒得去翻git了。

这样的坏处是什么呢?

第一,由于是模板代码,决定了这个过程非常不灵活。假如有个参数是可选的,不需要在new LogisticRegression()的时候set进去,模板代码不能很自然的搞定; 第二,可维护性比较差,因为模板跟replace的过程的割裂的,互相没有约束,很难在replace的代码段中清楚的看到有没有漏replace的占位符。 第三,每次请求都要从文件中读取模板代码,性能比较差。 第四,由于java中多行字符串只能使用 “string1 “ + “string2”这种形式来编写,并且如果字符串中有引号还需要加斜杠转义,造成如果想将代码拷贝出去运行的话需要做一系列的修改。


使用scala的字符串插值就可以很简单的解决这个问题。

//todo: 什么是scala 字符串插值,请google

还是上面的例子:

    val params: LogisticRegressionBody = JsonProc.toObj(jsonStr, classOf[LogisticRegressionBody])

    val createMethod =
      s"""
          val method = new LogisticRegression()
                      .setFeaturesCol("features")
                      .setLabelCol("label")
                      .setElasticNetParam(${params.getElasticNetParam})
                      .setFitIntercept(${params.getFitIntercept})
                      .setMaxIter(${params.getMaxIter})
                      .setRegParam(${params.getRegParam})
                      .setStandardization(${params.getStandardization})
                      .setThreshold(${params.getThreshold})
                      .setTol(${params.getTol})
      """

params即为json解析得到的body,字符串插值可以很灵活的将json的值“插”到模板代码中,并且scala支持多行字符串,可读性比较高。

由于有些问题使用java来解决比较熟悉,所以这个工程是java和scala混合的,其pom.xml需要注意下。下面贴spring boot框架和scala代码混合编程的maven工程。这种写法maven会mix方式的编译java和scala代码,即使2个子包有相互依赖也没问题。

maven还可以配置javaBeforeScala或者反过来,不过我觉得意义不大,不如mix以不变应万变。官方说明等后面找到了再贴一下链接。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.datastart</groupId>
    <artifactId>ml</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.1.RELEASE</version>
    </parent>

    <properties>
        <scala.version>2.10.5</scala.version>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-compiler</artifactId>
            <version>${scala.version}</version>
        </dependency>
...
    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <id>scala-compile-first</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>scala-test-compile</id>
                        <phase>process-test-resources</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>

纯记录,凌乱请见谅。