第十一章:Scala和Java相互集成

主要内容:

  1. Scala中使用Java
  2. 使用Java泛型和集合
  3. 集成问题
  4. 使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.Days;

import java.util.Date;

public class DateCalculator {
public int daysBetween(Date start, Date end) {
Days d = Days.daysBetween(new DateTime(start.getTime()), new DateTime(end.getTime()));
return d.getDays();
}

public static Chronology getChronologyUsed() {
return DateTime.now().getChronology();
}
}

在SBT项目中,要将上述代码保存到src/main/java/chap11/java文件夹。SBT能够识别跨编译的Java代码和Scala代码。要在Scala中使用这个类,继承这个类:

1
2
3
4
5
class PaymentCalculator(val payPerDay: Int = 100) extends DateCalculator {
def calculatePayment(start: Date, end: Date) = {
daysBetween(start, end) * payPerDay
}
}

这里使用了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.Days;

import java.util.Date;

public class DateCalculator {
public int daysBetween(Date start, Date end) {
Days d = Days.daysBetween(new DateTime(start.getTime()), new DateTime(end.getTime()));
return d.getDays();
}

public static Chronology getChronologyUsed() {
return DateTime.now().getChronology();
}
}

要访问这个静态成员,你需要引用它,并定义在一个伴生对象中,如下:

1
2
3
4
class PaymentCalculator(val payPerDay: Int = 100) extends DateCalculator {
...
def chronologyUsed = DateCalculator.getChronologyUsed
}

这样,通过定义在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
2
3
4
5
6
7
8
9
10
11
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class Writer {
public void writeToFile(String content) throws IOException {
File f = File.createTempFile("tmoFile", ".tmp");
new FileWriter(f).write(content);

}
}

在Scala,你可以不需要try/catch块调用这个方法:

1
2
3
4
5
6
scala> def write(content: String) = {
| val w = new Writer
| w.writeToFile(content)
| }
write: (content: String)Unit
scala> write("This is a test")

作为一个编程人员,你的职责是决定是否需要捕获异常。Scala编译器不会强迫要求你这样做。这里,当你需要捕获异常时,不要从Scala中抛出异常。这是一个不好的习惯。最好的方式是创建一个EitherOption类型的实例。下面代码片段调用了writeToFile方法,并返回Either[Exception,Boolean]的一个实例:

1
2
3
4
5
6
7
8
9
def write(content: String): Either[Exception, Boolean] = {
val w = new Writer
try {
w.writeToFile(content)
Right(true)
} catch {
case e: java.io.IOException => Left(e)
}
}

这样的好处是,你可以组合这些结果。要永远记住,异常不应该进行组合。但有些时候,你需要抛出一个异常,因为某些框架或客户端代码期望异常的抛出,这种情况,你可以使用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
2
Vector<?> names = new Vector<?>()
List numbers = new ArrayList()

这两种情况,类型参数都是未知的。像这些在Scala中称作原生类型(raw types),以及已有的类型让你处理这些原生类型。Vector<?> 在Scala中可以表示为 Vector[T] forSome {type T}。从左到右读取,这个类型表达式代表的是一个类型为 T 的向量。这个 T 类型未知,它可以表示为任何东西。但 T 固定为向量的某些类型。

让我们看看如何在Scala中使用Java的原生类型。下面创建了一个带通配类型的Java向量:

1
2
3
4
5
6
7
8
9
10
import java.util.*;
class JavaRawType {
public static Vector<?> languages() {
Vector languages = new Vector();
languages.add("Scala");
languages.add("Java");
languages.add("Haskell");
return languages;
}
}

JavaRawType.languages返回一个向量,但用通配符 ? 表示。要在Scala中使用这个language 方法,你需要使用已有的类型。类型声明为 Vector[T] forSome { type T},如下:

1
2
3
4
import java.util.{Vector => JVector }
def printLanguages[C <: JVector[T] forSome { type T}](langs: C):Unit = {
for(i <- 0 until langs.size) println(langs.get(i))
}

类型C的上边界为存在类型集合,并打印所有Java向量元素。

有一种占位符的语法JVector[_]。它和 JVector[T] forSome {type T}是同一个意思。因此,上述代码等价地表示为:

