39

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

 5 years ago
source link: http://www.infoq.com/cn/articles/micronaut-tutorial-microservices-jvm?amp%3Butm_medium=popular_widget&%3Butm_campaign=popular_content_list&%3Butm_content=homepage
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文要点

  • 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客户端实现它们之间的通信。

下图说明了你将要构建的应用程序:

8741-1539964751215.png

微服务#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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK