361 lines
13 KiB
Plaintext
361 lines
13 KiB
Plaintext
[[kotlin-spring-projects-in-kotlin]]
|
|
= Spring Projects in Kotlin
|
|
|
|
This section provides some specific hints and recommendations worth for developing Spring projects
|
|
in Kotlin.
|
|
|
|
|
|
|
|
[[final-by-default]]
|
|
== Final by Default
|
|
|
|
By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[all classes in Kotlin are `final`].
|
|
The `open` modifier on a class is the opposite of Java's `final`: It allows others to inherit from this
|
|
class. This also applies to member functions, in that they need to be marked as `open` to be overridden.
|
|
|
|
While Kotlin's JVM-friendly design is generally frictionless with Spring, this specific Kotlin feature
|
|
can prevent the application from starting, if this fact is not taken into consideration. This is because
|
|
Spring beans (such as `@Configuration` annotated classes which by default need to be extended at runtime for technical
|
|
reasons) are normally proxied by CGLIB. The workaround is to add an `open` keyword on each class and
|
|
member function of Spring beans that are proxied by CGLIB, which can
|
|
quickly become painful and is against the Kotlin principle of keeping code concise and predictable.
|
|
|
|
NOTE: It is also possible to avoid CGLIB proxies for configuration classes by using `@Configuration(proxyBeanMethods = false)`.
|
|
See {api-spring-framework}/context/annotation/Configuration.html#proxyBeanMethods--[`proxyBeanMethods` Javadoc] for more details.
|
|
|
|
Fortunately, Kotlin provides a
|
|
https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`]
|
|
plugin (a preconfigured version of the `kotlin-allopen` plugin) that automatically opens classes
|
|
and their member functions for types that are annotated or meta-annotated with one of the following
|
|
annotations:
|
|
|
|
* `@Component`
|
|
* `@Async`
|
|
* `@Transactional`
|
|
* `@Cacheable`
|
|
|
|
Meta-annotation support means that types annotated with `@Configuration`, `@Controller`,
|
|
`@RestController`, `@Service`, or `@Repository` are automatically opened since these
|
|
annotations are meta-annotated with `@Component`.
|
|
|
|
https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io] enables
|
|
the `kotlin-spring` plugin by default. So, in practice, you can write your Kotlin beans
|
|
without any additional `open` keyword, as in Java.
|
|
|
|
NOTE: The Kotlin code samples in Spring Framework documentation do not explicitly specify
|
|
`open` on the classes and their member functions. The samples are written for projects
|
|
using the `kotlin-allopen` plugin, since this is the most commonly used setup.
|
|
|
|
|
|
|
|
[[using-immutable-class-instances-for-persistence]]
|
|
== Using Immutable Class Instances for Persistence
|
|
|
|
In Kotlin, it is convenient and considered to be a best practice to declare read-only properties
|
|
within the primary constructor, as in the following example:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
class Person(val name: String, val age: Int)
|
|
----
|
|
|
|
You can optionally add https://kotlinlang.org/docs/reference/data-classes.html[the `data` keyword]
|
|
to make the compiler automatically derive the following members from all properties declared
|
|
in the primary constructor:
|
|
|
|
* `equals()` and `hashCode()`
|
|
* `toString()` of the form `"User(name=John, age=42)"`
|
|
* `componentN()` functions that correspond to the properties in their order of declaration
|
|
* `copy()` function
|
|
|
|
As the following example shows, this allows for easy changes to individual properties, even if `Person` properties are read-only:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
data class Person(val name: String, val age: Int)
|
|
|
|
val jack = Person(name = "Jack", age = 1)
|
|
val olderJack = jack.copy(age = 2)
|
|
----
|
|
|
|
Common persistence technologies (such as JPA) require a default constructor, preventing this
|
|
kind of design. Fortunately, there is a workaround for this
|
|
https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell["`default constructor hell`"],
|
|
since Kotlin provides a https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin[`kotlin-jpa`]
|
|
plugin that generates synthetic no-arg constructor for classes annotated with JPA annotations.
|
|
|
|
If you need to leverage this kind of mechanism for other persistence technologies, you can configure
|
|
the https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin[`kotlin-noarg`]
|
|
plugin.
|
|
|
|
NOTE: As of the Kay release train, Spring Data supports Kotlin immutable class instances and
|
|
does not require the `kotlin-noarg` plugin if the module uses Spring Data object mappings
|
|
(such as MongoDB, Redis, Cassandra, and others).
|
|
|
|
|
|
|
|
[[injecting-dependencies]]
|
|
== Injecting Dependencies
|
|
|
|
Our recommendation is to try to favor constructor injection with `val` read-only (and
|
|
non-nullable when possible) https://kotlinlang.org/docs/reference/properties.html[properties],
|
|
as the following example shows:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@Component
|
|
class YourBean(
|
|
private val mongoTemplate: MongoTemplate,
|
|
private val solrClient: SolrClient
|
|
)
|
|
----
|
|
|
|
NOTE: Classes with a single constructor have their parameters automatically autowired.
|
|
That's why there is no need for an explicit `@Autowired constructor` in the example shown
|
|
above.
|
|
|
|
If you really need to use field injection, you can use the `lateinit var` construct,
|
|
as the following example shows:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@Component
|
|
class YourBean {
|
|
|
|
@Autowired
|
|
lateinit var mongoTemplate: MongoTemplate
|
|
|
|
@Autowired
|
|
lateinit var solrClient: SolrClient
|
|
}
|
|
----
|
|
|
|
|
|
|
|
[[injecting-configuration-properties]]
|
|
== Injecting Configuration Properties
|
|
|
|
In Java, you can inject configuration properties by using annotations (such as pass:q[`@Value("${property}")`)].
|
|
However, in Kotlin, `$` is a reserved character that is used for
|
|
https://kotlinlang.org/docs/reference/idioms.html#string-interpolation[string interpolation].
|
|
|
|
Therefore, if you wish to use the `@Value` annotation in Kotlin, you need to escape the `$`
|
|
character by writing pass:q[`@Value("\${property}")`].
|
|
|
|
NOTE: If you use Spring Boot, you should probably use
|
|
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties[`@ConfigurationProperties`]
|
|
instead of `@Value` annotations.
|
|
|
|
As an alternative, you can customize the property placeholder prefix by declaring the
|
|
following configuration beans:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@Bean
|
|
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
|
setPlaceholderPrefix("%{")
|
|
}
|
|
----
|
|
|
|
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
|
|
that uses the `${...}` syntax, with configuration beans, as the following example shows:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@Bean
|
|
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
|
setPlaceholderPrefix("%{")
|
|
setIgnoreUnresolvablePlaceholders(true)
|
|
}
|
|
|
|
@Bean
|
|
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
|
|
----
|
|
|
|
|
|
|
|
[[checked-exceptions]]
|
|
== Checked Exceptions
|
|
|
|
Java and https://kotlinlang.org/docs/reference/exceptions.html[Kotlin exception handling]
|
|
are pretty close, with the main difference being that Kotlin treats all exceptions as
|
|
unchecked exceptions. However, when using proxied objects (for example classes or methods
|
|
annotated with `@Transactional`), checked exceptions thrown will be wrapped by default in
|
|
an `UndeclaredThrowableException`.
|
|
|
|
To get the original exception thrown like in Java, methods should be annotated with
|
|
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-throws/index.html[`@Throws`]
|
|
to specify explicitly the checked exceptions thrown (for example `@Throws(IOException::class)`).
|
|
|
|
|
|
|
|
[[annotation-array-attributes]]
|
|
== Annotation Array Attributes
|
|
|
|
Kotlin annotations are mostly similar to Java annotations, but array attributes (which are
|
|
extensively used in Spring) behave differently. As explained in the
|
|
https://kotlinlang.org/docs/reference/annotations.html[Kotlin documentation] you can omit
|
|
the `value` attribute name, unlike other attributes, and specify it as a `vararg` parameter.
|
|
|
|
To understand what that means, consider `@RequestMapping` (which is one of the most widely
|
|
used Spring annotations) as an example. This Java annotation is declared as follows:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
public @interface RequestMapping {
|
|
|
|
@AliasFor("path")
|
|
String[] value() default {};
|
|
|
|
@AliasFor("value")
|
|
String[] path() default {};
|
|
|
|
RequestMethod[] method() default {};
|
|
|
|
// ...
|
|
}
|
|
----
|
|
|
|
The typical use case for `@RequestMapping` is to map a handler method to a specific path
|
|
and method. In Java, you can specify a single value for the annotation array attribute,
|
|
and it is automatically converted to an array.
|
|
|
|
That is why one can write
|
|
`@RequestMapping(value = "/toys", method = RequestMethod.GET)` or
|
|
`@RequestMapping(path = "/toys", method = RequestMethod.GET)`.
|
|
|
|
However, in Kotlin, you must write `@RequestMapping("/toys", method = [RequestMethod.GET])`
|
|
or `@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])` (square brackets need
|
|
to be specified with named array attributes).
|
|
|
|
An alternative for this specific `method` attribute (the most common one) is to
|
|
use a shortcut annotation, such as `@GetMapping`, `@PostMapping`, and others.
|
|
|
|
NOTE: If the `@RequestMapping` `method` attribute is not specified, all HTTP methods will
|
|
be matched, not only the `GET` method.
|
|
|
|
|
|
|
|
[[testing]]
|
|
== Testing
|
|
|
|
This section addresses testing with the combination of Kotlin and Spring Framework.
|
|
The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with
|
|
https://mockk.io/[Mockk] for mocking.
|
|
|
|
NOTE: If you are using Spring Boot, see
|
|
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-kotlin-testing[this related documentation].
|
|
|
|
|
|
[[constructor-injection]]
|
|
=== Constructor injection
|
|
|
|
As described in the <<testing#testcontext-junit-jupiter-di, dedicated section>>,
|
|
JUnit 5 allows constructor injection of beans which is pretty useful with Kotlin
|
|
in order to use `val` instead of `lateinit var`. You can use
|
|
{api-spring-framework}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`]
|
|
to enable autowiring for all parameters.
|
|
|
|
====
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@SpringJUnitConfig(TestConfig::class)
|
|
@TestConstructor(autowireMode = AutowireMode.ALL)
|
|
class OrderServiceIntegrationTests(val orderService: OrderService,
|
|
val customerService: CustomerService) {
|
|
|
|
// tests that use the injected OrderService and CustomerService
|
|
}
|
|
----
|
|
====
|
|
|
|
|
|
[[per_class-lifecycle]]
|
|
=== `PER_CLASS` Lifecycle
|
|
|
|
Kotlin lets you specify meaningful test function names between backticks (```).
|
|
As of JUnit 5, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
|
|
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
|
|
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
|
|
|
|
You can also change the default behavior to `PER_CLASS` thanks to a `junit-platform.properties`
|
|
file with a `junit.jupiter.testinstance.lifecycle.default = per_class` property.
|
|
|
|
The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on non-static methods:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
class IntegrationTests {
|
|
|
|
val application = Application(8181)
|
|
val client = WebClient.create("http://localhost:8181")
|
|
|
|
@BeforeAll
|
|
fun beforeAll() {
|
|
application.start()
|
|
}
|
|
|
|
@Test
|
|
fun `Find all users on HTML page`() {
|
|
client.get().uri("/users")
|
|
.accept(TEXT_HTML)
|
|
.retrieve()
|
|
.bodyToMono<String>()
|
|
.test()
|
|
.expectNextMatches { it.contains("Foo") }
|
|
.verifyComplete()
|
|
}
|
|
|
|
@AfterAll
|
|
fun afterAll() {
|
|
application.stop()
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
[[specification-like-tests]]
|
|
=== Specification-like Tests
|
|
|
|
You can create specification-like tests with JUnit 5 and Kotlin.
|
|
The following example shows how to do so:
|
|
|
|
[source,kotlin,indent=0]
|
|
----
|
|
class SpecificationLikeTests {
|
|
|
|
@Nested
|
|
@DisplayName("a calculator")
|
|
inner class Calculator {
|
|
val calculator = SampleCalculator()
|
|
|
|
@Test
|
|
fun `should return the result of adding the first number to the second number`() {
|
|
val sum = calculator.sum(2, 4)
|
|
assertEquals(6, sum)
|
|
}
|
|
|
|
@Test
|
|
fun `should return the result of subtracting the second number from the first number`() {
|
|
val subtract = calculator.subtract(4, 2)
|
|
assertEquals(2, subtract)
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
[[kotlin-webtestclient-issue]]
|
|
=== `WebTestClient` Type Inference Issue in Kotlin
|
|
|
|
Due to a https://youtrack.jetbrains.com/issue/KT-5464[type inference issue], you must
|
|
use the Kotlin `expectBody` extension (such as `.expectBody<String>().isEqualTo("toys")`),
|
|
since it provides a workaround for the Kotlin issue with the Java API.
|
|
|
|
See also the related https://jira.spring.io/browse/SPR-16057[SPR-16057] issue.
|
|
|
|
|
|
|
|
|