Kotlin - лаконичный язык
Одна из ключевых особенностей языка Kotlin - значительное сокращение шаблонного кода (boilerplate code). Если, к примеру при создании простого класса в Java мы явно объявляем поля, конструктор и геттеры этого класса, то в Kotlin все намного проще. Рассмотрим класс, который обозначает абстрактного Клиента. Класс имеет следующие поля: Name, Email и Company. Вот как выглядит код, написанный на языке Java:
public class Customer{
String name;
String email;
String company;
//constructor to initialise fields
public Customer(String name, String email, String company) {
this.name = name;
this.email = email;
this.company = company;
}
//getter method for name
public String getName() {
return name;
}
//getter method for email
public String getEmail() {
return email;
}
//getter method for company
public String getCompany() {
return company;
}
}
А теперь посмотрим на этот же класс в Kotlin:
class Customer(val name: String, val email: String, val company: String)
Как можно заметить в коде на Kotlin отсутствует множество шаблонного кода, предназначенного лишь для того, чтобы реализовать базовые действия над полями класса. Все эти действия в Kotlin работают из коробки, хотя и не указываются явно как в Java. Как итог, код становится лаконичнее, не теряя при этом своей функциональности.
Kotlin - безопасный язык
Нулевые ссылки (null references) являются наиболее распространенной причиной исключений (exception) в приложениях на Java. Нам всем знакомо досадное исключение NullPointerException (NPE). Держа в уме эту проблему, разработчики Kotlin сделали многое, чтобы ее решить.
В Kotlin по умолчанию переменной не может быть присвоено нулевое значение — это первая линия защиты от NPE. Используя оператор nullable '?', переменная может получить нулевое значение, но для доступа к атрибутам переменной необходимо использовать оператор null-safety '?'. Если переменная имеет нулевое значение, она будет рассматриваться как строка. Единственный способ получить NPE в Kotlin - использовать оператор not-null '!!', который вызовет исключение KotlinNullPointerException.
Вот несколько примеров:
//Нулевая ссылка
var a: String = "abc"
a = null //ошибка компиляции
//вывод: Null cannot be a value of a non-null type String
//Оператор nullable ?
var a: String? = "abc"
a = null //ok
print(a)
//вывод: null
var a: String? = "abc"
a = null //ok
print(a.length) //ошибка компиляции
//вывод: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
//Оператор null-safety ?.
var a: String? = "abc"
a = null
print(a?.length) //ok
//вывод: null
//Оператор not-null !!
var a: String? = "abc"
a = null
print(a!!.length)
/*вывод: Exception in thread "main" kotlin.KotlinNullPointerException
at …………
*/
Kotlin - функционально совместимый язык
И Java, и Kotlin компилируются в один и тот же байткод. Благодаря этому возможна функциональная совместимость. Это означает, что Kotlin может вызывать и выполнять код Java. Интероперабельность Kotlin делает его совместимым с существующими библиотеками JVM, Android и браузерами.
Kotlin и Spring
Spring официально поддерживает Kotlin, начиная со Spring Framework 5.0. Интересной особенностью является то, что разработчики Spring пытаются сделать свой фреймворк null-safe в части, поддерживающей Kotlin, и стремятся позволить разработчикам исправлять ошибки с нулевыми значениями еще на этапе компиляции и не допускать NullPointerExceptions, которые могут возникнуть во время выполнения.
Для того, чтобы начать разрабатывать в связке Spring + Kotlin, рекомендуется зайти на сайт https://start.spring.io. На этом сайте можно инициализировать новый проект Spring. Важно при этом выбрать язык Kotlin. Также необходимо выбрать инструмент сборки проекта. Доступны два инструмента - Gradle и Maven. Обычно для проектов на Kotlin рекомендуется использовать Gradle, так как он хорошо поддерживает Kotlin DSL - язык по умолчанию при генерации проекта с Kotlin. Однако можно использовать и Maven, если он вам более удобен.
Пример реализации контроллера на Kotlin
Часто веб-приложения построены по архитектуре, в которой применяется понятие Контроллера (например Model-View-Controller, MVC).
Давайте рассмотрим, как можно реализовать типовой контроллер в связке Spring + Kotlin.
Предположим, мы создали простенькую view (blog.html):
<html>
<head></head>
<body>
<h1>{{title}}</h1>
</body>
</html>
Теперь напишем работающий с этой view контроллер:
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation.GetMapping
@Controller
class HtmlController {
@GetMapping("/blog")
fun showBlogSite(model: Model): String {
model["title"] = "Blog"
return "blog"
}
}
И так, когда мы попытаемся получить доступ к пути /blog с помощью GET-запроса, сработает наш контроллер. Сначала он добавит атрибут title со значением «Blog» к модели, а затем возвратит нашу view «blog.html», заполненную в согласии с этой моделью.
Бывает, что вместо того, чтобы возвратить view целиком или частично, необходимо просто вернуть сериализованные данные. В таком случае используется REST-контроллер. Рассмотрим пример:
@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository) {
@GetMapping("/")
fun findAll() = repository.findAllByOrderByAddedAtDesc()
@GetMapping("/{id}")
fun findOne(@PathVariable id: UUID) =
repository.findById(id) ?: ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
}
В этом примере, мы создали контроллер «ArticleController». Контроллер взаимодействует с репозиторием данных «ArticleRepository» (доступ передается через конструктор контроллера).
Репозиторий напрямую связан с хранилищем данных (это может быть, например реляционная база данных или NoSQL источник). Если мы попытаемся получить доступ к пути «/» с помощью GET запроса, контроллер возвратит нам из репозитория все объекты типа «Article», упорядоченные по дате добавления в порядке убывания. Так как мы явно не указали тип возвращаемых данных, метод «findAll» возвратит данные из репозитория, в том виде (и того типа) в котором они там хранятся.
Во второй конечной точке (endpoint) с адресом «GET /{id}», используется elvis-оператор «?:». Это означает что, если метод репозитория «findById» возвратит значение null, контроллер выбросит исключение типа «ReponseStatusException», иначе – будет возвращен найденный по «id» объект. В языке Java отсутствует оператор elvis, поэтому, для реализации того же функционала, в нем порою приходится писать существенно больший объем кода.
Тестирование кода
Для тестирования кода рекомендуется использовать @MockMvcTest и библиотеку SpringMockK, которая похожа на Mockito, но лучше подходит для Kotlin:
@MockMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {
@MockkBean
private lateinit var articleRepository: ArticleRepository
@Test
fun `List articles`() {
val user = User("testUser", "John", "Doe")
val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", user)
val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", user)
every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(spring5Article, spring43Article)
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].author.login").value(user.login))
.andExpect(jsonPath("\$.[0].slug").value(spring5Article.slug))
.andExpect(jsonPath("\$.[1].author.login").value(user.login))
.andExpect(jsonPath("\$.[1].slug").value(spring43Article.slug))
}
}
В примере выше мы создали одного тестового пользователя и два сымитированных объекта вида «Статья» (mocked Article entities). Затем задали, что для каждого (every) запроса репозитория на поиск всех «статей» должен возвращаться список (list) из этих сымитированных статей. Дальше с помощью MockMVC мы составили имитирующий (mocked) HTTP-запрос. Мы указали MockMVC выполнить GET-запрос на '/api/article' и ожидать содержимое в формате JSON. Тест будет считаться пройденным, если полученный на сымитированный вызов ответ будет «OK», содержимое будет в формате JSON, и в нем будут содержаться данные наших сымитированные «статей», такие как: логин автора и название статьи.
Конец статьи
==============================================================
Данная статья является отредактированным переводом статьи "Kotlin in Web Development" автора Kristijan Zdelarec.