Micronaut 教程:如何使用基于 JVM 的框架构建微服务?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Micronaut 教程:如何使用基于 JVM 的框架构建微服务?相关的知识,希望对你有一定的参考价值。

本文要点:

Micronaut 是一种基于 jvm 的现代化全栈框架,用于构建模块化且易于测试的微服务应用程序。Micronaut 提供完全的编译时、反射无关的依赖注入和 AOP。该框架的开发团队和 Grails 框架的开发团队是同一个。Micronaut 框架集成了云技术,服务发现、分布式跟踪、断路器等微服务模式也内置到了框架中。在本教程中,你将使用不同的语言创建三个微服务:Java、Kotlin 和 Groovy。你还将了解使用 Micronaut HTTP 客户端消费其他微服务是多么容易,以及如何创建快速执行的功能测试。

与使用传统 JVM 框架构建的应用程序不同, Micronaut 提供 100% 的编译时、反射无关的依赖注入和 AOP。因此,Micronaut 应用程序很小,内存占用也很低。使用 Micronaut,你可以开发一个很大的单体应用或一个可以部署到 AWS Lambda 的小函数。框架不会限制你。

Micronaut 框架还集成了云技术,服务发现、分布式跟踪、断路器等微服务模式也内置到了框架中。

Micronaut 在 2018 年 5 月作为开源软件发布,计划在 2018 年底之前发布 1.0.0 版本。现在你可以试用 Micronaut,因为里程碑版本和发行候选版本已经可用。

Micronaut 框架的开发团队和 Grails 框架的开发团队是同一个。Grails 最近迎来了它的 10 周年纪念,它继续用许多生产力促进器帮助开发人员来编写 Web 应用程序。Grails 3 构建在 Spring Boot 之上。你很快就会发现,对于使用 Grails 和 Spring Boot 这两个框架的开发人员来说,Micronaut 有一个简单的学习曲线。

教程简介

在本系列文章中,我们将使用几个微服务创建一个应用程序:

  • 一个 books 微服务,使用 Groovy 编写;
  • 一个 inventory 微服务,使用 Kotlin 编写;
  • 一个 gateway 微服务,使用 Java 编写。
    你将完成以下工作:
  • 编写端点,使用编译时依赖注入;
  • 编写功能测试;
  • 配置那些 Micronaut 应用程序,注册到 Consul;
  • 使用 Micronaut 声明式 HTTP 客户端实现它们之间的通信。
    下图说明了你将要构建的应用程序:
    技术图片

微服务#1 Groovy 微服务

创建 Micronaut 应用的最简单方法是使用其命令行接口( Micronaut CLI ),使用 SDKMan 可以轻松安装。
Micronaut 应用程序可以使用 Java、Kotlin 和 Groovy 编写。首先,让我们创建一个 Groovy Micronaut 应用:

mn create-app example.micronaut.books --lang groovy .

上面的命令创建一个名为 books 的应用,默认包为 example.micronaut。

Micronaut 是测试框架无关的。它根据你使用的语言选择一个默认测试框架。在默认情况下,Java 使用 JUnit。如果你选择了 Groovy,在默认情况下,将使用 Spock。你可以搭配使用不同的语言和测试框架。例如,用 Spock 测试一个 Java Micronaut 应用程序。

而且,Micronaut 是构建工具无关的。你可以使用 Maven 或 Gradle 。默认使用 Gradle。

生成的应用中包含一个基于 Netty 的非阻塞 HTTP 服务器。

创建一个控制器暴露你的第一个 Micronaut 端点:

books/src/main/groovy/example/micronaut/BooksController.groovy
package example.micronaut
import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
@CompileStatic
@Controller("/api")
class BooksController 
 private final BooksRepository booksRepository
 BooksController(BooksRepository booksRepository) 
 this.booksRepository = booksRepository
 
 @Get("/books")
 List<Book> list() 
 booksRepository.findAll()
 

在上面的代码中,有几个地方值得一提:

  • 控制器暴露一个 route/api/books 端点,可以使用 GET 请求调用;
  • 注解 @Get 和 @Controller 的值是一个 RFC-6570 URI 模板;
  • 通过构造函数注入,Micronaut 提供了一个协作类:BooksRepository;
  • Micronaut 控制器默认消费和生成 JSON。
    上述控制器使用了一个接口和一个 POGO:
books/src/main/groovy/example/micronaut/BooksRepository.groovy
package example.micronaut
interface BooksRepository 
 List<Book> findAll()

books/src/main/groovy/example/micronaut/Book.groovy
package example.micronaut
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
@CompileStatic
@TupleConstructor
class Book 
 String isbn
 String name

Micronaut 在编译时把一个实现了 BooksRepository 接口的 bean 连接起来。

对于这个应用,我们创建了一个单例,我们是使用 javax.inject.Singleton 注解定义的。

books/src/main/groovy/example/micronaut/BooksRepositoryImpl.groovy
package example.micronaut
import groovy.transform.CompileStatic
import javax.inject.Singleton
@CompileStatic
@Singleton
class BooksRepositoryImpl implements BooksRepository 
 @Override
 List<Book> findAll() 
 [
 new Book("1491950358", "Building Microservices"),
 new Book("1680502395", "Release It!"),
 ]
 

功能测试的价值最大,因为它们测试了整个应用程序。但是,对于其他框架,很少使用功能测试和集成测试。大多数情况下,因为它们涉及到整个应用程序的启动,所以速度很慢。

然而,在 Micronaut 中编写功能测试是一件乐事。因为它们很快,非常快。

上述控制器的功能测试如下:

books/src/test/groovy/example/micronaut/BooksControllerSpec.groovy
package example.micronaut
import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class BooksControllerSpec extends Specification 
 @Shared
 @AutoCleanup
 EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
 @Shared @AutoCleanup RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())
 void "test books retrieve"()  
 when:
 HttpRequest request = HttpRequest.GET(‘/api/books‘)
 List<Book> books = client.toBlocking().retrieve(request, Argument.of(List, Book))
 then:
 books books.size() == 2
 

在上述测试中,有几个地方值得一提:

  • 借助 EmbeddedServer 接口,很容易从单元测试运行应用程序;
  • 很容易创建一个 HTTP 客户端 bean 来消费嵌入式服务器;
  • Micronaut Http 客户端很容易把 JSON 解析成 Java 对象。

    微服务#2 Kotlin 微服务

    运行下面的命令,创建另外一个名为 inventory 的微服务。这次,我们使用 Kotlin 语言。

&gt; mn create-app example.micronaut.inventory --lang kotlin
这个新的微服务控制着每本书的库存。
创建一个 Kotlin数据类,封装属性域:

inventory/src/main/kotlin/example/micronaut/Book.kt
package example.micronaut
data class Book(val isbn: String, val stock: Int)

创建一个控制器,返回一本书的库存。

inventory/src/main/kotlin/example/micronaut/BookController.kt
package example.micronaut
import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Produces
@Controller("/api") 
class BooksController 
 @Produces(MediaType.TEXT_PLAIN) 
 @Get("/inventory/isbn") 
 fun inventory(isbn: String): HttpResponse<Int> 
 return when (isbn)  
 "1491950358" -> HttpResponse.ok(2) 
 "1680502395" -> HttpResponse.ok(3) 
 else -> HttpResponse.notFound()
 
 

微服务#3 Java 微服务

创建一个 Java 网关应用,该应用会消费 books 和 inventory 这两个微服务。

mn create-app example.micronaut.gateway

如果不指定 lang 标识,就会默认选用 Java。

在 gateway 微服务中,创建一个声明式HTTP 客户端和books 微服务通信。

首先创建一个接口:

gateway/src/main/java/example/micronaut/BooksFetcher.java
package example.micronaut;
import io.reactivex.Flowable;
public interface BooksFetcher  
 Flowable<Book> fetchBooks(); 

然后,创建一个声明式 HTTP 客户端,这是一个使用了 @Client 注解的接口。

gateway/src/main/java/example/micronaut/BooksClient.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.micronaut.http.annotation.Get; 
import io.micronaut.http.client.Client; 
import io.reactivex.Flowable;
@Client("books") 
@Requires(notEnv = Environment.TEST) 
public interface BooksClient extends BooksFetcher 
 @Override @Get("/api/books") Flowable<Book> fetchBooks();

Micronaut 声明式 HTTP 客户端方法将在编译时实现,极大地简化了 HTTP 客户端的创建。

此外,Micronaut 支持应用程序环境的概念。在上述代码清单中,你可以看到,使用 @Requires 注解很容易禁止某些 bean 在特定环境中加载。

