Одностраничный сайт на Kotlin и SpringBoot без использования JSP

  • Tutorial
Дисклеймер

Автор не прогер, кодить не умеет
Я не являюсь гуру или крутым специалистом ни в Котлине, ни в Spring, ни в любой другой технологии используемой в данной статье. Я обычный java junior, который решил опробовать kotlin. Все сделано в "Сапсане" на коленке по дороге с techtrain


Для кого


Для java разработчиков, которые только слышали про котлин, но руками его пока не трогали


Для чего


Показать что kotlin отлично работает с spring boot, а в сочетании с DSL в части работы с html быть удобнее классического подхода с jsp.


Конфигурация для spring boot


Тут все мало отличается от java


  1. Создать проект (kotlin)
  2. Добавить repository jcenter, dependency на kotlin-stdlib и spring-boot-starter-web, плагин kotlin-maven-plugin
  3. Обязательно создать пакет для работы, иначе Spring свалится
  4. Создать open класс Application
  5. В Application добавить метод main со строчкой runApplication <Application>(*args)
  6. Добавить контроллер HelloWorld, который вернет приветствие миру

Код
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>habr-sample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <repositories>
        <repository>
            <id>jcenter</id>
            <name>jcenter</name>
            <url>https://jcenter.bintray.com</url>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>RELEASE</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmTarget>1.8</jvmTarget>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>RELEASE</version>
        </dependency>
    </dependencies>
</project>

Классы
package example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
open class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

package example

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseBody

@Controller
class HelloWorld{
    @RequestMapping("/")
    @ResponseBody
    fun mainPage(): String {
        return "Привет Хабр"
    }
}

Добавляем kotlinx.html


Kotlin имеет расширение в виде DSL для удобной работы с html. Оно позволяет смешать декларативный подход html c императивный подходом обычного языка.


table {//Объявление тега
    thead{
        tr {
            td {
                +"Имя" //+ добавляет контент в элемент
            }
            td {
                +"Возраст"
            }
        }
    }
    for (person in PersonGenerator().generate()) { //Обычный for из kotlin
        tr {

            td {
                +person.name
            }
            td {
                text(person.age)  //аналог "+" который принимает не только String
            }
        }

    }
}

Этот код сформирует html страницу в которой будет табличка с именами людей и их возрастом


Результат


Код
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>habr-sample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <repositories>
        <repository>
            <id>jcenter</id>
            <name>jcenter</name>
            <url>https://jcenter.bintray.com</url>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>RELEASE</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmTarget>1.8</jvmTarget>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-html-jvm</artifactId>
            <version>RELEASE</version>
        </dependency>
    </dependencies>
</project>

Классы
package example

data class Person(val name: String,var age: Int)

package example
import java.util.*
import kotlin.collections.ArrayList

class PersonGenerator{
    private val nameList = arrayOf("Петя", "Вася", "Женя", "Марк", "Иван", "Леопольд");
    fun generate(): List<Person> {
        val random = Random()
        val personList = ArrayList<Person>()
        for (i in 1..15) {
            personList.add(Person(nameList.get(random.nextInt(nameList.size)), random.nextInt(30) + 18))
        }
        return personList;
    }
}

package example
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseBody

@Controller
class HelloWorld {

    @RequestMapping("/")
    @ResponseBody
    fun mainPage(): String {
        return createHTML()
                .html {
                    body {
                        table {
                            thead() {
                                tr {
                                    td {
                                        +"Имя"
                                    }
                                    td {
                                        +"Возраст"
                                    }
                                }
                            }
                            val personList= PersonGenerator().generate()
                            for (person in personList ) {
                                tr {

                                    td {
                                        +person.name
                                    }
                                    td {
                                        text(person.age)
                                    }
                                }

                            }
                        }
                    }
                }
    }

}

Добавляем css


Kotlinx.html практически не умеет работать с css, с помощью него можно добавить ссылку на уже готовый стиль или вставить свой с помощью unsafe. Все становиться намного лучше если добавить Aza-Kotlin-CSS — DSL добавляющий работу с css


head {
//Добавить стилей bootstrap
    styleLink("https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css")
    styleLink("https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css")
//Добавление своего стиля
    style("text/css") {
        unsafe {// Заходим в unsafe kotlinx.html
            raw(
                    Stylesheet { // и формируем стиль с Aza-Kotlin-CSS
                        table and td and th {
                            color = 0xFC0Fc0
                        }
                    }.render()
            )
        }
    }
    meta {
        charset = "utf-8"
    }

}

Результат


Работа с js идет также через unsafe


Резюме


На мой взгляд писать view на DSL удобнее чем на JSP. Мне не нужно сначала лезть в базу, класть результат вычисления в промежуточный объект, который я потом буду вытаскивать в jsp. Я могу сразу наполнять свой jsp из кодовой базы проекта минуя ненужный мне слой абстракции. К сожалению, я использовал jsp только в своих pet-project и вообще не занимаюсь фронтом. Мне было бы интересно почитать мнение профессионалов по этому вопросу.


Полезные ссылки


Пример разработанной в статье розовой таблички
Документация по котлину
Документация подключения spring boot к котлину
Документация по kotlinx.html
Документация по Aza-Kotlin-CSS

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 20

    +3
    DSL для удобной работы с html

    — это ужасно. Как забавный эксперимент годится, но активно работать с этим невозможно. С другой стороны я как то видел сайт, где HTML генерился хранимыми процедурами, — это было хуже (но код выглядел даже проще чем у Вас)
      0
      Вы считаете что это хуже jsp?
      Подскажите как вы наполняете html страницу с стороны сервера?
        0
        ИМХО, вам имеет смысл посмотреть в сторону шаблонизаторов. Какой-нибудь Velocity или Groovy template engine.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Согласен, что это полезно только если нет разделения на frontend и backend разработку. Но если разделения нет можно делать очень удобные компоненты, например
            Кусок кода, который можно вызвать внутри table и он заполнит его
               fun <T>TABLE.generate(arr: Collection<T>, vararg rowArgs: Pair<String, KProperty1<T, Any>>) {
                    thead {
                        tr {
                        for (el in rowArgs) {
            
                                td {
                                    +el.first
                                }
                            }
                        }
                    }
                    for (arrElement in arr) {
                        tr {
                            for (el in rowArgs) {
                                td {
                                    +el.second.get(arrElement).toString()
                                }
            
                            }
                        }
                    }
                }
            


            В результате, теперь в любом месте table можно передать коллекцию для отображения, и пару из названия колонки и откуда эту колонку брать. При этом сохраняется статическая типизация и компилятор будет следить за тем, чтобы брались только существующие колонки и элемента Collection.
              0
              это только на первый взгляд выглядит удобно, но становится адом и лапшой при совершенно незначительном росте проекта
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Выглядит так
                  table("table table-striped table-bordered") {
                      generate(AnimalStorage().get(),
                              Pair("Название вида",Animal::name),
                              Pair("Сколько детей",Animal::numberOfChildren),
                              Pair("Место обитания",Animal::place))
                  }
                  


                  Тест чего именно показать?
                  Верстка меняется обычным hot swap.
              0
              Конкретно сейчас занимаюсь разработкой чистого бэка, но раньше… ) — CGI/Perl, ASP, PHP, сервлеты, JSP/JSTL, JSF, Thymeleaf, GWT, Vaadin, JS/REST, может еще чего забыл. Но если говорить о простоте — то Вам нужен PHP)
                0
                Это может и не хуже, но это совсем другое. И сравнивать их некорректно. При работе с разметкой есть два базовых подхода: начинать с html и начинать с DSL. Совместить их воедино и сделать это хорошо удается очень редко кому (лично у меня не было претензий только к Flex и его mxml, и их интеграции в AS3).

                Ровно тоже самое, что вы нарисовали, может быть сделано (и делалось еще лет 10 назад) на javascript. Такой же точно DSL. Десятки их я только лично видел, подобных. Только вся разница в том, что:

                * это при отключении js не работает
                * это нельзя редактировать html редактором, про wisiwig можно забыть

                Ну и у вас недостатки (и достоинства) примерно те же самые. Оно не статическое, и без выполнения кода на котлине вы вообще ничего не видите. Иногда это нормально и удобно. Но иногда — ужасно. Но в целом этот подход вполне живой (хотя и не новый). То что вы изобрели, ровно в таком же виде существует в groovy под названием MarkupBuilder. Уже тоже лет 10 как наверное.
                  0
                  Вы считаете что это хуже jsp?

                  Да, хуже. Верстальщик отдаст не html, который сделан специально под этот dsl. Он отдаст произвольный текст, в котором могут быть такие теги, о существовании которых вы даже не подозреваете. В jsp эти теги можно просто скопировать, а с dsl придётся отжиматься.

                    0
                    Я уже соглашался выше — при жестком разделении front и back в этом нет смысла, так как front живет в своем стеке.
                    Насчет отжиматься, все не так грустно
                    Прелесть этого подхода как раз в том, что не нужно сидеть в окнах jsp и контроллера одновременно. Все что будет написано, в плане подстановки данных, будет 100% валидно, ибо за этим следит компилятор. Кстати, если нужно, можно комбинировать jsp и dsl отдавая в jsp куски html сделанного dsl.
                0
                Я не знаток java, но не ужели там нету никаких шаблонизаторов по типу pug или jade?
                  0
                  да есть все, дайте человеку по-велосипедить
                  0

                  Недостаток такого подхода по сравнению с JSP вот в чём:


                  HelloWorld.class
                  HelloWorld$mainPage$1.class
                  HelloWorld$mainPage$1$1.class
                  HelloWorld$mainPage$1$1$1.class
                  HelloWorld$mainPage$1$1$1$1.class
                  HelloWorld$mainPage$1$1$1$1$1.class
                  HelloWorld$mainPage$1$1$1$1$1$1.class
                  HelloWorld$mainPage$1$1$2.class
                  HelloWorld$mainPage$1$2.class
                  HelloWorld$mainPage$1$2$1.class
                  HelloWorld$mainPage$1$2$1$1.class
                  HelloWorld$mainPage$1$2$1$1$1.class
                  HelloWorld$mainPage$1$2$1$1$1$1.class
                  HelloWorld$mainPage$1$2$1$1$1$2.class
                  HelloWorld$mainPage$1$2$1$2.class
                  HelloWorld$mainPage$1$2$1$2$1.class
                  HelloWorld$mainPage$1$2$1$2$2.class

                  Хренова туча мелких анонимных классов. JSP обычно компилируется в один класс.

                    0

                    А в чем недостаток-то?

                    –2
                    Потыкав палочкой в Kotlin я увидел недоделанный долгострой. Первое что обнаружил: кросс-платформенный Reflect API отсутствует, хотя который год обещают.

                    Классический «for(;;)» убит (лучше бы убили «while» и «do/while»). Некоторые вещи пишутся громоздко.

                    Для HelloWorld работает хорошо. В прод не катит.
                      –2
                      Прошу НЛО удалить мой комментарий, а то фанаты заминусовали (да и статья, действительно, не про это).
                      0

                      Непонятно, как такие шаблоны верстать. Когда-то Thymeleaf мне очень понравился тем, что его шаблоны можно открыть в браузере в исходном виде и они будут выглядеть правильно.

                        0

                        Такой DSL аналог JSX и активно используется в React. Я бы использовал его для отдельных компонент, которые бы собирал привычным верстальщику шаблоном.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое