大家好,我的名字是Ivan Kozikov,我是一名全栈Java开发人员 尼克斯联队 .我有Oracle和Kubernetes认证,我喜欢探索Java领域的新技术和学习新主题。

JRebel resource每年都会对Java开发人员进行一次调查,调查他们使用的框架。在里面 2020 ,Spring Boot以83%的得票率获胜。然而,在 二千零二十一 ,其份额降至62%。其中一家公司Micronaut的市场占有率翻了一番多。这个框架的迅速普及提出了一个合乎逻辑的问题:它有什么有趣的地方?我决定找出Micronaut克服了哪些问题,并了解它是否可以成为Spring Boot的替代品。

在本文中,我将浏览软件体系结构的历史,这将有助于理解为什么会出现这样的框架,以及它们解决了什么问题。我将重点介绍Micronaut的主要功能,并比较两个采用相同技术的应用程序:一个在这个框架上,另一个在Spring Boot上。

从巨石到微服务再到其他…

现代软件开发始于单片式体系结构。在它中,应用程序通过一个可部署的文件提供服务。如果我们说的是Java,这是一个JAR文件,它隐藏了应用程序的所有逻辑和业务流程。然后将JAR文件卸载到任何需要的地方。

这种架构有其优点。首先,开发一种产品很容易。您创建一个项目并用业务逻辑填充它,而不考虑不同模块之间的通信。一开始只需要很少的资源,而且对整个应用程序执行集成测试更容易。

Image description

然而,这种架构也有缺点。单片体系结构上的应用程序几乎总是超过所谓的“大泥层”。应用程序的组件变得如此相互交织,以至于很难维护,产品越大,改变项目中的任何东西所需的资源和精力就越多。

因此,微服务架构已经取代了它。它将应用程序划分为小型服务,并根据业务流程创建单独的部署文件。但不要让“微”这个词误导你——它指的是服务的业务能力,而不是它的规模。

通常,微服务关注单个流程及其支持。这提供了几个优点。首先,因为它们是独立的应用程序,所以您可以根据特定的业务流程定制必要的技术。其次,组装和处理项目要容易得多。

然而,也有缺点。你首先需要考虑服务和渠道之间的关系。此外,微服务需要更多的资源来维护其基础设施,而不是单一的。当你转向云计算时,这个问题甚至更为关键,因为你必须为你的应用程序消耗的云基础设施资源付费。

Image description

框架和微框架之间有什么区别?
为了加速软件开发,人们开始创建框架。从历史上看,许多Java开发人员的模式是Spring Boot。然而,随着时间的推移,它的受欢迎程度下降了,这是可以解释的。多年来,Spring Boot的“重量”增加了很多,这使得它无法按照云环境中现代软件开发的要求快速工作和使用更少的资源。这就是为什么微框架开始取代它。

微框架是一种全新的框架,旨在最大限度地提高web服务开发的速度。通常,它们会削减大部分功能,而不是像Spring Boot这样的全栈解决方案。例如,它们通常缺乏身份验证和授权、数据库访问的抽象、映射到UI组件的web模板等。Micronaut以同样的方式起步,但已经超越了这一阶段。如今,它拥有使其成为完整堆栈框架的一切。

Micronaut的主要优势

这个框架的作者受到Spring Boot的启发,但强调了反射和代理类的最少使用,这加快了它的工作。Micronaut是多语言的,支持Java、Groovy和Kotlin。

在Micronaut的主要优势中,我强调以下几点:

  • 用于访问所有流行数据库的抽象。 Micronaut有现成的数据库解决方案。它们还提供了一个API来创建自己的类和方法来访问数据库。此外,它们支持两种变体:正常阻塞访问和反应访问。

  • 面向方面的API。 在Spring Boot中,借助注释,您可以快速开发软件。但这些指令建立在程序执行时代理类的反射和创建之上。Micronaut提供了一套现成的使用说明。您可以使用它的工具编写自己的注释,这些注释只在编译时使用反射,而不是在运行时使用反射。这将加快应用程序的启动并提高其性能。

  • 本机内置的云环境。 我们将进一步详细讨论这一点,我将分别揭示要点。

  • 内置一套测试工具。 这些允许您快速启动集成测试所需的客户端和服务器。您还可以使用熟悉的JUnit和Mockito库。

全职编辑给了我们什么?

我已经指出,Micronaut不使用反射和代理类——这可以通过提前编译实现。在创建包时执行应用程序之前,Micronaut会尝试全面解决所有依赖项注入和编译类,以便在应用程序本身运行时不必这样做。

现在有两种主要的编译方法:即时编译(JOT)和提前编译(AOT)。JIT编译有几个主要优点。首先是构建工件(JAR文件)的速度非常快。它不需要编译额外的类——它只是在运行时编译。在运行时加载类也更容易;对于AOT编译,这必须手动完成。

