Quarkus: модернизация приложений на примере helloworld из JBoss EAP Quickstart

    Привет всем в этом блоге, и с вами четвертый пост из серии про Quarkus! (Кстати, смотрите наш вебинар «Это Quarkus – Kubernetes native Java фреймворк». Покажем, как начать «с нуля» или перенести готовые решения)



    Предыдущий пост был о том, как Quarkus объединяет MicroProfile и Spring. Напомним, что Quarkus позиционируется как «сверхбыстрая субатомная Java», он же «Kubernetes-ориентированный Java-стек, заточенный под GraalVM и OpenJDK HotSpot и собранный из лучших библиотек и стандартов». Сегодня мы покажем, как модернизировать уже имеющиеся Java-приложения, задействуя возможности Quarkus, на примере приложения helloworld из репозитория Red Hat JBoss Enterprise Application Platform (JBoss EAP) Quickstart, в котором используются поддерживаемые в Quarkus технологии CDI и Servlet 3.

    Здесь важно отметить, что и Quarkus, и JBoss EAP делают упор на том, чтобы использовать инструменты, максимально построенные на базе стандартов. У вас нет приложения, работающего на JBoss EAP? Не проблема, его можно легко перенести с текущего сервера приложений на JBoss EAP с помощью Red Hat Application Migration Toolkit. После чего финальная и рабочая версия модернизированного кода будет доступна в репозитории github.com/mrizzi/jboss-eap-quickstarts/tree/quarkus, в модуле helloworld.

    При написании этого поста использовались руководства по Quarkus, в основном Creating Your First Application и Building a Native Executable.

    Обзаводимся кодом


    Первым делом создадим локальный клон репозитория JBoss EAP quickstarts:

    $ git clone https://github.com/jboss-developer/jboss-eap-quickstarts.git
    Cloning into 'jboss-eap-quickstarts'...
    remote: Enumerating objects: 148133, done.
    remote: Total 148133 (delta 0), reused 0 (delta 0), pack-reused 148133
    Receiving objects: 100% (148133/148133), 59.90 MiB | 7.62 MiB/s, done.
    Resolving deltas: 100% (66476/66476), done.
    $ cd jboss-eap-quickstarts/helloworld/
    

    Смотрим, как работает исходный helloworld


    Собственно, суть этого приложения понятна из названия, но мы будем модернизировать его код строго научно. Поэтому для начала посмотрим на это приложение в исходном его виде.

    Разворачиваем helloworld

    1. Открываем терминал и переходим в корень папки JBoss EAP (его можно загрузить здесь), то есть в папку EAP_HOME.

    2. Запускаем сервер JBoss EAP с дефолтным профилем:

    $ EAP_HOME/bin/standalone.sh
    

    Примечание: на Windows для запуска используется сценарий EAP_HOME\bin\standalone.bat.

    Через пару секунд в логе должно появится примерно следующее:

    [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: JBoss EAP 7.2.0.GA (WildFly Core 6.0.11.Final-redhat-00001) started in 3315ms - Started 306 of 527 services (321 services are lazy, passive or on-demand)
    

    3. Открываем в браузере 127.0.0.1:8080 и видим вот это:



    Рис. 1. Домашняя страница JBoss EAP.

    4. Следуем инструкциям в руководстве Build and Deploy the Quickstart: разворачиваем helloworld и выполняем (из корневой папки проекта) следующую команду:

    $ mvn clean install wildfly:deploy
    

    После успешного выполнения этой команды в логе увидим примерно следующее:

    [INFO] ------------------------------------------------------------------------ 
    [INFO] BUILD SUCCESS 
    [INFO] ------------------------------------------------------------------------ 
    [INFO] Total time: 8.224 s
    

    Итак, первое развертывание приложения helloworld на JBoss EAP заняло чуть больше 8 секунд.

    Тестируем helloworld

    Действуя строго по руководству Access the Application, открываем в браузере 127.0.0.1:8080/helloworld и видим вот что:



    Рис. 2. Исходный Hello World из JBoss EAP.

    Вносим изменения

    Меняем входной параметр createHelloMessage(String name) c World на Marco:

    writer.println("<h1>" + helloService.createHelloMessage("Marco") + "</h1>");
    

    Опять выполняем следующую команду:

    $ mvn clean install wildfly:deploy
    

    Затем обновляем страницу в браузере и видим, что текст изменился:



    Рис. 3. Hello Marco в JBoss EAP.

    Откатываем развертывание helloworld и завершаем работу JBoss EAP

    Это необязательно, но если вы хотите отменить развертывание, то это можно сделать следующей командой:

    $ mvn clean install wildfly:undeploy
    

    Чтобы завершить работу экземпляра JBoss EAP, просто нажмите Ctrl+C в окне терминала.

    Модернизируем helloworld


    Теперь займемся модернизацией исходного приложения helloworld.

    Создаем новую ветку

    Создаем новую рабочую ветку после того, как закончится выполнение проекта quickstart:

    $ git checkout -b quarkus 7.2.0.GA
    

    Меняем файл pom.xml

    Приложение мы начнем менять с файла pom.xml. Чтобы Quarkus мог вставлять в него XML-блоки, выполним следующую команду в папке helloworld:

    $ mvn io.quarkus:quarkus-maven-plugin:0.23.2:create
    

    При написании этой статьи использовалась версия 0.23.2. У Quarkus часто выходят новые версии, узнать, какая версия является самой последней, можно на сайте github.com/quarkusio/quarkus/releases/latest.

    Приведенная выше команда вставит в pom.xml следующие элементы:

    • Свойство <quarkus.version>, задающее используемую версию Quarkus.
    • Блок <dependencyManagement> для импорта Quarkus BOM (bill of materials), чтобы не добавлять версию для каждой зависимости Quarkus.
    • Плагин quarkus-maven-plugin, отвечающий за упаковку приложения и предоставляющий режим development mode.
    • Профиль native для создания исполняемых файлов приложения.

    Кроме того, в pom.xml мы вручную вносим следующие изменения:

    1. Вытаскиваем тег <groupId> из блока <parent> и размещаем его выше тега <artifactId>. Поскольку на следующем шаге мы удалим блок <parent>, то надо сохранить <groupId>.
    2. Удаляем блок <parent>, поскольку при работе с Quarkus этому приложению больше не понадобится родительский pom от JBoss.
    3. Добавляем тег <version> и размещаем его под тегом <artifactId>. Номер версии можно указать какой хотите.
    4. Удаляем тег <packaging>, поскольку это приложение больше не WAR, а обычный JAR.
    5. Модифицируем следующие зависимости:
      1. Меняем зависимость javax.enterprise:cdi-api на io.quarkus:quarkus-arc, удаляя <scope>provided</scope>, поскольку (согласно докам) это Quarkus-расширение обеспечивает injection зависимости CDI.
      2. Меняем зависимость org.jboss.spec.javax.servlet:jboss-servlet-api_4.0_spec на io.quarkus:quarkus-undertow, удаляя <scope>provided</scope>, поскольку (согласно докам) это Quarkus-расширение обеспечивает поддержку servlet’ов.
      3. Удаляем зависимость org.jboss.spec.javax.annotation:jboss-annotations-api_1.3_spec, поскольку они идет в комплекте с зависимостями, которые мы только что изменили.


    Версия файла pom.xml со всеми изменениями лежит по адресу github.com/mrizzi/jboss-eap-quickstarts/blob/quarkus/helloworld/pom.xml.

    Обратите внимание, что приведенная выше команда mvn io.quarkus:quarkus-maven-plugin:0.23.2:create не только меняет файл pom.xml, но и добавляет в проект ряд компонентов, а именно, следующие файлы и папки:

    • Файл mvnw and mvnw.cmd и папку .mvn: Maven Wrapper позволяет запускать проекты Maven заданной версии Maven без установки этой самой версии.
    • Папка docker (в каталоге src/main/): здесь лежат примеры файлов Dockerfile для режимов native и jvm (вместе с файлом .dockerignore).
    • Папка resources (в каталоге src/main/): здесь лежит пустой файл application.properties и образец стартовой страницы Quarkus index.html (подробнее см. Run the modernized helloworld ).

    Запускаем helloworld
    Чтобы протестировать приложение, используем quarkus:dev, который запускает Quarkus в режиме development mode (подробнее см. вот этот раздел в руководстве по Development Mode).

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

    Теперь запускаем команду, чтобы проверить, как это сработает:

    $ ./mvnw compile quarkus:dev
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ----------------< org.jboss.eap.quickstarts:helloworld >----------------
    [INFO] Building Quickstart: helloworld quarkus
    [INFO] --------------------------------[ war ]---------------------------------
    [INFO]
    [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ helloworld ---
    [INFO] Using 'UTF-8' encoding to copy filtered resources.
    [INFO] Copying 2 resources
    [INFO]
    [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ helloworld ---
    [INFO] Nothing to compile - all classes are up to date
    [INFO]
    [INFO] --- quarkus-maven-plugin:0.23.2:dev (default-cli) @ helloworld ---
    Listening for transport dt_socket at address: 5005
    INFO  [io.qua.dep.QuarkusAugmentor] Beginning quarkus augmentation
    INFO  [org.jbo.threads] JBoss Threads version 3.0.0.Final
    ERROR [io.qua.dev.DevModeMain] Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    	[error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: javax.enterprise.inject.spi.DeploymentException: javax.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type org.jboss.as.quickstarts.helloworld.HelloService and qualifiers [@Default]
    	- java member: org.jboss.as.quickstarts.helloworld.HelloWorldServlet#helloService
    	- declared on CLASS bean [types=[javax.servlet.ServletConfig, java.io.Serializable, org.jboss.as.quickstarts.helloworld.HelloWorldServlet, javax.servlet.GenericServlet, javax.servlet.Servlet, java.lang.Object, javax.servlet.http.HttpServlet], qualifiers=[@Default, @Any], target=org.jboss.as.quickstarts.helloworld.HelloWorldServlet]
    	at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:841)
    	at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:214)
    	at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:106)
    	at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:249)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at io.quarkus.deployment.ExtensionLoader$1.execute(ExtensionLoader.java:780)
    	at io.quarkus.builder.BuildContext.run(BuildContext.java:415)
    	at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    	at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2011)
    	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1535)
    	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1426)
    	at java.lang.Thread.run(Thread.java:748)
    	at org.jboss.threads.JBossThread.run(JBossThread.java:479)
    Caused by: javax.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type org.jboss.as.quickstarts.helloworld.HelloService and qualifiers [@Default]
    	- java member: org.jboss.as.quickstarts.helloworld.HelloWorldServlet#helloService
    	- declared on CLASS bean [types=[javax.servlet.ServletConfig, java.io.Serializable, org.jboss.as.quickstarts.helloworld.HelloWorldServlet, javax.servlet.GenericServlet, javax.servlet.Servlet, java.lang.Object, javax.servlet.http.HttpServlet], qualifiers=[@Default, @Any], target=org.jboss.as.quickstarts.helloworld.HelloWorldServlet]
    	at io.quarkus.arc.processor.Beans.resolveInjectionPoint(Beans.java:428)
    	at io.quarkus.arc.processor.BeanInfo.init(BeanInfo.java:371)
    	at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:206)
    	... 14 more
    

    Так, не работает… А почему?

    Исключение UnsatisfiedResolutionException указывает на класс HelloService, который является членом класса HelloWorldServlet (java member: org.jboss.as.quickstarts.helloworld.HelloWorldServlet#helloService). Проблема в том, что HelloWorldServlet нужен инжектированный экземпляр HelloService, а его не получается найти (хотя оба этих класса и находятся в одном и том же пакете).

    Самое время вернуться к документации и почитать, как в Quarkus работает Inject, а следовательно, и Contexts and Dependency Injection (CDI). Поэтому открываем руководство Contexts and Dependency Injection и в разделе Bean Discovery читаем: «Bean-класс, у которого нет определяющей bean аннотации, не ищется».

    Смотрим класс HelloService – в нем действительно нет такой аннотации. Поэтому ее надо добавить, чтобы Quarkus мог искать и находить bean. И поскольку это stateless-объект, мы вполне можем добавить аннотацию @ApplicationScoped следующим образом:

    @ApplicationScoped
    public class HelloService {
    

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

    import javax.enterprise.context.ApplicationScoped;
    

    Если сомневаетесь, какую область scope надо использовать в том случае, когда для исходного bean’а она не задана вообще, проштудируйте документацию JSR 365: Contexts and Dependency Injection for Java 2.0—Default scope.

    Теперь опять пробуем запустить приложение командой ./mvnw compile quarkus:dev:

    $ ./mvnw compile quarkus:dev
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ----------------< org.jboss.eap.quickstarts:helloworld >----------------
    [INFO] Building Quickstart: helloworld quarkus
    [INFO] --------------------------------[ war ]---------------------------------
    [INFO]
    [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ helloworld ---
    [INFO] Using 'UTF-8' encoding to copy filtered resources.
    [INFO] Copying 2 resources
    [INFO]
    [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ helloworld ---
    [INFO] Changes detected - recompiling the module!
    [INFO] Compiling 2 source files to /home/mrizzi/git/forked/jboss-eap-quickstarts/helloworld/target/classes
    [INFO]
    [INFO] --- quarkus-maven-plugin:0.23.2:dev (default-cli) @ helloworld ---
    Listening for transport dt_socket at address: 5005
    INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
    INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 576ms
    INFO  [io.quarkus] (main) Quarkus 0.23.2 started in 1.083s. Listening on: http://0.0.0.0:8080
    INFO  [io.quarkus] (main) Profile dev activated. Live Coding activated.
    INFO  [io.quarkus] (main) Installed features: [cdi]
    

    Теперь все проходит без ошибок.

    Запускаем модернизированный helloworld
    Как и написано в логе, открываем в браузере 0.0.0.0:8080 (стартовая страница Quarkus по умолчанию) и видим вот что:



    Рис. 4. Стартовая страница Quarkus dev.

    В аннотации WebServlet у этого приложения прописано следующее определение контекста:

    @WebServlet("/HelloWorld")
    public class HelloWorldServlet extends HttpServlet {
    

    Поэтому идем в браузере на 0.0.0.0:8080/HelloWorld и видим следующее:



    Рис. 5: Страница The Quarkus dev для приложения Hello World.

    Ну вот, всё работает.

    А теперь вносим изменения в код. Обратите внимание, что команда ./mvnw compile quarkus:dev все еще работает, и мы не собираемся ее останавливать. Теперь попробуем применить те же самые – тривиальнейшие – изменения к самому коду и посмотрим, как Quarkus облегчает жизнь разработчику:

    writer.println("<h1>" + helloService.createHelloMessage("Marco") + "</h1>");
    

    Сохраняем файл и затем обновляем веб-страницу, чтобы увидеть надпись Hello Marco, как показано на скрине ниже:



    Рис. 6. Страница Hello Marco в Quarkus dev.

    Теперь проверим вывод в терминале:

    INFO  [io.qua.dev] (vert.x-worker-thread-3) Changed source files detected, recompiling [/home/mrizzi/git/forked/jboss-eap-quickstarts/helloworld/src/main/java/org/jboss/as/quickstarts/helloworld/HelloWorldServlet.java]
    INFO  [io.quarkus] (vert.x-worker-thread-3) Quarkus stopped in 0.003s
    INFO  [io.qua.dep.QuarkusAugmentor] (vert.x-worker-thread-3) Beginning quarkus augmentation
    INFO  [io.qua.dep.QuarkusAugmentor] (vert.x-worker-thread-3) Quarkus augmentation completed in 232ms
    INFO  [io.quarkus] (vert.x-worker-thread-3) Quarkus 0.23.2 started in 0.257s. Listening on: http://0.0.0.0:8080
    INFO  [io.quarkus] (vert.x-worker-thread-3) Profile dev activated. Live Coding activated.
    INFO  [io.quarkus] (vert.x-worker-thread-3) Installed features: [cdi]
    INFO  [io.qua.dev] (vert.x-worker-thread-3) Hot replace total time: 0.371s
    

    Обновление страницы триггернуло детектирование изменений в исходном коде, и Quarkus автоматически выполнил процедуру «стоп-старт». И все это выполнилось всего за 0.371 секунды (вот она, та самая «сверхбыстрая субатомная Java»).

    Выполняем сборку helloworld в пакет JAR
    Теперь, когда код работает как надо, упакуем его следующей командой:

    $ ./mvnw clean package
    

    Эта команда создает два JAR-файла в папке /target: файл helloworld-.jar, который представляет собой стандартный артефакт, собранный Maven-командой вместе классами и ресурсами проекта. И файл helloworld--runner.jar, представляющий собой исполняемый JAR.

    Обратите внимание, что это не убер-jar, поскольку все зависимости просто скопированы в папку /target/lib (а не упакованы в файл JAR). Поэтому, чтобы запустить это JAR из другой папки или на другом хосте, туда надо скопировать как сам JAR-файл, так и папку /lib, учитывая что элемент Class-Path в файле MANIFEST.MF в пакете JAR содержит явное перечисление JAR’ов из папки lib.
    Чтобы узнать, как создавать приложения убер-jar, обратитесь к руководству Uber-Jar Creation.

    Запускаем helloworld, упакованный в JAR

    Теперь можно запускать наш JAR с помощью стандартной команды java:

    $ java -jar ./target/helloworld-<version>-runner.jar
    INFO  [io.quarkus] (main) Quarkus 0.23.2 started in 0.673s. Listening on: http://0.0.0.0:8080
    INFO  [io.quarkus] (main) Profile prod activated.
    INFO  [io.quarkus] (main) Installed features: [cdi]
    

    После того, как все это выполнится, зайдите в браузере на 0.0.0.0:8080 и проверьте, что все работает как надо.

    Собираем helloworld в нативный исполняемый файл

    Итак, наш helloworld работает как standalone-приложение Java, используя зависимости Quarkus. Но можно пойти дальше и превратить его в нативный исполняемый файл.

    Устанавливаем GraalVM
    Прежде всего, для этого надо установить необходимые инструменты:

    1. Скачиваем GraalVM 19.2.0.1 с github.com/oracle/graal/releases/tag/vm-19.2.0.1.

    2. Разворачиваем загруженный архив:

    $ tar xvzf graalvm-ce-linux-amd64-19.2.0.1.tar.gz
    

    3. Идем в папку untar.

    4. Запускаем приведенную ниже команду, чтобы скачать и добавить нативный образ:

    $ ./bin/gu install native-image
    

    5. Прописываем папку, созданную на шаге 2, в переменную среды GRAALVM_HOME:

    $ export GRAALVM_HOME={untar-folder}/graalvm-ce-19.2.0.1)
    

    Дополнительные сведения и инструкции по установке на других ОС можно найти в руководстве Building a Native Executable—Prerequisites.

    Выполняем сборку helloworld в нативный исполняемый файл
    Читаем руководство Building a Native Executable—Producing a native executable: «А теперь создадим нативный исполняемый файл для нашего приложения, чтобы сократить время его запуска и размер на диске. Исполняемый файл будет иметь все необходимое для запуска приложения, включая JVM-машину (вернее, ее усеченную версию, содержащую лишь то, что требуется для выполнения приложения) и само наше приложение».

    Чтобы создать нативный исполняемый файл, надо включить native-профиль Maven:

    $ ./mvnw package -Pnative
    

    У нас сборка заняла одну минуту и 10 секунд, а итоговый файл helloworld--runner f был создан в папке /target.

    Запускаем нативный исполняемый файл helloworld

    На предыдущем шаге мы получили исполняемый файл /target/helloworld--runner. Теперь запустим его:

    $ ./target/helloworld-<version>-runner
    INFO  [io.quarkus] (main) Quarkus 0.23.2 started in 0.006s. Listening on: http://0.0.0.0:8080
    INFO  [io.quarkus] (main) Profile prod activated.
    INFO  [io.quarkus] (main) Installed features: [cdi]
    

    Опять открываем в браузере 0.0.0.0:8080 и проверяем, что все работает как надо.

    Продолжение следует!

    Мы считаем, что рассмотренный в этом посте (пусть и на простейшем примере) метод модернизации Java-приложений с использованием возможностей Quarkus стоит активно применять в реальном жизни. При этом вы, скорее всего, столкнетесь с рядом проблем, решение которых мы частично рассмотрим в следующем посте, где речь пойдет о том, как замерять расход памяти для оценки улучшения производительности, важной части всего процесс модернизации приложений.

    При этом вы, скорее всего, столкнетесь с рядом проблем, решение которых мы частично рассмотрим в следующем, заключительном, посте нашей серии про Quarkus. Речь пойдет о том, как замерять расход памяти для оценки улучшения производительности, важной части всего процесса модернизации приложений – это и другое по ссылке.
    Red Hat
    Программные решения с открытым исходным кодом

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

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

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