Pull to refresh

Пишем простой RESTful сервис на kotlin и spring boot

Reading time 13 min
Views 32K
image

Введение


В преддверии выхода языка Kotlin из beta, хочется поделиться своим впечатлением от его использования.

Kotlin — это новый прекрасный язык от JetBrains (разработчиков IntelliJ Idea) для JVM, Android и браузера, который на первый взгляд выглядит как улучшенная java (или как упрощенная scala). Но это только на первый взгляд, язык не только впитал в себя многие интересные решения от других языков, но и представляет свои оригинальные:

— optional от swift, nullsafe в kotlin
— case классы из scala, data class в kotlin
— замена implicit методам из scala, extension функций
делегаты
null safely
smart cast
— и многое другое, подробнее можно посмотреть на официальном сайте kotlinlang.

Для тех кто знаком с java или scala, будет интересно сравнение kotlin & java, kotlin & scala.

Авторы языка стремятся добиться двух задач:
— сделать скорость компиляции сравнимой с java
— язык должен быть достаточно выразительным, и при этом быть простым насколько возможно
Поэтому, стоит оговориться, что если вы на текущей момент счастливы со scala, с ее «сложностью» и временем компиляции, тогда вам скорее всего не нужен будет kotlin, для всех остальных читать дальше:

Для тех кто в танке впервые слышит о языке, ниже несколько примеров с официального сайта:

Hello world
package hello

fun main(args: Array<String>) {
   println("Hello World!")
}


Чтение аргументов
fun main(args: Array<String>) {
   if (args.size() == 0) {
      println("Provide a name")
      return
   }
   println("Hello, ${args[0]}!")
}


hello world c ООП
class Greeter(val name: String) { 
   fun greet() { 
      println("Hello, $name")
   }
}

fun main(args: Array<String>) {
   Greeter(args[0]).greet()
}  


Из личного опыта применения kotlin особо хочется отметить несколько преимуществ языка:

— первое это конечно простоту взаимодействия с java. Все типы и коллекции из java преобразовываются в аналогичные из kotlin, и наоборот. Это особенно радует после всей той «анархии», которая творится в scala (да есть scala.collection.JavaConversions._ и scala.collection.JavaConverters._, но все же это не сравниться с полностью прозрачной конвертацией типов);
— также не может не радовать отличная поддержка от студии Intellij Idea, хоть язык и находится в Beta 4, уже на текущий момент плагин для студии позволяет комфортно работать;
— а для любителей implicit методов из scala, kotlin преподносит очень удобное решение в виде extension функций;
— помимо всего прочего разработчики языка ставят своей целью добиться времени компиляции сравнимой с java (привет scala), за что им только хочется пожать руки! Это особенно сильно радует после долгой работы в scala, когда редактирование одной строчки в достаточно небольшом файле компилируется с той же скоростью что и небольшой проект на java;
inline функции — отличное нововведение. С их помощью можно, например, расширить текущие возможности языка, или в некоторых ситуациях добиться повышения производительности;
— удобные функции стандартной библиотеки.
— удобные лямбды, в отличие от той же java 8. Очень похожи на реализацию из scala.

Тем не менее у языка есть и свои недостатки:

— не хватает pattern matching из scala, но в некоторых ситуациях спасает smart cast и Destructuring Declarations, в других же приходится выкручиваться другими средствами. Отсутствие pattern matching в целом понятно, разработчики стараются добиться максимального приближения к времени компиляции java, но его наличие позволило бы существенно упростить написание некоторых приложений, так что довольствуемся тем что есть;
try with resource пока реализован не очень удачно. Но тут авторы языка обещают в ближайшее время исправить ситуацию. А пока можно либо применять имеющееся решение, либо воспользоваться расширением языка:

try-with-resources
internal class ResourceHolder : AutoCloseable {
    val resources = ArrayList<AutoCloseable>()

    fun <T : AutoCloseable> T.autoClose(): T {
        resources.add(this)
        return this
    }

