From ed6a35b46510e20708e1082bcd3396c612605c27 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 29 Aug 2017 00:57:07 +0200 Subject: [PATCH] Add API and reference documentation for Kotlin support Issue: SPR-15659 --- build.gradle | 1 + gradle/docs.gradle | 36 ++- src/docs/asciidoc/index.adoc | 2 + src/docs/asciidoc/kotlin.adoc | 427 ++++++++++++++++++++++++++++++++++ 4 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 src/docs/asciidoc/kotlin.adoc diff --git a/build.gradle b/build.gradle index d4486b67c6..a78788836c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ plugins { id "com.gradle.build-scan" version "1.8" id "io.spring.dependency-management" version "1.0.3.RELEASE" apply false id "org.jetbrains.kotlin.jvm" version "1.1.4" apply false + id "org.jetbrains.dokka" version "0.9.15" id "org.asciidoctor.convert" version "1.5.3" } diff --git a/gradle/docs.gradle b/gradle/docs.gradle index cad51f9a61..6ff26b4c24 100644 --- a/gradle/docs.gradle +++ b/gradle/docs.gradle @@ -49,6 +49,36 @@ task api(type: Javadoc) { } } +// Need https://github.com/Kotlin/dokka/issues/184 to be fixed to avoid "Can't find node by signature" log spam +dokka { + dependsOn { + subprojects.collect { + it.tasks.getByName("jar") + } + } + doFirst { + classpath = subprojects.collect { project -> project.jar.outputs.files.getFiles() }.flatten() + classpath += files(subprojects.collect { it.sourceSets.main.compileClasspath }) + + } + moduleName = "spring-framework" + outputFormat = "html" + outputDirectory = "$buildDir/docs/kdoc" + + sourceDirs = files(subprojects.collect { project -> + project.sourceSets.main.kotlin.srcDirs + }) + externalDocumentationLink { + url = new URL("http://docs.spring.io/spring-framework/docs/${version}/javadoc-api/") + } + externalDocumentationLink { + url = new URL("http://projectreactor.io/docs/core/release/api/") + } + externalDocumentationLink { + url = new URL("http://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/") + } +} + asciidoctor { sources { include '*.adoc' @@ -74,7 +104,7 @@ asciidoctor { } -task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor']) { +task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'dokka']) { group = "Distribution" baseName = "spring-framework" classifier = "docs" @@ -92,6 +122,10 @@ task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor']) { from (asciidoctor) { into "spring-framework-reference" } + + from (dokka) { + into "kdoc-api" + } } task schemaZip(type: Zip) { diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 9a5dbcbc6f..e332d1ec82 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -33,6 +33,8 @@ This reference document provides the following sections: ** <> ** <> +* <> + * <> * <> \ No newline at end of file diff --git a/src/docs/asciidoc/kotlin.adoc b/src/docs/asciidoc/kotlin.adoc new file mode 100644 index 0000000000..56cddf701a --- /dev/null +++ b/src/docs/asciidoc/kotlin.adoc @@ -0,0 +1,427 @@ +[[kotlin]] += Kotlin support +:doc-root: https://docs.spring.io +:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework +:toc: left +:toclevels: 2 + +== Introduction + +https://kotlinlang.org[Kotlin] is a statically-typed language targeting the JVM which allows to write concise and elegant +code while providing a very good https://kotlinlang.org/docs/reference/java-interop.html[interoperability] with libraries +written in Java. + +Spring Framework 5 introduces first-class support for Kotlin in order to allow developers to write Spring + Kotlin +application almost like if Spring Framework was a native Kotlin framework. + +== Requirements == + +Spring Framework 5 supports Kotlin 1.1+ and requires both `kotlin-stdlib` (or one of its variants +`kotlin-stdlib-jre7` or `kotlin-stdlib-jre8`) and `kotlin-reflects` to be present on the classpath. +They are provided by default if you bootstrap a Kotlin project on +https://start.spring.io/#!language=kotlin[start.spring.io]. + +== Kotlin extensions for Spring API + +Thanks to its great https://kotlinlang.org/docs/reference/java-interop.html[Java interoperability] +and https://kotlinlang.org/docs/reference/extensions.html[Kotlin extensions], Spring +Framework 5 Kotlin API is leveraging the Java's one, completed by a few Kotlin specific API +available out of the box from Spring Framework JARs. + +{doc-root}/spring-framework/docs/{spring-version}/kdoc-api/[Spring Framework KDoc API] lists +and documents all Kotlin extensions and DSL available. + +[NOTE] +==== +Keep in mind that Kotlin extensions need to be imported to be use. That means for example that +`GenericApplicationContext.registerBean` Kotlin extensions will be available only if you write +`import org.springframework.context.support.registerBean`. That said, like with static imports, +IDEs should automatically suggest them in most cases. +==== + +For example, https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters[Kotlin reified type parameters] +provide a workaround for JVM https://docs.oracle.com/javase/tutorial/java/generics/erasure.html[generics type erasure], +so Spring Framework 5 introduces some extensions to take advantage of this feature to provide a better API when possible. +That allows to provide convenient API for `RestTemplate`, for the new `WebClient` from Spring WebFlux +and for various other API. + +To retrieve a list of `Foo` objects in Java you have to write: + +[source,java] +---- +Flux users = client.get().retrieve().bodyToFlux(User.class) +---- + +While in Kotlin with Spring Framework extensions, you are able to write: + +[source,kotlin] +---- +val users = client.get().retrieve().bodyToFlux() +// or (both are equivalent) +val users : Flux = client.get().retrieve().bodyToFlux() +---- + +Like in Java, `users` in Kotlin is strongly typed but Kotlin clever type inference allows shorter syntax. + + +[NOTE] +==== +Other libraries like Reactor or Spring Data also provide Kotlin extensions for their API +in order to allow a better Kotlin development experience. +==== + +== Null-safety of Spring API + +One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety] which allows to deal with +`null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes your applications +safer through clean nullability declarations, expressing "value or no value" semantics without paying the cost of wrapper like `Optional`. +(Kotlin allows using functional constructs with nullable values; check out this +http://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].) + +Although Java does not allow to express null-safety in its type-system, Spring Framework 5 introduces +https://jira.spring.io/browse/SPR-15540[null-safety of the whole Spring Framework APIs] via tooling-friendly annotations: + * `@NonNullApi` annotations at package level declare that non-null is the default behavior + * `@Nullable` annotations where specific parameters or return values can be `null`. + +Both annotations are meta-annotated with https://jcp.org/en/jsr/detail?id=305[JSR 305] meta-annotations (a dormant JSR but supported by tools +like IDEA, Eclipse, Findbugs, etc.) to provide useful warnings to Java developers. + +On the Kotlin side - as of the https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-1-4-is-out/[Kotlin 1.1.4 release] - +these annotations https://github.com/Kotlin/KEEP/blob/jsr-305/proposals/jsr-305-custom-nullability-qualifiers.md[are recognized by Kotlin] +in order to provide null-safety for the whole Spring API. That means you should never have `NullPointerException` in your code when +using Spring 5 and Kotlin because the compiler will not allow it. + +For now, you need to use a `-Xjsr305-annotations=enable` flag (specified via the `freeCompilerArgs` peroperty with Maven or Gradle Kotlin +plugin), but that will became the default behavior in an upcoming release of Kotlin. + +Make sure to https://github.com/sdeleuze/spring-kotlin-functional/blob/2d6ac07adfc2b8f25e91681dbb2b58a1c6cdf9a7/build.gradle.kts#L57[include JSR-305 JAR] +until Kotlin 1.1.5 is released (it will include https://youtrack.jetbrains.com/issue/KT-19419[KT-19419] fix). + +Currently null-safety does not apply to generic type parameters, but that could change in the future, the related issue is +https://youtrack.jetbrains.com/issue/KT-19592[KT-19592]. + +[NOTE] +==== +Other libraries like Reactor or Spring Data leverage these annotations to provide +null-safe APIs for Kotlin developers. +==== + +== Support for Kotlin classes + +Spring Framework 5 now supports various Kotlin constructs like instantiating Kotlin classes +via primary constructors, immutable classes data binding and optional parameters with default values. + +== Leveraging Kotlin nullable information in Spring annotations + +Spring Framework also takes advantage of https://kotlinlang.org/docs/reference/null-safety.html[Kotlin null-safety support] +to determine if an HTTP parameter is required without having to define explicitly the `required` attribute. +That means `@RequestParam name: String?` with be treated as not required and `@RequestParam name: String` as required. + +This is also supported on Spring Messaging `@Header` annotation. + +In a similar fashion, Spring bean injection with `@Autowired` or `@Inject` uses this information to know if a bean is required or not. +`@Autowired lateinit var foo: Foo` implies that a bean of type `Foo` must be registered in the application context while +`@Autowired lateinit var foo: Foo?` won’t raise an error if such bean does not exist. + +== Spring WebFlux functional DSL + +Spring Framework 5.0 comes with a {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-router-function-dsl/[Kotlin routing DSL] that allows you to leverage the +< new + Bar(context.getBean(Foo.class)) +); +---- + +While in Kotlin, reified type parameters and `GenericApplicationContext` Kotlin extensions allows to simply write: + +[source,kotlin] +---- +val context = GenericApplicationContext().apply { + registerBean() + registerBean { Bar(it.getBean()) } +} +---- + +A {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.context.support/-bean-definition-dsl/[dedicated DSL] +is provided in order to allow an fully idiomatic syntax. It conceptually declares a +`Consumer<GenericApplicationContext>` via a clean declarative API which allows you +to deal with profile and `Environment` for customizing how your beans are registered. + +[source,kotlin] +---- +beans { + bean() + bean { + Routes(ref(), ref()) + } + bean("webHandler") { + RouterFunctions.toWebHandler( + ref().router(), + HandlerStrategies.builder().viewResolver(ref()).build() + ) + } + bean("messageSource") { + ReloadableResourceBundleMessageSource().apply { + setBasename("messages") + setDefaultEncoding("UTF-8") + } + } + bean { + val prefix = "classpath:/templates/" + val suffix = ".mustache" + val loader = MustacheResourceTemplateLoader(prefix, suffix) + MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply { + setPrefix(prefix) + setSuffix(suffix) + } + } + profile("foo") { + bean() + } +} +---- + +`Routes(ref(), ref())` is the equivalent of `Routes(ref<UserHandler>(), ref<MessageSource>())` +(types are not required thanks to Kotlin type inference) where `ref<UserHandler>()` +is a shortcut for `applicationContext.getBean(UserHandler::class.java)`. + +[NOTE] +==== +This DSL is programmatic, thus also allows custom registration logic of beans via `if` expression, +`for` loop or any other Kotlin constructs. +==== + +== Kotlin Script based templates + +As of version 4.3, Spring Framework provides a +http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html[ScriptTemplateView] +to render templates using script engines that supports https://www.jcp.org/en/jsr/detail?id=223[JSR-223] +and Spring Framework 5.0 go even further by extending this feature to WebFlux and supporting +https://jira.spring.io/browse/SPR-15064[i18n and nested templates]. + +Kotlin 1.1 provides such support and allows to render Kotlin based templates, see +https://github.com/spring-projects/spring-framework/commit/badde3a479a53e1dd0777dd1bd5b55cb1021cf9e[this commit] for details. + +This enables some interesting use cases like writing type-safe templates using +https://github.com/Kotlin/kotlinx.html[kotlinx.html] DSL or simply Kotlin multiline `String` with interpolation, +see https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] project for more details. + +This can allow you to write this kind of templates with full autocompletion and refactoring support in your IDE: + +[source,kotlin] +---- +import io.spring.demo.* + +""" +${include("header")} +

${i18n("title")}

+
    + ${users.joinToLine{ "
  • ${i18n("user")} ${it.firstname} ${it.lastname}
  • " }} +
+${include("footer")} +""" +---- + +[NOTE] +==== +This feature is still experimental since it requires caching to reach production-level +performances, subscribe to https://github.com/sdeleuze/kotlin-script-templating/issues/5[this issue] +to follow progresses. +==== + +== Getting started + +The easiest way to start a new Spring Framework 5 project in Kotlin is to create a new Spring +Boot 2 project on https://start.spring.io/#!language=kotlin[start.spring.io]. + +It is also possible to create a standalone WebFlux project as described in +https://spring.io/blog/2017/08/01/spring-framework-5-kotlin-apis-the-functional-way[this blog post]. + +=== Choose you web flavor + +Spring Framework now comes with 2 different web stacks: Spring MVC and WebFlux. + +Spring WebFlux is recommended if you want to create applications that will deal with latency, +long-lived connections, streaming scenarios or simply if you want to use the web functional +Kotlin DSL. + +For other use cases, Spring MVC and its annotation-based programming model is a perfectly +valid and fully supported choice. + +=== Classes and member functions final by default + +By default, https://discuss.kotlinlang.org/t/classes-final-by-default/166[call classes in Kotlin are `final`]. +The `open` annotation on a class is the opposite of Java's `final`: it allows others to +inherit from this class. Same for member functions that need to be open to be overridden. + +While Kotlin JVM-friendly design is generally a good fit with Spring, this specific point +can prevent your application to start if not taken in account because Spring beans proxified +with CGLIB - like `@Configuration` classes - need to be inherited at runtime for technical +reasons. + +Before Kotlin 1.0.6, you needed to add an `open` keyword on each class and their member +functions of Spring beans proxified with CGLIB like `@Configuration` classes. + +Fortunately, Kotlin 1.0.6+ now provides a +https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin[`kotlin-spring`] +plugin that open classes and their member functions by default for classes annotated or meta-annotated with one of the following annotation: + +* `@Component` +* `@Async` +* `@Transactional` +* `@Cacheable` + +Meta-annotations support means that classes annotated with`@Configuration`, `@Controller`, +`@RestController`, `@Service` or `@Repository` are automatically opened since these +annotations are meta-annotated with `@Component`. + +http://start.spring.io/#!language=kotlin[start.spring.io] enables it by default. + +=== What is the recommended way to inject dependencies in Kotlin? + +Try to favor constructor injection with `val` properties. As of Spring Framework 4.3, you +just have to write `class MessageController(val repository: MessageService)` and Spring will automatically +autowire the constructor. + +If you really need to use field injection, use `lateinit var`: + +[source,kotlin] +---- +@Component +class YourBean { + + @Autowired + lateinit var mongoTemplate: MongoTemplate + + @Autowired + lateinit var solrClient: SolrClient +} +---- + +=== Easy testing Kotlin and JUnit 5 + +Kotlin allows to specify meaningful test function names betweeen backticks, +and as of JUnit 5.0 Kotlin test classes can use `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` +to enable a single instantiation of test classes which allows to use `@BeforeAll` and `@AfterAll` +annotations on non-static methods, which is a good fit for Kotlin. + +It is also now possible to change the default behavior to `PER_CLASS` thanks to a +`junit-platform.properties` file with a +`junit.jupiter.testinstance.lifecycle.default = per_class` property. + +[source] +---- +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() + .test() + .expectNextMatches { it.contains("Foo") } + .verifyComplete() + } + + @AfterAll + fun afterAll() { + application.stop() + } +} +---- + + +=== Resources + +=== Blog posts + +* https://spring.io/blog/2016/02/15/developing-spring-boot-applications-with-kotlin[Developing Spring Boot applications with Kotlin] +* https://spring.io/blog/2016/03/20/a-geospatial-messenger-with-kotlin-spring-boot-and-postgresql[A Geospatial Messenger with Kotlin, Spring Boot and PostgreSQL] +* https://spring.io/blog/2017/01/04/introducing-kotlin-support-in-spring-framework-5-0[Introducing Kotlin support in Spring Framework 5.0] +* https://spring.io/blog/2017/08/01/spring-framework-5-kotlin-apis-the-functional-way[Spring Framework 5 Kotlin APIs, the functional way] + +=== Examples + +* https://github.com/sdeleuze/spring-boot-kotlin-demo[spring-boot-kotlin-demo]: regular Spring Boot + Spring Data JPA project +* https://github.com/mixitconf/mixit[mixit]: Spring Boot 2 + WebFlux + Reactive Spring Data MongoDB +* https://github.com/sdeleuze/spring-kotlin-functional[spring-kotlin-functional]: standalone WebFlux + functional bean declaration DSL + +=== Tutorials + +* https://kotlinlang.org/docs/tutorials/spring-boot-restful.html[Creating a RESTful Web Service with Spring Boot] + +=== Pending issues to follow + +==== Spring Framework + +* https://jira.spring.io/browse/SPR-15541[Leveraging kotlin-reflect to determine interface method parameters] +* https://jira.spring.io/browse/SPR-15413[Add support for Kotlin coroutines] + + +==== Spring Boot + +* https://github.com/spring-projects/spring-boot/issues/5537[Improve Kotlin support] +* https://github.com/spring-projects/spring-boot/issues/8762[Allow @ConfigurationProperties binding for immutable POJOs] +* https://github.com/spring-projects/spring-boot/issues/8511[Provide support for Kotlin KClass parameter in `SpringApplication.run()`] + +==== Kotlin + +* https://youtrack.jetbrains.com/issue/KT-6380[Parent issue for Spring Framework support] +* https://youtrack.jetbrains.com/issue/KT-15667[Support "::foo" as a short-hand syntax for bound callable reference to "this::foo"] +* https://youtrack.jetbrains.com/issue/KT-11235[Allow specifying array annotation attribute single value without arrayOf()] +* https://youtrack.jetbrains.com/issue/KT-5464[Kotlin requires type inference where Java doesn't] +* https://youtrack.jetbrains.com/issue/KT-14984[Impossible to pass not all SAM argument as function] +* https://youtrack.jetbrains.com/issue/KT-19592[Apply JSR 305 meta-annotations to generic type parameters] +* https://youtrack.jetbrains.com/issue/KT-18398[Provide a way for libraries to avoid mixing Kotlin 1.0 and 1.1 dependencies] +* https://youtrack.jetbrains.com/issue/KT-15125[Support JSR 223 bindings directly via script variables]