You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
358 lines
13 KiB
358 lines
13 KiB
[[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 xref:testing/testcontext-framework/support-classes.adoc#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. |
|
|
|
|
|
|
|
|
|
|