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

от автора

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.

Однако, стоит оговориться, что если вы на текущей момент счастливы со 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 можно ждать их появление в самом ближайшем будущем.

Перейдем к примеру, в котором будет продемонстрировано небольшое 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)         }     } } 

mapperForKotlinTypes нужен для того чтобы подключить к jackson mapping для kotlin. Получается похожий аналог симбиоза scala и argonaut.

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

Также необходимо будет создать файл с настройками методов 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.

Спасибо всем за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/274997/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *