使用Micronaut框架的云原生Java
主要收获
- Micronaut框架为构建基于编译时方法的云原生Java微服务提供了坚实的基础。
- 与GraalVM Native Image AOT(Ahead-of-Time Compilation)的紧密集成使得应用程序可以轻松转换为本地可执行文件,这具有巨大的优势,特别是对于无服务器和微服务的工作负载。
- 减少对Java反射、运行时代理生成和动态类加载的使用,带来了性能、内存和启动方面的改进,使Micronaut应用程序的调试和测试变得更加容易。
- 主动的编译时检查增加了类型的安全性,并通过在构建时而非运行时浮现错误来提高开发人员的工作效率。
- 一个庞大的模块和集成的生态系统,如用于数据库访问的Micronaut Data,有助于在Micronaut框架内提供进一步的创新。
本文是"本地编译提升Java"系列文章的一部分。你可以通过RSS订阅来接收这个系列的新文章的通知。 Java在企业应用中占主导地位。但在云中,Java比一些竞争对手更昂贵。使用GraalVM的本地编译使云中的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的Native Image工具提供了进一步的下游优化机会。由于这种方法,Native Image不需要额外的配置就可以对Micronaut框架的应用进行封闭式的静态分析。
由于Micronaut框架和GraalVM之间的这种协同作用,Micronaut框架的联合创始人Graeme Rocher加入了Oracle实验室。Oracle实验室不仅拥有GraalVM,而且除了对象计算之外,还为Micronaut框架的持续发展做出了巨大贡献。
什么是Micronaut框架?
关于Micronaut框架的一个常见误解是,它只为微服务而设计。事实上,Micronaut框架具有极其模块化的架构 ,适用于一系列的应用程序类型。
在其基础上,Micronaut框架实现了JSR-330依赖性注入规范。该框架在上面提供了一些额外的内置功能,使其成为一个由基于注解的编程模型支持的通用框架的绝佳选择,包括:
- 配置注入
- 面向方面的编程概念,如拦截器
- 对许多基本的云原生应用概念的内置支持,如验证、缓存、弹性重试、作业调度等。
Micronaut有一个建立在Netty I/O工具箱上的HTTP服务器和HTTP客户端。
用户已经采用Micronaut框架来构建无服务器应用、命令行应用,甚至是JavaFX应用。
Micronaut框架坚实的核心基础为广泛的模块生态系统提供了基础,使Micronaut能够解决一系列的问题。这种灵活性是Micronaut框架在开发者中急剧增长的原因。下面的架构图描述了该框架的结构。
/filters:no_upscale()/articles/native-java-micronaut/en/resources/1micronaut-1651159019426.jpg)
基础层基于Java注释处理(APT),实现了编译时依赖注入,并支持各种模块的构建,包括基于Netty的HTTP服务器。但它也涵盖了其他领域,如数据访问、安全和JSON序列化。
为什么我应该使用Micronaut框架?
Micronaut框架的目标是为传统的Java框架提供一个轻量级的替代方案,它完全消除了这些框架的动态部分,这些动态部分使用了Java反射、动态类加载以及运行时生成的代理和字节码等功能。
消除传统框架的这些方面对提高性能、内存消耗、安全性、健壮性、调试的方便性和测试都有深远的影响。而且与其他解决方案不同的是,Micronaut框架的应用程序在JVM中也能快速启动!
启动时间的改善往往完全消除了在集成测试和单元测试之间分割代码的需要,大大改善了代码到测试的周期时间。在过去,我们经常因为应用程序的启动速度太慢而少写集成测试。Micronaut框架消除了这种担忧,因此不包括HTTP层的广泛嘲弄设施。许多框架都这样做,以避免启动应用程序的成本。
消除反射也有助于调试,减少堆栈痕迹的大小,在传统框架中堆栈痕迹往往是巨大的。
Micronaut框架还提供了机制和API来将你自己的代码转向构建时的方法。这就是为什么通过直接与Java编译器集成,Micronaut框架能够并且确实在注释使用不当时产生了编译错误,改善了代码的类型安全和整体的开发者体验。
开始使用Micronaut框架
本节将介绍如何开始使用Micronaut框架来构建云原生Java微服务。
有几种不同的方法可以使用Micronaut框架。至少,你需要一个Java SE 8或更高版本的JDK。要使用Native Image功能,你需要GralVM JDK for Java 11或更高版本。
要创建一个Micronaut应用程序,你可以使用集成到你最喜欢的IDE中的一个向导,例如,IntelliJ IDEA Ultimate或VSCode的GraalVM Tools for Micronaut Extension。
另外,使用Micronaut Launch通过网络创建一个新的Micronaut应用程序也非常容易。这是一个项目创建向导,让你选择你想建立的应用程序的种类和你想包括的功能。然后它生成一个ZIP文件,你可以下载该应用程序,或者让你把代码推送到你选择的Github仓库。
如果你对命令行更有感觉,你也可以通过常见的方法安装Micronaut CLI来创建一个应用程序,包括SDKMAN!、Homebrew等。一旦安装完毕,创建一个新的应用程序就非常简单了。
mn create-app demo –build gradle
然而,如果你不热衷于安装一个额外的CLI应用程序,没问题!你可以使用Micronaut Launcher。你可以通过curl直接使用Micronaut启动API。
curl https://start.micronaut.io/demo.zip\?build\=gradle -o demo.zip && unzip demo.zip && cd demo
上面的命令用Gradle构建工具创建应用程序。你可以把 "gradle "替换成 "maven "来代替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和Kotlin的Kotest,作为替代品。
一个新创建的项目有几个Java源来帮助你开始工作。第一个是位于src/main/java 的Application.java 类,其中包括Micronaut应用程序的主要入口点:
package demo;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
对Micronaut.run(..) 的调用触发了该框架的启动顺序。
第二个生成的类在src/test/java 目录中,测试应用程序可以成功启动而不出现任何错误:
package demo;
import io.micronaut.runtime.EmbeddedApplication;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import jakarta.inject.Inject;
@MicronautTest
class DemoTest {
@Inject
EmbeddedApplication<?> application;
@Test
void testItWorks() {
Assertions.assertTrue(application.isRunning());
}
}
这个JUnit 5测试被注释为@MicronautTest ,这是一个JUnit 5扩展,允许JUnit 5测试将任何组件注入到测试本身。在这种情况下,它是模拟运行中的应用程序的EmbeddedApplication 类型。
为Micronaut开发设置一个IDE
一般来说,Micronaut框架基于Java注释处理(APT)的优势之一是不需要任何特殊的构建工具就可以使用该框架。所有流行的IDE都支持注释处理,尽管有些IDE,如Eclipse,需要你明确地启用注释处理。
说到这里,Micronaut框架的普及已经让IDE供应商开发了对该框架的特殊支持。JetBrain的IntelliJ Ultimate为该框架的用户提供了优秀的工具,包括项目向导、配置的代码完成、Micronaut数据支持等等。
此外,通过免费的GraalVM扩展包,在Visual Studio Code中也有很棒的支持,它是基于NetBeans IDE的。它包括一个Micronaut项目创建向导,配置的代码完成,以及Micronaut应用程序的集成本地图像功能。
一旦你安装了这些选项中的任何一个,只需在IDE中打开Gradle或Maven项目就能设置好一切,你就可以开始工作了。
编写REST APIs
Micronaut框架支持广泛的服务器端工作负载,包括REST、gRPC、GraphQL,以及使用Kafka、RabbitMQ、JMS和MQTT等消息传递技术的消息驱动型微服务。这个介绍将着重于用默认的基于Netty的HTTP服务器构建REST应用程序。
Micronaut应用中的每一个HTTP路由都是由一个带有@Controller 注解的Java类来定义的。该注解的名字源自于模型视图控制器MVC模式。一个用@Controller 注解的类型可以指定一个或多个方法来映射到一个特定的HTTP动词和URI。
典型的 "Hello World "例子可以用Micronaut控制器实现,如下所示:
package demo;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/hello")
public class HelloController {
@Get(uri="/{name}", produces=MediaType.TEXT_PLAIN)
String hello(String name) {
return "Hello " + name;
}
}
该控制器被映射到URI/hello 。一个用@Get注释的方法来处理HTTP GET,并使用RFC 5741 URI模板来绑定该方法的name 参数。你可以通过在IDE中运行Application 类的main 方法来启动服务器,或者使用./gradlew run 或./mvnw mn:run 。现在你可以通过向Micronaut HTTP服务器使用的默认8080端口发送一个curl请求来手动测试端点:
curl -i http://localhost:8080/hello/John
HTTP/1.1 200 OK
date: Mon, 28 Mar 2022 13:08:54 GMT
Content-Type: text/plain
content-length: 10
connection: keep-alive
Hello John
然而,随着Micronaut框架对测试的强烈关注,还有什么比测试更好的方式来尝试API呢?下面是上面HelloController例子的一个简单的JUnit 5测试:
package demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
@MicronautTest
public class HelloControllerTest {
@Test
void testHello(@Client("/") HttpClient client) {
var message = client
.toBlocking()
.retrieve("/hello/John");
assertEquals("Hello John", message);
}
}
上面的测试注入了Micronaut的HTTP客户端,并向/hello/John URI发送了一个GET请求,断言结果是正确的。
Micronaut框架的一个巨大好处是,执行像上面这样的测试是非常快的,可以和常规的单元测试相媲美。尽管@MicronautTest注解启动了Micronaut服务器,并且测试执行了一个完整的HTTP请求/响应周期,但测试的执行速度并没有下降。没有必要为HTTP服务器学习大量的嘲弄API!这鼓励了开发人员编写更多的集成测试。这鼓励了开发人员编写更多的集成测试,提供了显著的长期可维护性和代码质量优势。
访问数据库
在服务器端应用程序中,访问数据库是如此常见的活动,以至于许多框架提供了简化功能来提高开发人员在这方面的生产力。Micronaut框架也不例外。
然而,Micronaut Data是一个具有扭曲性的数据库访问工具包。利用与Micronaut编译器的集成,Micronaut Data增加了数据库查询的编译时检查和构建时计算,以提高运行时效率。
与Spring Data JPA非常相似,Micronaut Data允许你使用存储库模式指定Java接口,在编译时自动为你实现数据库查询。
Micronaut Data在编译时对资源库接口的方法签名进行分析,如果可以的话就实现该接口。否则,将发生编译错误。
Micronaut Data支持几种不同的数据库和查询格式包括:
- Hibernate和JPA- 你可以使用JPA和Hibernate,Micronaut Data JPA在编译期间计算JPA查询(如上所述)。
- JDBC和SQL- 对于那些喜欢原始SQL和简单的数据映射器而不是对象关系映射(ORM)的人来说,Micronaut Data JDBC提供了一个简单的解决方案,可以从关系型数据库读写Java 17+记录和POJOs。
- MongoDB- 作为最新增加的功能,Micronaut Data MongoDB直接与MongoDB驱动集成,使用Micronaut Serialization以完全无反射的方式将对象编码到BSON。
- R2DBC- 基于Netty,Micronaut框架具有一个反应式、非阻塞式的核心。通过将Micronaut Netty服务器与反应式数据库连接(R2DBC)规范和支持的数据库相结合,你可以编写避免端到端阻塞的SQL应用程序。
- Oracle Coherence- 一个用于大规模的分布式数据网格,Coherence的特点是与Micronaut Data的专门集成,以轻松实现由Coherence集群支持的存储库。
涵盖Micronaut框架中所有不同的数据库访问选项本身就是一个文章系列。幸运的是,在这个问题上有一些优秀的指南。查看"用Micronaut Data JDBC访问数据库 "或"用Micronaut Data Hibernate/JPA访问数据库",以获得入门的帮助。
我个人最喜欢的是Micronaut Data JDBC,它是一个简单的JDBC的数据映射器。它基于编译时的bean introspections,完全消除了持久化层的反射。
一旦你将Gradle或Maven的构建配置为包括Micronaut Data JDBC,你就可以创建映射到数据库表、视图或查询结果的Java 17记录。这与JPA不同,JPA鼓励在Java类和表之间进行一对一的映射,并通过使用关联对模式进行完全建模。这些关联引入了诸如懒惰加载等概念,这些概念常常导致性能上的挑战(比如臭名昭著的N+1选择问题)。下面是一个记录定义的例子:
package demo;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
@MappedEntity
public record Person(
@Id
@GeneratedValue
@Nullable Long id,
String firstName,
String lastName,
int age
) {}
然后,你可以在接口中定义存储库的逻辑,实现构建应用程序所需的大部分通用逻辑,比如说:
package demo;
import java.util.List;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
@JdbcRepository(dialect=Dialect.H2)
public interface PersonRepository extends CrudRepository<Person, Long> {
Person findByAgeGreaterThan(int age);
List<Person> findByLastNameStartsWith(String str);
}
上面的例子通过CrudRepository超级接口提供了一整套的创建、读取、更新和删除操作(CRUD)。它还使用支持的查询表达式定义了自定义查询。
如果你有任何自定义需求,需要更高级的用例,你可以写自定义查询,标准查询,或者直接写JDBC逻辑来绑定结果。Micronaut Data JDBC使之变得轻而易举,同时完全不受反射和运行时生成的代理的影响。没有像JPA那样的状态和会话同步的概念,这有助于保持你的应用程序超级轻量级,并在GraalVM Native Image中表现出色。
此外,每个存储库的接口在编译时都会被检查。这可以防止存储库方法查询不存在的属性或使用不支持的返回类型,在保持Java的类型安全的同时,还可以实现这样一个很棒的动态功能。
构建一个本地可执行文件
Micronaut框架在GraalVM公开之前就已经发布了。尽管如此,这两个伟大的技术之间还是产生了自然的协同作用,主要是因为GraalVM的Native Image组件可以轻松地将Micronaut应用程序转化为本地可执行文件。
GraalVM Native Image对Java中的反射、运行时代理和动态类加载有很好的支持。然而,开发者确实需要为Native Image提供必要的配置,以声明这些使用何时何地发生。但是在Micronaut框架中,不需要声明这些使用,因为Micronaut应用程序在框架层面上不使用任何这些技术!这使得Micronaut框架中的封闭世界分析成为可能。这使得GraalVM Native Image在超前编译(AOT)期间的封闭世界分析变得更加简单。
当然,如果你使用了一个依赖反射的第三方库,那么你就需要声明其用途。但是,你所使用的框架内的大多数情况都是无反射的,这对你有很大的帮助。
Micronaut Gradle和Micronaut Maven插件利用了Oracle实验室的优秀的GraalVM本地构建工具项目来帮助简化本地可执行文件的构建。因此,用Gradle构建一个本地可执行文件就很简单。
./gradlew nativeCompile
Maven的对应命令是:
./mvnw package -Dpackaging=native-image
这两个命令都会在每个工具的构建目录中为它们所运行的平台生成一个本地可执行文件。
运行原生可执行文件将展示原生的第一个巨大好处:
./demo
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.4.0)
[main] INFO io.micronaut.runtime.Micronaut - Startup completed in 23ms. Server Running: http://localhost:8080
启动时间减少到几毫秒(上面的例子中为23毫秒),内存消耗也大大降低。有了这样一个巨大的减少,就有可能将Micronaut应用程序部署到内存限制更有限的环境中,或者在启动时间很关键的情况下(例如无服务器工作负载)。
请注意,进一步的研究正在进行,以通过在Oracle实验室开发的Micronaut AOT项目提供更显著的改进。在构建本地可执行文件之前,它在字节码上运行一个额外的静态分析步骤,以优化和消除死代码路径,并执行诸如将YAML转换为Java的任务,以避免在运行时需要YAML解析器。
为云构建
除了Native Image,Micronaut框架还支持一些不同的打包格式和部署目标,包括:
- 一个传统的可运行的JAR与
./gradlew assemble或./mvnw包。 - 带有Docker图像的
./gradlew dockerBuild or ./mvnw package -Dpackaging=docker - 含有来自GraalVM Native Image的本地可执行文件的Docker图像,带有
./gradlew dockerBuildNative或./mvnw package -Dpackaging=docker-native - 可以构建自定义的AWS Lambda运行时,将Micronaut应用部署到无服务器平台上。
- 与Kubernetes的广泛集成,简化了向Kubernetes的部署。
总的来说,Micronaut框架提供的功能集使其成为构建云原生Java应用的最佳选择,从分布式配置支持到集成服务发现,再到为AWS、Google Cloud、Azure和Oracle Cloud等云供应商提供常见抽象的实现的模块。这些抽象确保你的应用程序在云供应商之间保持可移植性。
总结
Micronaut框架为服务器端的Java工作负载引入了一股新鲜空气。它提供了一个创新的编译时间方法和功能集,使其成为构建现代云原生Java应用程序的最佳候选者。
与GraalVM Native Image的紧密结合以及与Oracle实验室的GraalVM团队的工作关系意味着重大的创新不断出现,如Micronaut AOT和Micronaut Serialization(Jackson Databind的无反射替代品)。
围绕着Micronaut框架出现了一个充满活力的社区,许多模块的实现提高了开发者的生产力,包括Micronaut数据,其中包括与数据库技术的关键集成。
社区的反馈继续推动着该框架的发展。因此,如果你有任何反馈,请不要犹豫,通过Micronaut社区分享关于新功能和改进的想法。