本教程向您展示如何通过结合Spring BootKotlin的功能来有效地构建示例博客应用程序。

如果您是从Kotlin开始的,则可以通过阅读参考文档 ,遵循在线Kotlin Koans教程或仅使用Spring Framework参考文档 (现在在Kotlin中提供代码示例)来学习该语言。

Spring Kotlin支持在Spring FrameworkSpring Boot参考文档中有所记录。如果您需要帮助,请通过springkotlin标记在StackOverflow上或在#spring Kotlin Slack的频道。

创建一个新项目

首先,我们需要创建一个Spring Boot应用程序,可以通过多种方式来完成。

使用Initializr网站

访问https://start.spring.io并选择Kotlin语言。Gradle是Kotlin中最常用的构建工具,它提供了Kotlin DSL,在生成Kotlin项目时默认使用该DSL,因此这是推荐的选择。但是,如果您更喜欢Maven,也可以使用它。请注意,您可以使用https://start.spring.io/#!language=kotlin&type=gradle-project默认情况下选择Kotlin和Gradle。

  1. 选择“ Gradle Project”或根据您要使用的构建工具设置默认的“ Maven Project”

  2. 输入以下工件坐标:blog

  3. 添加以下依赖项:

    • Spring Web

    • Mustache

    • Spring Data JPA

    • H2数据库

    • Spring Boot开发者工具

  4. 点击“生成项目”。

初始化

.zip文件在根目录中包含一个标准项目,因此您可能需要在解压缩之前创建一个空目录。

使用命令行

您可以在命令行上使用Initializr HTTP API,例如,在类似UN * X的系统上使用curl:

$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d style=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip

-d type=gradle-project如果您想使用Gradle。

使用IntelliJ IDEA

Spring Initializr也集成在IntelliJ IDEA Ultimate版本中,使您可以创建和导入新项目,而不必将IDE留给命令行或Web UI。

要访问该向导,请转到“文件” |“其他”。新增|项目,然后选择Spring Initializr。

请按照向导的步骤使用以下参数:

  • 工件:“博客”

  • 类型:Maven项目或Gradle项目

  • 语言:Kotlin

  • 名称:“博客”

  • 依赖项:“ Spring Web Starter”,“ Mustache”,“ Spring Data JPA”,“ H2 Database”和“ Spring Boot DevTools”

Gradle构建

Gradle构建

外挂程式

除了显而易见的Kotlin Gradle插件外 ,默认配置还声明了kotlin-spring插件 ,该插件会自动打开类和方法(与Java不同,默认限定符为final (在Kotlin中),或使用Spring注释进行元注释。这对于能够创建很有用@Configuration要么@Transactional bean 而不必添加open例如,CGLIB代理要求的限定词。

为了能够将Kotlin非空属性与JPA一起使用,还启用了Kotlin JPA插件 。它为任何带注解的类生成无参数构造函数@Entity@MappedSuperclass要么@Embeddable

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
  kotlin("plugin.jpa") version "1.3.50"
  id("org.springframework.boot") version "2.2.0.RELEASE"
  id("io.spring.dependency-management") version "1.0.8.RELEASE"
  kotlin("jvm") version "1.3.50"
  kotlin("plugin.spring") version "1.3.50"
}

编译器选项

Kotlin的主要功能之一是null安全 -可以干净地处理null编译时的值,而不是碰到著名的NullPointerException在运行时。这可以通过可空性声明和表示“值或无值”的语义来使应用程序更安全,而无需支付诸如此类的包装程序的费用。 Optional 。请注意,Kotlin允许使用具有可为空值的函数构造;查看有关Kotlin空安全性的全面指南

尽管Java不允许人们在其类型系统中表示null安全性,但是Spring Framework通过在声明的工具友好注释中提供了整个Spring Framework API的null安全性。 org.springframework.lang包。默认情况下,将Kotlin中使用的Java API中的类型识别为放松了空检查的平台类型Kotlin对JSR 305注释 + Spring可空性注释的支持为 Kotlin开发人员提供了整个Spring Framework API的空安全性,其优势在于null编译时的相关问题。

可以通过添加以下功能来启用此功能-Xjsr305带有的编译器标志strict选项。

