译者注:现在可以用来开发web应用的语言五花八门,每种语言都各有千秋,本文作者挑选了Java、Kotlin 、Scala这三种语言,开发同一个基础的Spring web应用,从而比对出他们之间的差别。以下为译文。
我一直在想,在JVM语言中选择一个(如 Scala 和 Kotlin )用来实现同一个基础的 Spring Boot 应用程序是多么的困难,所以我决定试试。
源代码可以这个地址看到: https://github.com/rskupnik/pet-clinic-jvm
这款应用程序是非常基础的,因为它只包含以下元素:
index
页面 我将用三种语言来做代码比较:
这个应用里面涉及到了两个实体: Customer
和 Pet
@Entity public 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; } }
@Entity public 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语言中有好几种方法可以定义一个 实体类, 我已经试过两种了。尽管作用都是一样的,但是后者可能更受用户欢迎,因为前者只是简单地在做一些Java里面也能做的事情。
// Implementation using a regular class, mimicking regular Java @Entity class 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) @Entity data 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
关键字显示的定义出来,这样做代码更容易阅读,出现错误的概率也会降低,所以我挺喜欢这种做法的。
@Entity class 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" }
@Entity class 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是一致的。
@Repository public interface CustomerRepository extends CrudRepository<Customer, Long> { List<Customer> findByLastName(String lastName); }
@Repository public interface PetRepository extends CrudRepository<Pet, Long> { }
注意, findByLastName
函数实际上并没有在其它地方进行调用,我定义它只是用来提供一个示例的。
@Repository interface CustomerRepository : CrudRepository<Customer, Long> { fun findByLastName(name: String): List<Customer> }
@Repository interface PetRepository : CrudRepository<Pet, Long>`
这里没有太大的区别,代码基本上是一样的。Kotlin版本的代码稍微短一点,这是因为 Kotlin的默认修饰符是public的
,而且有一个 :
符号而不是 extends
关键字。此外,也有可能是如果没有在body中定义任何内容的话,就有可能可能会忽略花括号。
@Repository trait CustomerRepository extends CrudRepository[Customer, java.lang.Long] { def findByLastName(lastName: String): List[Customer] }
@Repository trait PetRepository extends CrudRepository[Pet, java.lang.Long]
Scala使用的是 traits
,而不是 interfaces
,但在大部分情况下它们都是相同的概念,或者至少针对我们这个简单的例子而言它们是一样的。
由于某些原因,需要将 Long
类明确定义为 java.lang.Long
以避免编译错误(我再次对Scala感到失望)。
@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); } }
@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中会变得更加臃肿。
@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注解出于某种原因[这是原因])虽然看起来令人困惑如果类更大,更依赖项注入,我想说这是一个机会,在这种情况下适当的格式。
@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目前估计还有一段很长的路要走。