保持领先
VMware 提供培训和认证,助您快速提升。
了解更多继我的第一篇 Kotlin 博客文章 之后,今天我想介绍我为即将到来的 Spring I/O 2016 大会演讲“使用 Kotlin 和 Spring Boot 开发地理空间 Web 服务”而开发的新 Spring Boot + Kotlin 应用。
此应用的目标之一是展示如何利用原生数据库功能,就像我们在 NoSQL 领域所做的那样。这里我们想使用由 PostGIS 提供的地理空间支持,PostGIS 是 PostgreSQL 的空间数据库扩展。 原生 JSON 支持 也是一个不错的用例。
这个 Geospatial Messenger 示例应用 可在 GitHub 上获取,它提供了 2 种风格:
master
分支使用了 Exposed,这是一个由 JetBrains 创建的带有类型安全 API 的 Kotlin SQL 库。它可以与 Query DSL SQL 或 jOOQ 相媲美,但它提供了地道的 Kotlin API,并且不需要代码生成。spring-data-jdbc-repository
分支使用了 spring-data-jdbc-repository
,这是一个社区项目,允许使用 Spring Data PagingAndSortingRepository
API 执行原始 SQL 查询,而无需 JPA。我使用的是 Jakub Jirutka 的这个分支,它是 Tomasz Nurkiewicz 原始项目 的改进版本。一个 Spring Data JPA + Hibernate Spatial 版本 会很有趣,所以欢迎提交 pull request 来贡献 ;-) Kotlin Query DSL 支持也会很好,但目前尚不支持(如果您感兴趣,请在 此 issue 上评论)。在这篇博客文章中,我将重点介绍 Exposed 版本。
借助以下这 2 个 Kotlin 类,我们的领域模型很容易描述
class Message(
var content : String,
var author : String,
var location : Point? = null,
var id : Int? = null
)
class User(
var userName : String,
var firstName : String,
var lastName : String,
var location : Point? = null
)
Exposed 允许我们使用类型安全的 SQL API 描述表的结构,使用起来非常方便(支持自动补全、重构且不易出错)
object Messages : Table() {
val id = integer("id").autoIncrement().primaryKey()
val content = text("content")
val author = reference("author", Users.userName)
val location = point("location").nullable()
}
object Users : Table() {
val userName = text("user_name").primaryKey()
val firstName = text("first_name")
val lastName = text("last_name")
val location = point("location").nullable()
}
值得注意的是,Exposed 本身并不原生支持 PostGIS 功能,例如几何类型或地理空间请求。这时 Kotlin 扩展 就大显身手了,它只需几行代码即可添加此类支持,而无需使用扩展类。
fun Table.point(name: String, srid: Int = 4326): Column<Point>
= registerColumn(name, PointColumnType())
infix fun ExpressionWithColumnType<*>.within(box: PGbox2d) : Op<Boolean>
= WithinOp(this, box)
更新:现在我们可以使用 Exposed 的 @Transactional
支持了!事务管理只需在 Application
类中配置 @EnableTransactionManagement
注解和一个 PlatformTransactionManager
bean 即可。
我们的仓库也非常简洁灵活,因为它们允许你使用类型安全的 SQL API 编写任何类型的 SQL 请求,即使是带有复杂 WHERE
子句的请求。
请注意,由于我们使用的是 Spring Framework 4.3,因此在单构造函数类中 我们不再需要指定 @Autowired
注解。
interface CrudRepository<T, K> {
fun createTable()
fun create(m: T): T
fun findAll(): Iterable<T>
fun deleteAll(): Int
fun findByBoundingBox(box: PGbox2d): Iterable<T>
fun updateLocation(userName:K, location: Point)
}
interface UserRepository: CrudRepository<User, String>
@Repository
@Transactional // Should be at @Service level in real applications
class DefaultUserRepository(val db: Database) : UserRepository {
override fun createTable() = SchemaUtils.create(Users)
override fun create(user: User): User {
Users.insert(toRow(user))
return user
}
override fun updateLocation(userName:String, location: Point) = {
location.srid = 4326
Users.update({Users.userName eq userName})
{ it[Users.location] = location }
}
override fun findAll() = Users.selectAll().map { fromRow(it) }
override fun findByBoundingBox(box: PGbox2d) =
Users.select { Users.location within box }
.map { fromRow(it) }
override fun deleteAll() = Users.deleteAll()
private fun toRow(u: User): Users.(UpdateBuilder<*>) -> Unit = {
it[userName] = u.userName
it[firstName] = u.firstName
it[lastName] = u.lastName
it[location] = u.location
}
private fun fromRow(r: ResultRow) =
User(r[Users.userName],
r[Users.firstName],
r[Users.lastName],
r[Users.location])
}
控制器也非常简洁,并使用了 Spring Framework 4.3 即将推出的 @GetMapping
/ @PostMapping
注解,这些注解只是 @RequestMapping
注解针对特定方法的快捷方式。
@RestController
@RequestMapping("/user")
class UserController(val repo: UserRepository) {
@PostMapping
@ResponseStatus(CREATED)
fun create(@RequestBody u: User) { repo.create(u) }
@GetMapping
fun list() = repo.findAll()
@GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}")
fun findByBoundingBox(@PathVariable xMin:Double,
@PathVariable yMin:Double,
@PathVariable xMax:Double,
@PathVariable yMax:Double)
= repo.findByBoundingBox(
PGbox2d(Point(xMin, yMin), Point(xMax, yMax)))
@PutMapping("/{userName}/location/{x},{y}")
@ResponseStatus(NO_CONTENT)
fun updateLocation(@PathVariable userName:String,
@PathVariable x: Double,
@PathVariable y: Double)
= repo.updateLocation(userName, Point(x, y))
}
客户端是纯 HTML + Javascript 应用,使用 OpenLayers 地图库开发(详见 index.html 和 map.js),它可以对你进行地理定位,并通过 Server-Sent Events 与其他用户收发地理位置信息。
最后同样重要的是,借助出色的 Spring REST docs 项目,REST API 得到了充分的测试和文档化,详见 MessageControllerTests 和 index.adoc。
开发这个应用给我的主要印象是它既有趣又高效,而且通过 SQL API、Kotlin 类型系统和 空安全 提供了高度的灵活性和安全性。生成的 Spring Boot 应用是一个 18 MB 的独立可执行 jar 包,内存消耗很低(应用甚至可以在 -Xmx32m
下运行!!!)。使用 Spring REST docs 也令人愉快,再次展示了 Kotlin 良好的 Java 互操作性。
我遇到的一些痛点(数组注解属性、Java 8 Stream 支持、完整可调用引用支持)计划在 Kotlin 1.1 中修复。Exposed 库尚处于早期阶段,需要进一步成熟,但从我的角度来看,它很有前景,并展示了如何使用 Kotlin 构建类型安全的 DSL API(这个 HTML 类型安全构建器 也是一个很好的例子)。
请记住,官方支持的 Spring Data 项目 与 Kotlin 配合得很好,正如我在 上一篇博客文章 中的 spring-boot-kotlin-demo 项目所示。
如果您恰巧在五月中旬身处巴塞罗那(无论如何,巴塞罗那都是一个不错的选择!),请不要错过参加 Spring I/O 大会 的机会。此外,SpringOne Platform(八月初,拉斯维加斯)的注册最近也已开放,如果您想享受早鸟票价格,请尽快注册。后者也仍在接受演讲提案。因此,如果您有兴趣就 Spring 或 Pivotal 相关技术发表演讲,请随时提交!