还要注意,Kotlin编译器已配置为生成Java 8字节码(默认情况下为Java 6)。

build.gradle.kts

tasks.withType<KotlinCompile> {
  kotlinOptions {
    freeCompilerArgs = listOf("-Xjsr305=strict")
    jvmTarget = "1.8"
  }
}

依存关系

这种Spring Boot Web应用程序需要3个Kotlin特定的库,并且默认情况下对其进行配置:

  • kotlin-stdlib-jdk8是Kotlin标准库的Java 8变体

  • kotlin-reflect是Kotlin反射库

  • jackson-module-kotlin增加了对Kotlin类和数据类的序列化/反序列化的支持(可以自动使用单个构造函数类,也支持具有辅助构造函数或静态工厂的那些)

build.gradle.kts

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-data-jpa")
  implementation("org.springframework.boot:spring-boot-starter-mustache")
  implementation("org.springframework.boot:spring-boot-starter-web")
  implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
  implementation("org.jetbrains.kotlin:kotlin-reflect")
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
  runtimeOnly("com.h2database:h2:1.4.200") // See https://github.com/spring-projects/spring-boot/issues/18593 and https://github.com/h2database/h2database/issues/1841
  runtimeOnly("org.springframework.boot:spring-boot-devtools")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Spring Boot Gradle插件自动使用通过Kotlin Gradle插件声明的Kotlin版本。

Maven构建

Maven构建

外挂程式

除了显而易见的Kotlin Maven插件之外 ,默认配置还声明了kotlin-spring插件 ,该插件会自动打开类和方法(与Java不同,默认限定符为final (在Kotlin中),或使用Spring注释进行元注释。这对于能够创建很有用@Configuration要么@Transactional bean 而不必添加open例如,CGLIB代理要求的限定词。

为了能够将Kotlin非空属性与JPA一起使用,还启用了Kotlin JPA插件 。它为任何带注解的类生成无参数构造函数@Entity@MappedSuperclass要么@Embeddable

pom.xml

<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-plugin</artifactId>
        <configuration>
          <compilerPlugins>
            <plugin>jpa</plugin>
            <plugin>spring</plugin>
          </compilerPlugins>
          <args>
            <arg>-Xjsr305=strict</arg>
          </args>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
          </dependency>
          <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>

Kotlin的主要功能之一是null安全 -可以干净地处理null编译时的值,而不是碰到著名的NullPointerException在运行时。这可以通过可空性声明和表示“值或无值”的语义来使应用程序更安全,而无需支付诸如此类的包装程序的费用。 Optional 。请注意,Kotlin允许使用具有可为空值的函数构造;查看有关Kotlin空安全性的全面指南

尽管Java不允许人们在其类型系统中表示null安全性,但是Spring Framework通过在声明的工具友好注释中提供了整个Spring Framework API的null安全性。 org.springframework.lang包。默认情况下,将Kotlin中使用的Java API中的类型识别为放松了空检查的平台类型Kotlin对JSR 305注释 + Spring可空性注释的支持为 Kotlin开发人员提供了整个Spring Framework API的空安全性,其优势在于null编译时的相关问题。

可以通过添加以下功能来启用此功能-Xjsr305带有的编译器标志strict选项。

还要注意,Kotlin编译器已配置为生成Java 8字节码(默认情况下为Java 6)。

依存关系

这种Spring Boot Web应用程序需要3个Kotlin特定的库,并且默认情况下对其进行配置:

  • kotlin-stdlib-jdk8是Kotlin标准库的Java 8变体

  • kotlin-reflect是Kotlin反射库(从Spring Framework 5开始是强制性的)

  • jackson-module-kotlin增加了对Kotlin类和数据类的序列化/反序列化的支持(可以自动使用单个构造函数类,也支持具有辅助构造函数或静态工厂的那些)

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mustache</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
  </dependency>
  <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
  </dependency>
  <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
  </dependency>
  <!-- See https://github.com/spring-projects/spring-boot/issues/18593 and https://github.com/h2database/h2database/issues/1841 -->
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

了解生成的应用程序

src/main/kotlin/com/example/blog/BlogApplication.kt

package com.example.blog

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BlogApplication

fun main(args: Array<String>) {
  runApplication<BlogApplication>(*args)
}

与Java相比,您会注意到缺少分号,在空类上缺少括号(如果需要通过以下方式声明bean,则可以添加一些内容: @Bean注释)和使用runApplication顶级功能。 runApplication(*args)是Kotlin的惯用替代品吗SpringApplication.run(BlogApplication::class.java, *args)并可以使用以下语法来自定义应用程序。

src/main/kotlin/com/example/blog/BlogApplication.kt

fun main(args: Array<String>) {
  runApplication<BlogApplication>(*args) {
    setBannerMode(Banner.Mode.OFF)
  }
}

编写您的第一个Kotlin控制器

让我们创建一个简单的控制器来显示一个简单的网页。

src/main/kotlin/com/example/blog/HtmlController.kt

package com.example.blog

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping

@Controller
class HtmlController {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = "Blog"
    return "blog"
  }

}

