theme | css | background | lineNumbers | drawings | colorSchema | layout | highlighter | canvasWidth | exportFilename | export | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
apple-basic |
unocss |
true |
|
dark |
intro |
prism |
920 |
index |
|
<style>.smaller{ width: 300px }</style>
-
Pasha Finkelshteyn
- Dev at
- ≈10 years in JVM. Mostly and
- And
- asm0di0
- @[email protected]
- Simple nano-service
- MVC
- Validation
- JPA
- JDBC
- Tests
build.gradle.kts
SpringKotlinStartApplication.kt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.0.2"
id("io.spring.dependency-management") version "1.1.0"
kotlin("jvm") version "1.7.22"
kotlin("plugin.spring") version "1.7.22"
kotlin("plugin.jpa") version "1.7.22"
}
group = "com.github.asm0dey"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
}
extra["testcontainersVersion"] = "1.17.6"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
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("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
}
dependencyManagement {
imports {
mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
package com.github.asm0dey.springkotlinstart
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class SpringKotlinStartApplication
fun main(args: Array<String>) {
runApplication<SpringKotlinStartApplication>(*args)
}
inline fun <reified T : Any> runApplication(vararg args: String): ConfigurableApplicationContext =
SpringApplication.run(T::class.java, *args)
The first goodie of Spring for Kotlin
@RestController
@RequestMapping("/person")
class PersonController {
@PostMapping
fun createPerson(@RequestBody @Valid person: Person) {}
}
Person.kt
:
data class Person(
val name: String,
val age: Int
)
POST localhost:8080/person
Content-Type: application/json
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"timestamp": 1674735741056,
"status": 400,
"error": "Bad Request",
"path": "/person"
}
Since Person
is non-nullable — it's validated without @NotNull
annotation
build.greadle.kts
:
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
Nullness annotations (e.g.,
@NonNull
and@CheckForNull
)
Internationalization annotations, such as
@NonNls
or@Nls
POST localhost:8080/person
Content-Type: application/json
{"name": null, "age": null}
On client
HTTP/1.1 400 Bad Request
Content-Type: application/json
On server
…Instantiation of [simple type, class com.github.asm0dey.sample.Person]
value failed for JSON property name due to missing
POST localhost:8080/person
Content-Type: application/json
{"name": "Pasha", "age": null}
HTTP/1.1 200
Content-Length: 0
<style> img { width: 300px } </style>
::right::
data class Person(
val name: String,
val age: Double
)
- Double
- Int
- Float
- Char
- Short
- Byte
- Boolean
data class Person(
val name: String,
@field:NotNull val age: Double?
)
POST localhost:8080/person
Content-Type: application/json
{"name": "Pasha", "age": null}
HTTP/1.1 400 Bad Request
…
{ "timestamp": 1674760360096, "status": 400, "error": "Bad Request", "path": "/person" }
Field error in object 'person' on field 'age': rejected value [null]
spring:
jackson:
deserialization:
FAIL_ON_NULL_FOR_PRIMITIVES: true
-Xjsr305=strict
will make the validation easier- On JVM primitive types have default values
- For JVM primitive types we have to put
@field:NotNull
and mark them nullable
@Entity
data class Person(
@Id
@GeneratedValue(strategy = IDENTITY)
var id: Int? = null,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val age: Int,
)
-
data
class -
val name
andval age
- No no-arg constructor
data
classes have copy
, equals
, hashCode
, copy
, and componentX
defined
@Entity
data class Person(
@Id
@GeneratedValue(strategy = IDENTITY)
var id: Int? = null,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val age: Int,
)
data
classes have copy
, equals
, hashCode
, copy
, and componentX
defined
@Entity
class Person(
@Id
@GeneratedValue(strategy = IDENTITY)
var id: Int? = null,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val age: Int,
)
JPA won's be able to write to val
data
classes have copy
, equals
, hashCode
, copy
, and componentX
defined
@Entity
class Person(
@Id
@GeneratedValue(strategy = IDENTITY)
var id: Int? = null,
@Column(nullable = false)
var name: String,
@Column(nullable = false)
var age: Int,
)
JPA won's be able to write to val
How to make it work?
Magic:
kotlin("plugin.jpa") version "1.8.0"
- Puts annotations on the fields
- Adds a default constructor in bytecode*!
* In Kotlin the default constructor would not be possible, but in Java it is
@Entity
class Person(
@Id
@GeneratedValue(strategy = IDENTITY)
var id: Int? = null,
@Column(nullable = false)
var name: String,
@Column(nullable = false)
var age: Int,
)
Not quite.
At the very least we have to redefine equals
and hashCode
.
For example…
@Entity
class Person(
// properties
) {
// equals…
override fun hashCode(): Int {
return id ?: 0
}
}
It is perfect? It's perfectly working.
What we can think about?
@Id @GeneratedValue(strategy = IDENTITY)
val id: Long = 0,
This way we won't be able to rewrite an immutable property
Let's imagine we need to call the following:
SELECT *
FROM users
WHERE id = ?
SELECT DISTINCT book.id
, (SELECT COALESCE(JSON_GROUP_ARRAY(JSON_ARRAY(t.v0, t.v1, t.v2, t.v3, t.v 4, t.v5, t.v6, t.v7, t.v8,
t.v9)), JSON_ARRAY())
FROM (SELECT b.id AS v0 , b.path AS v1 , b.name AS v2 , b.date AS v3 , b.added AS v4 , b.sequence AS v5
, b.sequence_number AS v6 , b.lang AS v7 , b.zip_file AS v8 , b.seqid AS v9 FROM book AS b
WHERE b.id = book.id) AS t) AS book
, (SELECT COALESCE(JSON_GROUP_ARRAY(JSON_ARRAY(t.v0, t.v1, t.v2, t.v3, t.v4, t.v5, t.v6)), JSON_ARRAY())
FROM (SELECT DISTINCT author.id AS v0 , author.fb2id AS v1 , author.first _name AS v2
, author.middle_name AS v3 , author.last_name AS v4 , author.nickname AS v5 , author.added AS v6
FROM author
JOIN book_author ON book_author.author_id = author.id
WHERE book_author.book_id = book.id) AS t) AS authors
, (SELECT COALESCE(JSON_GROUP_ARRAY(JSON_ARRAY(t.v0, t.v1)), JSON_ARRAY())
FROM (SELECT DISTINCT genre.name AS v0, genre.id AS v1
FROM genre
JOIN book_genre ON book_genre.genre_id = genre.id
WHERE book_genre.book_id = book.id) AS t) AS genres
, book.sequence
FROM book
JOIN book_author ON book_author.book_id = book.id
WHERE (book.seqid = 40792 AND book_author.author_id = 34606)
ORDER BY book.sequence_number ASC NULLS LAST, book.name
```java {all|1|2|5-13|7|8|9|10|11} public List findById(int id) { return jdbcTemplate.query("SELECT * FROM users WHERE id = ?", new UserRowMapper(), id); }
private static class UserRowMapper implements RowMapper { @Override public Person mapRow(ResultSet resultSet, int i) throws SQLException { int id = resultSet.getInt("id"); String name = resultSet.getString("name"); Double age = resultSet.getDouble("age"); return new Person(id, name, age); } }
---
# Let's inline mapper
<logos-java />
```java {all|2|3-6|7}
public List<Person> findById(int userId) {
return jdbcTemplate.query("SELECT * FROM users WHERE id = ?", (resultSet, i) -> {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
Double age = resultSet.getDouble("age");
return new Person(id1, name, age);
}, userId);
}
- Too many mappers
- Parameters too far from query
Let's Look at the signature
public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args)
Because in vararg can be only the last…
---return jdbcTemplate.query("SELECT * FROM users WHERE id = ?", userId) { rs, _ ->
val id = rs.getInt("id")
val name = rs.getString("name")
val age = rs.getDouble("age")
Person(id, name, age)
}
vararg
doesn't have to be in the last position- unused parameter of a lambda can be named
_
fun <T> JdbcOperations.query(
sql: String,
vararg args: Any,
function: (ResultSet, Int) -> T
): List<T>
Which allows
return jdbcTemplate.query("SELECT * FROM users WHERE id = ?", userId)
{ rs, _ ->
// TODO: ResultSet → Person
}
val beans = beans {
bean { jacksonObjectMapper() }
}
Modified Jackson's ObjectMapper
to work with data
classes from jackson-module-kotlin
@Bean
fun kotlinMapper(): ObjectMapper {
return jacksonObjectMapper()
}
4 lines instead of 1
---class JsonLogger(private val objectMapper: ObjectMapper) {
fun log(o: Any) {
if (o::class.isData) {
println(objectMapper.writeValueAsString(o))
} else println(o.toString())
}
}
val beans = beans {
bean { jacksonObjectMapper() }
bean(::JsonLogger)
}
val beans = beans {
bean { jacksonObjectMapper() }
bean(::JsonLogger)
bean("randomGoodThing", isLazyInit = Random.nextBoolean()) {
if (Random.nextBoolean()) "Norway" else "Well"
}
}
Let's return to our very first file
runApplication<SampleApplication>(*args)
val beans = { /* */ }
Let's change it to
runApplication<SampleApplication>(*args) {
addInitializers(beans)
}
And run it…
Started SampleApplicationKt in 1.776 seconds (process running for 2.133)
Bean:
@Component
class MyBean(val jsonLogger: JsonLogger) {
fun test() = jsonLogger.log("Test")
}
Test:
@SpringBootTest
class ConfigTest {
@Autowired private lateinit var myBean: MyBean
@Test
fun testIt() = assertEquals("Test", myBean.test())
}
No qualifying bean of type
'com.github.asm0dey.sample.JsonLogger'
available: expected at least 1
bean which qualifies as autowire candidate
That's because our tests do not call main
!
val beans = { /* */ }
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) = beans.initialize(context)
}
application.yml
:
context.initializer.classes: "com.github.asm0dey.sample.BeansInitializer"
Main.kt
:
fun main(args: Array<String>) {
runApplication<SampleApplication>(*args)
}
val beans = beans {
bean { jacksonObjectMapper() }
bean(::JsonLogger)
bean("random", isLazyInit = Random.nextBoolean()) {
if (Random.nextBoolean()) "Norway" else "Well"
}
bean {
val http = ref<HttpSecurity>()
http {
csrf { disable() }
httpBasic { }
securityMatcher("/**")
authorizeRequests {
authorize("/auth/**", authenticated)
authorize(anyRequest, permitAll)
}
}
http.build()
}
}
- Always generate the project with start.spring.io
- Reified generics might make an API better
- Validation is better with Kotlin, but remember about primitives
data
classes should not be used for JPA- JDBC is simpler with Kotlin
- Bean definition DSL is awesome
- Specifically with security!
- asm0di0
- @[email protected]
- [email protected]
- asm0dey
- asm0dey
- asm0dey
- asm0dey
- asm0dey