然而,在AOT编译中,启动时间更短,因为应用程序需要运行的所有内容都将在启动之前进行编译。使用这种方法,工件的大小将更小,因为没有代理类来运行编译。从好的方面来看,这种汇编所需的资源更少。

需要强调的是,Micronaut具有对GraalVM的内置支持。这是另一篇文章的主题,因此我将不在这里深入讨论。让我说一件事:GraalVM是一个针对不同编程语言的虚拟机。它允许创建可执行的图像文件,这些文件可以在容器中运行。在那里,应用程序的启动和运行速度达到最大值。

然而,当我试图在Micronaut中使用它时,甚至在框架创建者的注释的指导下,在创建本机映像时,我必须指定应用程序的关键类,因为它们将在运行时预编译。因此,与广告承诺相比,这个问题应该仔细研究。

Micronaut如何与云技术合作

另外,应该披露对云技术的本机支持。我将强调四个要点:

  • Micronaut基本上支持封锁。 当我们使用云环境时,尤其是当有多个供应商时,我们需要为我们将使用应用程序的基础设施创建专门的组件。为此,Micronaut允许我们创建依赖于特定条件的条件组件。这为不同的环境提供了一组配置,并试图最大化其运行环境的定义。这大大简化了开发人员的工作。

  • Micronaut有嵌套的工具来确定运行应用程序所需的服务。 即使它不知道服务的真实地址,它仍然会尝试找到它。因此,有几个选项:您可以使用内置或附加模块(例如,领事、尤里卡或Zookeeper)。

  • Micronaut能够制造客户端负载平衡器。 可以在客户端调节应用程序副本的负载,这使开发人员的工作更轻松。

  • Micronaut支持无服务器架构。 我多次遇到开发人员说,“我永远不会用Java编写lambda函数。”在Micronaut中,我们有两种可能编写lambda函数。第一种是使用API,它由基础设施直接提供。第二种方法是定义控制器,就像在普通REST API中一样,然后在基础设施中使用它们。Micronaut支持AWS、Azure和谷歌云平台。

有些人可能会说,所有这些都可以在Spring Boot中使用。但连接云支持只有在附加库或外部模块的帮助下才有可能实现,而在Micronaut中,一切都是本地构建的。

让我们比较一下Micronaut和Spring Boot应用程序

让我们开始有趣的部分吧!我有两个应用程序——一个用Spring Boot编写,另一个用Micronaut编写。这是一个所谓的用户服务,它有一组CRUD操作与用户一起工作。我们有一个PostgreSQL数据库,通过反应式驱动程序、Kafka消息代理和WEB套接字连接。我们还有一个HTTP客户端,用于与第三方服务进行通信,以获取有关用户的更多信息。

为什么会有这样的申请?通常在关于Micronaut的演示中,度量是以Hello World应用程序的形式传递的,在这种应用程序中,没有连接的库,现实世界中也没有任何东西。我想用一个类似于实际使用的例子来说明它是如何工作的。

Image description

我想指出,从Boot切换到Micronaut是多么容易。我们的项目非常标准:我们有一个HTTP的第三方客户端,一个处理交易、服务、存储库等的REST控制器。如果我们进入控制器,我们可以看到在Spring引导后,一切都很容易理解。注释非常相似。学习这一切应该不难。即使是大多数指令,比如PathVariable,也与Spring Boot是一对一的。

@Controller("api/v1/users")
public class UserController {
  @Inject
  private UserService userService;

  @Post
  public Mono<MutableHttpResponse<UserDto>> insertUser(@Body Mono<UserDto> userDtoMono) {
      return userService.createUser(userDtoMono)
          .map(HttpResponse::ok)
          .doOnError(error -> HttpResponse.badRequest(error.getMessage()));
  }

服务也是如此。如果我们要在SpringBoot中编写一个服务注释,这里我们有一个单例注释,它定义了应用它的范围。还有一种类似的机制用于注入依赖项。与Spring Boot一样,它们可以通过构造函数使用,也可以通过属性或方法参数生成。在我的示例中,编写业务逻辑是为了让我们的课程正常工作:

@Controller("api/v1/users")
public class UserController {
  @Inject
  private UserService userService;

  @Post
  public Mono<MutableHttpResponse<UserDto>> insertUser(@Body Mono<UserDto> userDtoMono) {
      return userService.createUser(userDtoMono)
          .map(HttpResponse::ok)
          .doOnError(error -> HttpResponse.badRequest(error.getMessage()));
  }

  @Get
  public Flux<UserDto> getUsers() {
    return userService.getAllUsers();
  }

  @Get("{userId}")
  public Mono<MutableHttpResponse<UserDto>> findById(@PathVariable long userId) {
    return userService.findById(userId)
        .map(HttpResponse::ok)
        .defaultIfEmpty(HttpResponse.notFound());
  }

  @Put
  public Mono<MutableHttpResponse<UserDto>> updateUser(@Body Mono<UserDto> userDto) {
    return userService.updateUser(userDto)
        .map(HttpResponse::ok)
        .switchIfEmpty(Mono.just(HttpResponse.notFound()));
  }

  @Delete("{userId}")
  public Mono<MutableHttpResponse<Long>> deleteUser(@PathVariable Long userId) {
    return userService.deleteUser(userId)
        .map(HttpResponse::ok)
        .onErrorReturn(HttpResponse.notFound());
  }

  @Get("{name}/hello")
  public Mono<String> sayHello(@PathVariable String name) {
    return userService.sayHello(name);
  }

在Spring Boot之后,存储库还有一个熟悉的外观。唯一的问题是,我在这两个应用程序中都使用了反应式方法。

@Inject
private UserRepository userRepository;

@Inject
private UserProxyClient userProxyClient;

我个人非常喜欢HTTP客户端与其他服务进行通信。只需定义接口并指定它将是什么类型的方法、将传递什么查询值、它将是URL的什么部分以及它将是什么主体,就可以以声明方式编写它。这一切都很快,而且你可以创建自己的客户。同样,这可以通过Spring Boot中的第三方库以及反射和代理类来实现。

@R2dbcRepository(dialect = Dialect.POSTGRES)
public interface UserRepository extends ReactiveStreamsCrudRepository<User, Long> {
  Mono<User> findByEmail(String email);

  @Override
  @Executable
  Mono<User> save(@Valid @NotNull User entity);
}
@Client("${placeholder.baseUrl}/${placeholder.usersFragment}")
public interface UserProxyClient {

  @Get
  Flux<ExternalUserDto> getUserDetailsByEmail(@NotNull @QueryValue("email") String email);

  @Get("/{userId}")
  Mono<ExternalUserDto> getUserDetailsById(@PathVariable String userId);

}

现在我们直接去航站楼工作。我有两扇窗户开着。黄色背景的左侧是Spring Boot,灰色背景的右侧是Micronaut。我构建了这两个包——在Spring Boot中,它花费了将近5秒,而Micronaut由于AOT编译而花费了更长的时间;在我们的例子中,这个过程花费了几乎两倍的时间。

Image description

接下来,我比较了工件的大小。Spring Boot的JAR文件是40MB,Micronaut的JAR文件是38MB。没少多少,但还是少一些。

Image description

之后,我运行了一个应用程序启动速度测试。在Spring Boot Netty中,服务器在端口8081上启动,持续4.74秒。但在Micronaut中,我们只有1.5秒。在我看来,这是一个很大的优势。

Image description

下一步是一个非常有趣的测试。我有一个节点。js脚本,其路径作为参数传递到JAR文件。它运行应用程序,每半秒钟尝试从我写给它的URL(即我们的用户)获取数据。此脚本在收到第一个响应时终止。在Spring Boot中,它以6.1秒的速度完成,在Micronaut中,它以2.9秒的速度完成——同样,速度是Micronaut的两倍。同时,指标显示Spring启动只需4.5秒,结果只需1.5秒。对于Micronaut,这些数字分别约为1.5秒和1.3秒。也就是说,获得增益正是因为应用程序的启动速度更快,实际上,如果Spring Boot在启动时不进行额外编译,那么它的启动速度也同样快。

Image description

下一个测试:让我们启动应用程序(启动需要4.4秒和1.3秒,以Micronaut为例),看看两个框架都使用了多少内存。我使用jcmd——我将标识符传递给进程并获取堆信息。这些指标显示,Spring Boot应用程序总共请求运行149MB,实际使用了63MB。我们对Micronaut重复同样的操作,使用相同的命令,但更改了进程ID。结果是:应用程序要求55MB,使用26MB。也就是说,资源的差异是2.5-3倍。

Image description

最后,我将用另一个指标来说明Micronaut不是一颗银弹,它还有发展的空间。使用ApacheBench,我模拟了对Spring服务器的500个请求进行Spring引导,并发24个请求。也就是说,我们正在模拟24个用户同时向应用程序发出请求的情况。对于反应式数据库,Spring Boot显示了一个非常好的结果:它每秒可以传递大约500个请求。毕竟,JIT编译在系统峰值时运行良好。让我们把这个过程复制到Micronaut并重复几次。结果大约是每秒106个请求。我检查了不同系统和机器上的数据,结果大致相同。

Image description

结论很简单

Micronaut并不是一款可以立即取代Boot的理想产品。在第一个框架中,它还有一些更方便或更实用的地方。然而,在某些领域,较受欢迎的产品不如较不受欢迎的产品,而是一个相当先进的竞争对手。也就是说,Spring Boot还有一段路要走。例如,自2017年第9版以来,Java中也有相同的AOT编译。

我想补充一点:开发者不应该害怕尝试新技术。它们可以为我们提供巨大的机会,让我们超越通常使用的标准框架。