请注意,此处我们使用的是Kotlin扩展 ,该扩展允许向现有的Spring类型添加Kotlin函数或运算符。在这里我们导入org.springframework.ui.set扩展功能以便能够编写model["title"] = "Blog"代替model.addAttribute("title", "Blog")Spring Framework KDoc API列出了为丰富Java API而提供的所有Kotlin扩展。

我们还需要创建关联的Mustache模板。

src/main/resources/templates/header.mustache

<html>
<head>
  <title>{{title}}</title>
</head>
<body>

src/main/resources/templates/footer.mustache

</body>
</html>

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

{{> footer}}

通过运行以下命令启动Web应用程序main的功能BlogApplication.kt ,然后转到http://localhost:8080/ ,您应该会看到一个带有“博客”标题的醒目网页。

使用JUnit 5进行测试

现在在Spring Boot中默认使用的JUnit 5提供了Kotlin非常方便的各种功能,包括自动装配构造函数/方法参数 ,该参数允许使用非空值val属性和使用可能性@BeforeAll / @AfterAll使用常规的非静态方法。

用Kotlin编写JUnit 5测试

为了这个示例,让我们创建一个集成测试以演示各种功能:

  • 我们在反引号之间使用实词而不是驼峰式大小写来提供表达性的测试函数名称

  • JUnit 5允许注入构造函数和方法参数,这与Kotlin只读和不可为空的属性非常吻合

  • 此代码利用getForObjectgetForEntity Kotlin扩展(您需要导入它们)

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @Test
  fun `Assert blog page title, content and status code`() {
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>")
  }

}

测试实例生命周期

有时您需要在给定类的所有测试之前或之后执行一个方法。像Junit 4一样,默认情况下,JUnit 5要求这些方法是静态的(转化为companion object在Kotlin中,这是非常冗长且不直接的),因为每次测试都会实例化一次测试类。

但是Junit 5允许您更改此默认行为,并在每个类一次实例化测试类。这可以通过多种方式完成,这里我们将使用属性文件来更改整个项目的默认行为:

src/test/resources/junit-platform.properties

junit.jupiter.testinstance.lifecycle.default = per_class

通过此配置,我们现在可以使用@BeforeAll@AfterAll常规方法的注释,如更新后的版本中所示IntegrationTests以上。

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @BeforeAll
  fun setup() {
    println(">> Setup")
  }

  @Test
  fun `Assert blog page title, content and status code`() {
    println(">> Assert blog page title, content and status code")
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>")
  }

  @Test
  fun `Assert article page title, content and status code`() {
    println(">> TODO")
  }

  @AfterAll
  fun teardown() {
    println(">> Tear down")
  }

}

创建自己的扩展

在Kotlin中,通常不通过Java类将util类与抽象方法一起使用,而是通过Kotlin扩展提供此类功能。在这里我们要添加一个format()对现有功能LocalDateTime键入以生成具有英语日期格式的文本。

src/main/kotlin/com/example/blog/Extensions.kt

fun LocalDateTime.format() = this.format(englishDateFormatter)

private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }

private val englishDateFormatter = DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd")
    .appendLiteral(" ")
    .appendText(ChronoField.DAY_OF_MONTH, daysLookup)
    .appendLiteral(" ")
    .appendPattern("yyyy")
    .toFormatter(Locale.ENGLISH)

private fun getOrdinal(n: Int) = when {
  n in 11..13 -> "${n}th"
  n % 10 == 1 -> "${n}st"
  n % 10 == 2 -> "${n}nd"
  n % 10 == 3 -> "${n}rd"
  else -> "${n}th"
}

fun String.toSlug() = toLowerCase()
    .replace("\n", " ")
    .replace("[^a-z\\d\\s]".toRegex(), " ")
    .split(" ")
    .joinToString("-")
    .replace("-+".toRegex(), "-")

我们将在下一部分中利用这些扩展。

JPA的持久性

为了使延迟获取按预期方式工作,应将实体openKT-28525中所述 。我们将使用Kotlinallopen插件。

使用Gradle:

build.gradle.kts

plugins {
  ...
  kotlin("plugin.allopen") version "1.3.50"
}

allOpen {
  annotation("javax.persistence.Entity")
  annotation("javax.persistence.Embeddable")
  annotation("javax.persistence.MappedSuperclass")
}

或使用Maven:

pom.xml

<plugin>
  <artifactId>kotlin-maven-plugin</artifactId>
  <groupId>org.jetbrains.kotlin</groupId>
  <configuration>
    ...
    <compilerPlugins>
      ...
      <plugin>all-open</plugin>
    </compilerPlugins>
    <pluginOptions>
      <option>all-open:annotation=javax.persistence.Entity</option>
      <option>all-open:annotation=javax.persistence.Embeddable</option>
      <option>all-open:annotation=javax.persistence.MappedSuperclass</option>
    </pluginOptions>
  </configuration>
</plugin>

然后,我们使用Kotlin 主要构造函数的简洁语法创建模型,该语法允许同时声明属性和构造函数参数。

src/main/kotlin/com/example/blog/Entities.kt

@Entity
class Article(
    var title: String,
    var headline: String,
    var content: String,
    @ManyToOne var author: User,
    var slug: String = title.toSlug(),
    var addedAt: LocalDateTime = LocalDateTime.now(),
    @Id @GeneratedValue var id: Long? = null)

@Entity
class User(
    var login: String,
    var firstname: String,
    var lastname: String,
    var description: String? = null,
    @Id @GeneratedValue var id: Long? = null)

注意,我们在这里使用我们的String.toSlug()扩展名以提供默认参数slug的参数Article构造函数。带有默认值的可选参数定义在最后一个位置,以便在使用位置参数时可以忽略它们(Kotlin也支持命名参数 )。请注意,在Kotlin中,将简洁的类声明分组在同一文件中并不少见。

在这里我们不使用data val属性,因为JPA并非设计用于不可变的类或由自动生成的方法data类。如果您使用其他Spring Data风格,则大多数都旨在支持此类构造,因此您应该使用类似以下的类data class User(val login: String, …​)使用Spring Data MongoDB,Spring Data JDBC等时
尽管Spring Data JPA可以使用自然ID( login财产User类)通过Persistable ,由于KT-6653 ,它与Kotlin不合适,因此建议始终在Kotlin中使用具有生成ID的实体。

我们还声明了我们的Spring Data JPA存储库,如下所示。

src/main/kotlin/com/example/blog/Repositories.kt

interface ArticleRepository : CrudRepository<Article, Long> {
  fun findBySlug(slug: String): Article?
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, Long> {
  fun findByLogin(login: String): User?
}

我们编写JPA测试来检查基本用例是否按预期工作。

src/test/kotlin/com/example/blog/RepositoriesTests.kt

@DataJpaTest
class RepositoriesTests @Autowired constructor(
    val entityManager: TestEntityManager,
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository) {

  @Test
  fun `When findByIdOrNull then return Article`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    entityManager.persist(juergen)
    val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
    entityManager.persist(article)
    entityManager.flush()
    val found = articleRepository.findByIdOrNull(article.id!!)
    assertThat(found).isEqualTo(article)
  }

  @Test
  fun `When findByLogin then return User`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    entityManager.persist(juergen)
    entityManager.flush()
    val user = userRepository.findByLogin(juergen.login)
    assertThat(user).isEqualTo(juergen)
  }
}
我们在这里使用CrudRepository.findByIdOrNull Spring Data默认提供的Kotlin扩展,它是Optional基于CrudRepository.findById 。阅读Null是您的朋友,而不是错误的博客文章,了解更多详细信息。

实施博客引擎

我们更新了“博客” Mustache模板。

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

<div class="articles">

  {{#articles}}
    <section>
      <header class="article-header">
        <h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
        <div class="article-meta">By  <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
      </header>
      <div class="article-description">
        {{headline}}
      </div>
    </section>
  {{/articles}}
</div>

{{> footer}}

我们创建了一个新的“文章”。

src/main/resources/templates/article.mustache

{{> header}}

<section class="article">
  <header class="article-header">
    <h1 class="article-title">{{article.title}}</h1>
    <p class="article-meta">By  <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
  </header>

  <div class="article-description">
    {{article.headline}}

    {{article.content}}
  </div>
</section>

{{> footer}}

我们更新HtmlController为了呈现带有格式化日期的博客和文章页面。 ArticleRepositoryMarkdownConverter构造函数参数将自动自动关联,因为HtmlController具有单个构造函数(隐式@Autowired )。

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(private val repository: ArticleRepository) {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = "Blog"
    model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
    return "blog"
  }

  @GetMapping("/article/{slug}")
  fun article(@PathVariable slug: String, model: Model): String {
    val article = repository
        .findBySlug(slug)
        ?.render()
        ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
    model["title"] = article.title
    model["article"] = article
    return "article"
  }

  fun Article.render() = RenderedArticle(
      slug,
      title,
      headline,
      content,
      author,
      addedAt.format()
  )

  data class RenderedArticle(
      val slug: String,
      val title: String,
      val headline: String,
      val content: String,
      val author: User,
      val addedAt: String)

}

然后,我们将数据初始化添加到新的BlogConfiguration类。

src/main/kotlin/com/example/blog/BlogConfiguration.kt

@Configuration
class BlogConfiguration {

    @Bean
    fun databaseInitializer(userRepository: UserRepository,
                            articleRepository: ArticleRepository) = ApplicationRunner {

        val smaldini = userRepository.save(User("smaldini", "Stéphane", "Maldini"))
        articleRepository.save(Article(
                title = "Reactor Bismuth is out",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = smaldini
        ))
        articleRepository.save(Article(
                title = "Reactor Aluminium has landed",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = smaldini
        ))
    }
}
请注意使用命名参数来使代码更具可读性。

并且我们还将相应地更新集成测试。

src/test/kotlin/com/example/blog/IntegrationTests.kt

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @BeforeAll
  fun setup() {
    println(">> Setup")
  }

  @Test
  fun `Assert blog page title, content and status code`() {
    println(">> Assert blog page title, content and status code")
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>", "Reactor")
  }

  @Test
  fun `Assert article page title, content and status code`() {
    println(">> Assert article page title, content and status code")
    val title = "Reactor Aluminium has landed"
    val entity = restTemplate.getForEntity<String>("/article/${title.toSlug()}")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains(title, "Lorem ipsum", "dolor sit amet")
  }

  @AfterAll
  fun teardown() {
    println(">> Tear down")
  }

}

启动(或重新启动)Web应用程序,然后转到http://localhost:8080/ ,您应该看到带有可点击链接的文章列表,以查看特定文章。

公开HTTP API

现在,我们将通过来实现HTTP API @RestController带注释的控制器。

src/main/kotlin/com/example/blog/HttpControllers.kt

@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository) {

  @GetMapping("/")
  fun findAll() = repository.findAllByOrderByAddedAtDesc()

  @GetMapping("/{slug}")
  fun findOne(@PathVariable slug: String) =
      repository.findBySlug(slug) ?: ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")

}

@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {

  @GetMapping("/")
  fun findAll() = repository.findAll()

  @GetMapping("/{login}")
  fun findOne(@PathVariable login: String) =
      repository.findByLogin(login) ?: ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
}

对于测试,而不是集成测试,我们将利用@WebMvcTestMockk至极类似的Mockito ,但更适合Kotlin。

以来@MockBean@SpyBean注释特定于Mockito,我们将利用SpringMockK提供类似的功能@MockkBean@SpykBean Mockk的注释。

