Прощай, Grails. Привет, Micronaut. Окончание

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Это третья и последняя статья из цикла о миграции из Grails в Micronaut. Обратите внимание: ваше приложение должно быть создано в Grails 4.x или более поздней версии.

Всего в цикле публикаций о миграции из Grails в Micronaut 10 частей:

Многомодульный проект
Конфигурация
Статическая компиляция
Датасеты
Маршалинг
Классы предметной области
Сервисы
Контроллеры
Приложение Micronaut
Micronaut Data

В этой статье поговорим о сервисах, контроллерах, приложении Micronaut, Micronaut Data.

Первую статью (о многомодульных проектах, конфигурации и статической компиляции) можно прочитать здесь.

Вторую статью (о датасетах, маршалинге и классах предметной области) можно прочитать тут.

Часть 7. Сервисы

В рамках этого цикла мы уже извлекли классы предметной области в подпроект. Теперь мы продолжим переносить другие сервисы из исходного приложения Grails. Не будем мудрить с именованием и просто назовем эти библиотеки -core.

Создадим еще одну папку hello-core в директории libs и добавим в нее файл сборки hello-core.gradle.

dependencies {
    api project(':hello-data')

    annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    annotationProcessor "io.micronaut:micronaut-inject-java"
    annotationProcessor "io.micronaut:micronaut-validation"

    implementation platform("io.micronaut:micronaut-bom:$micronautVersion")

    implementation "io.micronaut:micronaut-core"
    implementation "io.micronaut:micronaut-inject"
    implementation "io.micronaut:micronaut-validation"
    implementation "io.micronaut:micronaut-runtime"

    compileOnly "io.micronaut:micronaut-inject-groovy"

    testImplementation project(":hello-data-test-data")

    testImplementation("org.spockframework:spock-core") {
        exclude group: "org.codehaus.groovy", module: "groovy-all"
    }

    testImplementation "io.micronaut:micronaut-inject-groovy"
    testImplementation "io.micronaut.test:micronaut-test-spock"
}

tasks.withType(JavaExec) {
    classpath += configurations.developmentOnly
    jvmArgs('-noverify', '-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
}

tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
    options.compilerArgs.add('-parameters')
}

Также нам придется перенести зависимости из исходного приложения. Когда какой-либо извлеченный сервис не сможет скомпилироваться по причине отсутствия зависимостей, вам нужно будет переместить их в файл hello-core.gradle.

Из проекта приложения Grails можно скопировать все содержимое каталогов

  • src/main/groovy

  • grails-app/services

в папку src/main/groovy новой библиотеки.

Не забудьте добавить новую библиотеку в качестве зависимости в исходное приложение:

implementation project(':hello-core')

Бины Micronaut лучше работают с внедрением через конструктор в отличие от Grails, где применяется FieldByName.

Рассмотрим простой сервис Grails:

import groovy.transform.CompileStatic
import hello.legacy.VehicleDataService

@CompileStatic
class VehicleService {

    VehicleDataService vehicleDataService

    // methods

}

В этом сервисе мы внедряем по имени VehicleDataService в VehicleService в публичное свойство vehicleDataService. Именование играет критически важную роль в Grails.

Мы переместим этот класс из hello/grails-app/services в hello-core/src/main/groovy.

Теперь мы его преобразуем в бин Micronaut

Объявления в Micronaut хоть и длиннее, зато предлагают больше возможностей. Добавим к этому классу аннотацию javax.inject.Singleton, объявим поля как private final и инициализируем их в конструкторе.

С другой стороны, имя внедренного бина уже ни на что не влияет, да и в целом возможности внедрения зависимостей в Micronaut богаче базовых возможностей Grails.

Вам потребуется обновить все оставшиеся точки внедрения в приложении Grails. Делается это почти так же, как и в случае конфигурационных объектов. Разница лишь в наличии аннотации @Inject перед объявлением поля.

class VehicleController {
    
    @Inject VehicleService vehicleService
    
    // controller actions

}

Интеграция Micronaut Grails

Поскольку теперь вместо сервисов и контроллеров Grails из бинов Micronaut вызывается GORM, мы должны указать, какие пакеты должен сканировать Micronaut. К сожалению, стандартная реализация GrailsApp не передает пакеты в контекст Micronaut, но здесь на выручку приходит библиотека Micronaut Grails:

Нам нужно добавить еще две зависимости в файл сборки приложения Grails — hello.gradle:

implementation 'com.agorapulse:micronaut-grails:3.0.7'
implementation 'com.agorapulse:micronaut-grails-web-boot:3.0.7'

Вдобавок нужно удалить следующую строку:

compile "org.grails:grails-web-boot"

Сейчас нам предстоит немного изменить класс Application, отвечающий за запуск приложения.

import com.agorapulse.micronaut.grails.MicronautGrailsApp
import com.agorapulse.micronaut.grails.MicronautGrailsAutoConfiguration

import groovy.transform.CompileStatic
import hello.legacy.Vehicle

@CompileStatic
class Application extends MicronautGrailsAutoConfiguration {

    final Collection<Package> packages = [
            Vehicle.package,
    ]

    static void main(String[] args) {
        MicronautGrailsApp.run(Application, args)
    }

}

Теперь он расширяет класс MicronautGrailsAutoConfiguration и использует MicronautGrailsApp для выполнения приложения.

Еще нам нужно обновить тесты. Чтобы и дальше пользоваться сервисами имитированных данных, нам нужно создать фабрику, производящую мок-объекты.

import groovy.transform.CompileStatic
import hello.legacy.VehicleDataService
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import spock.mock.DetachedMockFactory

import javax.inject.Singleton

@Factory
@CompileStatic
class MockDataServicesFactory {

    @Bean
    @Singleton
    VehicleDataService vehicleDataService() {
        return new DetachedMockFactory().Mock(VehicleDataService)
    }

}

После этого мы сможем внедрять мок-объекты в спецификацию посредством Autowire в Spring и AutowiredTest в Grail. Аннотация AutoAttach из Spock гарантирует, что мок-объект будет корректно прикреплен к спецификации.

import com.agorapulse.dru.Dru
import com.agorapulse.gru.Gru
import com.agorapulse.gru.grails.Grails
import com.fasterxml.jackson.databind.ObjectMapper
import grails.testing.gorm.DataTest
import grails.testing.spring.AutowiredTest
import grails.testing.web.controllers.ControllerUnitTest
import hello.legacy.Vehicle
import hello.legacy.VehicleDataService
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.AutoCleanup
import spock.lang.Specification
import spock.mock.AutoAttach


class VehicleControllerSpec extends Specification implements ControllerUnitTest<VehicleController>, DataTest, AutowiredTest{

    @AutoCleanup Dru dru = Dru.create {
        include HelloDataSets.VEHICLES
    }

    @AutoCleanup Gru gru = Gru.create(Grails.create(this)).prepare {
        include UrlMappings
    }

    @Autowired @AutoAttach VehicleDataService vehicleDataService

    @Override
    Closure doWithSpring() {
        return {
            objectMapper(ObjectMapper)
        }
    }

    void 'render with gru'() {
        given:
            dru.load()

        when:
             gru.test {
                get '/vehicle/1'
                expect {
                    json 'vehicle.json'
                }
            }

        then:
            gru.verify()

            1 * vehicleDataService.findById(1) >> dru.findByType(Vehicle)
    }

}

Плагины Grails и другие недостающие компоненты

Несмотря на мнимую простоту этого этапа, зачастую именно он вызывает больше всего трудностей в процессе миграции. Обычно ваши сервисы полагаются на какие-то плагины Grails, которым вам нужно подыскать аналоги для Micronaut. В поисках вдохновения вы можете обратиться к документации Micronaut, Agorapulse OSS Hub или к каталогу Awesome Micronaut:

 Следующий шаг — миграция самих контроллеров.


Часть 8. Контроллеры

На прошлом этапе мы мигрировали сервисы в отдельную библиотеку. В этой части мы займемся миграцией контроллеров Grails в аналогичные на Micronaut.

Сначала добавим декларативную обработку ошибок в существующий контроллер Grails.

import com.fasterxml.jackson.databind.ObjectMapper
import groovy.transform.CompileStatic
import hello.legacy.Vehicle
import hello.legacy.VehicleDataService

import javax.inject.Inject

@CompileStatic
class VehicleController {

    @Inject ObjectMapper objectMapper
    @Inject VehicleService vehicleService
    @Inject VehicleDataService vehicleDataService

    Object show(Long id) {
        Vehicle vehicle = vehicleDataService.findById(id)
        if (!vehicle) {
            throw new NoSuchElementException("No vehicle found for id: $id")
        }
        render contentType: 'application/json', text: objectMapper.writeValueAsString(
                new VehicleResponse(
                        id: vehicle.id,
                        name: vehicle.name,
                        make: vehicle.make,
                        model: vehicle.model
                )
        )
    }

    Object noSuchElement(NoSuchElementException e) {
        render status: 404
    }

}

Тест по-прежнему должен завершаться успехом.

Создадим новую библиотеку hello-api в папке libs с файлом сборки, аналогичным следующему файлу hello-api.gradle:


dependencies {
    api project(':hello-core')

    annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    annotationProcessor "io.micronaut:micronaut-inject-java"
    annotationProcessor "io.micronaut:micronaut-validation"

    implementation platform("io.micronaut:micronaut-bom:$micronautVersion")

    implementation "io.micronaut:micronaut-core"
    implementation "io.micronaut:micronaut-inject"
    implementation "io.micronaut:micronaut-validation"
    implementation "io.micronaut:micronaut-runtime"
    implementation "io.micronaut:micronaut-http"
    implementation "io.micronaut:micronaut-router"

    compileOnly "io.micronaut:micronaut-inject-groovy"

    testAnnotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    testAnnotationProcessor "io.micronaut:micronaut-inject-java"
    testAnnotationProcessor "io.micronaut:micronaut-validation"

    testImplementation project(':hello-data-test-data'), {
        // we'll be using pure mocks
        exclude group: 'com.agorapulse', module: 'dru-client-gorm'
    }

    testImplementation("org.spockframework:spock-core") {
        exclude group: "org.codehaus.groovy", module: "groovy-all"
    }

    testImplementation "io.micronaut:micronaut-inject-groovy"
    testImplementation "io.micronaut:micronaut-http-server-netty"
    testImplementation "io.micronaut.test:micronaut-test-spock"

    testImplementation "com.agorapulse:gru-micronaut:0.9.2"
}

tasks.withType(JavaExec) {
    classpath += configurations.developmentOnly
    jvmArgs('-noverify', '-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
}

tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
    options.compilerArgs.add('-parameters')
}

Файл сборки содержит все необходимое для создания и тестирования контроллеров Micronaut. Так как мы уже перешли на маршалинг Jackson, переписать контроллеры не составит особого труда. Основное отличие заключается в необходимости применения аннотаций для сопоставления HTTP-запросов.

import groovy.transform.CompileStatic
import hello.legacy.model.Vehicle
import hello.legacy.model.VehicleRepository
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@CompileStatic
@Controller('/vehicle')
class VehicleController {

    private final VehicleRepository vehicleRepository

    VehicleController(VehicleRepository vehicleDataService) {
        this.vehicleRepository = vehicleDataService
    }

    @Get('/{id}')
    Optional<VehicleResponse> show(Long id) {
        return vehicleRepository
            .findById(id)
            .map { vehicle -> toResponse(vehicle) }
    }

    private static VehicleResponse toResponse(Vehicle vehicle) {
        return new VehicleResponse(
            id: vehicle.id,
            name: vehicle.name,
            make: vehicle.make,
            model: vehicle.model
        )
    }
}

Сейчас внедрение происходит в конструкторе. Поскольку в Micronaut используется маршалинг Jackson, мы избавляемся от явного применения objectMapper. Класс VehicleReponse можно скопировать из существующего приложения Grails в неизменном виде. Micronaut возвращает ошибку 404 Not Found в случае пустого параметра Optional, следовательно, мы можем воспользоваться тем, что интерфейс репозитория теперь возвращает Optional.

В спецификациях требуются лишь незначительные изменения. Вы можете скопировать их из приложения Grails и сделать следующие модификации:

  • убрать все трейты, такие как ControllerUnitTest;

  • добавить аннотацию MicronautTest к классу;

  • заменить определение Gru на @Inject Gru gru;

  • назначить id загруженных сущностей, поскольку нами используется POGO-реализация Dru.

import com.agorapulse.dru.Dru
import com.agorapulse.gru.Gru
import hello.HelloDataSets
import hello.legacy.Vehicle
import hello.legacy.VehicleDataService
import io.micronaut.test.annotation.MicronautTest
import spock.lang.AutoCleanup
import spock.lang.Specification
import spock.mock.AutoAttach