1
2
3
def printLanguages[C <: JVector[_]](langs: C):Unit = {
for(i <- 0 until langs.size) println(langs.get(i))
}

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
scala> import java.util.{ArrayList => JList }
import java.util.{ArrayList => JList}
scala> val jList = new JList[Int]()
jList: java.util.ArrayList[Int] = []
scala> jList.add(1)
res1: Boolean = true
scala> jList.add(2)
res2: Boolean = true
scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._
scala> jList.asScala foreach println

作用在jList的asScala方法,将java.util.ArrayList转换为scala.collection.mutable.Buffer,这样你可以调用foreach方法。下面为将scala的List转换为java.util.List:

1
2
scala> 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
2
3
4
5
6
7
8
trait Persistable[T] {
def getEntity: T
def save(): T = {
persistToDb(getEntity)
getEntity
}
private def persistToDb(t: T) = {...}
}

这里带有一个抽象方法getEntity和两个具体方法,save和persistToDb。当这段代码被编译时,Scala编译器会生成两个类文件,Persistable.classPersistable$class。要检验每个类文件的内容,你可以使用SBT控制台 :javap 选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
scala> :javap chap11.scala.Persistable
Compiled from "ScalaJavaMash.scala"
public interface chap11.scala.Persistable extends scala.ScalaObject{
public abstract java.lang.Object getEntity();
public abstract java.lang.Object save();
}
scala> :javap chap11.scala.Persistable$class
Compiled from "ScalaJavaMash.scala"
public abstract class chap11.scala.Persistable$class extends
java.lang.Object{
public static java.lang.Object save(chap11.scala.Persistable);
public static void $init$(chap11.scala.Persistable);
}

文件 Persistable.class 表示Java接口,包含所有定义在Persistable特质的公共方法,以及继承了scala.ScalaObject。Scala中所有用户定义的类都继承了scala.ScalaObject。另一方面,Persistable$class文件定义了一个抽象类,该抽象类定义特质中所有的具体方法。可以把这个抽象类认为是一个门面,作用在trait定义的所有具体方法。

在Java中,你将继承这个接口,并使用抽象类作为一个门面来访问特质中的具体方法。下面示例中Account实现了Persistable接口,并使用Persistable$class的静态方法,来访问Persistable特质的具体方法:

1
2
3
4
5
6
7
8
9
public class Account implements Persistable<Account> {
public Account getEntity() {
return this;
}
@Override
public Account save() {
return (Account) Persistable$class.save(this);
}
}

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
2
3
4
5
6
7
8
scala> :javap chap11.scala.ScalaBean
Compiled from "ScalaJavaMash.scala"
public class chap11.scala.ScalaBean extends java.lang.Object
implements scala.ScalaObject{
public java.lang.String name();
public void name_$eq(java.lang.String);
public chap11.scala.ScalaBean(java.lang.String);
}

比较下面代码,便会说得通:

1
2
3
4
5
6
scala> val s = new chap11.scala.ScalaBean("Nima")
s: chap11.scala.ScalaBean = chap11.scala.ScalaBean@6cd4be25
scala> s.name
res0: String = Nima
scala> s.name = "Paul"
s.name: String = Paul

如果你添加 scala.beans.BeanProperty注解到一个属性中,Scala编译器会生成相应的get 和 set方法。例如这里的name,便会生成getName 和 setName 方法:

1
class ScalaBean(@scala.beans.BeanProperty var name: String)

使用javap检视,则有:

1
2
3
4
5
6
7
8
9
10
scala> :javap chap11.scala.ScalaBean
Compiled from "ScalaJavaMash.scala"
public class chap11.scala.ScalaBean extends java.lang.Object implements
scala.ScalaObject{
public java.lang.String name();
public void name_$eq(java.lang.String);
public void setName(java.lang.String);
public java.lang.String getName();
public chap11.scala.ScalaBean(java.lang.String);
}

使用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
2
3
4
trait RemoteLogger extends java.rmi.Remote {
@throws(classOf[java.rmi.RemoteException])
def log(m: String)
}

特质RemoteLogger继承了标准java.rmi.Remote,标记该接口为一个RMI远程接口;要生成throws捕获,只需要使用Scala标准库中定义的scala.throws注解。查阅生成的字节码,你将看到 throws 从句:

1
2
3
4
5
6
scala> :javap chap11.scala.RemoteLogger
Compiled from "ScalaJavaMash.scala"
public interface chap11.scala.RemoteLogger extends java.rmi.Remote{
public abstract void log(java.lang.String) throws
java.rmi.RemoteException;
}

