本文是“Native Compilations Boosts Java”系列文章的一部分。你可以通过订阅RSS接收更新通知。
Java 主导着企业级应用。但在云计算领域,采用 Java 的成本比它的一些竞争对手更高。原生编译降低了在云端采用 Java 的成本:用它创建的应用程序启动速度更快,使用的内存更少。
那么,Java 用户的问题来了:原生 Java 是如何改变开发方式的?我们在什么情况下应该切换到原生 Java?什么情况下又不应该切换?我们应该使用什么框架?本系列文章将回答这些问题。
对服务器端 Java 认识的改变
2017 年,Java 服务器端出现了一个认知问题。随着向微服务和轻量级容器化运行时的转变,开发人员开始注意到传统 Java 应用程序的膨胀,它们被打包并部署到 Servlet 容器的共享 Java 虚拟机(JVM)上。Serverless 的出现进一步加剧了这种认知。
正是在这段时间,Object Computing的一个团队开始重新思考如何从头开始设计 Java 框架。于是Micronaut框架诞生了,这是一个采用了不同做法的 Java 框架,它通过使用 Java 注释将框架的组装计算工作所转移到了编译阶段。这完全消除了传统 Java 框架使用的反射、运行时生成代理和复杂的动态类加载。
2018 年 4 月,Micronaut 框架首次公开发布,引发了 Java 社区巨大的思维变化,改变了人们对 Java“缓慢而臃肿”的看法。许多较新的项目也采取了类似的做法:将更多的逻辑转移到应用程序的构建和编译阶段,以此来优化应用程序的启动性能和消除反射。
构建编译时的好处很明显:在编译时计算更多的东西,那么在运行时就可以以最优的方式执行。消除了反射、动态类加载和运行时生成代理,为我们提供了进一步的下游优化机会,包括 JIT 和(关键的是)GraalVM 的原生镜像工具。因为采用了这种方法,原生镜像不需要额外的配置信息就可以对 Micronaut 框架应用程序进行静态分析。
由于 Micronaut 框架和 GraalVM 之间存在这种协同作用,Micronaut 框架联合创始人 Graeme Rocher 加入了Oracle Labs。Oracle Labs 不仅有 GraalVM,也为正在进行的 Micronaut 框架的开发做出了重大贡献。
Micronaut 框架介绍
人们对 Micronaut 框架的一个常见误解是,它是专为微服务设计的。事实上,Micronaut 框架也为一系列应用程序类型提供了一种极限模块化的架构!
Micronaut 框架实现了 JSR-330依赖注入规范,并提供了许多附加的内置特性,是一个绝佳的基于注解编程模型的通用框架。它的特性包括:
配置注入;
AOP编程概念,如拦截器;
内置了对云原生应用程序基本概念的支持,如验证、缓存、弹性重试、作业调度等。Micronaut 基于 Netty I/O 工具包构建了一个HTTP服务器和HTTP客户端。
用户已经用 Micronaut 框架来构建无服务器应用程序、命令行应用程序,甚至是 JavaFX 应用程序。
Micronaut 框架为广泛的模块生态系统提供了基础,Micronaut 可以帮助它们解决一系列问题。正是由于这种灵活性,Micronaut 框架在开发者当中得到了极大的普及。以下 Micronaut 的架构图:
基础层基于 Java Annotation Processing (APT),实现了编译时依赖注入,支持各种模块的构建,包括基于 Netty 的 HTTP 服务器。但它也涵盖了其他领域,如数据访问、安全性和 JSON 序列化。
为什么要用 Micronaut 框架?
Micronaut 框架的目标是完全消除框架中使用的 Java 反射、动态类加载和运行时生成的代理和字节码等特性,以此来提供传统 Java 框架的轻量级替代方案。
消除传统框架对这些特性的依赖对提高性能、内存消耗、安全性、健壮性、调试和测试的便捷性有着深远的影响。与其他解决方案不同的是,Micronaut 框架应用程序也可以在 JVM 中快速启动!
因为启动速度得到了极大改进,就没有必要再区分集成测试和单元测试代码,这极大缩短了从编码到测试之间的时间。在过去,我们常常因为应用程序启动太慢不得不减少集成测试。Micronaut 框架消除了这种情况,所以框架中没有包含大量 HTTP 层的模拟工具。其他的许多框架之所以提供大量的模拟工具,是为了降低启动应用程序的成本。
消除反射减少了堆栈跟踪信息的数量,而在传统框架中,堆栈跟踪信息通常非常繁杂。
Micronaut 框架还提供了将代码转换成构建时编译的机制和 API。Micronaut 框架直接与 Java 编译器集成,当注解使用不当时,它会生成编译错误,从而提高代码的类型安全性和整体开发者体验。
Micronaut 框架入门
本节将介绍如何使用 Micronaut 框架来构建云原生 Java 微服务。
使用 Micronaut 框架有几种不同的方法。你至少需要 Java SE 8 或更高版本的 JDK。如果要使用原生镜像特性,你需要 Java 11 或更高版本的 GraalVM JDK。
要创建一个 Micronaut 应用程序,你可以使用已经集成到 IDE(例如,IntelliJ IDEA Ultimate或GraalVM Tools的 VSCode Micronaut 扩展)中的向导。
另外,通过Micronaut Launch创建一个新的 Micronaut 应用程序也非常容易。它是一个项目创建向导,你可以选择想要构建的应用程序类型和要包含的特性。然后,它会生成一个包含应用程序的 ZIP 文件,你可以将下载它,或者将代码推送到你的 Github 存储库。
如果你对命令行更熟悉,还可以通过常见方法(包括 SDKMAN、Homebrew)安装 Micronaut CLI 来创建应用程序。在安装好以后,创建一个新的应用程序就很简单:
如果你不喜欢安装额外的 CLI,可以通过 curl 直接调用 Micronaut Launch API:
上面的命令使用 Gradle 构建工具创建了一个应用程序。你也可以将“gradle”替换成“maven”。
Micronaut 框架生成的项目结构与其他 Java 项目一样:
- 一个 Gradle 或 Maven 构建文件(尽管也可以配置其他的构建工具,如 Bazel)。
- 默认配置文件是 src/main/resources/application.yml。但是,如果你不喜欢 YAML,可以使用 Java 属性、JSON、HOCON 或 TOML 作为替代。
- 默认的日志记录器是 SLF4J+Logback 的组合,配置文件为 src/main/resources/logback.xml。你也可以将 SLF4J 换成其他日志记录系统。
- 单元测试是 JUnit 5,但也支持其他测试框架,如 Spock 和 Kotest for Kotlin 等。一个新创建的项目提供了一些 Java 源代码来帮助你入门。第一个是位于 src/main/java 中的 Application.java 类,它包含了 Micronaut 应用程序的主入口点:
对 Micronaut.run(..)的调用将触发框架的启动过程。
第二个类在 src/test/java 目录中,用于验证应用程序可以成功启动,而且没有任何错误:
这个 JUnit 5 测试用例用 @MicronautTest 进行了注解。这个注解是一个 JUnit 5 扩展,用于将组件注入到测试中。在本例中,将为运行中的应用程序注入 EmbeddedApplication。
为 Micronaut 开发准备 IDE
一般来说,Micronaut 框架基于 Java Annotation Processing(APT)的优势之一是使用这个框架时不需要其他特殊的构建工具。所有流行的 IDE 都支持 APT,尽管有些 IDE(如 Eclipse)需要显式地启用它。
随着 Micronaut 框架越来越流行,IDE 厂商已经提供对这个框架的支持。JetBrain 的 IntelliJ Ultimate 就为这个框架的用户提供了优秀的工具,包括项目向导、配置自动完成、Micronaut 数据支持等。
此外,Visual Studio Code 有免费的GraalVM扩展包,包括一个 Micronaut 项目创建向导、配置自动完成以及 Micronaut 应用程序的原生镜像功能。
如果你安装了这些 IDE 中的任何一个,只需在 IDE 中打开 Gradle 或 Maven 项目,一切就都设置好了,你就准备就绪了。
开发 REST API
Micronaut 框架支持广泛的服务器端工作负载,包括 REST、gRPC、GraphQL 和基于 Kafka、RabbitMQ、JMS 和 MQTT 消息驱动的微服务。本文将重点介绍使用默认的基于 Netty 的 HTTP 服务器构建 REST 应用程序。
Micronaut 应用程序中的每个 HTTP 路由都通过一个带 @Controller 注解的 Java 类来定义。注解的名称来源于 Model View Controller(MVC)模式。带 @Controller 注解的类可以包含一个或多个映射到特定 HTTP 动词和 URI 的方法。
“Hello World”示例可以通过 Micronaut 控制器来实现,如下所示:
控制器被映射到了/hello。带 @Get 注解的方法负责处理 HTTP GET 请求,并使用 RFC 5741 URI 模板绑定了方法的 name 参数。你可以在 IDE 中运行 Application 类的 main 方法或通过./gradlew run 或./mvnw mn:run 来启动服务器。然后,你可以通过向 Micronaut HTTP 服务器的默认 8080 端口发送 curl 请求来测试端点:
既然 Micronaut 框架非常注重测试,那么还有什么比单元测试更好的方法来测试 API 呢?下面是 HelloController 示例的一个简单的 JUnit 5 测试:
上面的测试注入了一个 Micronaut 的 HTTP Client,向/hello/John URI 发送了一个 GET 请求,并断言结果是正确的。
Micronaut 框架的一个巨大好处是测试执行得非常快,可以与常规单元测试相媲美。即使 @MicronautTest 注解启动了 Micronaut 服务器,并运行了完整的 HTTP 请求响应周期,执行速度也不会受到影响。这样就没有必要再去学习大量用于模拟 HTTP 服务器的 API 了!开发人员因此可以编写更多的集成测试,提高代码可维护性和质量。
访问数据库
访问数据库是服务器端应用程序的一种非常常见的活动,因此许多框架都为此提供了简化,以提高开发人员在这方面的生产力。Micronaut 框架也不例外。
Micronaut Data是一个具有特殊功能的数据库访问工具包:通过与 Micronaut 编译器的集成,Micronaut Data 增加了数据库查询的编译时检查和构建时计算,从而提高了运行时效率。
与 Spring Data JPA 非常相似,Micronaut Data 允许你使用 Repository 模式定义 Java 接口,它会在编译时自动为你实现数据库查询。
Micronaut Data 对 Repository 接口的方法签名进行编译时分析,并在可能的情况下实现接口,否则将发生编译错误。
Micronaut Data 支持多种不同的数据库和查询格式,包括:
- Hibernate和JPA——你可以使用 JPA 和 Hibernate,并且 Micronaut Data JPA 会在编译时计算 JPA 查询(如上所述)。
- JDBC和SQL——对于那些更喜欢原始 SQL 和简单的数据映射而不是对象关系映射(ORM)的人来说,Micronaut Data JDBC 提供了一个更简单的解决方案,可以用它向关系数据库写入或读取 Java 17+的记录类对象和 POJO。
- MongoDB——作为最新添加的功能,Micronaut Data MongoDB 直接与 MongoDB 驱动程序集成,Micronaut序列化以完全无反射的方式在 BSON 之间编解码对象。
- R2DBC——Micronaut 框架提供了一个基于 Netty 的反应式非阻塞核心。结合使用 Micronaut Netty 服务器和响应式数据库连接(Reactive Database Connectivity,R2DBC)规范及数据库实现,你可以开发出无端到端阻塞的 SQL 应用程序。
- Oracle Coherence——一个大规模分布式数据网格,Coherence 的特点是专门与 Micronaut Data 集成,可以轻松实现由 Coherence 集群提供支持的 Repository。关于 Micronaut 框架的所有不同的数据库访问选项可以单独写成一系列文章。不过好在已经有一些优秀的指南可参考:“使用Micronaut Data JDBC访问数据库”或“使用Micronaut Data Hibernate/JPA访问数据库”。
我个人喜欢 Micronaut Data JDBC,它是一个简单的 JDBC 数据映射器。它是基于编译时 Bean 自省,完全消除了持久化层的反射。
如果你在Gradle或Maven构建文件中配置了Micronaut Data JDBC,就可以创建映射到数据库表、视图或查询结果的 Java 17 记录对象。这与 JPA 不同,JPA 中的 Java 类和表之间是一对一的映射,并通过关联对模式进行建模。这些关联引入了延迟加载等概念,而延迟加载往往会导致性能问题(比如臭名昭著的 N+1 查询问题)。下面是一个记录类定义示例:
然后,你可以为实现了大多数应用程序逻辑的接口定义 Repository 逻辑。例如:
上面的示例通过 CrudRepository 接口提供了完整的创建、读取、更新和删除操作(CRUD)。它还使用查询表达式定义了自定义查询。
如果你有更高级的用例,可以编写自定义查询、标准查询,或者直接编写 JDBC 逻辑来绑定结果。Micronaut Data JDBC 在完全不需要反射和运行时生成代理的情况下让这些变得轻而易举,没有 JPA 中的那种状态和会话同步概念,有助于保持应用程序的轻量级以及构建成 GraalVM 原生镜像之后的出色性能。
此外,每个 Repository 接口的检查都发生在编译时。这样可以防止 Repository 方法查询不存在的属性或使用不支持的返回类型,这在支持强大的动态特性的同时,维护了 Java 的类型安全。
构建原生可执行文件
Micronaut 框架的第一个版本是在 GraalVM 之前发布的。然后,这两项伟大的技术之间产生了自然而然的协同作用,主要是因为 GraalVM 的原生镜像组件可以很容易地将一个 Micronaut 应用程序转换为一个原生可执行文件。
GraalVM 原生镜像可以很好地支持 Java 反射、运行时代理和动态类加载。开发人员需要为原生镜像提供必要的配置,说明在何时何地可以使用它们。但对于 Micronaut 框架就不需要提供这些声明,因为 Micronaut 应用程序没有在框架级别使用这些技术!这使得 GraalVM 原生镜像的提前编译(AOT)分析变得更加简单。
当然,如果你使用了依赖反射的第三方库,则需要声明。但是,你所使用的框架中的大多数东西都是无反射的。
Micronaut Gradle和Micronaut Maven插件利用了 Oracle Labs 提供的GraalVM原生构建工具来简化原生可执行文件的构建。因此,用 Gradle 构建一个原生可执行文件就是这么简单:
使用 Maven 是这样:
这两个命令都将在工具的构建目录中为目标平台生成原生可执行文件。
运行原生可执行文件:
启动时间减少到几毫秒(上面的例子是 23 毫秒),内存消耗也显著下降。有了这样一个巨大的改进,就可以将 Micronaut 应用程序部署到内存限制有限或启动速度非常关键的环境中(例如,无服务器工作负载)。
需要注意的是,目前正在进一步研究通过由 Oracle Labs 开发的Micronaut AOT项目来实现更重大的改进。它在构建原生可执行文件之前会对字节码进行额外的静态分析,以优化和消除死代码路径,并将 YAML 转换为 Java,避免在运行时使用 YAML 解析器,等等。
为云而构建
除了原生镜像,Micronaut 框架还支持许多不同的打包格式和部署目标,包括:
- 使用./gradlew assemble 或./mvnw package 构建的传统 JAR 包。
- 使用./gradlew dockerBuild 或./mvnw package -Dpackaging=docker 构建的 Docker 镜像。
- 使用./gradlew dockerBuildNative 或./mvnw package -Dpackaging=docker-native 构建的包含原生可执行文件(来自 GraalVM 原生镜像)的 Docker 镜像。
- 自定义AWS Lambda运行时,可以将 Micronaut 应用程序部署到无服务器平台。
- 与Kubernetes集成,可以简化在 Kubernetes 集群中的部署。总的来说,Micronaut 框架提供的特性使其成为构建云原生 Java 应用程序的最佳选择,从分布式配置支持到集成服务发现,再到为 AWS、Google Cloud、Azure 和 Oracle Cloud 等云供应商提供的公共抽象实现模块。这些抽象确保你的应用程序可以在云供应商之间保持可移植性。
总结
Micronaut 框架为服务器端 Java 工作负载带来了一股新鲜的空气。它提供了一种创新的编译时方法和特性,使其成为构建现代云原生 Java 应用程序的最佳候选。
与 GraalVM 原生镜像的紧密集成,以及与 Oracle Labs GraalVM 团队的工作关系,意味着 Micronaut AOT 和 Micronaut 序列化(Jackson Databind 的无反射替代方案)等项目将继续带来重大创新。
Micronaut 框架有一个充满活力的社区,提供了许多提高开发人员生产力的模块,包括与数据库技术集成的 Micronaut Data。
社区的反馈将继续推动框架的发展。因此,如果你有任何反馈,请不要犹豫,通过Micronaut社区分享关于新功能和改进的想法。
作者简介
Graeme Rocher 是几个流行开源项目的创建者,包括 Grails 和 Micronaut,也是《Grails 权威指南》的合著者。Graeme 目前是 Oracle 的架构师。Graeme 是 Java Champions 成员,并因其在开源方面的重要贡献于 2018 年荣获由 Oracle 颁发的 Groundbreaker 奖项。