import javax.inject.Inject

@MicronautTest
class VehicleControllerSpec extends Specification {

    @AutoCleanup Dru dru = Dru.create {
        include HelloDataSets.VEHICLES
    }

    @Inject Gru gru

    @Inject @AutoAttach VehicleDataService vehicleDataService

    void 'render with gru'() {
        given:
            dru.load()

        when:
            gru.test {
                get '/vehicle/1'
                expect {
                    json 'vehicle.json'
                }
            }

        then:
            gru.verify()

            1 * vehicleDataService.findById(1) >> dru.findByType(Vehicle).tap { id = 1 }
    }

}

Также нужно скопировать MockDataServiceFactory в проект hello-api. Файлы фикстур наподобие vehicle.json нужно скопировать из папок src/test/resources приложения Grails, чтобы возвращаемый JSON-объект имел прежний вид.

Продвинутая миграция

Миграция самих контроллеров — относительно простая процедура. Но вы можете столкнуться с отсутствием некоторых элементов, таких как HTTP-фильтры и плагины безопасности Grails. Подобрать подходящие аналоги поможет документация к Micronaut:

В следующей части мы создадим новое приложение Micronaut.


Часть 9. Приложение Micronaut

На этом этапе в нашем распоряжении должны быть все необходимые составляющие для миграции приложения. Значит, уже можно создать новое приложение Micronaut.

Создадим новый проект с суффиксом -mn (должно получиться что-то наподобие hello-mn) и поместим его в подпапку apps с файлом сборки hello-mn.gradle, имеющим примерно следующее содержимое:

plugins {
    id 'application'
    id 'com.github.johnrengelman.shadow' version '5.2.0'
}

dependencies {
    annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    annotationProcessor 'io.micronaut:micronaut-inject-java'
    annotationProcessor 'io.micronaut:micronaut-validation'

    implementation project(':hello-api')

    implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
    implementation 'io.micronaut:micronaut-http-client'
    implementation 'io.micronaut:micronaut-inject'
    implementation 'io.micronaut:micronaut-validation'
    implementation 'io.micronaut:micronaut-runtime'
    implementation 'io.micronaut:micronaut-http-server-netty'


    implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.14.1'
    implementation 'mysql:mysql-connector-java:8.0.26'



    compileOnly 'io.micronaut:micronaut-inject-groovy'

    testAnnotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion")
    testImplementation('org.spockframework:spock-core') {
        exclude group: 'org.codehaus.groovy', module: 'groovy-all'
    }
    testImplementation 'io.micronaut:micronaut-inject-groovy'
    testImplementation 'io.micronaut.test:micronaut-test-spock'
    testImplementation 'io.micronaut.test:micronaut-test-junit5'
}

mainClassName = 'hello.Application'

shadowJar {
    mergeServiceFiles()
    mergeGroovyExtensionModules()
}

tasks.withType(JavaCompile){
    options.encoding = 'UTF-8'
    options.compilerArgs.add('-parameters')
}

Подпроект может включать один класс, запускающий приложение.

import groovy.transform.CompileStatic
import hello.legacy.Vehicle
import io.micronaut.runtime.Micronaut

@CompileStatic
class Application {

    static void main(String[] args) {
        Micronaut.build(args)
                .mainClass(Application)
                .packages(Vehicle.package.name)
                .start()
    }

}

Обратите внимание на объявление пакета; в случае ошибки система не сможет найти классы предметной области.

Чтобы запустить приложение, можно создать для базы данных файл Docker Compose и поместить его в папку hello-mn:

services:
  db:
    image: mariadb
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_PASSWORD: grails
      MYSQL_USER: grails
      MYSQL_DATABASE: hello
    ports:
      - 3306:3306

В этом примере используется MariaDB в связи с лучшей совместимостью с последними процессорными чипами Apple.

Также потребуется создать базовый файл конфигурации и сохранить его как

dataSource:
  dbCreate: update
  url: jdbc:mysql://localhost:3306/hello?useSSL=false
  driverClassName: com.mysql.cj.jdbc.Driver
  dialect: org.hibernate.dialect.MySQL5InnoDBDialect
  username: grails
  password: grails

Кроме того, вы можете создать класс для предзагрузки некоторых данных в базу в качестве образца:

