迁移春季启动应用程序到Quarkus
#java #spring #quarkus #framework

介绍

在传统Java企业版中运行的Java应用程序不适合云环境。

应用程序服务器启动时间很高,通常以上一分钟以上,并且所需的内存足迹很高。通常,它们需要复杂的集群配置。

这与云中引入的规模和缩小概念不兼容。

市场上有无数的Java框架。

Quarkus是一个红帽Java框架,不需要应用程序服务器,其目标是使用Graalvm支持Kubernetes和Java本机汇编。

Quarkus允许重复使用许多现有的Java库,提供本机汇编的特定扩展。

使用Quarkus构建的应用程序可以在几秒钟内启动,如果本机编译,它们的内存和磁盘足迹非常有限。

如果您不知道什么是Quarkus,我鼓励您阅读有关该主题的Quarkus fundamentals帖子(15分钟阅读)。

使用Quarkus,无法替换Java Enterprise Edition应用程序服务器的100%功能:EJB,JSP和其他类似技术将无法用于为Quarkus编写的应用程序。

从Spring Boot应用程序迁移到Quarkus并不是直接的任务,尤其是在针对本机汇编的情况下:需要许多适应。

尽管有很棒的指南可以向您解释如何将春季启动应用程序迁移到Quarkus,但这些指南并没有真正强调从春季到Quarkus迁移多服务代码基础的方法。
这里有一些示例:

在本文中,我将详细介绍将大量弹簧引导代码基础应用程序(用其他术语,单片ð)迁移到Quarkus的方法。

我还将突出一些我们在将我的公司服务代码基础之一迁移到Quarkus时所遇到的一些陷阱。

迁移到Quarkus的方法

我将在本节中解释我们如何在我的一家公司服务向Quarkus的迁移方面进步。

首先,无论采用什么方法,我都建议任何人在介绍中突出显示的帖子来了解Quarkus。

完成后,您还应该在官方website上使用Quarkus启动指南,以便您可以熟悉包装并在不超过一个小时内使用Quarkus构建您的第一个应用程序。

hothead方法

我喜欢称之为hothead方法的第一种方法包括:

  1. 将Quarkus Universe bom依赖性添加到您的服务pom.xml文件,遵循指南:https://quarkus.io/guides/maven-tooling#build-tool-maven
  2. 使用以下方式构建服务:mvn quarkus:dev命令行并点燃蜡烛,希望一切都可以起作用!

构建当然会产生大量错误,其中大多数与依赖注入问题有关。