你也可以使用Scala的目标元注解的方式,来控制注解的位置,例如下面代码 @Id注解会仅添加到Bean getter的 getX上面:

1
2
import javax.persistence.Id
class A { @(Id @beanGetter) @BeanProperty val x = 0 }

否则,默认地,字段上的注解最终在字段上面。这个很重要,因为当你处理某些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
2
3
4
5
6
package chap11.top.artists.model
class Artist {
var name: String = ""
var playCount: Long = 0
var listeners: Long = 0
}

因为使用了Hibernate作为你的ORM工具,你需要使你的领域对象与Hibernate相适。首先使用@BeanProperty来生成JavaBean-style的get/set方法。再使用javax.persistence的注解标注属性。下面为完整的领域对象代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package chap11.top.artists.model
import reflect.BeanProperty
import javax.persistence._
import scala.annotation.target.field
@Entity
class Artist {
@(Id @field) @(GeneratedValue @field) @BeanProperty
var id: Long = 0
@BeanProperty
var name: String = ""
@BeanProperty
var playCount: Long = 0
@BeanProperty
var listeners: Long = 0
}
object Artist {
def apply(name: String, playCount: Long, listeners: Long) = {
val a = new Artist
a.name = name
a.playCount = playCount
a.listeners = listeners
a
} }

Hibernate实现了Java Persistence API (JPA),以及使用JPA注解Entity标明Hibernate将持久化该对象到数据库中。你可以使用Id注解来标识ID字段,以及使用scala.annotation.target来生成带有Id和GeneratedValue注解的字段。

为了将接收的艺术家信息保存到数据库中,需要用到Hibernate的session factory。创建一个ArtistDb封装。它将隐藏Hibernate规范的详细剩余内容。该类可以作为一个数据访问对象。因为你使用了Spring,你可以轻松地必要的Hibernate依赖到新类中。下面列出ArtistDb类的完整实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.{List => JList}

import net.scala.chapter6.top_artists.model.Artist
import org.hibernate.SessionFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

trait ArtistDb {
def findAll: JList[Artist]
def save(artist: Artist): Long
}

@Repository
class ArtistRepository extends ArtistDb {

@Autowired
var sessionFactory: SessionFactory = _

@Transactional
def save(artist: Artist): Long = currentSession.save(artist).asInstanceOf[Long]

@Transactional(readOnly = true)
def findAll: JList[Artist] = currentSession.createCriteria(classOf[Artist]).
list().asInstanceOf[JList[Artist]]

private def currentSession = sessionFactory.getCurrentSession
}

类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
2
3
4
5
@Controller
class ArtistsController {
@Autowired
val db: ArtistDb = null
}

在controller添加一个URL的方法映射,并返回艺术家列表到视图:

1
2
3
@RequestMapping(value = Array("/artists"), method = Array(GET))
def loadArtists() =
new ModelAndView("artists", "topArtists", db.findAll)

@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
2
3
4
val rootUrl = "http://ws.audioscrobbler.com/2.0/"
val apiMethod = "chart.gettopartists"
val apiKey = sys.props("api.key")
val req = url(rootUrl + "?method=" + apiMethod + "&api_key=" + apiKey)

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
2
3
4
5
6
7
8
9
10
11
12
<lfm status="ok">
<artists page="1" perPage="50" totalPages="20"
total="1000">
<artist>
...
</artist>
<artist>
...
</artist>
...
</artists>
</lfm>

你可以使用Scala的本地XML解析该结果。Dispatch早已将response转换为一个NodeSeq实例,现在你可以解压这些艺术家信息,创建一个Hibernate Artist对象,并保存到数据库中。下面方法为解压操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private def retrieveAndLoadArtists() {
val rootUrl = "http://ws.audioscrobbler.com/2.0/"
val apiMethod = "chart.gettopartists"
val apiKey = sys.props("api.key")
val req = url(rootUrl + "?method=" + apiMethod + "&api_key=" + apiKey)
Http(req OK as.xml.Elem).map {resp =>
val artists = resp \\ "artist"
artists.foreach {node =>
val artist = makeArtist(node)
println(artist.name)
db.save(artist)
}
}() //applying the promise
}