import groovy.transform.CompileStatic
import hello.legacy.Vehicle
import hello.legacy.VehicleDataService
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.runtime.event.ApplicationStartupEvent

import javax.inject.Singleton

@Singleton
@CompileStatic
class VehicleLoader implements ApplicationEventListener<ApplicationStartupEvent> {

    private final VehicleDataService vehicleDataService

    VehicleLoader(VehicleDataService vehicleDataService) {
        this.vehicleDataService = vehicleDataService
    }

    @Override
    void onApplicationEvent(ApplicationStartupEvent event) {
        if (vehicleDataService.findById(1) == null) {
            Vehicle vehicle = new Vehicle(
                    name: 'The Box',
                    make: 'Citroen',
                    model: 'Berlingo'
            )
            vehicleDataService.save(vehicle)
        }
    }
}

Класс реализует событие ApplicationEventListener<ApplicationStartupEvent>, выполняющее код сразу после успешного запуска приложения.

Затем необходимо запустить файл Docker Compose из папки hello-mn:

docker compose up -d

После этого станет возможным запуск приложения из корневой папки проекта:

./gradlew :hello-mn:run

Если сделать запрос на сервер с помощью cURL или какого-нибудь HTTP-клиента, вы получите стандартный ответ:

curl http://localhost:8080/vehicle/1
# {"id":1,"name":"The Box","make":"Citroen","model":"Berlingo"}

Далее нам предстоит пересмотреть остальное содержимое приложения Grails. При этом можно удалить контроллеры и связанные с ними тесты.

Обычно еще нужно перенести файл application.groovy в виде серии файлов application.yml. Обратите внимание, что блоки environment { ... } не поддерживаются в Micronaut — вам придется их перенести в отдельные файлы application-development.yml и application-production.yml.

Если на этом этапе у вас остались какие-то другие элементы в старом приложении Grails, обязательно сообщите нам об этом. Мы постараемся описать такие случаи в приложениях к статьям.

Хотя приложение Micronaut готово к работе, остался последний шаг — мигрировать базу данных из GORM в Micronaut.


Часть 10. Micronaut Data

До этого момента мы сочетали Micronaut с сущностями GORM. Такая комбинация далека от оптимальной, но поскольку больше всего трудностей ожидается именно в момент перехода с GORM на Micronaut Data, лучше всего переносить уровень данных в самую последнюю очередь.

Micronaut Data предлагает две реализации — Micronaut Data JPA и Micronaut Data JDBC. Давайте рассмотрим их, начав с более легковесной — Micronaut Data JDBC.

Micronaut Data JDBC

Micronaut Data JDBC хорошо подойдет для:

  • сущностей, в которые ведется исключительно запись, таких как журналы, метрики и другие аналитические данные;

  • очень простых объектных моделей с минимальным количеством связей или вообще без них;

  • приложений с ограниченным параллелизмом.

Метод save в Micronaut Data JDBC всегда порождает новую строку в базе данных. Здесь нет встроенного эквивалента saveOrUpdate, что вынуждает нас постоянно проверять, а не существует ли уже такая строка в базе данных, чтобы затем вызывать метод save или update. Такой подход не очень подходит для сущностей, регулярно требующих проверки совпадения с имеющимися записями во время сохранения или обновления. Кроме того, методы update* обычно требуют перечисления всех свойств, подлежащих обновлению.

С другой стороны, такая близость Micronaut Data JDBC к базе данных позволяет успешно применять это решение для работы с сущностями, которые полагаются на множество нативных SQL-запросов, таких как упомянутые ранее аналитические данные.

Реализация Micronaut Data JDBC требует явного указания соединений для обработки связей между сущностями. Например, если в следующем примере будет отсутствовать аннотация @Join, тогда в поле author на странице подхватится только его id.

@JdbcRepository(dialect = Dialect.MYSQL)
public interface PageRepository extends CrudRepository<Page, Long> {      
    
    @Override
    @Join(value = "author", type = Join.Type.FETCH)       
    Optional<Page> findById(@NonNull @NotNull Long aLong);
}

Micronaut Data JDBC не поддерживает оптимистическую блокировку (optimistic locking), поэтому единственный способ гарантировать актуальность данных — использовать транзакционные операции чтения и записи.