[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:2.2.3.Final:build (default) on project webapp: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR] [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: javax.enterprise.inject.spi.DeploymentException: Found 107 deployment problems:
[ERROR] [1] Unsatisfied dependency for type org.springframework.web.client.RestTemplate and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
[ERROR] [2] Unsatisfied dependency for type javax.ws.rs.ext.Provider and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
[ERROR] [3] Unsatisfied dependency for type java.util.concurrent.ExecutorService and qualifiers [@Default]
[ERROR] - java member: com.myapp.server_impl.ServerImpl#<init>()
[ERROR] - declared on CLASS bean [types=[com.myapp.server_impl.ServerImpl, java.lang.Object], qualifiers=[@Named(value = "serverImpl"), @Default, @Any], target=com.myapp.server_impl.ServerImpl]
...
...
hundreds of errors later
...
...
[ERROR] at io.quarkus.arc.processor.BeanDeployment.processErrors(BeanDeployment.java:1108)
[ERROR] at io.quarkus.arc.processor.BeanDeployment.init(BeanDeployment.java:265)
[ERROR] at io.quarkus.arc.processor.BeanProcessor.initialize(BeanProcessor.java:129)
[ERROR] at io.quarkus.arc.deployment.ArcProcessor.validate(ArcProcessor.java:418)

您真的认为它会像ð吗?!
好吧,让我们退后一步,解释基础知识。

使用我的大脑方法

依赖注射

Quarkus旨在使用最广泛使用的Java标准,框架和库,例如Eclipse Microprofile,Apache Kafka,Resteasy(Jax-rs),Hibernate Orm(JPA)等。

>

Quarkus编程模型基于另一个标准:Java 2.0规范的上下文和依赖注入。

如果您完全不熟悉依赖注入,我鼓励您将Quarkus introduction阅读到上下文和依赖注入中。

关于quarkus bean发现和注入的第一件事是,它不会从外部模块扫描类。

如果您有一个多Maven模块项目,就像我们为我们一直在迁移的服务所做的那样,您会发现Quarkus在其他模块中的默认类都不会找到。

您有多种方法使Quarkus找到您的豆子。他们在这里列出:https://quarkus.io/guides/cdi-reference#bean_discovery

这是引用链接的摘录:

“ bean存档是从:

合成的
  1. 应用程序类,

  2. 包含beans.xml描述符的依赖项(忽略内容),

  3. 包含Jandex索引元I-Inf/jandex.idx的依赖项,

  4. quarkus.Index依赖性的依赖项。properties配置文件,

  5. 和Quarkus集成代码。”

如果需要,您没有手的外部模块或第三方库(这意味着您无法修改它),Quarkus扫描您应该在application.properties配置文件中添加依赖项。

如果您对模块/项目具有控制权,则可以在Meta-Inf文件夹中直接添加一个空Beans.xml文件。

也就是说,您可能需要在弄脏手之前清理依赖项,而越好的代码基础越好。

我们将稍后再回到这一点。

春天

让我们现在专注于春天。在我们一直在迁移的服务中,开发人员一直在大量使用Spring。

多年来的服务增长,春季依赖已添加到该项目中。在这里和那里使用了弹簧依赖注射,而不是标准的CDI specifications

举例来说,@Component Spring DI注释可能已被使用,而不是@Singleton CDI注释。

另一个示例是使用@Bean spring di注释而不是@Produces cdi注释。

还有更多示例,您可以在Quarkus网站上找到一个转换表(Spring DI注释与CDI):https://quarkus.io/guides/spring-di#conversion-table

因此,向Quarkus的迁移并不麻烦,Quarkus团队进行了一系列扩展,可以帮助您将春季项目迁移到Quarkus:Spring-Di,Spring-Web,Spring-Web,Spring-Data-JPA,Spring-数据,弹簧安全性,弹簧搜索,弹簧式播放,春季启动properies,spring-cloud-config-client。

例如,如果您决定使用Spring-Di Quarkus扩展名,则Spring DI处理器将映射Spring DI注释为CDI注释。

也就是说,建议将所有弹簧豆迁移到CDI规格。

迁移到Quarkus

遵循上述依赖注入和春季的说明,我们提出了以下工作计划,可以为基于春季靴的任何迁移到Quarkus实施。

1.依赖项分析

首先,我们要分析构建和运行以要迁移到Quarkus Framework的依赖项。

  • 为什么?此步骤是基础,以识别所有必需的外部依赖关系以及内部依赖项。

  • 如何使用类依赖分析仪(CDA)工具来满足此目标。您可以在此page上找到如何使用它。

您也可以使用内置的IDE依赖分析仪,但是我发现CDA确实很方便,如果您想改善工具的可能性,也可以将其用作项目中的库。

2. Maven模块清洁

其次,我们要清洁所有不需要的内部依赖。

  • 为什么?如前所述,我们正在迁移一个整体,它基于Maven软件管理工具。

代码库由多个在不同服务中使用的Maven模块组成。

我们想要迁移的服务未使用的一些代码,是该服务依赖的Maven模块的一部分。

通过移动到新/其他Maven模块清理所有不需要的内部依赖项,我们最终将减少要迁移到Quarkus的代码库的范围。

遵循此原则,我们在代码库中进行了重大清洁,以消除我们服务的无关紧要和不必要的内部依赖。

我强烈鼓励您在此迁移之前进行此类清洁。此初步步骤最终将使您能够在以后的步骤中节省时间。

  • 如何?类依赖分析仪工具的输出允许您检查服务所依赖的所有类,并最终删除/移动所有不需要的类。

具体而言,这是通过将服务不需要的一些类,将服务不依赖的新的Maven模块或现有的Maven模块进行操作来执行的。

我们还重构了一些代码:通过将一些类分开,通过创建新类以将其用于服务的使用新类,我们一直在迁移到Quarkus Framework。

3.嘲笑

下一步是模拟我们在步骤1中突出显示的所有外部依赖关系,以在迁移到我们代码库的Quarkus上进行进展。

  • 为什么?通过嘲笑所有外部依赖关系,我们确保首先在代码基础上迁移,并且我们不会被外部依赖性所阻止。

  • 如何仅通过实现模拟行为的界面。

例如,在我们一直迁移到Quarkus的服务中,我们使用外部依赖关系接口可以访问上下文。

使我们的应用程序正在使用Quarkus构建,我们必须使用静态模拟实现暂时模拟上下文接口。

我们实现了界面,并确保bean遵循CDI规格。

4.内部/外部依赖团队支持

以前的步骤应该突出组织内部和外部处理的所有依赖项。

现在,您可以向组织内部/外部拥有依赖关系的团队寻求支持,以解锁您的进步。

  • 如何?您要么要求组织内部/外部的团队的支持,要么直接为依赖性的Quarkus迁移。

这是在迭代方法中完成的,这意味着您或外部团队正在准备好Quarkus,然后将其交付,您将其集成到代码库中,您可以删除与此外部依赖关系相关的模拟,然后去不断地迁移到Quarkus。

5. Spring-Di Quarkus扩展

使Quarkus的迁移并不麻烦,Quarkus团队出现了一系列扩展,可以帮助您将春季项目迁移到Quarkus。

  • 为什么?要遵守CDI规格,我们需要将所有非CDI符合CDI的豆类迁移到符合CDI的豆类,这意味着我们需要将所有弹簧豆迁移到CDI BEAN 。这可能是一项挑剔的作品。

  • 如何?避免执行这项不可扣除的任务,我们一直在使用quarkus spring-di扩展名,为您为标称案例提供工作。

    <

    < /li>

您可以在这里找到一个转换表(春季di注释与CDI):https://quarkus.io/guides/spring-di#conversion-table

使用此扩展名仅通过将以下依赖项添加到您的服务pom.xml文件:
来完成

<dependencies>
    <!-- Spring DI extension -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-spring-di</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

6.多Maven模块处理

说实话,没有一个巨石只有一个maven模块。

此步骤是关于在您要迁移到Quarkus的所有必需的Maven模块中制作Quarkus扫描豆。

  • 为什么?如依赖注入部分所述,Quarkus不会扫描外部模块的类。

如果需要,您没有手的外部模块或第三方库(这意味着您无法修改它),Quarkus扫描您应该在应用程序中添加依赖项。Properties配置文件。

如果您对模块/项目具有控制权,则可以在Meta-Inf文件夹中直接添加一个空Beans.xml文件。

这将确保Quarkus扫描您的豆子。

  • 如何?只需在您拥有的每个Maven模块的元I-Inf文件夹中创建一个空的beans.xml文件,或在应用程序中创建依赖项。用于我们不拥有的模块的Properties配置文件。

您可以在此处找到更多详细信息:https://quarkus.io/guides/cdi-reference#bean_discovery

image
image

7.迁移您的代码库

此步骤着重于将不合格的基本代码迁移到Quarkus Framework compriancy。

  • 为什么?到目前为止,我们嘲笑了外部依赖关系以进步代码基础,我们使用了Spring-Di Quarkus扩展程序来简化我们的迁移,但是某些代码必须迁移以遵守Quarkus建立服务的标准。

的确,某些软件不能由Quarkus团队提供的扩展名处理,必须迁移;其他一些代码也不遵循CDI标准规格,也必须迁移。

此步骤确实取决于您的软件。

我们将稍后审查我们在将服务迁移到Quarkus时一直面临的反复错误。

  • 如何?遵守CDI规格并迁移一些无法通过Quarkus Extensions处理的春季依赖。

  • 示例。

在“服务代码基础”中,我们已经迁移了我们使用的ThreadPoolTaskExecutor是Java bean,它允许以Bean样式配置Java标准ThreadPoolExecutor(通过其CorePoolSize,MaxPoolSize,MaxPoolSize,keepaliveSeconds,keepaliveSeconds,queuecapacity)。

此类也非常适合管理和监视(例如,通过JMX)提供了几个有用的属性:CorePoolSize,MaxPoolSize,keepaliveSeconds(所有在运行时支持更新);池化,ActiveCount(仅适用于内省)。

此类是Spring-Context库的一部分。

快速查看spring-di quarkus扩展pom.xml您可以很容易地发现弹簧上下文依赖性被排除在外,以便我们无法作为最终用户访问它。

弹簧上下文被排除在依赖项中以过滤弹簧上下文类,仅保留在quarkus-spring-context-api依赖项中必需的类别。

这意味着我们不能同时使用Spring-Di Quarkus扩展(这绝对是必须迁移一个巨石的必要位置)和Spring threadpooltaskexecutor。

为了减轻此问题,我们已将我们的Spring threadpooltaskexecutor迁移到ManagedExecutor org.microprofile库,这是Quarkus支持的标准。

这是此服务代码特别的示例,因为并非所有人都使用Spring threadpooltaskexecutor。

在复发错误和提示部分中,我们将在迁移到Quarkus Framework时肯定会遇到更多的下议院错误。

8.优化软件以拥抱graalvm习语

此步骤是您应该执行的优化步骤,以拥抱graalvm习语:启动速度更快,交付较小的软件包。

  • 为什么? graalvm习语需要更改框架的工作方式,而不是在运行时而是在启动时。一个框架实际上带来的大部分动态性实际上是在启动时间出现的,这就是将Quarkus转移到构建时间的原因。

quarkus也可以突出显示为使框架从建筑时间开始。

在启动时

  • parse配置文件(例如:persistance.xml文件)
  • classPath和类扫描注释(例如:@Entity@Bean等...),Getters或其他元数据
  • 从所有上述信息构建框架将在运行时运行的所有信息。例如,Hibernate不能将.xml文件保存在内存中,而是构建在运行时表示的内部模型,正是该模型在运行时使用以节省实体等...
  • ...
  • 准备反射(将获取对方法对象的引用和字段以执行调用)并构建代理
  • 启动和打开IO,线程等...(例如:数据库连接等...)

从概念上讲,当您查看这些步骤时,可以在构建时间而不是在启动时间上轻松完成。

最后一步之前的所有内容,甚至开始的某些部分都可以在构建时间完成。

这是Quarkus所做的,它需要像Hibernate这样的框架并使其正常工作,以便在构建时间执行最大步骤。

在以下模式中,您可以在运行时(配置负载,classpath扫描,模型创建,启动管理)的顶部看到一个典型的Java框架,而在底部您可以看到一个Quarkus框架,其中大部分工作是在建造时间执行的。

image

也就是说,您可能需要认可这种方法,并确保从运行时取出所有可能在构建时间执行的动作并将其驱逐到构建时间。

让我们以一个具体的示例来解释这些概念,我们在将服务迁移到Quarkus时面对。

一旦我们能够包装夸克的应用程序公开我们的服务,我们就开始了它并向它启动了第一条消息。

第一个查询需要很长时间才能处理,而第二个查询要快得多(x10倍更快ð®)。

我们必须调查为什么第一个查询要花这么长时间。

使用Async Profiler,我们能够为第一个和第二个查询构建flamegraphs,以想象两个交易执行的路径长度的差异。

在第一个flamegraph中,我们看到我们花费了大部分交易时间来初始化jaxb上下文,负责邮票/从输入查询中删除上下文。

可以在构建时间传输此操作,而不是在启动时进行操作,因为所需的所有信息均在构建时间中。

这只是一个例子,但是,我很肯定,在您的代码库中,您的操作也可以从运行时传输到构建时间!

经常出现的错误和提示

在本节中,我们将突出显示您在将服务迁移到Quarkus框架时可能遇到的一些常见错误,以及解决这些错误的提示。

包装私有化

您会不时看到构建Quarkus应用程序时以下信息消息:

[INFO] [io.quarkus.arc.processor.BeanProcessor] Found unrecommended usage of private members (use package-private instead) in application beans:
    - @Inject field com.myapp.service.MyService#someBean

如果属性是包裹私有化的,Quarkus可以直接注入它,而无需任何反思才能发挥作用。

这就是为什么Quarkus推荐包装私有者进行注入的原因,因为它试图避免尽可能多地反思(原因是较少的反射意味着更好的性能,这是Quarkus努力实现的目标)。

)。

Quarkus正在使用graalvm来构建本机可执行文件。 GRAALVM的局限性之一是反射的用法。支持反思性操作,但必须明确注册所有相关成员。这些注册会导致更大的本机可执行文件。

,如果Quarkus di需要访问私人成员,则必须使用反射。这就是为什么鼓励Quarkus用户不要在其豆类中使用私人会员。这涉及注射字段,构造函数和初始化器,观察者方法,生产者方法和领域,分配器和拦截器方法。

豆名单注射

豆类注入列表与春季的运作良好:

@Inject List<PaymentProcessor> paymentProcessor;

,但不是CDI标准规格的一部分。

在某些情况下,注射不是获得上下文参考的最方便方法。例如,当以下内容时可能不会使用它。

  • bean类型或限定器在运行时动态变化,或

  • 根据部署的不同,可能没有满足类型和预选赛的bean,

  • 我们想迭代某种类型的所有豆子。

在这些情况下,可以注入javax.enterprise.inject.Instance接口的实例:

@Inject Instance<PaymentProcessor> paymentProcessor;

有关更多详细信息,您可以检查instance interface的CDI规格。

也就是说,您必须将弹簧列表注入到CDI规格的解决方案中。

通常您会使用生产者图案生产这些豆子。

未使用的豆子

这个特定的观点呼应了夸克的文档:https://quarkus.io/guides/cdi-reference#remove_unused_beans

