以Java、Kotlin和Scala编写基本Spring Web应用的比较

  • 时间: 2017-07-17 11:14:12

译者注:现在可以用来开发web应用的语言五花八门,每种语言都各有千秋,本文作者挑选了Java、Kotlin 、Scala这三种语言,开发同一个基础的Spring web应用,从而比对出他们之间的差别。以下为译文。

我一直在想,在JVM语言中选择一个(如Scala和Kotlin )用来实现同一个基础的Spring Boot应用程序是多么的困难,所以我决定试试。

源代码可以这个地址看到: https://github.com/rskupnik/pet-clinic-jvm

这款应用程序是非常基础的,因为它只包含以下元素:

  • 两个数据库实体
  • 两个Repository注解
  • 两个controller控制器
  • 六个endpoint
  • 一个虚拟的静态的index页面

我将用三种语言来做代码比较:

  • Java
  • Kotlin
  • Scala
实体

这个应用里面涉及到了两个实体:Customer 和 Pet

Java
@Entitypublic class Customer {    @Id    @GeneratedValue(strategy = GenerationType.AUTO)    private Long id;    private String firstName, lastName;    @JsonIgnore    @OneToMany(mappedBy = "owner")    private List<Pet> pets;    protected Customer() {    }    public Customer(String firstName, String lastName) {        this.firstName = firstName;        this.lastName = lastName;    }    // A whole lot of getters and setters here...    // Ommited for the sake of brevity    @Override    public String toString() {        return firstName+" "+lastName;    }}
@Entitypublic class Pet {    @Id    @GeneratedValue(strategy = GenerationType.AUTO)    private Long id;    private String name;    @ManyToOne    @JoinColumn(name = "ownerId", nullable = false)    private Customer owner;    protected Pet() {    }    public Pet(String name) {        this.name = name;    }    // A whole lot of getters and setters here...    // Ommited for the sake of brevity    @Override    public String toString() {        return name;    }}

这里无需多言——因为很显然Java是很冗长的,即使去掉getter和setter方法之后,还是会有很多的代码。除了使用Lombok可以帮助用户生成模板文件以外,或者类似的工具,我们也没有什么更好的办法。

Kotlin

在Kotlin语言中有好几种方法可以定义一个实体类,我已经试过两种了。尽管作用都是一样的,但是后者可能更受用户欢迎,因为前者只是简单地在做一些Java里面也能做的事情。

// Implementation using a regular class, mimicking regular Java@Entityclass Pet {    constructor() {    }    constructor(name: String) {        this.name = name    }    @Id    @GeneratedValue(strategy = GenerationType.AUTO)    var id: Long = 0    var name: String = ""    @ManyToOne    @JoinColumn(name = "ownerId", nullable = false)    var owner: Customer? = null    override fun toString(): String = "$name"}
// Implementation using a data class (preferred)@Entitydata class Customer(        @Id @GeneratedValue(strategy = GenerationType.AUTO)         var id: Long = 0,        var firstName: String = "",        var lastName: String = "",        @JsonIgnore @OneToMany(mappedBy = "owner")         var pets: List<Pet>? = null) {    override fun toString(): String = "$firstName $lastName"}

尽管第一眼看上去,它不像Java代码那样比较直观,但是用数据类实现的话,代码量就要短得多,而且也不需要大量的模板文件。这里的大部分冗余代码都是因为需要做必要的注释。

注意,实体类需要一个默认的没有参数的构造函数——它在常规类的情况下显式提供,而数据类通过为单个构造函数中的每个参数定义 默认值 来提供的 - 包括一个默认值,而没有参数 ,它只是将默认值分配给每个变量。

由于需要将override关键字显示的定义出来,这样做代码更容易阅读,出现错误的概率也会降低,所以我挺喜欢这种做法的。

Scala
@Entityclass Customer {  // Need to specify a parameterized constructor explicitly  def this(firstName: String, lastName: String) {    this()    this.firstName = firstName    this.lastName = lastName  }  // BeanProperty needed to generate getters and setters  @Id  @GeneratedValue(strategy = GenerationType.AUTO)  @BeanProperty  var id: Long = _  @BeanProperty  var firstName: String = _  @BeanProperty  var lastName: String = _  @JsonIgnore  @OneToMany(mappedBy = "owner")  @BeanProperty  var pets: java.util.List[Pet] = _  override def toString(): String = s"$firstName $lastName"}
@Entityclass Pet {  def this(name: String, owner: Customer) {    this()    this.name = name    this.owner = owner  }  @Id  @GeneratedValue(strategy = GenerationType.AUTO)  @BeanProperty  var id: Long = _  @BeanProperty  var name: String = _  @ManyToOne  @JoinColumn(name = "ownerId", nullable = false)  @BeanProperty  var owner: Customer = _}

实际上仅针对这种情况,我对Scala感到失望——它的实现几乎和Java一样冗长,它们的区别就在于Scala不需要显示的定义好getter和setter方法,它只需要使用额外的字段注释(@beanproperty)就可以了。

我试图使用一个 case class来减少代码实现的行数,这在理论上是可以行的通的,但是我不能让它运行起来(也许这根本原因就是因为我使用Scala不熟)。

至少它提供了字符串插值(String interpolation),允许在一行中使用大括号,并且需要显式的

override关键字,这与Kotlin是一致的。

Repositories Java
@Repositorypublic interface CustomerRepository extends CrudRepository<Customer, Long> {    List<Customer> findByLastName(String lastName);}
@Repositorypublic interface PetRepository extends CrudRepository<Pet, Long> {}

注意,findByLastName函数实际上并没有在其它地方进行调用,我定义它只是用来提供一个示例的。

Kotlin
@Repositoryinterface CustomerRepository : CrudRepository<Customer, Long> {    fun findByLastName(name: String): List<Customer>}
@Repositoryinterface PetRepository : CrudRepository<Pet, Long>`

这里没有太大的区别,代码基本上是一样的。Kotlin版本的代码稍微短一点,这是因为Kotlin的默认修饰符是public的,而且有一个:符号而不是extends关键字。此外,也有可能是如果没有在body中定义任何内容的话,就有可能可能会忽略花括号。

Scala
@Repositorytrait CustomerRepository extends CrudRepository[Customer, java.lang.Long] {  def findByLastName(lastName: String): List[Customer]}
@Repositorytrait PetRepository extends CrudRepository[Pet, java.lang.Long]

Scala使用的是 traits,而不是interfaces,但在大部分情况下它们都是相同的概念,或者至少针对我们这个简单的例子而言它们是一样的。

由于某些原因,需要将Long类明确定义为java.lang.Long以避免编译错误(我再次对Scala感到失望)。

Controllers控制器 Java
@RestController@RequestMapping("/customers")public class CustomerController {    private CustomerRepository customerRepository;    @Autowired    public CustomerController(CustomerRepository customerRepository) {        this.customerRepository = customerRepository;    }    @GetMapping(value = "/{id}", produces = "application/json")    public Customer getCustomer(@PathVariable("id") Long id) {        return customerRepository.findOne(id);    }    @GetMapping(produces = "application/json")    public List<Customer> getAllCustomers() {        return (List<Customer>) customerRepository.findAll();    }    @GetMapping(value = "/formatted", produces = "application/json")    public List<String> getAllCustomersFormatted() {        return ((List<Customer>) customerRepository.findAll())                .stream()                .map(                    customer -> customer.getFirstName()+" "+customer.getLastName()                )                .collect(Collectors.toList());    }    @PostMapping(produces = "application/json",                 consumes = "application/json")    public Customer addCustomer(@RequestBody Customer customer) {        return customerRepository.save(customer);    }}
@RestController@RequestMapping("/pets")public class PetController {    @Autowired    private PetRepository petRepository;    @GetMapping(produces = "application/json")    public List<Pet> getAllPets() {        return (List<Pet>) petRepository.findAll();    }    @PostMapping(produces = "application/json",                 consumes = "application/json")    public Pet addPet(@RequestBody Pet pet) {        return petRepository.save(pet);    }}
Scala
@RestController@RequestMapping(Array("/customers"))class CustomerController (  private val customerRepository: CustomerRepository) {  @GetMapping(value = Array("/{id}"),              produces = Array("application/json"))  def getCustomer(@PathVariable("id") id: Long) = customerRepository.findOne(id)  @GetMapping(produces = Array("application/json"))  def getAllCustomers() = customerRepository.findAll()  @GetMapping(value = Array("/formatted"),              produces = Array("application/json"))  def getAllCustomersFormatted() = {    customerRepository      .findAll()      .asScala      .map(_.toString())      .asJava  }  @PostMapping(produces = Array("application/json"),               consumes = Array("application/json"))  def addCustomer(@RequestBody customer: Customer) = customerRepository.save(customer)}
@RestController@RequestMapping(Array("/pets"))class PetController {  @Autowired  var petRepository: PetRepository = null  @GetMapping(produces = Array("application/json"))  def getAllPets = petRepository.findAll()  @PostMapping(produces = Array("application/json"),               consumes = Array("application/json"))  def addPet(@RequestBody pet: Pet) = petRepository.save(pet)}

CustomerController是通过构造函数注入的,而PetController则是通过字段注入的,这么做是为了提供出两种不同的方式——Kotlin和Scala也是同样的处理逻辑。

同样,Java的话,代码还是显得很冗长,尽管其中很大一部分来自于健壮的注释(使用@get/PostMapping代替@requestmapping来减少注释的大小)。值得注意的是,Java 8将会解决这个问题,因为由于缺少lambda函数,getAllCustomersFormatted()函数在Java 7中会变得更加臃肿。

Kotlin
@RestController@RequestMapping("/customers")class CustomerController(val customerRepository: CustomerRepository) {    @GetMapping(value = "/{id}", produces = arrayOf("application/json"))    fun getCustomer(@PathVariable("id") id: Long): Customer? =             customerRepository.findOne(id)    @GetMapping(value = "/formatted", produces = arrayOf("application/json"))    fun getAllCustomersFormatted() =             customerRepository.findAll().map { it.toString() }    @GetMapping(produces = arrayOf("application/json"))    fun getAllCustomers() = customerRepository.findAll()    @PostMapping(produces = arrayOf("application/json"),                 consumes = arrayOf("application/json"))    fun addCustomer(@RequestBody customer: Customer): Customer? =             customerRepository.save(customer)}
@RestController@RequestMapping("/pets")class PetController {    // When using Autowired like this we need to make the variable lateinit    @Autowired    lateinit var petRepository: PetRepository    @GetMapping(produces = arrayOf("application/json"))    fun getAllPets() = petRepository.findAll()    @PostMapping(produces = arrayOf("application/json"),                 consumes = arrayOf("application/json"))    fun addPet(@RequestBody pet: Pet): Pet? = petRepository.save(pet)}

乍一看,这似乎和Java一样冗长,这很让人吃惊,但我们必须注意到,这种冗长的代码大部分来自于所需的注释。除去这些,控制器的主体仅仅只有4行。

当然,如果我要将@requestmapping注释写在一行中,那么它就不会那么简单了,但是在博客文章中,可读性就会首先出现。

使用@get/PostMapping注释可以让我们至少跳过方法参数,以减少注释的大小。理论上,我们可以去掉produces和consumes,但这也会使XML成为一个可行的选择——所以这些params并不是多余的。

需要指出的一件令人讨厌的事情是,如果需要使用多个参数(除了默认值以外),那么在注解中使用arrayif()是必要的。这将在 Kotlin 1.2中得到修复

我喜欢这个构造函数注入芬兰湾的科特林提供了(我们甚至不需要一个@ autowired注解出于某种原因[这是原因])虽然看起来令人困惑如果类更大,更依赖项注入,我想说这是一个机会,在这种情况下适当的格式。

我喜欢这个构造函数注入芬兰湾的科特林提供了(我们甚至不需要一个@ autowired注解出于某种原因[这是原因])虽然看起来令人困惑如果类更大,更依赖项注入,我想说这是一个机会,在这种情况下适当的格式。

Scala
@RestController@RequestMapping(Array("/customers"))class CustomerController (  private val customerRepository: CustomerRepository) {  @GetMapping(value = Array("/{id}"),              produces = Array("application/json"))  def getCustomer(@PathVariable("id") id: Long) = customerRepository.findOne(id)  @GetMapping(produces = Array("application/json"))  def getAllCustomers() = customerRepository.findAll()  @GetMapping(value = Array("/formatted"),              produces = Array("application/json"))  def getAllCustomersFormatted() = {    customerRepository      .findAll()      .asScala      .map(_.toString())      .asJava  }  @PostMapping(produces = Array("application/json"),               consumes = Array("application/json"))  def addCustomer(@RequestBody customer: Customer) = customerRepository.save(customer)}
@RestController@RequestMapping(Array("/pets"))class PetController {  @Autowired  var petRepository: PetRepository = null  @GetMapping(produces = Array("application/json"))  def getAllPets = petRepository.findAll()  @PostMapping(produces = Array("application/json"),               consumes = Array("application/json"))  def addPet(@RequestBody pet: Pet) = petRepository.save(pet)}

Scala还需要在提供参数时使用Array关键字,即使是默认的参数也需要。

getAllCustomersFormatted()函数,这是一种暴行,但我不能让Java集合正确地使用Scala集合——所以,对不起,我的眼睛(划痕,代码在Teemu Pöntelin的帮助下得到了改进,谢谢:))。

请注意,必须在构造函数中包含@autowired(),这可能在Kotlin中跳过(如果您只有一个构造函数,那么实际上根本不需要@autowired),如这里所解释的那样)。

总结

尽管这个应用程序非常简单,但是对于我来说,这足以让我对如何在每一门特色语言中做一些更深入的了解有一个基本的感觉。

如果需要在 Kotlin 和 Scala 之间做个选择,毫无疑问我的选择是 Kotlin

为什么呢?

首先,我觉得Scala就好像是IntelliJ IDEA中的二等公民一样,而Kotlin无疑是一等公民。这是显而易见的,因为创建IDE(Jetbrains)的公司和创建Kotlin语言的公司是同一家的——所以他们当然非常支持这门语言。另一方面,Scala是通过一个插件集成的。两者的区别是显而易见的,至少对我个人来说,这种区别是非常重要的。

其次,如果我想用Scala为web应用程序开发框架,我就会选择 Play Framework,原因很简单,就是因为它设计的思维是基于Scala 的,而且开发语言能使得某些事情变得更容易,而不是妨碍你(就像在这个小应用程序的情况下)。

这些都是 我个人的原因,但也 有更多、更普遍的原因。

我觉得Scala比Kotlin更脱离Java,因为后者基本上算是一种扩展,旨在解决Java最初存在的问题,而前者的目标是将命令式编程和函数式编程混合在一起。尽管如此,我相信Scala在其他领域更好地使用,比如 大数据,而Kotlin在它应该做的事情上做得很好——取代Java解决一些比较常见的问题,并提供紧密的互操作性。

此外, Spring本身似乎对Kotlin 的支持远远超过了对 Scala的支持。

最后,我相信,从Java程序员的角度来看,Kotlin比Scala更容易学习。这主要是因为Kotlin被设计为基于Java进行的改进,并没有像Scala那样重视函数式编程。在Kotlin中,与Java的互操作性也更加紧密,这使得调试问题更加容易。

最后,但同样重要的是——我想明确地声明 我不会以任何方式抨击Scala。就我个人而言,我认为 如果用一门非Java的JVM 语言去开发一个Spring Boot的web应用程序——Kotlin会是更好的选择。粗体部分是很重要的:)正如前面提到的,在其他领域,Scala是很优秀的,比如前面提到的大数据,但想要取代Java目前估计还有一段很长的路要走。