¶主要内容:
- Scala中使用Java
- 使用Java泛型和集合
- 集成问题
- 使用Java框架构建Web应用
最激动人心的事情是,Scala可以运行在JVM上。这带来的好处是你可以使用构建在JVM语言上的所有框架和工具。基于JVM,更有一些公司甚至不使用Java作为他们的首选编程语言。对于大多数企业软件项目,我坚信不支持JVM的语言,几乎不可能实现。
Scala的一个主要设计目的,是令其运行在JVM上,并提供对Java的相互协作。Scala被编译为Java字节码,所以你可以使用如javap(Java class file disassembler)工具,对有Scala编译器生成的字节码进行反编译。大部分情况下,Scala的特性被转换为Java的特性,因此Scala可以轻松和Java集成。例如,Scala使用类型擦除来兼容Java。类型擦除1 (Type erasure)也允许Scala对JVM的动态类型进行集成。一些Scala特性(如traits),不会直接地映射为Java,在这种情况下,你需要灵活变通地使用。
虽然对Java的大部分集成都可轻松实现,我仍然更推荐你使用 pure Scala。我尝试查找两者之间某些等价的部分,以及Scala不能实现的,则使用Java来解决。使用Java库的不好的方面是,你必须处理可变性、异常、空值这些Scala中绝对不会出现的问题。在Scala中,需要特别小心地选择Java的库或者框架。以一个编码良好的Java库Joda-Time为例。
Scala和Java最通常的集成,是指部分项目由Scala编写的。小节11.4将介绍Scala中使用Java框架,Hibernate、Spring等的web项目构建。
多数情况下,Scala和Java之间的集成是无缝的,但也需要注意某些例外。本章的目的是讲述,如何轻松地在Scala和Java之间进行集成,以及练习避免集成问题。Java类和框架的集成,尽管本书没有阐述,相信你已经很好地处理,但这里,你面临是两个语言的集成问题,并吸收接纳彼此的优势。
在一个已有的Java项目中阐述Scala,最好的方式是在其中编写Scala代码,并证明其超越Java语言的优势,并逐渐把Java部分,重写为Scala。这种过渡的工作会出现多次。
让我们开始本章中两个语言间的集成例子。你会学习到,解决处理那些在Java中可用,在Scala中不可用的特性问题,如Java的static静态成员、exceptions异常处理,以及Scala的特性在Java中怎样解决处理。你也将会学习Scala的注解在集成中的帮助,例如,生成JavaBean-style的get和set。本章的最后,将带领你学习构建一个使用Java框架的web应用。
¶11~1〖Using Java classes in Scala〗P324
在Java中集成Scala是很容易的。因为在Java中使用日期总是一个痛苦的过程,下面Java代码片段使用了Joda-Time库,用于计算两个日期之间的天数:
1 | import org.joda.time.Chronology; |
在SBT项目中,要将上述代码保存到src/main/java/chap11/java文件夹。SBT能够识别跨编译的Java代码和Scala代码。要在Scala中使用这个类,继承这个类:
1 | class PaymentCalculator(val payPerDay: Int = 100) extends DateCalculator { |
这里使用了daysBetween方法进行计算。该集成是无缝的,你不会发现有什么不同。
¶Compiling Java and Scala together
SBT知道如何构建混有Scala和Java的项目。Scala编译器允许你同时构建Java类和Java源代码。这样,如果你有Java和Scala间的双向依赖,你可以同时构建它们,而不用担心顺序问题。
当然,你也可以在Maven工具中构建混有Java和Scala的项目。要这样做,你应该在Maven中添加额外的Scala插件。在本章的最后,你将会使用Maven来构建一个示例项目。
下小节,你会学习在Scala中如何使用Java的static成员。
¶1111〖Working with Java static members〗P325
当使用声明了static成员的Java类是,你需要理解它们在Scala中是如何解析的。
Scala没有任何静态关键字,Scal解析Java的静态成员方法时,把它们认为是一个伴生对象的方法。看看下面的例子是如何工作的。代码中添加一个静态方法,返回Chronology:
1 | import org.joda.time.Chronology; |
要访问这个静态成员,你需要引用它,并定义在一个伴生对象中,如下:
1 | class PaymentCalculator(val payPerDay: Int = 100) extends DateCalculator { |
这样,通过定义在DateCalculator访问一个伴生对象,对静态方法访问。
¶Visibility issues between Scala and Java
Scala和Java间的可见性实现不同。
Scala在编译期强制可见性,但在运行期所有都是public的。这样做的一个原因是:在Scala中,伴生对象被允许访问伴生类的protected成员,但在字节层面,如果不另所有为public,则不能对其编码(encode)。
Java则在编译和运行期都强制可见性规则。这会带来一些例外。例如,如果你有一个定义在Java类的protected静态成员,在Scala中则没有任何方式对其访问。唯一变通的方法,是转换为一个public成员,以对其访问。
接下来,将看到如何处理Java的异常检查,因为Scala没有这些东西。
¶1112〖Working with Java checked exceptions〗P326
Scala缺少异常检测,Java基础代码每次编译时执行异常检测,这会带来不少疑惑。在Scala中你调用下面Java方法,你不需要在try/catch块中封装:
1 | import java.io.File; |
在Scala,你可以不需要try/catch块调用这个方法:
1 | def write(content: String) = { |
作为一个编程人员,你的职责是决定是否需要捕获异常。Scala编译器不会强迫要求你这样做。这里,当你需要捕获异常时,不要从Scala中抛出异常。这是一个不好的习惯。最好的方式是创建一个Either
或 Option
类型的实例。下面代码片段调用了writeToFile
方法,并返回Either[Exception,Boolean]
的一个实例:
1 | def write(content: String): Either[Exception, Boolean] = { |
这样的好处是,你可以组合这些结果。要永远记住,异常不应该进行组合。但有些时候,你需要抛出一个异常,因为某些框架或客户端代码期望异常的抛出,这种情况,你可以使用Scala的注解来生成抛出异常的字节码(小节11.2.1有更多这方面内容)。现在,让我们转到Java的泛型上面来。应理解Java泛型如何工作的,因为它们在Java集合中用到。
¶1113〖Working with Java generics using existential types〗P327
Java泛型直接转换为Scala的类型参数。例如 Comparator<T>
转换为 Comparamot[T]
,ArrayList<T>
转换为 ArrayList[T]
。但类以通配符方式定义在Java中,会变得有趣。下面是两个Java集合带通配符类型的例子:
1 | Vector<?> names = new Vector<?>() |
这两种情况,类型参数都是未知的。像这些在Scala中称作原生类型(raw types),以及已有的类型让你处理这些原生类型。Vector<?>
在Scala中可以表示为 Vector[T] forSome {type T}
。从左到右读取,这个类型表达式代表的是一个类型为 T
的向量。这个 T
类型未知,它可以表示为任何东西。但 T
固定为向量的某些类型。
让我们看看如何在Scala中使用Java的原生类型。下面创建了一个带通配类型的Java向量:
1 | import java.util.*; |
JavaRawType.languages
返回一个向量,但用通配符 ?
表示。要在Scala中使用这个language 方法,你需要使用已有的类型。类型声明为 Vector[T] forSome { type T}
,如下:
1 | import java.util.{Vector => JVector } |
类型C的上边界为存在类型集合,并打印所有Java向量元素。
有一种占位符的语法JVector[_]
。它和 JVector[T] forSome {type T}
是同一个意思。因此,上述代码等价地表示为:
1 | def printLanguages[C <: JVector[_]](langs: C):Unit = { |
¶Working with Java collections
一旦你习惯了Scala强大的集合库,在Scala中使用Java集合库会变得痛苦。理想情况下,在Scala代码中使用Scala集合,并等价转换为Java代码,反之亦然。这样,你既可以使用强大的Scala集合库,在需要的使用便可轻松集成到Java基础代码中。Scala库为此提供了两个工具类用于转换:
1
2 scala.collection.JavaConversions
scala.collection.JavaConverters这两个类都提供了同样的特性,但实现方式不同。JavaConversions提供了一系列的隐式转换,来对Java集合和Scala集合的近似转换。JavaConverters使用了一个 “Pimp my Library” 模式对Java集合添加了asScala方法、对Scala则添加了asJava方法。我推荐使用JavaConverters,因为它是显式的转换。下面例子使用了JavaConverters将java.util.List转换为Scala,然后再转换为Java:
1
2
3
4
5
6
7
8
9
10
11 import java.util.{ArrayList => JList }
import java.util.{ArrayList => JList}
val jList = new JList[Int]()
jList: java.util.ArrayList[Int] = []
jList.add(1)
res1: Boolean = true
jList.add(2)
res2: Boolean = true
import scala.collection.JavaConverters._
import scala.collection.JavaConverters._
jList.asScala foreach println作用在jList的asScala方法,将java.util.ArrayList转换为scala.collection.mutable.Buffer,这样你可以调用foreach方法。下面为将scala的List转换为java.util.List:
1
2 List(1, 2).asJava
res4: java.util.List[Int] = [1, 2]
¶11~2〖Using Scala classes in Java〗P329
Scala语言最有趣的特性之一是特质(traits),基础代码中被大量用到。如果你定义一个特质仅带有抽象方法,它可以编译得到Java接口,并直接在Java中使用,而不会带来任何问题。但如果这个特质带有具体的方法,则会有些微妙。让我们以一个例子来看看,这种情况下是如何编译成Java字节码的。
下面是一个Scala特质,它以混入的方式将对象持久化到数据库中:
1 | trait Persistable[T] { |
这里带有一个抽象方法getEntity和两个具体方法,save和persistToDb。当这段代码被编译时,Scala编译器会生成两个类文件,Persistable.class
和Persistable$class
。要检验每个类文件的内容,你可以使用SBT控制台 :javap
选项:
1 | :javap chap11.scala.Persistable |
文件 Persistable.class 表示Java接口,包含所有定义在Persistable特质的公共方法,以及继承了scala.ScalaObject。Scala中所有用户定义的类都继承了scala.ScalaObject。另一方面,Persistable$class文件定义了一个抽象类,该抽象类定义特质中所有的具体方法。可以把这个抽象类认为是一个门面,作用在trait定义的所有具体方法。
在Java中,你将继承这个接口,并使用抽象类作为一个门面来访问特质中的具体方法。下面示例中Account实现了Persistable接口,并使用Persistable$class的静态方法,来访问Persistable特质的具体方法:
1 | public class Account implements Persistable<Account> { |
Persistable接口的实现是直接的。 getEntity返回Account对象的一个实例,save方法代表Persistable$class类中的静态方法save,来访问trait的实现定义。注意当使用层叠的特质时,创建一个具体的Scala类,然后在Java中直接使用或继承会更好些。
当集成Scala和Java框架,第一个障碍是Scala类没有JavaBean-style的 get 和 set 方法。Scala注解提供了这样的灵活性,来指定让Scala编译器生成字节码。
¶1121〖Using Scala annotations〗P331
Scala不遵循标准的Java getter和 setter 模式。在Scala中,getters 和 setters看起来不一样。例如,要创建一个Scala-style的getter和setter的Scala类,你需要做的是将成员声明为var,如下:
1 | class ScalaBean(var name: String) |
当被编译,该类生成下面字节码:
1 | :javap chap11.scala.ScalaBean |
比较下面代码,便会说得通:
1 | val s = new chap11.scala.ScalaBean("Nima") |
如果你添加 scala.beans.BeanProperty注解到一个属性中,Scala编译器会生成相应的get 和 set方法。例如这里的name,便会生成getName 和 setName 方法:
1 | class ScalaBean(@scala.beans.BeanProperty var name: String) |
使用javap检视,则有:
1 | :javap chap11.scala.ScalaBean |
使用BeanProperty注解,Scala编译器会同时生成Scala和Java风格的get和set方法。使用BeanProperty的确会增加生成class文件的大小,但对于和Java协作性方面是个很小的代价。现在,如果你想要生成JavaBean-compliant BeanInfo,你可以使用scala.beans.BeanInfo。
小节11.1展示了Scala不对异常检测作处理,因为Scala没有throws关键字来声明方法抛出异常。问题来了。例如,你想要使用Scala来声明一个java.rmi.Remote接口,你困惑的是Remote中每个声明的方法都需要抛出RemoteException。再一次,使用注解,你可以指明Scala编译器生成方法的throws。下面代码定义一个个RMI接口:
1 | trait RemoteLogger extends java.rmi.Remote { |
特质RemoteLogger继承了标准java.rmi.Remote,标记该接口为一个RMI远程接口;要生成throws捕获,只需要使用Scala标准库中定义的scala.throws注解。查阅生成的字节码,你将看到 throws 从句:
1 | :javap chap11.scala.RemoteLogger |
你也可以使用Scala的目标元注解的方式,来控制注解的位置,例如下面代码 @Id注解会仅添加到Bean getter的 getX上面:
1 | import javax.persistence.Id |
否则,默认地,字段上的注解最终在字段上面。这个很重要,因为当你处理某些Java框架时,有些特别要求注解的定义。下个小节将看到目标注解的一些用法,以及如何在Scala中使用流行框架,如Spring、Hibernate。
¶11~3〖Building web applications in Scala using Java frameworks〗P332
本章介绍使用Scala和Java框架构建一个web应用。这个例子将展示在Java-heavy环境中使用Scala,当吸收或迁移到Scala,你不必丢弃你在Java框架上的投入和基础。显然,使用框架来构建Scala,某些示例代码会有出入。无碍,在没有接触任何Scala框架之前,学习Java框架同样重要。在本小节,你将构建一个web应用,使用到Spring和Hibernate框架。你将离开你最喜欢的构建工具,SBT,以及使用Maven来构建你的Java,因为它是Scala中最常用的。
注意 本小节要求你已经掌握了Spring、Hibernate以及Maven构建工具来构建Java Web应用。如果没有,这会很难跟上。保守起见,如果你对Java框架不感兴趣,也可以跳过该小节。
在此之前,让我们理清你将构建一个怎样的应用。你将构建一个i额小的web应用,叫做topArtists,用于展示来自Last.fm的艺术家。Last.fm是一个流行的音乐网站,可以通过收音机频道进入访问。Last.fm同时也提供了一个API,通过该API可以获得各种各样的音乐排行榜。你将使用它的chart.getTopArtists的REST API来获取所有当前的艺术家,并保存到本地数据库中。你也将从本地数据库中展示所有的艺术家数据给用户。
注意 你首先要做的是获得一个来自Last.fm的API key。你可以从Last.fm的 网站获得。
如果你做过Java开发,你可能最经常使用的是Maven。Maven知道如何编译Java源文件,但要编译Scala文件,你需要添加Maven的Scala插件2。要使用Maven创建一个空白web应用,执行下面命令:
1 | mvn archetype:generate -DgroupId=scala.in.action -DartifactId=top.artists -DarchetypeArtifactId=maven-archetype-webapp |
该项目的结构,实质上和SBT项目是一样的(SBT 遵循Maven的约定)。创建了pom.xml文件后,便可以配置所有的依赖。
对于topArtists应用,你将使用Spring来构建web层,并作为依赖注入框架。Hibernate则作为ORM层,应用保存所有artists到数据库中。作为练手,你将使用数据库HSQLDB。但要作REST请求,你将使用纯的Scala库 dispatch3。 Dispatch是一个Scala库,构建在Async Http Client库上,使得它易于作web services4。
¶1131〖Building the model, view, and controller〗P334
该topArtists应用展示了来自Last.fm的REST API。要看从Last.fm接收的信息,在任何浏览器窗口调用下面的URL:
1 | http://ws.audioscrobbler.com/2.0/?method=chart.gettopartists&api_key=<your api key> |
但前提先确保你在 Last.fm 拥有API Key。如果该请求成功,你可以看到艺术家的相关信息,如名字、播放次数、听众、链接等其它属性。为了使问题简单化,我们仅取来自第一页的结果,并存储艺术家的名字、播放次数、以及听众。这个领域模型看起来如下:
1 | package chap11.top.artists.model |
因为使用了Hibernate作为你的ORM工具,你需要使你的领域对象与Hibernate相适。首先使用@BeanProperty来生成JavaBean-style的get/set方法。再使用javax.persistence的注解标注属性。下面为完整的领域对象代码。
1 | package chap11.top.artists.model |
Hibernate实现了Java Persistence API (JPA),以及使用JPA注解Entity标明Hibernate将持久化该对象到数据库中。你可以使用Id注解来标识ID字段,以及使用scala.annotation.target来生成带有Id和GeneratedValue注解的字段。
为了将接收的艺术家信息保存到数据库中,需要用到Hibernate的session factory。创建一个ArtistDb封装。它将隐藏Hibernate规范的详细剩余内容。该类可以作为一个数据访问对象。因为你使用了Spring,你可以轻松地必要的Hibernate依赖到新类中。下面列出ArtistDb类的完整实现。
1 | import java.util.{List => JList} |
类ArtistRepository类用Spring的原型注解Repository标识,这样Spring框架可以自动地浏览并装载。当Spring装载该类时,也设置了sessionFactory依赖。下个小节,将看到这些组件是如何配置的。现在,先假设sessionFactory在ArtistRepository是可以用的。save方法比较直接:使用当前Hibernate session,它将Artist的一个实例存储到数据库中。 asInstanceOf[Long]
类型转换得到Long。在这里,你应该知道save操作返回对象Id。findAll方法,将查询数据库,并返回所有的artists。这里类型转换为list,Hibernate默认list方法返回一个List对象。至此,你已经可以将接收的领域对象保存到数据库中。下面来构建控制器。
前面讨论过,你将使用Spring来构建你的web应用层。这里,将使用Spring的原型注解@Controller来标识类为控制器。控制器的工作是获得来自Last.fm的艺术家信息,以及展示存储在本地数据库的艺术家信息。你已经有一个ArtistDb来获得来自数据库的艺术家信息,所以你可以注入一个ArtistDb的实例到控制器中:
1 |
|
在controller添加一个URL的方法映射,并返回艺术家列表到视图:
1 | Array("/artists"), method = Array(GET)) (value = |
@RequestMapping注解映射地址 “/artists” URI到方法loadArtists中。该方法使用db.findAll来查找来自数据库的所有艺术家。ModelAndView的第一个参数为视图渲染名称。topArtists参数为db.findAll的响应内容。使用视图里面的topArtists名称,你可以访问所有调用findAll得到的艺术家。但在获取艺术家信息之前,先要得到来自Last.fm的信息列表。即允许用户刷新艺术家信息,并保存到本地数据库中。要实现刷新,调用Last.fm的REST API。使用Dispatch库来调用Last.fm。Dispatch提供了一个优秀的DSL或者Apache HttpClient库的转换器。下面代码片段创建一个Http请求对象:
1 | val rootUrl = "http://ws.audioscrobbler.com/2.0/" |
API key来自system属性。当运行该应用,你必须指定API key为一个系统属性。url方法接收一个字符URL作为输入,并返回一个Http请求实例。但创建一个Http请求不会再作更多内容,因此要告诉Dispatch如何处理接收来自请求的响应内容。你可以通过一个具体的操作实现。这里我们使用内建的 as.xml.Elem来处理这些XHTML响应:
1 | Http(req OK as.xml.Elem).map {resp => ...} |
Http返回scala.xml.Elem的Promise(因为每个HTTP请求都是异步处理的),我们使用map来访问Promise对象的内容。因为我们没有使用Spring的异步支持,我们需要等待Promise来完成结果渲染。来自Last.fm的响应包含一个XML,它是个web service接口:
1 | <lfm status="ok"> |
你可以使用Scala的本地XML解析该结果。Dispatch早已将response转换为一个NodeSeq实例,现在你可以解压这些艺术家信息,创建一个Hibernate Artist对象,并保存到数据库中。下面方法为解压操作:
1 | private def retrieveAndLoadArtists() { |
controller的刷新动作需要用到retrieveAndLoad方法来装载并保存艺术家到数据库中,并展示到view。下面为该controller的完整实现:
1 | package chap11.top.artists.controller |
现在已经有了model和controller,现在切换到view上。这个简单的视图接收来自controller的响应内容,并使用JSP渲染。该视图使用单纯的Java处理,但你完全可以使用有Java编写的模版库,如Scalate。你的JSP视图会接收topArtists参数,并迭代渲染响应内容。下面为该视图代码清单:
1 | <%"text/html;charset=utf-8"%> contentType= |
你使用topArtists来访问artists列表,并展示。下个小节你将集成Spring配置文件的所以片段。
¶1132〖Configuring and running the application〗P340
你会用到Spring的配置来配置Spring MVC和 Hibernate。这样Spring会确保所有需要的依赖被合适得初始化并注入。因为你遵循Spring配置层上的约定,在配置Scala类上,完全没问题。这在Scala和Java互操作上优越是明显的。下面为spring-context-data.xml文件,它应用配置模型和控制器对象。
1 |
|
该文件中,你配置了Hibernate方言为HSQLDB。以及使用Spring组件浏览方式来查找ArtistDb,以此初始化Hibernate的必要依赖项。接下来为spring-context-web.xml文件,它配置了控制器以及HTTP请求拦截。
1 |
|
接下来为web.xml配置文件。
1 |
|
所有的Java web容易读取web.xml来初始化Java-based web应用。监听器listener属性允许应用监听由容器生成的事件,例如一个应用被加载或没被加载。这里,监听器的配置是ContextLoaderListener,该类通过读取context-param,知道如何配置Spring。要运行应用,你可以使用早以配置的jetty服务器:
1 | mvn -Dapi.key=<your-last.fm-pai-key> jetty:run |
正如你所看,使用Scala和Java框架,建立并创建web应用是容易的。当使用Java框架是,某些样板配置是不可避免的,但你让然可以编写有趣的Scala代码。
¶11~4〖Summary〗P343
本章所阐述的是,Scala对Java的互操作是无痛的(pain-free)。很少地方你需要做防护措施,但对于大部分你都可以不用顾虑太多地集成已有的Java基础代码。额外要小心的,则是集成某些Scala特性,在Java中并不支持,反之亦然。在本章介绍了如何处理这种问题。因为Scala被设计来自渐渐成长的Java,大多数变通的地方都可以简单地学习和实现。
简单集成Java,意味着你可以容易地在已有代码中使用Scala。在最后的一个例子中证明了,你可以使用Scala和已有的流行的Java框架进行集成,而不用重写整个应用。
下个章节将进入到最激动人心的Scala框架:Akka。该框架让你使用丰富的并发模型来构建大型的、可伸缩的、分布式的应用。
- Java Tutorials: Type erasure, http://download.oracle.com/javase/tutorial/java/generics/erasure.html. ↩
- Maven Scala plug-in download, http://scala-tools.org/mvnsites/maven-scala-plugin/. ↩
- Dispatch Scala library: http://dispatch.databinder.net/Dispatch.html. ↩
- See “AsyncHttpClient/async-http-client,” https:/github.com/AsyncHttpClient/async-http-client. ↩