private def makeArtist(n: Node) = {
val name = (n \ "name").text
val playCount = (n \ "playcount").text.toLong
val listeners = (n \ "listeners").text.toLong
Artist(name = name, playCount = playCount, listeners = listeners)
}

controller的刷新动作需要用到retrieveAndLoad方法来装载并保存艺术家到数据库中,并展示到view。下面为该controller的完整实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package chap11.top.artists.controller

import org.springframework.stereotype.Controller
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod._
import chap11.top.artists.db.ArtistDb
import chap11.top.artists.model.Artist
import org.springframework.web.servlet.ModelAndView
import dispatch._
import scala.xml.Node

@Controller
class ArtistsController {
@Autowired
val db: ArtistDb = null

@RequestMapping(value = Array("/artists"), method = Array(GET))
def loadArtists() =
new ModelAndView("artists", "topArtists", db.findAll)

@RequestMapping(value = Array("/refresh"), method = Array(GET))
def refresh() = {
retrieveAndLoadArtists()
new ModelAndView("artists", "topArtists", db.findAll)
}

private def retrieveAndLoadArtists() {
val rootUrl = "http://ws.audioscrobbler.com/2.0/"
val apiMethod = "chart.gettopartists"
val apiKey = sys.props("api.key")
val req = url(rootUrl + "?method=" + apiMethod + "&api_key=" + apiKey)
Http(req OK as.xml.Elem).map {resp =>
val artists = resp \\ "artist"
artists.foreach {node =>
val artist = makeArtist(node)
println(artist.name)
db.save(artist)
}
}() //applying the promise
}

private def makeArtist(n: Node) = {
val name = (n \ "name").text
val playCount = (n \ "playcount").text.toLong
val listeners = (n \ "listeners").text.toLong
Artist(name = name, playCount = playCount, listeners = listeners)
}
}

现在已经有了model和controller,现在切换到view上。这个简单的视图接收来自controller的响应内容,并使用JSP渲染。该视图使用单纯的Java处理,但你完全可以使用有Java编写的模版库,如Scalate。你的JSP视图会接收topArtists参数,并迭代渲染响应内容。下面为该视图代码清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<%@page contentType="text/html;charset=utf-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Top Artists from Last.fm</title>
</head>
<body>
<p>
<a href="<c:url value="/refresh.html"/>">Refresh from Last.fm</a>
</p>
<h2>Top artists</h2>
<p>
<c:if test="${fn:length(topArtists) == 0}">
<h3>No artists found in database. Refresh from Last.fm</h3>
</c:if>
<table>
<tr>
<th>Name</th>
<th>Play count</th>
<th>Listeners</th>
</tr>
<c:forEach items="${topArtists}" var="artist">
<tr>
<td>${artist.name}</td>
<td>${artist.playCount}</td>
<td>${artist.listeners}</td>
</tr>
</c:forEach>
</table>
</p>
</body>
</html>

你使用topArtists来访问artists列表,并展示。下个小节你将集成Spring配置文件的所以片段。

1132〖Configuring and running the application〗P340

你会用到Spring的配置来配置Spring MVC和 Hibernate。这样Spring会确保所有需要的依赖被合适得初始化并注入。因为你遵循Spring配置层上的约定,在配置Scala类上,完全没问题。这在Scala和Java互操作上优越是明显的。下面为spring-context-data.xml文件,它应用配置模型和控制器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

<tx:annotation-driven/>

<context:component-scan base-package="chap11.top.artists.db"/>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:scala-spring-hibernate"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="annotatedClasses">
<list>
<value>chap11.top.artists.model.Artist</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
</bean>
</beans>

该文件中,你配置了Hibernate方言为HSQLDB。以及使用Spring组件浏览方式来查找ArtistDb,以此初始化Hibernate的必要依赖项。接下来为spring-context-web.xml文件,它配置了控制器以及HTTP请求拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package="chap11.top.artists.controller"/>

<bean id="viewResolver"
class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

<bean id="openSessionInViewInterceptor"
class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<list><ref bean="openSessionInViewInterceptor"/></list>
</property>
</bean>

</beans>

接下来为web.xml配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="scala-spring-hibernate"
version="2.5">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-context-data.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-context-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

</web-app>

所有的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。该框架让你使用丰富的并发模型来构建大型的、可伸缩的、分布式的应用。