    override fun close() {
        resources.reverse()
        resources.forEach {
            try {
                it.close()
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
    }
}

inline internal fun <R> using(block: ResourceHolder.() -> R): R {
    val holder = ResourceHolder()
    try {
        return holder.block()
    } finally {
        holder.close()
    }
}


Пример использования
fun copy(from: Path, to: Path) {
    using {
        val input = Files.newInputStream(from).autoClose()
        val output = Files.newOutputStream(to).autoClose()
        input.copyTo(output)
    }
}


— пока нет async и yield, но по словам авторов, после релиза 1.0 можно ждать их появление в самом ближайшем будущем.

Update В комментариях наибольший интерес вызывает сравнение со scala, и в частности преимущество kotlin:
Преимущества kotlin по сравнению со scala
— взаимодействие с java и обратно. На kotlin вообще можно забыть про то что ты вызываешь что-то из java, все предельно прозрачно и удобно. Тоже самое наоборот. Из java также удобно работать с kotlin. Тут даже не только сходство типов помогает, сколько сама ориентированность kotlin на прозрачную работу с java. Проще всего это продемонстрировать на примерах:
Примеры
1. Использование лямбд вместо анонимных классов (в scala правда тоже скоро это может появиться, но пока такой возможности нет).
Код в java:
ExecutorService executor = Executors.newFixedThreadPool(1);
    executor.execute(System.out::println);


В scala же уже не получится такое провернуть. А вот с kotlin проблем нет:
val executor = Executors.newFixedThreadPool(1)
    executor.execute { println() }


2. Есть тот же try-with-resources (да, можно сделать extension к языку, но из коробки этого нет, а значит придется тянуть решение из одного проекта в другой)
Это по крайней мере что первое приходит в голову

— отсутствие implicit. Сами по себе implicit конечно мощное решение, но без студии разобраться в чужом коде что к чем, почти нереально. Особенно если авторы «сильно увлеклись». А для добавления методов kotlin позволяет писать extension, которые на практике выглядят гораздо удобнее аналогичных решений в scala. Пример:
Scala:
implicit class RichInt(val x: Int) extends AnyVal {
  def square: Int = x * x
}
object App  {
   def print(): Unit = {
      val two = 2
      println(s"The square of 2 is ${two.square}")
  }
}

Kotlin:
fun Int.richInt(): Int = this * this

object App {
    fun print(): Unit {
        val two = 2
        println("The square of 2 is ${two.richInt()}")
    }
}


— nullable типы вместо Option.

— удобные функции стандартной библиотеки

— ну и конечно же скорость компиляции. На практике действительно все очень быстро компилируется, особенно по сравнению со scala.

— результирующий jar меньше аналогичного в scala.

— и что немаловажно, JetBrains обещают полную обратную совместимость (привет еще раз scala c либами под 2.10, 2.11, 2.12 и т.д.)


Перейдем к примеру, в котором будет продемонстрировано небольшое RESTful приложение на spring boot, со сборкой через gradle.

Настройка студии


Для работы необходимо поставить IntelliJ Idea Community (но можно использовать и Eclipse, под нее также есть плагин), в которой после установки обновить плагин kotlin. Обновить его необходимо вручную, через settings -> plugin, даже если вы перед этим выбрали обновление плагина через всплывающее окно (по крайней мере на данный момент, пока язык в beta).

Также лучше поставить локальный gradle, и прописать его в настройках в студии (settings -> build, execution, deployment -> gradle -> user local gradle distribution. После чего указать путь к gradle в gradle home).

Настройка проекта


Создаем проект gradle kotlin (new project -> gradle -> kotlin) и изменяем содержимое build.gradle на следующее:

Содержимое build.gradle
buildscript {
    ext.kotlin_version = '1.0.0-beta-4584'
    repositories {
        mavenCentral()
        maven { url "http://repo.spring.io/snapshot" }
        maven { url "http://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.0.RELEASE")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
    }
}

apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'kotlin'

jar {
    baseName = 'test-spring-kotlin-project'
    version = '0.1.0'
}

repositories {
    mavenCentral()
    maven { url "http://repo.spring.io/snapshot" }
    maven { url "http://repo.spring.io/milestone" }

    maven { url "http://10.10.10.67:8081/nexus/content/groups/public" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.3.0.RELEASE")
    compile("org.springframework:spring-jdbc:4.2.3.RELEASE")
    compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.6.4")

    compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
}


Создаем файл application.properties в папке src/main/resources, в котором укажем порт для запуска spring boot:

application.properties
server.port = 8080


Создаем файл Application.kt в папке src/main/kotlin/test.kotlin.spring.project. В нем будут основные настройки для запуска spring boot:

Application.kt
package test.kotlin.spring.project

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.context.web.SpringBootServletInitializer
import org.springframework.context.annotation.Bean
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

@SpringBootApplication
@EnableAutoConfiguration(exclude = arrayOf(DataSourceAutoConfiguration::class))
open class Application : SpringBootServletInitializer() {

    @Bean
    open fun mapperForKotlinTypes(): MappingJackson2HttpMessageConverter {
        return MappingJackson2HttpMessageConverter().apply { objectMapper = jacksonMapper }
    }

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder =
            application.sources(Application::class.java)

    companion object {

        val jacksonMapper = ObjectMapper().registerKotlinModule()
                .setSerializationInclusion(JsonInclude.Include.NON_ABSENT)
                .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)

        @Throws(Exception::class)
        @JvmStatic fun main(args: Array<String>) {
            println("starting application...")
            SpringApplication.run(Application::class.java, *args)
        }
    }
}



Также необходимо будет создать файл с настройками методов rest сервиса. Будет несколько методов:

— метод будет выдавать AckResponse на введенные с запроса данные об имени и фамилии.
— метод, на вход поступает массив строк, из которого выбирается наименьшая строка по длине, которая потом разбивается по '_', сортируется и собирается в строку уже с символом ',' (демонстрирует возможности языка)

Создаем файл ServiceController.kt в папке src/main/kotlin/test.kotlin.spring.project.

ServiceController.kt
package test.kotlin.spring.project

import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

data class AckResponse(val status: Boolean, val result: String, val message: String? = null)

@RestController
class ServiceController {
    @RequestMapping(
            path = arrayOf("/request"),
            method = arrayOf(RequestMethod.GET),
            produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))
    fun nameRequest(
            @RequestParam(value = "name") name: String,
            @RequestParam(value = "surname", required = false) surname: String?): AckResponse {
        return if (surname == null)
            AckResponse(status = true, result = "Hi $name", message = "surname is empty")
        else
            AckResponse(status = true, result = "Hi $surname,$name")
    }

    @RequestMapping(
            path = arrayOf("/sort_request"),
            method = arrayOf(RequestMethod.GET),
            produces = arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE))
    fun findMinimum(
            @RequestParam(value = "values") values: Array<String>): AckResponse {
        println("values:")
        values.forEach { println(it) }

        val minValue = values.apply { sortBy { it.length } }
            .firstOrNull()
            ?.split("_")
            ?.sorted()
            ?.joinToString(",") ?: ""

        return AckResponse(status = true, result = minValue)
    }
}



