使用 Scala 开发不可避免地会涉及与 Java 的混合编译,这里记录下这两天的发现。 全文的基础如下:

Java 和 Scala 混合编译流程

图片来源:如何在 Java App 中導入 Scala,另外本文也大量参考了 sbt 官方文档和 Alvin Alexander 的 Scala Cookbook, 先行谢过。

在 Scala 中使用 Java - sbt

Scala 下的构建工具首推 sbt, sbt 可以简化软件构建和依赖管理过程,同时还可以使用 Scala 语言定制复杂的 task.

目录结构(Directory Structure)

受 Java 平台的 Maven 影响,sbt 使用的目录结构和 Maven 类似,一个基本的 sbt 目录结构如下所述:

sbtProject
├── build.sbt
├── lib <unmanaged dependencies jars>
├── project
│   └── Build.scala (optional)
├── src
│   ├── main
│   │   ├── java <main Java sources>
│   │   ├── resources <files to include in main jar here>
│   │   └── scala <main Scala sources>
│   └── test
│       ├── java <test Java sources>
│       ├── resources <files to include in test jar here>
│       └── scala <test Scala sources>
└── target <compiled classes, packaged jars, managed files, caches, and documentation>

目录结构的初始化可以通过 Giter8 或者自定义 shell 脚本完成,以下是一个简易的目录生成脚本。

#!/bin/sh

mkdir -p src/{main,test}/{java,resources,scala}
mkdir lib project target
touch project/Build.scala

# create an initial build.sbt file
cat <<EOF > build.sbt
name := "sbtProject"

version := "1.0"

scalaVersion := "2.10.5"
EOF

构建控制

构建过程由项目目录下的build.sbt和子目录project下的*.scala 共同控制。

依赖库管理

依赖的库可通过 Unmanaged 和 Managed 两种方式添加,Unmanaged dependencies 通常放置于当前项目目录下的子目录lib, sbt 会将其作为 classpath.

另一种常见的方式为 Managed dependencies, 底层使用 Apache Ivy 作为库的依赖管理。添加依赖库在 sbt 中十分简单,最简洁的方式可以在build.sbt 中添加一行:

libraryDependencies += groupID % artifactID % revision

对于多个依赖,自然可以写多行libraryDependencies, 也可使用Seq合并多行。

实战1 - 使用 Java 源码进行联合编译

首先我们使用 Java 新建一 Employee 类,然后在 Scala 中调用之,目录结构如下:

sbtTest
├── build.sbt
├── lib
├── project
│   └── Build.scala
├── src
│   ├── main
│   │   ├── java
│   │   │   └── me
│   │   │       └── yuanbin
│   │   │           └── java
│   │   │               └── Employee.java
│   │   ├── resources
│   │   └── scala
│   │       └── me
│   │           └── yuanbin
│   │               └── testproject
│   │                   └── Hello.scala
│   └── test
│       ├── java
│       ├── resources
│       └── scala
└── target

我们在 Java 中新建 Employee 类:

package me.yuanbin.java;

public class Employee {
    private String name;
    
    public Employee(String n) {
        name = n;
    }
    
    public String getName() {
        return name;
    }
}

Scala 中使用 Java 中创建的类:

package me.yuanbin.testproject

import me.yuanbin.java._

object Hello {
  def main(args: Array[String]) {
    val p = new Employee("Bill Ryan")
    println("Hello from " + p.getName())
  }
}

在项目的基目录下运行sbt run应该就能看的Hello from Bill Ryan字样了。如果想深入了解 sbt 中编译 Java 代码的话,其官网链接值得关注 - sbt Reference Manual — Java Sources

实战2 - 使用 Java 生成的 jar 包(Unmanaged dependencies)

源码和目录结构同实战1,不过为了说明问题可在生成 jar 包后删除 Java 部分的源码和 class 文件。

Note: Java 经验不足的建议看看打包相关知识,Java Fundamentals Tutorial: Packaging 不错。

  1. 进入 Java 源码目录下, cd src/main/java (打包依赖于目录结构, 否则生成的 jar 包无法正常使用)
  2. 编译生成.class类文件,javac javac me/yuanbin/java/Employee.java
  3. .class 生成 jar 包,jar cvf Employee.jar me/yuanbin/java/Employee.class
  4. 将生成的 jar 包放入项目的lib 目录下供 sbt 添加至 classpath, mv Employee.jar lib/

在项目基目录下运行sbt run应该同样能看到Hello from Bill Ryan 字样。

实战3 - 使用Maven Repository Library

本小节参考自 Simple Scala Akka Actor examples (Hello, world examples) | alvinalexander.com

在 sbt 中使用 Maven 仓库非常简单,将 Maven 的groupId, artifactId, 和 version 域更改为 sbt libraryDependencies 字符串即可。以 Akka Actor 为例,相应的 Maven 依赖写法如下:

<dependency>
	<groupId>com.typesafe.akka</groupId>
	<artifactId>akka-actor_2.10</artifactId>
	<version>2.3.12</version>
</dependency>

转化为 sbt 的 libraryDependencies 如下所示:

libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.3.12"

将以上一行加入至项目根目录的build.sbt下,添加 scala 源码,vim src/main/scala/Hello.scala

import akka.actor.Actor
import akka.actor.ActorSystem
import akka.actor.Props
 
class HelloActor extends Actor {
  def receive = {
    case "hello" => println("hello back at you")
    case _       => println("huh?")
  }
}
 
object Main extends App {
  val system = ActorSystem("HelloSystem")
  // default Actor constructor
  val helloActor = system.actorOf(Props[HelloActor], name = "helloactor")
  helloActor ! "hello"
  helloActor ! "buenos dias"
}

在项目基目录下运行sbt run, 终端下应该会显示hello ... 等字样。

使用 sbt 的三种常见的 Scala/Java 混合编译方法就介绍到这了。

部署单一可执行的 jar 文件

使用sbt package的确可以生成 jar 包,但是问题来了,怎样执行这些 jar 包呢?这些 jar 包由 sbt 创建,自然是可以在 scala 解释器中运行,但是想让 Java 也能执行这个 jar 包就得废些功夫了,原因在于 sbt 生成的 jar 包中包含一些 Scala 特有的东西,而这些东西则包含在scala-library.jar 这个 jar 包中。所以我们可以总结出如下三种方法运行 sbt 生成的 jar 包。

  1. 将需要的 jar 包分发至 classpath 并用 scala 执行。这种方法需要在执行系统上安装有 Scala.
  2. 将 jar 包及 Scala 的核心库分发至 classpath, 使用 java 执行。这种方法只需要在执行系统上安装 Java.
  3. 使用 sbt 插件如 sbt-assembly 构建生成单一完整的 jar 包供 Java 执行,这样一来就不需要 Scala 的核心库了。

sbt 的使用可参考官方文档,另外 CSUG/real_world_scala 也可参考看看别人的实践。

在 Java 中使用 Scala - Maven

Scala 中如何使用 Java 的部分已梳理完毕,这里就 Java 中如何编译 Scala 做些记录。

这一部分可参考如下两大链接: