Оглавление:
Статический анализатор Idea Analize
Статический анализатор SonarCube
JPA Entity классы с Kotlin
Unit тестирование
Интеграционное тестирование в проекте
Покрытие тестами
Spring profiles
Тестовый запуск
Создание запускаемого файла и его запуск
Publishing SpringBoot "FAT" jar
Интеграционное тестирование
Примеры тестов httpie
DataJpa tests
RestAssured tests
Нагрузочное тестирование
Prometheus
Пример просмотра использования CPU в Prometheus
Запуск prometheus в docker
Docker
Grafana
Кеширование
Сборка Jenkins
Nexus
Просмотр ресурсов с помощью Java Mission Control
Логирование
Использование "ChatGPT-EasyCode" в Idea
Использование "ChatGPT-EasyCode" в VSCode
Частный параметр конфигурации в application.yaml
Переопределение значения переменных application.yaml
Цель
Cоздать небольшое приложение на Kotlin с использованием Spring Boot. Справочник товаров со следующими типами:
- Настольные компьютеры
- Ноутбуки
- Мониторы
- Жесткие диски
Каждый товар имеет следующие свойства:
- номер серии
- производитель
- цена
- количество единиц продукции на складе
Дополнительные свойства:
- Настольные компьютеры имеют форм-фактор: десктопы, неттопы, моноблоки
- Ноутбуки подразделяются по размеру: 13, 14, 15, 17 дюймовые
- Мониторы имеют диагональ
- Жесткие диски имеют объем
Необходимо реализовать back-end приложение, которое имеет RESTful HTTP методы выполняющие:
- Добавление товара
- Редактирование товара
- Просмотр всех существующих товаров по типу
- Просмотр товара по идентификатору
В качестве базы данных использовать in memory database, например H2.
Статический анализатор Idea Analize
Проверка кода. Вызывается из контекстного меню Analize - Inspect Code.
Статический анализатор SonarCube
https://github.com/cherepakhin/shop_kotlin/blob/dev/doc/sonarqube/use_sonarcube.md
JPA Entity классы с Kotlin
Источник: https://habr.com/ru/companies/haulmont/articles/572574/
Примеры из источника: https://github.com/Klimenkoob/spring-kotlin-hibernate
Рекомендации:
1. Data class not recommended for JPA Entity . Warning in Idea. Почему не использовать Data-классы? Потому что они финальны сами по себе, имеют по всем полям определенные equals, hashCode и toString. А это недопустимо в связке с Hibernate.
2. Явно помечать ключевым словом open все Entity. Согласно спецификации JPA, все классы и свойства, связанные с JPA, не должны быть final. В отличие от Java, в Kotlin классы, свойства и методы по умолчанию final. Поэтому их нужно явно помечать ключевым словом open.
3. Явно определять hashCode(), equals(), toString(). При определении внимание на lazy поля (как и в Java).
4. Добавить nullable = false
@ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "client_id", nullable = false) lateinit var client: Client
5. Kotlin классы по умолчанию final. Для Entity это проблема. Можно применить plugin "allopen":
plugins { kotlin("plugin.allopen") } ... allopen { annotation("javax.persistence.Entity") }
Unit тестирование
./gradlew test
Возможно , пригодятся параметры продолжения тестирования, если текущий тест упал:
$ gradle --continue ... $ mvn install -Dmaven.test.failure.ignore=true $ mvn --fail-at-end test $ mvn -fae test
Примеры отбора тестов:
./gradlew test --tests '*EntityTest' ./gradlew test --tests '*Rest*' ./gradlew test --tests ProductDTOTest ./gradlew test --tests '*TestIntegration' ./gradlew test --tests '*MockMvcTest'
Интеграционное тестирование в проекте
Вообще, интеграционные тесты должны быть в отдельном проекте (см. https://github.com/cherepakhin/shop_kotlin_restassured_test). В этом проекте оставил только тесты работы с базой данных. Имена интеграционных тестов должны заканчиваться ..TestIntegration. Прогнать все, кроме интеграционных (включены только *Test, исключены *TestIntegration):
./gradlew clean test --tests *Test
Только интеграционные:
./gradlew clean test --tests *TestIntegration* ./gradlew clean test --tests *TestIntegration
Другие примеры:
gradle test --tests org.gradle.SomeTest.someSpecificFeature gradle test --tests *SomeTest.someSpecificFeature gradle test --tests *SomeSpecificTest gradle test --tests all.in.specific.package* gradle test --tests *IntegTest gradle test --tests *IntegTest*ui* gradle test --tests *IntegTest.singleMethod gradle someTestTask --tests *UiTest someOtherTestTask --tests *WebTest*ui
Возможно , пригодятся параметры продолжения тестирования, если текущий тест упал:
$ gradle --continue ... $ mvn install -Dmaven.test.failure.ignore=true $ mvn --fail-at-end test $ mvn -fae test
Включено протоколирование тестов build.gradle.kts:
... tasks.withType<Test> { ... // Show test log testLogging { events("passed", "skipped", "failed") } ... }
Вывод в консоль:
... ProductServiceImplMockTest > getByGroupProductN() PASSED ProductServiceIntegrationTest > existByN() PASSED ProductServiceIntegrationTest > notExistByN() PASSED ProductServiceIntegrationTest > checkSortByName_ByDslFilterByName() PASSED ...
с events("standardOut", "started", "passed", "skipped", "failed") логируется вывод в консоль.
Покрытие тестами
Использован jacoco. Отчет формируется при прогоне тестов
./gradlew test jacocoTestReport
Отчет будет в папке build/reports/jacoco/test/html. В отчете НЕТ информации о результатах тестирования, только протестирован участок кода или нет.
Пример отчета по конкретному классу:
Красным или желтым выделены непротестированные участки кода, зеленым протестировано.
Spring profiles
Настроены два Spring профиля: application-prod.yml и application-dev.yml В этом же файле указан профиль по умолчанию:
spring: profiles: active: dev application: name: shop_kotlin
Запуск с указанием профиля:
java -D"spring.profiles.active=dev" -jar app.jar
или установить env переменную:
SPRING_PROFILES_ACTIVE = dev
Запуск с gradle:
./gradlew bootRun --args='--spring.profiles.active=dev'
или
SPRING_PROFILES_ACTIVE=dev ./gradlew clean bootRun
Тестовый запуск
./gradlew bootRun
Создание запускаемого файла и его запуск
Создание:
./gradlew bootJar
(bootJar не bootRun!!!)
Собранный файл будет в папке ./build/libs/
запуск с RAM 256Мб:
shop_kotlin/$ java -Xmx256M -jar build/libs/shop_kotlin-0.1.20.jar
или так:
cd shop_kotlin/build/libs shop_kotlin/build/libs$ java -Xmx256M -jar shop_kotlin-0.1.20.jar
Publishing SpringBoot "FAT" jar
Настройка:
publishing {
repositories {
maven {
url = uri("https://v.perm.ru:8082/repository/ru.perm.v/")
isAllowInsecureProtocol = true
// for publish to nexus "./gradlew publish"
// export NEXUS_CRED_USR=admin
// echo $NEXUS_CRED_USR
credentials {
username = System.getenv("NEXUS_CRED_USR")
password = System.getenv("NEXUS_CRED_PSW")
}
}
}
publications {
create<MavenPublication>("maven"){
artifact(tasks["bootJar"]) // build and publish bootJar
}
}
Примеры тестов httpie
Echo запрос для простой проверки работоспособности (:8980/shop_kotlin/ базовый путь проекта):
$ http :8980/shop_kotlin/api/echo/aaa HTTP/1.1 200 Connection: keep-alive Content-Length: 3 Content-Type: text/plain;charset=UTF-8 Date: Thu, 15 Jun 2023 07:30:38 GMT Keep-Alive: timeout=60 aaa
Поиск по имени Product:
$ http :8980/shop_kotlin/api/group_product/find?name='Comp' [ { "haveChilds": true, "id": 2, "name": "Computers", "parentId": 1 }, { "haveChilds": false, "id": 3, "name": "Desktop Computers", "parentId": 2 }
POST запрос на изменение Product:
$ http POST :8980/shop_kotlin/api/product/ < ./src/test/json_test/product.json
Интеграционное тестирование
Два варианта тестирования - cо Spring @DataJpaTest и через RestAssured (это bdd тестирование). Совершенно разные тесты, для совершенно разных целей. DataJpaTest на уровне БД, RestAssured - сквозное тестирование от rest до БД.
DataJPA test
Тестируется работа с базой данных с использованием @DataJpaTest. Находятся в пакете https://github.com/cherepakhin/shop_kotlin/blob/dev/src/test/kotlin/ru/perm/v/shopkotlin/datajpatest.
RestAssured
Для этих тестов сделан отдельный проект https://github.com/cherepakhin/shop_kotlin_reastassured_test
Результат тестов:
Docker
Работа с Docker:
# create docker image $./docker_build.sh # run app $ docker run -p 8080:8980 shop_kotlin/app $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c70ef82f3a54 shop_kotlin/app "java -jar /app.jar" 43 seconds ago Up 42 seconds 0.0.0.0:8080->8980/tcp inspiring_germain # simple test $ http :8080/api/group_product/find?name='Comp' $ docker ps -a # 2d1325e1222a shop_kotlin:0.24.0105 "/cnb/process/web" ... # stop docker app $docker stop 2d1 # rm container shop_kotlin 2d1325e1222a (id from docker ps -a) $docker container rm 2d1 # rm image $docker image ls shop_kotlin 0.24.0105 94560d28efb3 ... $ docker image rm 945 Untagged: shop_kotlin:0.24.0105 # clear ALL images $docker image prune -a # clear all $docker system prune -af
ИЛИ средствами gradle:
Создание docker image:
$ ./gradlew bootBuildImage
...
[creator] Saving docker.io/library/shop_kotlin:0.1.18...
[creator] *** Images (f80a4e623a3d):
[creator] docker.io/library/shop_kotlin:0.1.18
Successfully built image 'docker.io/library/shop_kotlin:0.1.18'
...
В файле META-INF/MANIFEST.MF указать Main-Class
Main-Class: ru.perm.v.shopkotlin.ShopKotlinApplication
Запуск docker image:
$ docker run -p 8980:8980 -p 8988:8988 docker.io/library/shop_kotlin:0.1.18
(8980 - основной порт, 8988 - spring actuator)
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6544af7b0adb shop_kotlin:0.1.18 12 seconds ago Up 11 seconds 0.0.0.0:8980 - 8980/tcp
Проверка:
$ http http://127.0.0.1:8980/shop_kotlin/api/echo/aaa
HTTP/1.1 200
Connection: keep-alive
Content-Length: 3
Content-Type: text/plain<span class="pl-k">;</span>charset=UTF-8
Date: Thu, 30 Nov 2023 15:36:25 GMT
Keep-Alive: timeout=60
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
aaa
Swagger
Swagger доступен по адресу http://127.0.0.1:8980/shop_kotlin/api/swagger-ui/
Spring Actuator
Spring Actuator предназначен для получения информации о работающем приложении - статус приложения (жив/нет), использовании памяти, cpu и т.п.. Поключен по адресу http://127.0.0.1:8988/shop_kotlin/api/actuator
порт указан в application.yaml:
management: ... server: port: 8988
Использование из командной строки:
$ http http://127.0.0.1:8988/shop_kotlin/api/actuator { "_links": { "beans": { "href": "http://127.0.0.1:8988/shop_kotlin/api/actuator/beans", "templated": false }, "caches": { "href": "http://127.0.0.1:8988/shop_kotlin/api/actuator/caches", "templated": false }, "caches-cache": { "href": "http://127.0.0.1:8988/shop_kotlin/api/actuator/caches/{cache}", "templated": true }, ....
Пример "Сколько памяти используется?":
$ http http://127.0.0.1:8988/shop_kotlin/api/actuator/metrics/jvm.memory.used .... "baseUnit": "bytes", "description": "The amount of used memory", "measurements": [ { "statistic": "VALUE", "value": 147665416.0 } ], "name": "jvm.memory.used" ....
Нагрузочные тесты показали следующие результаты: 64M - приложение выпадает с OutOfMemory 128M - тесты проходят
Prometheus
На моем сервере запущен Prometheus. Prometheus ОПРАШИВАЕТ приложение, согласно заданию (ниже yaml), и собирает метрики (metrics_path+targets). Пример задания для опроса в файле doc/prometheus/prometheus.yml. Содержимое doc/prometheus/prometheus.yml поместить в настройки prometheus. Пример задания:
scrape_configs: - job_name: 'spring boot scrape' metrics_path: '/shop_kotlin/api/actuator/prometheus' scrape_interval: 5s static_configs: - targets: ['192.168.1.20:8988']
Опрашивать '192.168.1.20:8988/shop_kotlin/api/actuator/prometheus' каждые 5 сек.
Для просмотра получаемых prometheus-ом метрик можно выполнить:
$ http http://127.0.0.1:8788/api/actuator/prometheus**
(Использован httpie)
Ответ:
# HELP jvm_threads_daemon_threads The current number of live daemon threads # TYPE jvm_threads_daemon_threads gauge jvm_threads_daemon_threads 13.0 # HELP hikaricp_connections Total connections ...
Подключение к Prometheus из браузера: http://192.168.1.20:9090/targets
http://192.168.1.20:9090/graph
Основной экран: 192.168.1.20 - адрес хоста с prometheus http://192.168.1.20:9090/targets
Приложение остановлено:
Приложение запущено:
Пример просмотра использования CPU в Prometheus
Prometheus запущен на 192.168.1.20:9090
Перейти на http://192.168.1.20:9090 В меню "Graph":
- В "Expression" ввести system_cpu_usage
- Перейти на Graph
- Отрегулировать период (н.п. 15 мин.)
Итоговый запрос: http://192.168.1.20:9090/graph?g0.range_input=15m&g0.expr=system_cpu_usage&g0.tab=0
Запуск prometheus в docker:
docker run -d -p 9090:9090 -v "/$(pwd)/for_prometheus/prometheus.yml":/etc/prometheus/prometheus.yml prom/prometheus
Просмотр состояния prometheus, запущенного в docker:
Вычисление ID container
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e081bb1f500c prom/prometheus /bin/prometheus --c… 4 minutes ago Up 4 minutes 0.0.0.0:9090->9090/tcp reverent_newton
Просмотр логов контейнера prometheus (e08 id контейнера)
$ docker logs e08 --follow
Ответ (что-тотипа такого):
ts=2023-09-11T13:13:17.850Z caller=main.go:1009 level=info msg="Server is ready to receive web requests." ts=2023-09-11T13:13:17.850Z caller=manager.go:1009 level=info component="rule manager" msg="Starting rule manager..."
Grafana
Graphana отображает метрики, собранные Prometheus. На домашнем сервере развернута Grafana. На каринках ниже отображено использование CPU двух приложений, расположенных на двух разных хостах в упрощенном и полном формате:
Запуск grafana:
vasi@v$ sudo service grafana-server start
Остановка grafana:
vasi@v$ sudo service grafana-server stop
user(pass): admin/admin
Подробнее о нагрузочном тестировании и метриках в этом проекте https://github.com/cherepakhin/shop_kotlin_yandex_tank_test
Кеширование
Кеширование сделано для RestController:
@GetMapping("/") @Cacheable("allGroupProductDTO") @ApiOperation("Get all groups of product") fun all(): List<GroupProductDTO> { ... }
Использован org.springframework.cache. Ручная проверка работы кеш:
# Очистка кеша $ http :8780/api/group_product/clear_cache # Для проверки, несколько раз сделать get запрос. # При этом в лог будет только один запрос к репозиторию. $ http :8780/api/group_product/
Сборка Jenkins
Сборка происходит в Jenkins, развернутом на домашнем сервере. Pipeline для Jenkins описан в файле ./Jenkinsfile
Установка и настройка домашнего Jenkins описана в https://v.perm.ru/main/index.php/50-organizatsiya-sobstvennogo-ci-cd
Для ограничения памяти при сборке Gradle, добавлен параметр в gradle.properties:
org.gradle.jvmargs=-Xmx512M
Nexus
В проекте используется внешняя зависимость из домашнего NEXUS репозитория https://github.com/cherepakhin/shop_kotlin_extdto:
implementation("ru.perm.v:shop_kotlin_extdto:0.0.3")
Deploy to NEXUS repository
Возможен с использованием Jenkins (описано выше) или ручной deploy в Nexus с личного компьютера.
Для deploy выполнить:
$ ./gradlew publish
Путь к репозиторию установлен в build.gradle.kts:
url = uri("https://v.perm.ru:8082/repository/ru.perm.v/")
Для установки переменных доступа к Nexus repository выполнить в shell:
$ export NEXUS_CRED_USR=admin $ export NEXUS_CRED_PSW=pass
Секция publishing в build.gradle.kts:
publishing { repositories { maven { url = uri("https://v.perm.ru:8082/repository/ru.perm.v/") isAllowInsecureProtocol = true // publish в nexus "./gradlew publish" из ноута и Jenkins проходит // export NEXUS_CRED_USR=admin // echo $NEXUS_CRED_USR credentials { username = System.getenv("NEXUS_CRED_USR") password = System.getenv("NEXUS_CRED_PSW") } } } publications { register("mavenJava", MavenPublication::class) { groupId artifactId version from(components["java"]) }
Просмотр ресурсов с помощью Java Mission Control
jmc-8.3.1_linux-x64/JDK Mission Control$ jmc
Логирование
Настройка сделана в application.yaml:
logging: level: root: info file: path: log/
Использование "ChatGPT-EasyCode" в Idea
Пример работы плагина Idea EasyCode:
Использование "ChatGPT-EasyCode" в VSCode
Вещь!
А отечественный GigaCode, в течении нескольких недель, отвечал унылым
и в конце концов какие-то шестеренки провернулись:
В IntelliJ IDEA 2023.3.2 (Community Edition):
Частный параметр конфигурации в application.yaml
Бывает нужно добавить СВОЙ параметр конигурации в application.yaml для удобства тестирования, разработки, развертывания. Пример описан в проекте: https://github.com/cherepakhin/camel_rest в разделе "Дополнительно". В application.yaml добавлен ЧАСТНЫЙ параметр конфигурации "myconfig".
Описание:
@ConfigurationProperties("myconfig") @ConstructorBinding data class MyConfig(val testDirectory: String)
myconfig: testDirectory: file:~/temp/testarea
@RestController class ParamCtrl { @Autowired lateinit var myConfig: MyConfig @GetMapping("/test_directory") fun getTestDirectory(): String? { logger.info("GET param test_directory=${myConfig.testDirectory}") return myConfig.testDirectory } }
Переопределение значения переменных application.yaml
Можно задать значения переменных application.yaml через специальную переменную SPRING_APPLICATION_JSON. Пример замены порта приложения на 8960 (в application.yaml server: port=8980):
$ export SPRING_APPLICATION_JSON='{"server":{"port":8960}}'
$ java -jar build/libs/spring_config_k-0.0.1-SNAPSHOT.jar
....
INFO 23327 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8960 (http)
....
TODO
cache on rest (сделано)RestAssured tests (сделано)(https://github.com/cherepakhin/shop_kotlin_reastassured_test)by lazy (сделано)Docker (сделано)Swagger (сделано)Actuator (сделано)flyway (использована БД в памяти url: jdbc:h2:mem:easybotdb). Миграции не требуются.- webmvc тесты (ProductRestMockMvcTest)
тесты service слоя с СУБД @DataJpaTest (сделано)- вертикальные БД (?)
Различные SQL запросы через native Sql и Spring JPA(order by, group by) (сделано)- Pagination
Grafana (сделано)- ElasticSearch
- Авторизация
Примечания:
В программе не используются слои controller и service, т.к. проект сделан только для демонстрации Kotlin, и не планируется какая-либо бизнес-логика. Конвертация в DTO сделана в REST контроллерах.
Поля в entity классах д.б. обозначены "open" (н.п. open val name: String = ""). В Kotlin используется следующий подход - можно наследовать от суперклассов и переопределять их свойства и функции только в том случае, если они снабжены префиксом "open".
Имя для "id" поля в entity классах названы "n". Причины:
- В некоторых БД "id" ключевое слово
- В interface repository есть отдельные методы repository.getById(), repository.findById(). Имеется ввиду поиск по primary key. Это путает.
Правила использования модификатора lateinit:
- используется только совместно с ключевым словом var;
- свойство может быть объявлено только внутри тела класса (не в основном конструкторе);
- тип свойства не может быть нулевым и примитивным;
- у свойства не должно быть пользовательских геттеров и сеттеров;
- с версии Kotlin 1.2 можно применять к свойствам верхнего уровня и локальным переменным.
- сообщает что инициализация будет как-то сделана (в примере ниже аннотациями @Mock, @InjectMocks). @Mock mockProductService будут инжектированы в @InjectMocks productRest. Пример:
internal class ProductRestTest { @Mock private lateinit var mockProductService: ProductService @InjectMocks private lateinit var productRest: ProductRest ... }
byLazy
val catName: String by lazy { getName() }
свойство объявлено как VAL, но присвоение будет выполнено при первом использовании и потом уже не будет инициализироваться.
Ссылки о lazy и lateinit:
- https://github.com/cherepakhin/kotlin_in_action
- https://developer.alexanderklimov.ru/android/kotlin/lateinit.php
- https://www.baeldung.com/kotlin/lazy-initialization
- https://bimlibik.github.io/posts/kotlin-lateinit-and-lazy/
Статические члены и компаньон объекты в Kotlin:
Как правило объекты-компаньоны используются для объявления переменных и функций, к которым требуется обращаться без создания экземпляра класса. Либо для объявления констант. По сути они своего рода замена статическим членам класса (в отличие от Java, в Kotlin нет статики).
- Как бы static:
class Someclass { ... object AsStaticObject { fun create() { ... } } } ... val someClass = SomeClass.AsStaticObject.create()
- Companion Object (вложенный объект. Метод create() принадлежит родительскому классу):
class SomeClass { companion object { fun create() } } ... val someClass = SomeClass.create()
Различия между val и const val в Kotlin
val MY_VAL = 1 const val MY_CONST_VAL = 2
- Обе эти переменные немодифицируемые
- MY_VAL станет приватной переменной, для доступа к которой будет создан геттер
- MY_CONST_VAL будет заинлайнена, то есть компилятор заменит все полученные значения этой переменной на само значение
- Kotlin const, var, and val Keywords
Ошибка "ERROR: Cannot resume build because FlowNode 19"
Ответ тут https://community.jenkins.io/t/error-cannot-resume-build-because-flownode-19/9477/3
В двух словах: добавить в Jenkinsfile "максмальную выживаемость"
... pipeline { agent any options { durabilityHint 'MAX_SURVIVABILITY' } ...
-
В Windows на сетевом диске компилировалось с проблемами query-dsl. После переноса на диск C: проблемы ушли.
-
Если появилась ошибка: "./gradlew: строка 2: $'\r': команда не найдена", то выполнить:
-
Похоже, при расшаривании папки для Windows и работе в Windows изменился символ конца строки.
- Смена версии gradlew: в ./gragle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
- Тесты проходят (из idea и из shell)
- Jetty рекомендуют, как более оптимизированный контейнер:
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") { exclude("org.springframework.boot:spring-boot-starter-tomcat") } implementation("org.springframework.boot:spring-boot-starter-jetty") // jetty uses less memory
- Описание Data class: https://kotlinlang.ru/docs/reference/data-classes.html. "Классы данных не могут быть абстрактными, open, sealed или inner. Компилятор автоматически формирует следующие члены данного класса из свойств, объявленных в основном конструкторе: equals()/hashCode(), toString() в форме "User(name=John, age=42)", компонентные функции componentN(), которые соответствуют свойствам, в соответствии с порядком их объявления. Основной конструктор должен иметь как минимум один параметр. Все параметры основного конструктора должны быть отмечены, как val или var."