使用Gradle:

build.gradle.kts

testImplementation("org.springframework.boot:spring-boot-starter-test") {
  exclude(module = "junit")
  exclude(module = "mockito-core")
}
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("com.ninja-squad:springmockk:1.1.3")

或使用Maven:

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>com.ninja-squad</groupId>
  <artifactId>springmockk</artifactId>
  <version>1.1.3</version>
  <scope>test</scope>
</dependency>

src/test/kotlin/com/example/blog/HttpControllersTests.kt

@WebMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {

  @MockkBean
  private lateinit var userRepository: UserRepository

  @MockkBean
  private lateinit var articleRepository: ArticleRepository

  @Test
  fun `List articles`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
    val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
    every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(spring5Article, spring43Article)
    mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk)
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("\$.[0].author.login").value(juergen.login))
        .andExpect(jsonPath("\$.[0].slug").value(spring5Article.slug))
        .andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
        .andExpect(jsonPath("\$.[1].slug").value(spring43Article.slug))
  }

  @Test
  fun `List users`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    val smaldini = User("smaldini", "Stéphane", "Maldini")
    every { userRepository.findAll() } returns listOf(juergen, smaldini)
    mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk)
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(jsonPath("\$.[0].login").value(juergen.login))
        .andExpect(jsonPath("\$.[1].login").value(smaldini.login))
  }
}
$需要在字符串中转义,因为它用于字符串插值。

配置属性

在Kotlin中,建议的管理应用程序属性的方法是利用@ConfigurationProperties@ConstructorBinding为了能够使用只读属性。

src/main/kotlin/com/example/blog/BlogProperties.kt

@ConstructorBinding
@ConfigurationProperties("blog")
data class BlogProperties(var title: String, val banner: Banner) {
  data class Banner(val title: String? = null, val content: String)
}

为了生成自己的元数据以使IDE识别这些自定义属性, 应该为kapt配置 spring-boot-configuration-processor依赖关系如下。

build.gradle.kts

plugins {
  ...
  kotlin("kapt") version "1.3.50"
}

dependencies {
  ...
  kapt("org.springframework.boot:spring-boot-configuration-processor")
}
请注意,由于kapt提供的模型的限制,某些功能(例如检测默认值或不推荐使用的项目)无法正常工作。此外,由于KT-18022 ,Maven还不支持注释处理,有关更多详细信息,请参见initializr#438

在IntelliJ IDEA中:

  • 确保在菜单File | File中启用了Spring Boot插件。设置|插件| Spring Boot

  • 通过菜单文件|启用注释处理设置|构建,执行,部署|编译器注释处理器|启用注释处理

  • 由于Kapt尚未集成在IDEA中 ,因此您需要手动运行以下命令./gradlew kaptKotlin生成元数据

现在,您的自定义属性应在编辑时被识别application.properties (自动完成,验证等)。

src/main/resources/application.properties

blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.

相应地编辑模板和控制器。

src/main/resources/templates/blog.mustache

{{> header}}

<div class="articles">

  {{#banner.title}}
  <section>
    <header class="banner">
      <h2 class="banner-title">{{banner.title}}</h2>
    </header>
    <div class="banner-content">
      {{banner.content}}
    </div>
  </section>
  {{/banner.title}}

  ...

</div>

{{> footer}}

src/main/kotlin/com/example/blog/HtmlController.kt

@Controller
class HtmlController(private val repository: ArticleRepository,
           private val properties: BlogProperties) {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = properties.title
    model["banner"] = properties.banner
    model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
    return "blog"
  }

  // ...

以来@ConfigurationProperties不会在测试片中自动扫描,我们需要在测试中显式配置@WebMvcTest

src/main/kotlin/com/example/blog/BlogApplication.kt

@WebMvcTest
@EnableConfigurationProperties(BlogProperties::class)
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {
    // ...
}

重新启动Web应用程序,刷新http://localhost:8080/ ,您应该在博客首页上看到横幅。

结论

现在,我们已经完成了构建示例Kotlin博客应用程序的工作。源代码在Github上可用 。如果需要有关特定功能的更多详细信息,还可以查看Spring FrameworkSpring Boot参考文档。