В целом Micronaut Data JDBC применяется в узкоспециализированных случаях. Обычно миграция осуществляется на Micronaut Data JPA.

Micronaut Data JPA

Micronaut Data JPA обеспечивает максимально тесное взаимодействие с GORM. В основе обоих фреймворков лежит Hibernate со всеми его достоинствами и недостатками. С одной стороны — самые продвинутые возможности объектно-реляционного отображения, с другой — распространенные проблемы с недоступностью сессий, ленивой загрузкой и проксированием.

Micronaut Data JPA предлагает больше возможностей и лучше вписывается в проекты, нуждающиеся в такой расширенной функциональности ORM, как:

  • ленивая загрузка и проксирование связей;

  • механизм «грязной» проверки (dirty checking);

  • кэширование первого уровня и проксирование сущностей;

  • оптимистическая блокировка.

Миграция из GORM в Micronaut Data

Как в случае с Micronaut Data JPA, так и в случае с Micronaut Data JDBC вам доступна библиотека-генератор, облегчающая миграцию.

Прежде всего, добавим суффикс -legacy к именам папки проекта данных и его файла сборки. Например, папка hello-data превратится в hello-data-legacy, а файл hello-data.gradle — в hello-data-legacy.gradle. Не забудьте также указать новое имя проекта в остальных файлах сборки.

Затем мы добавим новую зависимость в файл сборки hello-data-legacy.gradle:

sourceSets {
    main {
        groovy {
            // the source folder for the GORM domain classes
            srcDir 'grails-app/domain'
            // if you also want to include some services
            srcDir 'grails-app/services'
        }
    }
}

dependencies {
    api platform("io.micronaut:micronaut-bom:$micronautVersion")
    api 'io.micronaut.configuration:micronaut-hibernate-gorm'

    // GORM
    compile "org.grails:grails-datastore-gorm-hibernate5:${project['gorm.hibernate.version']}"

    // required for Grails Plugin generation
    compileOnly "org.grails:grails-core:$grailsVersion"

    // required for Micronaut service generation, if present
    compileOnly "io.micronaut:micronaut-inject-groovy:$micronautVersion"

    // required for jackson ignores generation for Micronaut
    compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.8.11.3'

    testImplementation("org.spockframework:spock-core") {
        exclude group: "org.codehaus.groovy", module: "groovy-all"
    }

    // generator library
    testImplementation 'com.agorapulse:micronaut-grails-jpa-generator:3.0.6'

    // MariaDB support for Testcontainers
    // more universal than MySQL as it works well even on M1 processors
    testImplementation 'org.mariadb.jdbc:mariadb-java-client:2.7.3'
    testImplementation "org.testcontainers:mariadb:1.15.3"

    // only required for M1 processors, due issues in 1.15.3
    testImplementation 'com.github.docker-java:docker-java-api:3.2.8'
    testImplementation 'com.github.docker-java:docker-java-transport-zerodep:3.2.8'
    testImplementation 'net.java.dev.jna:jna:5.8.0'
}

test {
    systemProperty 'project.root', rootDir.absolutePath
}

Мы добавили зависимости в генератор JPA и Testcontainers. Кроме того, мы экспортируем корневой каталог проекта в качестве системного свойства project.root.

Теперь нам предстоит протестировать конфигурацию источника данных в hello-data-legacy/src/test/resources/application-test.yml.

dataSource:
  dbCreate: "create-drop"
  url: "jdbc:tc:mariadb:10.5.10:///test?useSSL=false"
  driverClassName: "org.testcontainers.jdbc.ContainerDatabaseDriver"
  dialect: org.hibernate.dialect.MySQL5InnoDBDialect
  username: "username"
  password: "password"

И проверить MicronautGeneratorSpec, который должен сгенерировать мигрированные классы сущностей.

class MicronautGeneratorSpec extends Specification {

    @AutoCleanup ApplicationContext context = ApplicationContext.run()

    MicronautJpaGenerator generator = new MicronautJpaGenerator(
        context.getBean(HibernateDatastore),
        new DefaultConstraintEvaluator()
    )

    void 'generate domains'() {
        given:
            File root = new File(System.getProperty('project.root'), 'libs/hello-data/src/main/groovy')
        when:
            generator.generate(root)
        then:
            noExceptionThrown()
    }

}

После запуска теста классы новых сущностей и репозитория сгенерируются в папке libs/hello-data в соответствии с названием проекта.