Запуск и проверка работы


Запускаем приложение из Application.kt. В случае успешного запуска в логе будет что-то вроде:

Логи приложения
starting application...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.3.0.RELEASE)

2016-01-12 12:47:48.242  INFO 88 --- [           main] t.k.s.project.Application$Companion      : Starting Application.Companion on Lenovo-PC with PID 88 (D:\IDA_Projects\test\build\classes\main started by admin in D:\IDA_Projects\test)
2016-01-12 12:47:48.247  INFO 88 --- [           main] t.k.s.project.Application$Companion      : No profiles are active
2016-01-12 12:47:48.413  INFO 88 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@dbf57b3: startup date [Tue Jan 12 12:47:48 MSK 2016]; root of context hierarchy
2016-01-12 12:47:50.522  INFO 88 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2016-01-12 12:47:51.066  INFO 88 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$ede1977c] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2016-01-12 12:47:51.902  INFO 88 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-01-12 12:47:51.930  INFO 88 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-01-12 12:47:51.937  INFO 88 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.28
2016-01-12 12:47:52.095  INFO 88 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-01-12 12:47:52.095  INFO 88 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 3688 ms
2016-01-12 12:47:52.546  INFO 88 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2016-01-12 12:47:52.556  INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-01-12 12:47:52.557  INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-01-12 12:47:52.559  INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-01-12 12:47:52.559  INFO 88 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'requestContextFilter' to: [/*]
2016-01-12 12:47:52.985  INFO 88 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@dbf57b3: startup date [Tue Jan 12 12:47:48 MSK 2016]; root of context hierarchy
2016-01-12 12:47:53.089  INFO 88 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/request],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public final test.kotlin.spring.project.AckResponse test.kotlin.spring.project.ServiceController.pullUpdate(java.lang.String,java.lang.String)
2016-01-12 12:47:53.094  INFO 88 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2016-01-12 12:47:53.094  INFO 88 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2016-01-12 12:47:53.138  INFO 88 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-12 12:47:53.139  INFO 88 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-12 12:47:53.195  INFO 88 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-01-12 12:47:53.512  INFO 88 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-01-12 12:47:53.612  INFO 88 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-01-12 12:47:53.620  INFO 88 --- [           main] t.k.s.project.Application$Companion      : Started Application.Companion in 6.076 seconds (JVM running for 7.177)
2016-01-12 12:47:57.874  INFO 88 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-01-12 12:47:57.874  INFO 88 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-01-12 12:47:57.897  INFO 88 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 23 ms


После успешного запуска, пробуем открыть страницу Запрос с именем. Ответ должен выглядеть следующем образом:
{
  status: true,
  result: "Hi Kris",
  message: "surname is empty"
}

И Запрос с именем и фамилией, тогда ответ будет немного другой:

{
  status: true,
  result: "Hi Eagle, Kris"
}

Вызов для проверки сортировки данных: Сортировка. В результате должно быть:

{
  status: true,
  result: "1,3,value,virst"
}

Тот же вызов, но с пустым массивом: Вызов
{
  status: true,
  result: ""
}

При необходимости можно собрать весь проект в один runnable jar, командой: gradle build. В результате проект будет собран в один архив, содержащий все зависимости без распаковки. При таком подходе существенно повышается время сборки проекта, по сравнению с тем же assemble, когда проект собирается в один архив с распаковкой всех зависимостей.

Заключение


В заключении хочется отметить что kotlin оказался весьма удобным языком для работы над любым проектом, где используется java, в качестве ее замены. Экосистема языка пока не такая обширная как та же scala, но уже сейчас можно использовать его в том же big data, где есть java api. К тому же из kotlin очень просто взаимодействовать с java, так что все что есть в java, можно использовать и в kotlin. К тому же из студии есть возможность легкой конвертации java файлов в аналогичные на kotlin (правда нужно будет немного руками подправить файл после конвертации). JetBrains проделали замечательную работу на пути создания идеального языка на смену java и scala. И надеюсь в будущем тенденция в сторону использования kotlin будет только расти.

Исходники доступны на github.

Спасибо всем за внимание.
Tags:
Hubs:
+10
Comments 31
Comments Comments 31

Articles