使用 Kotlin、Spring Boot 和 PostgreSQL 构建地理空间消息应用

工程 | Sébastien Deleuze | 2016年3月20日 | ...

继我之前关于 Kotlin 博客文章 之后,今天我想介绍一下我为即将到来的 Spring I/O 2016 大会 演讲“使用 Kotlin 和 Spring Boot 开发地理空间 Web 服务”而开发的新 Spring Boot + Kotlin 应用程序。

处理原生数据库功能

该应用程序的目标之一是了解如何利用原生数据库功能,就像我们在 NoSQL 世界中所做的那样。在这里,我们想要使用 PostGIS 提供的地理空间支持,PostGIS 是 PostgreSQL 的空间数据库扩展。 原生 JSON 支持 也可以是一个很好的用例。

此地理空间消息示例应用程序在 GitHub 上可用,提供两种版本

一个 Spring Data JPA + Hibernate Spatial 变体 会很有趣,所以欢迎您通过 pull request 贡献它 ;-) Kotlin Query DSL 支持也很好,但这目前不受支持(如果您感兴趣,请在 此问题 上发表评论)。在这篇博文中,我将重点介绍 Exposed 变体。

地理空间消息代码概览

领域模型

借助这两个 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
)

SQL 架构

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 请求,即使是具有复杂 WHERE 子句的类型安全的 SQL API。

请注意,由于我们使用的是 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))
}

客户端是一个使用 OpenLayers 映射库开发的纯 HTML + Javascript 应用程序(有关更多详细信息,请参见 index.htmlmap.js),它可以对您进行地理定位,并通过服务器发送事件向其他用户发送/接收地理定位消息。

Screenshot

最后但并非最不重要的是,REST API 得益于出色的 Spring REST 文档 项目得到了充分的测试和记录,有关更多详细信息,请参见 MessageControllerTestsindex.adoc

结论

开发此应用程序给我留下的主要印象是:它很有趣、高效,并且 SQL API 和 Kotlin 类型系统以及 空安全 提供了高度的灵活性和安全性。生成的 Spring Boot 应用程序是一个 18 MB 的自包含可执行 jar 文件,内存消耗低(应用程序可以使用 -Xmx32m 运行!!!)。使用 Spring REST 文档也是一种乐趣,再次证明了 Kotlin 良好的 Java 互操作性。

我遇到的几个痛点(数组注释属性Java 8 Stream 支持完整的可调用引用支持)计划在 Kotlin 1.1 中修复。Exposed 库还很年轻,需要进一步成熟,但在我看来,它很有前景,并展示了如何使用 Kotlin 构建类型安全的 DSL API(这个 HTML 类型安全构建器 也是一个很好的例子)。

请记住,官方支持的 Spring Data 项目 可以很好地与 Kotlin 协同工作,如我之前博客文章中的 spring-boot-kotlin-demo 项目所示

如果您碰巧在 5 月中旬在巴塞罗那(无论如何,在巴塞罗那都不是糟糕的时机!),请不要错过参加 Spring I/O 大会 的机会。此外,SpringOne Platform(8 月初,拉斯维加斯)的注册最近刚刚开放,如果您想享受早鸟票价,可以考虑报名。后者也仍然开放演讲提案。因此,如果您有兴趣就 Spring 或 Pivotal 相关技术发表演讲,请随时提交!

获取 Spring 新闻通讯

关注 Spring 电子报

订阅

抢先一步

VMware 提供培训和认证,助您快速提升技能。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部