Исходные классы предметной области, такие как Vehicle, теперь должны иметь свой JPA-аналог и класс репозитория:

import grails.gorm.annotation.Entity

@Entity
class Vehicle {

    String name

    String make
    String model

    static Closure constraints = {
        name maxSize: 255
        make inList: ['Ford', 'Chevrolet', 'Nissan', 'Citroen']
        model nullable: true
    }

}

Исходный класс предметной области

import groovy.transform.CompileStatic
import javax.annotation.Nullable
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Version
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

@Entity
@CompileStatic
class Vehicle {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id

    @Version
    Long version

    @NotNull
    @Pattern(regexp = 'Ford|Chevrolet|Nissan|Citroen')
    @Size(max = 255)
    String make

    @Nullable
    @Size(max = 255)
    String model

    @NotNull
    @Size(max = 255)
    String name

}

Сгенерированная сущность

import io.micronaut.data.annotation.Repository
import io.micronaut.data.repository.CrudRepository

@Repository
interface VehicleRepository extends CrudRepository<Vehicle, Long> {

}

Сгенерированный интерфейс репозитория

Проверьте сгенерированный код на предмет пометок «TODO» и убедитесь, что весь нужный код на месте. Алгоритм генератора пытается по возможности перенести все, но в редких и нестандартных случаях возможны упущения.

Чтобы включить модуль, требуется подходящий файл сборки hello-data.gradle:

dependencies {
    annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    annotationProcessor 'io.micronaut.data:micronaut-data-processor'
    annotationProcessor 'io.micronaut:micronaut-inject-java'
    annotationProcessor 'io.micronaut:micronaut-validation'

    api platform("io.micronaut:micronaut-bom:$micronautVersion")
    api 'io.micronaut.data:micronaut-data-hibernate-jpa'

    api 'jakarta.persistence:jakarta.persistence-api:2.2.2'

    implementation 'io.micronaut:micronaut-core'
    implementation 'io.micronaut:micronaut-inject'
    implementation 'io.micronaut:micronaut-validation'
    implementation 'io.micronaut:micronaut-runtime'

    implementation 'io.micronaut.configuration:micronaut-jdbc-tomcat'

    compileOnly 'io.micronaut:micronaut-inject-groovy'
    compileOnly 'io.micronaut.data:micronaut-data-processor'
}

Также мы должны упростить скрипт convention.groovy, чтобы он не распространял правила GORM на новые сгенерированные классы:

Map conventions = [
        disable                     : false,
        whiteListScripts            : true,
        disableDynamicCompile       : false,
        dynamicCompileWhiteList     : [
                'UrlMappings',
                'Application',
                'BootStrap',
                'resources',
                'org.grails.cli'
        ],
        limitCompileStaticExtensions: false,
        defAllowed                  : false,    // For controllers you can use Object in place of def, and in Domains add Closure to constraints/mappings closure fields.
        skipDefaultPackage          : true,     // For GSP files
]
System.setProperty(
        'enterprise.groovy.conventions',
        "conventions=${conventions.inspect()}"
)

После проверки новых сущностей и репозиториев добавьте аннотацию @Ingore в MicronautGeneratorSpec, чтобы исключить перезапись сущностей.

Теперь можно перейти к тестовым данным. Скопируйте весь каталог с тестовыми данными по тому же принципу, по которому мы переименовывали проект модели данных, например, hello-data-test-data следует скопировать как папку hello-data-legacy-test-data.

Сейчас давайте изменим исходный файл сборки hello-data-test-data.gradle, чтобы подготовить тестовые данные с помощью Micronaut JPA.

dependencies {
    api project(':hello-data')

    api "com.agorapulse:dru-client-micronaut-data:0.8.1"
    api "com.agorapulse:dru-parser-json:0.8.1"

    compileOnly "io.micronaut:micronaut-inject-groovy:$micronautVersion"

    testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion")

    testImplementation "io.micronaut:micronaut-inject-groovy:$micronautVersion"

    testImplementation "io.micronaut.test:micronaut-test-spock"
    testImplementation("org.spockframework:spock-core") {
        exclude group: "org.codehaus.groovy", module: "groovy-all"
    }

    testImplementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.14.1'

    // MariaDB support for Testcontainers
    // more universal than MySQL as it works well even on M1 processors
    testImplementation 'org.mariadb.jdbc:mariadb-java-client:2.7.3'
    testImplementation "org.testcontainers:mariadb:1.15.3"

    // only required for M1 processors, due issues in 1.15.3
    testImplementation 'com.github.docker-java:docker-java-api:3.2.8'
    testImplementation 'com.github.docker-java:docker-java-transport-zerodep:3.2.8'
    testImplementation 'net.java.dev.jna:jna:5.8.0'
}