而且,就像你在前面的代码示例中看到的那样,非阻塞类型在 Micronaut 中是一等公民。BooksClient::fetchBooks() 方法返回 Flowable<Book>,其中 Book 是一个 Java POJO:

gateway/src/main/java/example/micronaut/Book.java
package example.micronaut;
public class Book 
 private String isbn; 
 private String name; 
 private Integer stock;
 public Book() 
 public Book(String isbn, String name)  
 this.isbn = isbn; 
 this.name = name; 
 
 public String getIsbn()  return isbn; 
 public void setIsbn(String isbn)  this.isbn = isbn; 
 public String getName()  return name; 
 public void setName(String name)  this.name = name; 
 public Integer getStock()  return stock; 
 public void setStock(Integer stock)  this.stock = stock; 

创建另外一个声明式 HTTP 客户端,与 inventory 微服务通信。

首先创建一个接口:

gateway/src/main/java/example/micronaut/InventoryFetcher.java
package example.micronaut;
import io.reactivex.Maybe;
public interface InventoryFetcher  
 Maybe<Integer> inventory(String isbn); 

然后,一个 HTTP 声明式客户端:

gateway/src/main/java/example/micronaut/InventoryClient.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.micronaut.http.annotation.Get; 
import io.micronaut.http.client.Client; 
import io.reactivex.Flowable;
import io.reactivex.Maybe; 
import io.reactivex.Single;
@Client("inventory") 
@Requires(notEnv = Environment.TEST)
public interface InventoryClient extends InventoryFetcher 
 @Override 
 @Get("/api/inventory/isbn") 
 Maybe<Integer> inventory(String isbn);

现在,创建一个控制器,注入两个 bean,创建一个反应式应答。

gateway/src/main/java/example/micronaut/BooksController.java
package example.micronaut;
import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.reactivex.Flowable;
@Controller("/api") public class BooksController 
 private final BooksFetcher booksFetcher; 
 private final InventoryFetcher inventoryFetcher;
 public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher) 
 this.booksFetcher = booksFetcher;
 this.inventoryFetcher = inventoryFetcher; 
 
 @Get("/books") Flowable<Book> findAll()  
 return booksFetcher.fetchBooks()
 .flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn())
 .filter(stock -> stock > 0)
 .map(stock ->  
 b.setStock(stock); 
 return b; 
 )
 );
 

在为控制器创建功能测试之前,我们需要在测试环境中为(BooksFetcher 和 InventoryFetcher)创建 bean 实现。

创建符合 BooksFetcher 接口的 bean,只适用于测试环境;参见 @Requires 注解。

gateway/src/test/java/example/micronaut/MockBooksClient.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.reactivex.Flowable;
import javax.inject.Singleton;
@Singleton 
@Requires(env = Environment.TEST) 
public class MockBooksClient implements BooksFetcher 
 @Override
 public Flowable<Book> fetchBooks()  
 return Flowable.just(new Book("1491950358", "Building Microservices"), new Book("1680502395", "Release It!"), new Book("0321601912", "Continuous Delivery:"));
  

创建符合 InventoryFetcher 接口的 bean,只适用于测试环境;

gateway/src/test/java/example/micronaut/MockInventoryClient.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.reactivex.Maybe;
import javax.inject.Singleton;
@Singleton 
@Requires(env = Environment.TEST) 
public class MockInventoryClient implements InventoryFetcher 
 @Override 
 public Maybe<Integer> inventory(String isbn)  
 if (isbn.equals("1491950358"))  
 return Maybe.just(2); 
  
 if (isbn.equals("1680502395"))  
 return Maybe.just(0); 
  
 return Maybe.empty();
  

创建功能测试。在 Groovy 微服务中,我们编写了一个 Spock 测试,这次,我们编写 JUnit 测试。

gateway/src/test/java/example/micronaut/BooksControllerTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.List;
public class BooksControllerTest 
 private static EmbeddedServer server; 
 private static HttpClient client;
 @BeforeClass 
 public static void setupServer() 
 server = ApplicationContext.run(EmbeddedServer.class); 
 client = server .getApplicationContext() .createBean(HttpClient.class, server.getURL());
 
 @AfterClass 
 public static void stopServer() 
 if (server != null)  
 server.stop();
 
 if (client != null)  
 client.stop();
 
 
 @Test 
 public void retrieveBooks()  
 HttpRequest request = HttpRequest.GET("/api/books"); 
 List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class)); 
 assertNotNull(books); 
 assertEquals(1, books.size());
  

服务发现

我们将配置我们的 Micronaut 微服务,注册到 Consul 服务发现。

Consul 是一个分布式服务网格,用于跨任何运行时平台和公有或私有云连接、防护和配置服务。

Micronaut 与 Consul 的集成很简单。

首先向 books、inventory 和 gateway 三个微服务中的每一个添加服务发现客户端依赖项:

gateway/build.gradle
runtime "io.micronaut:discovery-client"
books/build.gradle
runtime "io.micronaut:discovery-client"
inventory/build.gradle
runtime "io.micronaut:discovery-client"

我们需要对每个应用的配置做一些修改,以便应用启动时注册到 Consul。

gateway/src/main/resources/application.yml
micronaut:
 application:
 name: gateway 
 server:
 port: 8080
consul:
 client:
 registration: 
 enabled: true
 defaultZone: "$CONSUL_HOST:localhost:$CONSUL_PORT:8500"
books/src/main/resources/application.yml
micronaut:
 application:
 name: books
 server:
 port: 8082
consul:
 client:
 registration: 
 enabled: true
 defaultZone: "$CONSUL_HOST:localhost:$CONSUL_PORT:8500"
inventory/src/main/resources/application.yml
micronaut:
 application:
 name: inventory
 server:
 port: 8081
consul:
 client:
 registration: 
 enabled: true
 defaultZone: "$CONSUL_HOST:localhost:$CONSUL_PORT:8500"

每个服务在 Consul 中注册时都使用属性 microaut.application .name 作为服务 id。这就是为什么我们在前面的 @Client 注解中使用那些明确的名称。

前面的代码清单展示了 Micronaut 的另一个特性,配置文件中有带默认值的环境变量插值,如下所示:

defaultZone: "$CONSUL_HOST:localhost:$CONSUL_PORT:8500"

另外,在 Micronaut 中可以有特定于环境的配置文件。我们将在每个环境中创建一个名为 application-test.yml 的文件,用于测试阶段的 Consul 注册。


gateway/src/test/resources/application-test.yml
consul:
 client:
 registration: enabled: false
books/src/test/resources/application-test.yml
consul:
 client:
 registration: enabled: false
inventory/src/test/resources/application-test.yml
consul:
 client:
 registration: enabled: false

**运行应用
开始使用 Consul 的最简单方式是通过 Docker。现在,运行一个 Docker 实例。

docker run -p 8500:8500 consul

使用 Gradle 创建一个多项目构建。在根目录下创建一个settings.gradle 文件。

settings.gradle
include ‘books‘ 
include ‘inventory‘ 
include ‘gateway‘

现在,你可以并行运行每个应用了。Gradle 为此提供了一个方便的标识(-parallel):

./gradlew -parallel run

每个微服务都在配置好的端口上启动:8080、8081 和 8082。

Consul 提供了一个 html UI。在浏览器中打开 http://localhost:8500/ui,你会看到
技术图片

每个 Micronaut 微服务都已注册到 Consul。

你可以使用下面的 curl 命令调用网关微服务:


$ curl http://localhost:8080/api/books ["isbn":"1680502395","name":"Release It!","stock":3, "isbn":"1491950358","name":"Building Microservices","stock":2]

恭喜你已经创建好了第一个 Micronaut 微服务网络!

小结

在本教程中,你用不同的语言创建了三个微服务:Java、Kotlin 和 Groovy。你还了解了使用 Micronaut HTTP 客户端消费其他微服务是多么容易,以及如何创建快速执行的功能测试。

此外,你创建的一切都可以利用完全反射无关的依赖注入和 AOP。

以上是关于Micronaut 教程:如何使用基于 JVM 的框架构建微服务?的主要内容,如果未能解决你的问题,请参考以下文章

在 GCP 上运行基于 Java 的 API 后端(Spring Boot、Micronaut、Quarkus)最经济有效的方法是啥?

云原生Java框架有哪些呢?

Micronaut 3:如何使用 PubSubEmulatorContainer

您如何使用本地属性从 gradle 运行 micronaut

如何安装 Micronaut CLI?

如何配置 Jackson 在 Micronaut 中使用 SNAKE_CASE?