Micronaut教程:如何使用基於JVM的框架構建微服務
本文要點
與使用傳統JVM框架構建的應用程式不同,ofollow,noindex" target="_blank">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語言。
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。
歡迎感興趣的讀者和我一起編寫即將到來的第二部分。同時,請在下面的評論區自由提問。
關於作者
Sergio del Amo
Caballero
是一名專門從事以Grails/Micronaut為後端的移動手機應用程式(iOS、Android)開發的開發人員。自2015年以來,Sergio del Amo圍繞Groovy生態系統和微服務撰寫簡訊“Groovy Calamari
”。Groovy、Grails、Micronaut, Gradle、…
檢視英文原文:Micronaut Tutorial: How to Build Microservices with this JVM-based Framework