Также нам нужно прописать новую тестовую конфигурацию в файле src/test/resources/application-test.yml:

datasources:
  default:
    url: jdbc:tc:mariadb:10.5.10:///test?useSSL=false&TC_DAEMON=true
    driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
    username: grails
    password: grails
    dialect: MYSQL
jpa:
  default:
    entity-scan:
      packages: 'hello.legacy.model'
    properties:
      hibernate:
        hbm2ddl:
          auto: create-drop

Изменения в тесте, проверяющем модель данных, будут минимальными. Класс спецификации теперь сопровождается аннотацией @MicronautTest, он реализует ApplicationContextProvider, внедряет ApplicationContext и класс репозитория.

import com.agorapulse.dru.Dru
import hello.legacy.model.Vehicle
import hello.legacy.model.VehicleRepository
import io.micronaut.context.ApplicationContext
import io.micronaut.context.ApplicationContextProvider
import io.micronaut.test.annotation.MicronautTest
import spock.lang.AutoCleanup
import spock.lang.Specification

import javax.inject.Inject

@MicronautTest
class HelloDataSetsSpec extends Specification implements ApplicationContextProvider {

    @AutoCleanup Dru dru = Dru.create(this)

    @Inject ApplicationContext applicationContext
    @Inject VehicleRepository vehicleRepository

    void 'vehicles are loaded'() {
        given:
            dru.load(HelloDataSets.VEHICLES)
        when:
            Vehicle box = vehicleRepository.findByName('The Box')
        then:
            box
            box.name == 'The Box'
            box.make == 'Citroen'
            box.model == 'Berlingo'
    }

}

На текущий момент у нас есть новая модель данных, реализованная на Micronaut Data вместо GORM. На следующем этапе мы полностью удалим устаревшую реализацию GORM и перейдем на новую модель данных.

Одновременная работа GORM и Micronaut Data возможна, но неофициальными обходными путями. Следовательно, лучше полностью перейти на новую модель данных. Удаляем устаревшую модель данных, тестируем каталоги данных и заменяем зависимости проекта с учетом новой модели. Также удалите старое приложение Grails, если вы еще этого не сделали. Устраните все ошибки компиляции. Для этого потребуется немного модифицировать сущности или репозитории. Повторяйте следующую команду до полной замены всех вхождений.

./gradlew classes testClasses

Для наглядности можете посмотреть пример изменений в проекте здесь.

Что дальше? В описанном случае выбор версии Micronaut был ограничен ввиду необходимости поддержки Grails, поэтому в дальнейшем логично было бы заняться переходом на актуальную версию Micronaut. Мы не будем рассматривать этот процесс в рамках данного цикла статей, но если вы столкнетесь с библиотеками, созданными для Micronaut 1.x, вам может помочь следующая статья.

Оригиналы публикаций: часть 7, часть 8, часть 9, часть 10.


Материал подготовлен в рамках курса «Groovy Developer». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Источник: https://habr.com/ru/company/otus/blog/593989/


Интересные статьи

Интересные статьи

VUE.JS - это javascript фрэймворк, с версии 18.5 его добавили в ядро битрикса, поэтому можно его использовать из коробки.
Устраивать конкурсы в инстаграме сейчас модно. И удобно. Инстаграм предоставляет достаточно обширный API, который позволяет делать практически всё, что может сделать обычный пользователь ручками.
Несмотря на то, что “в коробке” с Битриксом уже идут модули как для SOAP (модуль “Веб сервисы” в редакции “Бизнес” и старше), так и для REST (модуль “Rest API” во всех редакциях, начиная с...
Cтатья будет полезна тем, кто думает какую выбрать CMS для интернет-магазина, сравнивает различные движки, ищет в них плюсы и минусы важные для себя.
В статье описаны необходимые параметры сервера для оптимальной работы сайта на платформе 1С-Битрикс.