我们的一些豆子在建筑时间被删除,因为它们被夸克被认为是未使用的。

例如,以下弹簧豆:

@Component
public class PaymentMapper extends Mapper<Payment> {
...
}

从:
延伸

public abstract class Mapper<T> {

  @Inject
  private MapperFactory mapperFactory;

  @PostConstruct
  private void register() {
    mapperFactory.register(type_of_the_class, this);
  }
}

已注册:

@Named
public class MapperFactory {

  private static final Map<Class, Mapper> mappers = new HashMap<>();

  public void register(Class type, Mapper mapper) {
    mappers.put(type, mapper);
  }
}

Bean Paymentmapper被认为是未使用的,因为除了其定义外,该代码中的其他任何地方都没有引用它。

不幸的是,问题是实际上是通过在mapperfactory中的@PostConstruct方法调用中注册的。

静态映射器地图最终总是空的,因为映射豆被标记为未使用。

对于这种情况,我们必须更改代码,以便mapperFactory注册一个实现相同界面的bean列表,无论如何,这使得更有意义。

合法的豆类类型

显然是在CDI specifications中写的:包含通配符类型参数的参数化类型不是合法的bean类型。

我在github上制作了一个小的reproducer,向您展示了包含通配符型参数的CDI参数化bean的失败,注射:

由于这些豆不被认为是合法的,因此Quarkus不考虑它们。

提供商No-Arg构造函数

在我们的代码库中,我们使用@Provider类来解码输入或编码输出。

这些提供商实现ReaderInterceptor/WriterInterceptor接口。

当Quarkus-Ressy库发挥作用时,它会在编译时告诉我们:

WARN  [io.qua.res.com.dep.ResteasyCommonProcessor] (build-9) Classes annotated with @Provider should have a single, no-argument constructor, otherwise dependency injection won't work properly. Offending class is com.myapp.interceptor.BaseReaderInterceptor

规则如下:用@Provider注释的类应具有一个单一的,无主的构造函数,并且类必须公开。

例如,此代码没有编译:

@Provider
class BaseReaderInterceptor implements ReaderInterceptor {

  private StatsCollector statsCollector;

  @Inject
  public BaseReaderInterceptor(StatsCollector statsCollector) {
    this.statsCollector = statsCollector;
  }
  ...
}

虽然这是汇编:

@Provider
public class BaseReaderInterceptor implements ReaderInterceptor {

  // Injection by constructor makes REST-EASY unable to initialize the ReaderInterceptor...!!!!
  // Hence we inject at member level
  @Inject
  private StatsCollector statsCollector;

  ...
}

参考

  • Quarkus Devoxx上的Emmanuel Bernard talk视频: Quarkus为什么,如何,如何和什么
  • Oleg Selaje和Thomas Wuerthinger talk在Graalvm Devoxx视频上: 您需要了解的有关Graalvm的一切
  • for fr speakers: Emmanuel Bernard & Crèd Éscoffier talk On Using Quarkus With Graalvm Devoxx Video: Quarkus: How to make a Java Cloud Native App with Grail VM