Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fabianogoes/tutorial-spring-boot-kotlin-ptbr
Construindo aplicações web com Spring Boot e Kotlin :: Aprenda como construir e testar facilmente aplicações web com Spring, Kotlin, Junit 5 e JPA
https://github.com/fabianogoes/tutorial-spring-boot-kotlin-ptbr
Last synced: 3 months ago
JSON representation
Construindo aplicações web com Spring Boot e Kotlin :: Aprenda como construir e testar facilmente aplicações web com Spring, Kotlin, Junit 5 e JPA
- Host: GitHub
- URL: https://github.com/fabianogoes/tutorial-spring-boot-kotlin-ptbr
- Owner: fabianogoes
- Fork: true (spring-guides/tut-spring-boot-kotlin)
- Created: 2023-06-24T14:29:01.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2023-09-05T01:12:21.000Z (over 1 year ago)
- Last Synced: 2023-09-05T08:32:26.328Z (over 1 year ago)
- Language: Kotlin
- Homepage: https://spring.io/guides/tutorials/spring-boot-kotlin/
- Size: 841 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.adoc
- Contributing: CONTRIBUTING.adoc
- License: LICENSE.code.txt
Awesome Lists containing this project
README
:toc:
:icons: font
:source-highlighter: prettify
:project_id: tut-spring-boot-kotlin
:images: https://raw.githubusercontent.com/spring-guides/tut-spring-boot-kotlin/master/images
:tabsize: 2Este tutorial mostra como criar eficientemente uma aplicação de um blog de exemplo combinando o poder de https://spring.io/projects/spring-boot/[Spring Boot] and https://kotlinlang.org/[Kotlin].
Se você está começando com Kotlin, você pode aprender a linguagem lendo a https://kotlinlang.org/docs/reference/[documentação de referência], seguindo o online https://play.kotlinlang.org/[Tutorial de Kotlin Koans] ou apenas usando a https://docs.spring.io/spring/docs/current/spring-framework-reference/[documentação de referência do Spring Framework] que agora fornece exemplos de código no Kotlin.
O suporte ao Spring Kotlin está documentado na documentação de referência do https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] e https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation. Se precisar de ajuda, pesquise ou tire suas dúvidas com o https://stackoverflow.com/questions/tagged/kotlin+spring[`spring` e `kotlin` tags no StackOverflow] ou venha discutir no canal do https://slack.kotlinlang.org/[Kotlin Slack] `#spring`.
== Criando um novo projeto
Primeiro precisamos criar uma aplicação `Spring Boot`, o que pode ser feito de várias maneiras.
[[usando-o-site-initializr]]
=== Usando o site InitializrVisite https://start.spring.io e escolha a linguagem `Kotlin`.
Gradle é a ferramenta de compilação mais comumente usada no Kotlin, e fornece uma DSL Kotlin que é usada por padrão ao gerar um projeto Kotlin, então esta é a escolha recomendada. Mas você também pode usar o Maven se estiver mais confortável com ele
Observe que você pode usar https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin para ter Kotlin e Gradle selecionados por padrão. Selecione "Gradle - Kotlin" ou "Maven" dependendo de qual ferramenta de compilação você deseja usar
. Insira as seguinte artefato: `blog`
. Adicione as seguintes dependências:
- Spring Web
- Mustache
- Spring Data JPA
- H2 Database
- Spring Boot DevTools
. Clique em "Generate Project".O arquivo .zip contém um projeto padrão no diretório raiz, portanto, convém criar um diretório vazio antes de descompactá-lo.
[[usando-linha-de-comando]]
=== Usando linha de comandoVocê pode usar a API HTTP `Initializr` https://docs.spring.io/initializr/docs/current/reference/html/#command-line[por linha de comando], por exemplo, `curl` em sistemas baseados em UNIX like system:
[source]
----
$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip
----Adicione `-d type=gradle-project` Se vocë quiser usar `Gradle`.
[[usando-intellij-idea]]
=== Usando IntelliJ IDEAO `Spring Initializr` também esta integrado no IntelliJ IDEA Ultimate edition e e permite que você `crie` e `import` um novo projeto sem ter que sair da IDE para a linha de comando ou a interface do usuário da Web.
Para acessar o wizard, para o o menu File | New | Project, e selecione `Spring Initializr`.
Siga as etapas do assistente para usar os seguintes parâmetros::
- Artifact: "blog"
- Type: "Gradle - Kotlin" or "Maven"
- Language: Kotlin
- Name: "Blog"
- Dependências: "Spring Web Starter", "Mustache", "Spring Data JPA", "H2 Database" and "Spring Boot DevTools"[[gradle-build]]
== Entendendo o Gradle BuildIf you're using a Maven Build, you can <>.
Se você estiver usando o Maven, poderá pular para a seção dedicada.=== Plugins
Além do óbvio https://kotlinlang.org/docs/gradle.html[Kotlin Gradle plugin], a configuração padrão declara https://kotlinlang.org/docs/all-open-plugin.html#spring-support[kotlin-spring plugin] que abre automaticamente classes e métodos (ao contrário do Java, o qualificador padrão no Kotlin é `final`) anotados ou meta-anotados com anotações Spring. Isso é útil para poder criar beans `@Configuration` ou `@Transactional` sem ter que adicionar o qualificador `open` por exemplo, exigido por proxies CGLIB.
Para poder usar as propriedades não non-nullable do Kotlin com JPA, o https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] também está ativado. Ele gera construtores `no-arg` para qualquer classe anotada com `@Entity`, `@MappedSuperclass` or `@Embeddable`.`build.gradle.kts`
[source,kotlin]
----
import org.jetbrains.kotlin.gradle.tasks.KotlinCompileplugins {
id("org.springframework.boot") version "3.0.1"
id("io.spring.dependency-management") version "1.1.0"
kotlin("jvm") version "1.8.0"
kotlin("plugin.spring") version "1.8.0"
kotlin("plugin.jpa") version "1.8.0"
}
----=== Opções do compilador
Uma das principais características de Kotlin é https://kotlinlang.org/docs/null-safety.html[null-safety] -
Que lida de forma limpa com o valores `null` em tempo de compilação em vez de esbarrar no famoso `null` `NullPointerException` em tempo de execução.
Isso torna as aplicações mais seguras por meio de declarações nullability e expressando "value or no value" semântica sem pagar o custo de usar `Optional`.
Observe que Kotlin permite o uso de construções funcionais com valores anuláveis; veja https://www.baeldung.com/kotlin/null-safety[comprehensive guide to Kotlin null-safety].Embora o Java não permita que alguém expresse `null-safety` em seu `type-system`, Spring Framework fornece `null-safety` de toda API Spring Framework por meio de anotações `tooling-friendly` declarada no pacote `org.springframework.lang`.
Por padrão, tipos de APIs Java usado em Kotlin são reconhecidos como https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] para os quais `null-checks` são relaxados. https://kotlinlang.org/docs/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + anotações Spring nullability fornece `null-safety` por toda API Spring Framework para desenvolvedores Kotlin, com a vantagem de lidar com problemas relacionados com `null` em tempo de compilação.Este recurso pode ser habilitado adicionando a flag de compilação `-Xjsr305` com a opção `strict`.
`build.gradle.kts`
[source,kotlin]
----
tasks.withType {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
}
}
----=== Dependências
2 bibliotecas específicas do Kotlin são necessárias (a biblioteca padrão é adicionada automaticamente com o Gradle) para essa aplicação Web Spring Boot e configuradas por padrão:
- `kotlin-reflect` é a biblioteca de `reflection` do kotlin
- `jackson-module-kotlin` adiciona suporte para serialization/deserialization de classes Kotlin e data classes (classes de construtor único podem ser usadas automaticamente, e aquelas com construtores secundários ou static factories também são suportadas)`build.gradle.kts`
[source,kotlin]
----
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")
runtimeOnly("com.h2database:h2")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
----Recente versões de `H2` exigem configurações especiais adequadamente de keywords reservadas `user`.
`src/main/resources/application.properties`
[source,properties]
----
spring.jpa.properties.hibernate.globally_quoted_identifiers=true
spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions=true
----O plugin Spring Boot Gradle usa automaticamente a versão Kotlin declarada através do plugin Kotlin Gradle.
Você pode ver <>.
[[maven-build]]
== Entendendo o Maven Build=== Plugins
Além do óbvio https://kotlinlang.org/docs/reference/using-maven.html[Kotlin Maven plugin], a configuração padrão declara o https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support[kotlin-spring plugin] que automaticamente classes e métodos são `opens` (ao contrário do Java, o qualificador padrão do kotlin é `final`) anotado ou meta-annotated com Spring annotations.
Isto é útil para habilitar a criação de beas `@Configuration` or `@Transactional` sem ter que adicionar o qualificador `open` exigido por exemplo por CGLIB proxies.Para poder usar a propriedade do Kotlin non-nullable com JPA, https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] também está habilitado.
Gera construtores `no-arg` para qualquer classe anotada com `@Entity`, `@MappedSuperclass` or `@Embeddable`.`pom.xml`
[source,xml]
----${project.basedir}/src/main/kotlin
${project.basedir}/src/test/kotlin
org.springframework.boot
spring-boot-maven-plugin
org.jetbrains.kotlin
kotlin-maven-plugin
jpa
spring
-Xjsr305=strict
org.jetbrains.kotlin
kotlin-maven-noarg
${kotlin.version}
org.jetbrains.kotlin
kotlin-maven-allopen
${kotlin.version}
----Uma das principais características de Kotlin é https://kotlinlang.org/docs/null-safety.html[null-safety] - que lida de forma limpa com valores `null` em tempo de compilação em vez de esbarrar no famoso `NullPointerException` em tempo de execução.
Isso torna os aplicativos mais seguros por meio de declarações de `nullability` e expressões "value or no value" semântica sem pagar o custo de usar `Optional`.
Observe que Kotlin permite o uso de construções funcionais com valores anuláveis; veja https://www.baeldung.com/kotlin/null-safety[comprehensive guide to Kotlin null-safety].Embora o Java não permita `null-safety` em seu `type-system`, Spring Framework fornece `null-safety` em toda toda sua API Spring Framework por meio de anotações `tooling-friendly` declaradas no pacote `org.springframework.lang`.
Por padrão, tipos de APIs Java usado em Kotlin são reconhecidos como https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] onde `null-checks` são relaxados. https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + anotação Spring nullability fornece `null-safety` por toda API Spring Framework para desenvolvedores Kotlin, com a vantagem de lidar com problemas relacionados com `null` em tempo de compilação.Este recurso pode ser habilitado adicionando a flag de compilação `-Xjsr305` com a opção `strict`.
Observe também que o compilador Kotlin está configurado para gerar código de bytes Java 8 (Java 6 por padrão).
=== Dependências
3 Kotlin specific libraries are required for such Spring Boot web application and configured by default:
- `kotlin-stdlib` is the Kotlin standard library
- `kotlin-reflect` is Kotlin reflection library
- `jackson-module-kotlin` adiocional suporte de serialization/deserialization de Kotlin class and data classes (classes de construtor único podem ser usadas automaticamente, e aquelas com construtores secundários ou `static factories` também são suportadas)`pom.xml`
[source,xml]
----
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-mustache
org.springframework.boot
spring-boot-starter-web
com.fasterxml.jackson.module
jackson-module-kotlin
org.jetbrains.kotlin
kotlin-reflect
org.jetbrains.kotlin
kotlin-stdlib
org.springframework.boot
spring-boot-devtools
runtime
com.h2database
h2
runtime
org.springframework.boot
spring-boot-starter-test
test
----
[[understanding-generated-app]]
== Entendendo a aplicação gerada`src/main/kotlin/com/example/blog/BlogApplication.kt`
[source,kotlin]
----
package com.example.blogimport org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication@SpringBootApplication
class BlogApplicationfun main(args: Array) {
runApplication(*args)
}
----Em comparação com Java, você pode notar a falta de ponto-e-vírgula, a falta de colchetes na classe vazia (você pode adicionar alguns se precisar declarar beans por meio de anotação `@Bean`) e o uso de `runApplication` função de nivel superior. `runApplication(*args)` é uma alternativa Kotlin idiomática para `SpringApplication.run(BlogApplication::class.java, *args)` e pode ser usado para customizar a aplicação com a seguinte sintaxe.
`src/main/kotlin/com/example/blog/BlogApplication.kt`
[source,kotlin]
----
fun main(args: Array) {
runApplication(*args) {
setBannerMode(Banner.Mode.OFF)
}
}
----== Escrevendo sua primeiro controller Kotlin
Vamos criar um simples Controller para exibir uma página da Web simples.
`src/main/kotlin/com/example/blog/HtmlController.kt`
[source,kotlin]
----
package com.example.blogimport 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"
}}
----Perceba que estamos usando aqui uma https://kotlinlang.org/docs/extensions.html[Kotlin extension] que permite adicionar funções ou operadores Kotlin aos tipo Spring existentes.
Aqui nós importamos o `org.springframework.ui.set` extension function in order to be able to write `model["title"] = "Blog"` em vez de `model.addAttribute("title", "Blog")`.
A https://docs.spring.io/spring-framework/docs/current/kdoc-api/[Spring Framework KDoc API] lista todas as extensions Kotlin extensions fornecidas para entiquecer a API Java.Também precisamos criar o associado template `Mustache`.
`src/main/resources/templates/header.mustache`
[source]
----{{title}}
----
`src/main/resources/templates/footer.mustache`
[source]
--------
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}{{title}}
{{> footer}}
----Inicie a aplicação Web rodando a função `main` de `BlogApplication.kt`, e vá para `http://localhost:8080/`, você deve ver uma página da Web simples com um título "Blog".
== Testando com JUnit 5
JUnit 5 agora usado como padrão no Spring Boot fornece vários recursos muito úteis com Kotlin, incluindo https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-junit-jupiter-di[autowiring of constructor/method parameters] que permite usar propriedades `non-nullable` `val` e a possibilidade de usar `@BeforeAll`/`@AfterAll` sobre métodos regulares não estáticos.
=== Escrevendo teste in Kotlin com JUnit 5
Para fins deste exemplo, vamos criar um teste de integração para demonstrar vários recursos:
- Usamos sentenças reais entre backticks em vez de camel-case para fornecer nomes de função de teste expressivos
- JUnit 5 permite injetar parâmetros do construtor e do método, o que é um bom ajuste com as propriedades `read-only` e `non-nullable` do Kotlin
- Este código aproveita as Kotlin extensions `getForObject` e `getForEntity` (você precisa importá-las)`src/test/kotlin/com/example/blog/IntegrationTests.kt`
[source,kotlin]
----
@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("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("Blog
")
}}
----=== Ciclo de vida da instância de teste
Às vezes, você precisa executar um método antes ou depois de todos os testes de uma determinada classe. Como no Junit 4, JUnit 5 precisa por padrão que esses métodos sejam estáticos (que se traduz em https://kotlinlang.org/docs/object-declarations.html#companion-objects[`companion object`] em Kotlin, o que é bastante verbose e não é simples) porque as classes de teste são instanciadas uma vez por teste.
Mas Junit 5 Permite alterar esse comportamento padrão e instanciar classes de teste uma vez por classe. Isto pode ser feito em https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle[various ways], Aqui usaremos um arquivo de propriedade para alterar o comportamento padrão de todo o projeto:
`src/test/resources/junit-platform.properties`
[source,properties]
----
junit.jupiter.testinstance.lifecycle.default = per_class
----Com essa configuração, agora podemos usar as annotations `@BeforeAll` e `@AfterAll` em métodos regulares como mostrado na versão atualizada do `IntegrationTests` abanixo.
`src/test/kotlin/com/example/blog/IntegrationTests.kt`
[source,kotlin]
----
@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("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("Blog
")
}@Test
fun `Assert article page title, content and status code`() {
println(">> TODO")
}@AfterAll
fun teardown() {
println(">> Tear down")
}}
----== Criando suas próprias extensions
Em vez de usar classes util com métodos abstratos como em Java, é usual em Kotlin fornecer tais funcionalidades através de extension Kotlin. Aqui vamos adicionar uma função `format()` o tipo existente `LocalDateTime` para gerar texto com o formato de data em inglês.
`src/main/kotlin/com/example/blog/Extensions.kt`
[source,kotlin]
----
fun LocalDateTime.format(): String = 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() = lowercase(Locale.getDefault())
.replace("\n", " ")
.replace("[^a-z\\d\\s]".toRegex(), " ")
.split(" ")
.joinToString("-")
.replace("-+".toRegex(), "-")
----Na próxima secção, utilizaremos estas extensões.
== Persistencia de dados com JPA
Para fazer `lazy fetching` trabalhar como esperado, entidades devem ser `open` como descrito em https://youtrack.jetbrains.com/issue/KT-28525[KT-28525]. Nós usaresmo o plugin Kotlin `allopen` para este propósito.
Com Gradle:
`build.gradle.kts`
[source,kotlin]
----
plugins {
...
kotlin("plugin.allopen") version "1.8.0"
}allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
----Ou com Maven:
`pom.xml`
[source,xml]
----kotlin-maven-plugin
org.jetbrains.kotlin
...
...
all-open
all-open:annotation=jakarta.persistence.Entity
all-open:annotation=jakarta.persistence.Embeddable
all-open:annotation=jakarta.persistence.MappedSuperclass
----
Então criamos nosso modelo usando Kotlin https://kotlinlang.org/docs/reference/classes.html#constructors[com sintaxe concisa do construtor primário] que permite declarar ao mesmo tempo as propriedades e os parâmetros do construtor.
`src/main/kotlin/com/example/blog/Entities.kt`
[source,kotlin]
----
@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)
----Repare que estamos a utilizar aqui o nossa extension `String.toSlug()` para fornecer um argumento padrão ao parametro `slug` do constructor `Article`.
Os parâmetros opcionais com valores padrão são definidos na última posição para permitir a sua omissão quando se utilizam argumentos posicionais (Kotlin também suporta https://kotlinlang.org/docs/reference/functions.html#named-arguments[named arguments]). Observe que, em Kotlin, não é incomum agrupar declarações de classes concisas no mesmo arquivo.Observe: Aqui não estamos uando https://kotlinlang.org/docs/data-classes.html[`data` classes] com propriedades `val` porque JPA não foi projetado para funcionar com classes imutáveis ou com os métodos gerados automaticamente pelo `data`. Se você estiver usando outra variante do Spring Data, a maioria deles foi projetada para suportar tais construções, portanto, você deve usar classes como `data class User(val login: String, ...)` Quando estiver usando Spring Data MongoDB, Spring Data JDBC, etc.
Observe: Enquanto Spring Data JPA possibilita o uso de IDs naturais (poderia ter sido a propriedade `login` em `User` class) via https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites[`Persistable`], não é uma boa opção para o Kotlin devido a https://youtrack.jetbrains.com/issue/KT-6653[KT-6653], É por isso que é recomendável sempre usar entidades com IDs gerados em Kotlin.
Também declaramos nosso repositorio Spring Data JPA como seguinte.
`src/main/kotlin/com/example/blog/Repositories.kt`
[source,kotlin]
----
interface ArticleRepository : CrudRepository {
fun findBySlug(slug: String): Article?
fun findAllByOrderByAddedAtDesc(): Iterable
}interface UserRepository : CrudRepository {
fun findByLogin(login: String): User?
}
----E escrevemos JPA tests para verificar se os casos de uso básicos funcionam conforme o esperado.
`src/test/kotlin/com/example/blog/RepositoriesTests.kt`
[source,kotlin]
----
@DataJpaTest
class RepositoriesTests @Autowired constructor(
val entityManager: TestEntityManager,
val userRepository: UserRepository,
val articleRepository: ArticleRepository) {@Test
fun `When findByIdOrNull then return Article`() {
val johnDoe = User("johnDoe", "John", "Doe")
entityManager.persist(johnDoe)
val article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
entityManager.persist(article)
entityManager.flush()
val found = articleRepository.findByIdOrNull(article.id!!)
assertThat(found).isEqualTo(article)
}@Test
fun `When findByLogin then return User`() {
val johnDoe = User("johnDoe", "John", "Doe")
entityManager.persist(johnDoe)
entityManager.flush()
val user = userRepository.findByLogin(johnDoe.login)
assertThat(user).isEqualTo(johnDoe)
}
}
----Observação: Usamos aqui o `CrudRepository.findByIdOrNull` Kotlin extension fornecido por padrão com Spring Data, que é um `nullable` variante do `Optional` com base `CrudRepository.findById`. Leia o excelente blog post https://medium.com/@elizarov/null-is-your-friend-not-a-mistake-b63ff1751dd5[Null is your friend, not a mistake] para mais detalhes.
== Implementando o mecanismo de blog
Atualizamos o template Mustache "blog".
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}{{title}}
{{> footer}}
----E criamos um novo "article".
`src/main/resources/templates/article.mustache`
[source]
----
{{> header}}
{{article.title}}
{{article.headline}}{{article.content}}
{{> footer}}
----Atualizamos o `HtmlController` para renderizar páginas de blogs e artigos com a data formatada. `ArticleRepository` e `MarkdownConverter` com parametro de constructor que serão automaticamente injetados além de `HtmlController` que tem um constructor unico (implicito `@Autowired`).
`src/main/kotlin/com/example/blog/HtmlController.kt`
[source,kotlin]
----
@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)}
----Então, adicionamos a inicialização de dados a uma nova classe `BlogConfiguration`.
`src/main/kotlin/com/example/blog/BlogConfiguration.kt`
[source,kotlin]
----
@Configuration
class BlogConfiguration {@Bean
fun databaseInitializer(userRepository: UserRepository,
articleRepository: ArticleRepository) = ApplicationRunner {val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
articleRepository.save(Article(
title = "Lorem",
headline = "Lorem",
content = "dolor sit amet",
author = johnDoe
))
articleRepository.save(Article(
title = "Ipsum",
headline = "Ipsum",
content = "dolor sit amet",
author = johnDoe
))
}
}
----Observação: Perceba o uso de parametros nomeados para tornar o código mais legível.
E também Atualizamos o teste integrado accordingly consequentemente.
`src/test/kotlin/com/example/blog/IntegrationTests.kt`
[source,kotlin]
----
@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("/")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("Blog
", "Lorem")
}@Test
fun `Assert article page title, content and status code`() {
println(">> Assert article page title, content and status code")
val title = "Lorem"
val entity = restTemplate.getForEntity("/article/${title.toSlug()}")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains(title, "Lorem", "dolor sit amet")
}@AfterAll
fun teardown() {
println(">> Tear down")
}}
----Inicie (ou reinicie) a aplicação Web, e acesse `http://localhost:8080/`, Você deverá ver a lista de artigos com links clicáveis para ver um artigo específico.
== Expondo a API HTTP
Agora vamos implementar a API HTTP anotando os controller com `@RestController`.
`src/main/kotlin/com/example/blog/HttpControllers.kt`
[source,kotlin]
----
@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) ?: throw 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) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
}
----Para teste, em vez de testes integrados, usaremos `@WebMvcTest` e https://mockk.io/[Mockk] que são similares a https://site.mockito.org/[Mockito] mas mais integrado para Kotlin.
Como as anotações `@MockBean` e `@SpyBean` são específicas do Mockito, vamos aproveitar as anotações https://github.com/Ninja-Squad/springmockk[SpringMockK] que fornece anotações `@MockkBean` e `@SpykBean` semelhantes para o Mockk.
Com Gradle:
`build.gradle.kts`
[source,kotlin]
----
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(module = "mockito-core")
}
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("com.ninja-squad:springmockk:4.0.0")
----Ou com Maven:
`pom.xml`
[source,xml]
----org.springframework.boot
spring-boot-starter-test
testorg.junit.jupiter
junit-jupiter-engine
testcom.ninja-squad
springmockk
4.0.0
test----
`src/test/kotlin/com/example/blog/HttpControllersTests.kt`
[source,kotlin]
----
@WebMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {@MockkBean
lateinit var userRepository: UserRepository@MockkBean
lateinit var articleRepository: ArticleRepository@Test
fun `List articles`() {
val johnDoe = User("johnDoe", "John", "Doe")
val lorem5Article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
val ipsumArticle = Article("Ipsum", "Ipsum", "dolor sit amet", johnDoe)
every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(lorem5Article, ipsumArticle)
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].author.login").value(johnDoe.login))
.andExpect(jsonPath("\$.[0].slug").value(lorem5Article.slug))
.andExpect(jsonPath("\$.[1].author.login").value(johnDoe.login))
.andExpect(jsonPath("\$.[1].slug").value(ipsumArticle.slug))
}@Test
fun `List users`() {
val johnDoe = User("johnDoe", "John", "Doe")
val janeDoe = User("janeDoe", "Jane", "Doe")
every { userRepository.findAll() } returns listOf(johnDoe, janeDoe)
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].login").value(johnDoe.login))
.andExpect(jsonPath("\$.[1].login").value(janeDoe.login))
}
}
----Observação: `$` precisa ser escapado em string, pois é usada para interpolação de strings.
== Propriedades de configuração
Em Kotlin, a maneira recomendada de gerenciar as propriedades da aplicação é usando propriedades `read-only`.
`src/main/kotlin/com/example/blog/BlogProperties.kt`
[source,kotlin]
----
@ConfigurationProperties("blog")
data class BlogProperties(var title: String, val banner: Banner) {
data class Banner(val title: String? = null, val content: String)
}
----Em seguida, ativamos no nivel de `BlogApplication`.
`src/main/kotlin/com/example/blog/BlogApplication.kt`
[source,kotlin]
----
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
// ...
}
----Para gerar https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#configuration-metadata-annotation-processor[seus próprios metadados] para que essas propriedades personalisadas sejam reconhecidas pela IDE, https://kotlinlang.org/docs/reference/kapt.html[kapt deve ser configuraro] com a dependência `spring-boot-configuration-processor` como a seguir.
`build.gradle.kts`
[source,kotlin]
----
plugins {
...
kotlin("kapt") version "1.8.0"
}dependencies {
...
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
----Observação: Perceba que alguns recursos (como a detecção do valor padrão ou itens obsoletos) não estão funcionando devido a limitações no modelo fornecido pelo kapt.
Além disso, o processamento de anotações ainda não é compativel com o Maven devido ao https://youtrack.jetbrains.com/issue/KT-18022[KT-18022], veja https://github.com/spring-io/initializr/issues/438[initializr#438] para mais detalhes.No IntelliJ IDEA:
- Certifique-se de que o plugin do Spring Boot esteja ativado através do menu File | Settings | Plugins | Spring Boot
- Habilite o processamento de anotações pelo menu File | Settings | Build, Execution, Deployment | Compiler | Annotation Processors | Enable annotation processing
- Se https://youtrack.jetbrains.com/issue/KT-15040[Kapt ainda não está integrado no IDEA], você precisa executar manualmente o comando`./gradlew kaptKotlin` para gerar os metadados.Suas propriedades personalizadas agora devem ser reconhecidas ao editar `application.properties` (autocomplete, validation, etc.).
`src/main/resources/application.properties`
[source,properties]
----
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
----Edite o template e o controller adequadamente.
`src/main/resources/templates/blog.mustache`
[source]
----
{{> header}}{{#banner.title}}
{{banner.title}}
{{/banner.title}}...
{{> footer}}
----`src/main/kotlin/com/example/blog/HtmlController.kt`
[source,kotlin]
----
@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"
}// ...
----Reinicie a aplicação Web, atualize `http://localhost:8080/`, você deverá ver o banner na tela blog homepage.
== Conclusão
Agora, terminados de criar esta aplicação de amostra de blog com kotlin. O código fonte https://github.com/spring-guides/tut-spring-boot-kotlin[está disponivel no Github]. Você também pode dar uma olhada na documentação de referencia https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] e https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] se precisar de mais detalhes sobre recursos específicos.