Micronaut教程(二):分布式跟踪、JWT安全和AWS Lambda部署

关键要点

  • Micronaut提供了与Zipkin和Jaeger等多种分布式跟踪解决方案的无缝集成。
  • 框架提供了几种“开箱即用”的安全解决方案,例如基于JWT的认证。
  • Micronaut提供了“令牌传播”之类的功能,用以简化微服务之间的安全通信。
  • 因为内存占用少,Micronaut能够运行在功能即服务(FaaS)无服务器环境中。

在本系列的第一篇文章中,我们使用基于JVM的Micronaut框架开发并部署了三个微服务。在第二篇文章中,我们将为应用程序添加几个功能:分布式跟踪、JWT安全性和无服务器功能。此外,我们也将介绍Micronaut提供的用户输入验证功能。

分布式跟踪

将系统分解为更小、更细粒度的微服务可以带来多种好处,但也会给生产环境的监控系统增加复杂性。

你应该假设你的网络将会受到恶意实体的骚扰,它们时刻准备着随心所欲地释放它们的愤怒。
——Sam Newman,《构建微服务》

Micronaut与Jaeger和Zipkin原生集成——它们都是顶级的开源分布式跟踪解决方案。

Zipkin是一种分布式跟踪系统,用于收集时序数据,这些数据可用于解决微服务架构中的延迟问题。它负责收集和查找这些数据。

启动Zipkin的简单方法是通过Docker:

$ docker run -d -p 9411:9411 openzipkin/zipkin

这个应用程序由三个微服务组成,也就是我们在第一篇文章中开发的三个微服务(gateway、inventory、books)。

我们需要对这三个微服务做出修改。

修改build.gradle,加入跟踪依赖项:

build.gradle  compile \u0026quot;io.micronaut:micronaut-tracing\u0026quot;

将以下依赖项添加到build.gradle中,这样就可以将跟踪数据发送到Zipkin。

build.gradle  runtime 'io.zipkin.brave:brave-instrumentation-http'  runtime 'io.zipkin.reporter2:zipkin-reporter'  compile 'io.opentracing.brave:brave-opentracing'

配置跟踪选项:

src/main/resources/application.ymltracing:    zipkin:        http:            url: http://localhost:9411        enabled: true        sampler:            probability: 1

设置tracing.zipkin.sample.probability = 1,意思是我们要跟踪所有的请求。在生产环境中,你可能希望设置较低的百分比。

在测试时禁用跟踪:

src/test/resources/application-test.yml    tracing:        zipkin:            enabled: false

只需要很少的配置更改,就可以将分布式跟踪集成到Micronaut中。

运行应用程序

现在让我们运行应用程序,看看分布式跟踪集成是否能够正常运行。在第一篇文章中,我们集成了Consul,用于实现服务发现。因此,在启动微服务之前需要先启动Zipkin和Consul。在微服务启动好以后,它们将在Consul服务发现中进行注册。当我们发出请求时,它们会向Zipkin发送数据。

Gradle提供了一个flag(-parallel)用来启动微服务:

./gradlew -parallel run

你可以通过cURL命令向三个微服务发起请求:

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

然后,你可以通过http://localhost:9411来访问Zipkin UI。

JWT安全性

Micronaut提供了多种开箱即用的安全选项,你可以使用基本的身份验证、基于会话的身份验证、JWT身份验证、Ldap身份验证,等等。JSON Web Token(JWT)是一种开放的行业标准(RFC 7519)用于在参与方之间声明安全。

Micronaut提供了开箱即用的用于生成、签名、加密和验证JWT令牌的功能。

我们将把JWT身份验证集成到我们的应用程序中。

修改gateway微服务,让它支持JWT

gateway微服务将负责生成和传播JWT令牌。

修改build.gradle,为每个微服务(gateway、inventory和books)添加micronaut-security-jwt依赖项:

gateway/build.gradle      compile \u0026quot;io.micronaut:micronaut-security-jwt\u0026quot;       annotationProcessor \u0026quot;io.micronaut:micronaut-security\u0026quot;

修改application.yml:

gateway/src/main/resources/application.ymlmicronaut:    application:        name: gateway    server:        port: 8080    security:        enabled: true        endpoints:            login:                enabled: true            oauth:                enabled: true        token:            jwt:                enabled: true               signatures:                   secret:                       generator:                           secret: pleaseChangeThisSecretForANewOne            writer:                header:                   enabled: true            propagation:                enabled: true                service-id-regex: \u0026quot;books|inventory\u0026quot;

我们做了几个重要的配置变更:

  • micronaut.security.enable = true启用了安全,并默认为每个端点提供安全保护。
  • micronaut.security.endpoints.login.enable = true启用了/login端点,我们将用它进行身份验证。
  • micronaut.security.endpoints.oauth.enable = true启用了/oauth/access_tokenendpoint端点,在令牌过期时,我们可以使用它来获取新的JWT访问令牌。
  • micronaut.security.jwt.enable = true启用了JWT功能。
  • 我们让应用程序启用签名的JWT。更多的签名和加密选项,请参阅JWT令牌生成文档。
  • micronaut.security.token.propagation.enabled = true表示启用了令牌传播。这是一种在微服务架构中简化JWT或其他令牌安全机制的功能。
  • micronaut.security.writer.header.enabled = ture启用了一个令牌写入器,它将为开发人员在HTTP标头中写入JWT令牌。
  • micronaut.security.token.propagation.service-id-regex设置了一个正则表达式,用于匹配需要进行令牌传播的服务。我们匹配了应用程序中的其他两个服务。

你可以使用@Secured注解来配置Controller或Controller Action级别的访问。

使用@Secured(“isAuthenticated()”)注解BookController.java,只允许经过身份验证的用户访问。同时记得使用@Secured(“isAuthenticated()”)注解inventory和books微服务的BookController类。

/login端点被调用时,会尝试通过任何可用的AuthenticationProvider对用户进行身份验证。为了简单起见,我们将允许两个用户访问,他们是福尔摩斯和华生。创建SampleAuthenticationProvider:

gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.javapackage example.micronaut;import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.security.authentication.AuthenticationFailed; import io.micronaut.security.authentication.AuthenticationProvider; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import io.micronaut.security.authentication.UserDetails; import io.reactivex.Flowable; import org.reactivestreams.Publisher;import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays;@Requires(notEnv = Environment.TEST) @Singleton public class SampleAuthenticationProvider implements AuthenticationProvider {    @Override    public Publisher\u0026lt;AuthenticationResponse\u0026gt; authenticate(AuthenticationRequest authenticationRequest) {         if (authenticationRequest.getIdentity() == null) {             return Flowable.just(new AuthenticationFailed());         }         if (authenticationRequest.getSecret() == null) {             return Flowable.just(new AuthenticationFailed());         }         if (Arrays.asList(\u0026quot;sherlock\u0026quot;, \u0026quot;watson\u0026quot;).contains(authenticationRequest.getIdentity().toString()) \u0026amp;\u0026amp; authenticationRequest.getSecret().equals(\u0026quot;elementary\u0026quot;))     {             return Flowable.just(new UserDetails(authenticationRequest.getIdentity().toString(), new ArrayList\u0026lt;\u0026gt;()));         }         return Flowable.just(new AuthenticationFailed());     } }

修改inventory和books,让它们支持JWT

对于inventory和books,除了添加micronaut-security-jwt依赖项并使用@Secured注解控制器之外,我们还需要修改application.yml,以便能够验证在gateway中生成和签名的JWT令牌。

修改application.yml:

inventory/src/main/resources/application.ymlmicronaut:    application:        name: inventory    server:        port: 8081    security:        enabled: true         token:            jwt:                 enabled: true                 signatures:                     secret:                           validation:                               secret: pleaseChangeThisSecretForANewOne

请注意,我们使用与gateway配置中相同的秘钥,这样就可以验证由gateway微服务签名的JWT令牌。

运行安全的应用程序

在启动了Zipkin和Consul之后,你就可以同时启动这三个微服务。Gradle提供了一个方便的flag(-parallel):

./gradlew -parallel run

你可以运行cURL命令,然后会收到401错误,表示未授权!

$ curl -I http://localhost:8080/api/books HTTP/1.1 401 UnauthorizedDate: Mon, 1 Oct 2018 18:44:54 GMT transfer-encoding: chunked connection: close

我们需要先登录,并获得一个有效的JWT访问令牌:

$ curl -X \u0026quot;POST\u0026quot; \u0026quot;http://localhost:8080/login\u0026quot; \\-H 'Content-Type: application/json; charset=utf-8' \\-d $'{ \u0026quot;username\u0026quot;: \u0026quot;sherlock\u0026quot;, \u0026quot;password\u0026quot;: \u0026quot;password\u0026quot; }' {\u0026quot;username\u0026quot;:\u0026quot;sherlock\u0026quot;,\u0026quot;access_token\u0026quot;:\u0026quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWI iOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYX Rld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiOjE1Mzg0MTI0MDl9.1W4CXbN1bJgM CQlCDKJtm7zHWzyZeIr1rHpTuDy6h0\u0026quot;,\u0026quot;refresh_token\u0026quot;:\u0026quot;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ zaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2 F5IiwiaWF0IjoxNTM4NDEyNDA5fQ.l72msZKwHmYeLs7T0vKtRxu7_DZr62rPCILNmC 7UEZ4\u0026quot;,\u0026quot;expires_in\u0026quot;:3600,\u0026quot;token_type\u0026quot;:\u0026quot;Bearer\u0026quot;}

Micronaut提供了开箱即用的RFC 6750 Bearer Token规范支持。我们可以使用从/login响应标头中获得的JWT来调用/api/books端点。

curl \u0026quot;http://localhost:8080/api/books\u0026quot; \\ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOS wicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiO jE1Mzg0MTI0MDl9.1W4CXbN1bJgMCQlCDKJtm7zHWz-yZeIr1rHpTuDy6h0'[{\u0026quot;isbn\u0026quot;:\u0026quot;1680502395\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Release It!\u0026quot;,\u0026quot;stock\u0026quot;:3}, {\u0026quot;isbn\u0026quot;:\u0026quot;1491950358\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Building Microservices\u0026quot;,\u0026quot;stock\u0026quot;:2}]

Serverless

我们将添加一个部署到AWS Lambda的功能来验证books的ISBN。

mn create-function example.micronaut.isbn-validator

注意:我们使用了Micronaut CLI提供的create-function命令。

验证

我们将创建一个单例来处理ISBN 10验证。

创建一个封装操作的接口:

package example.micronaut;import javax.validation.constraints.Pattern;public interface IsbnValidator {    boolean isValid(@Pattern(regexp = \u0026quot;\\\\d{10}\u0026quot;) String isbn);}

Micronaut的验证基于标准框架JSR 380,也称为Bean Validation 2.0。

Hibernate Validator是这个标准的参考实现。

将以下代码段添加到build.gradle中:

isbn-validator/build.gradle     compile \u0026quot;io.micronaut.configuration:micronaut-hibernatevalidator\u0026quot;

创建一个实现了IsbnValidator的单例。

isbn-validator/src/main/java/example/micronaut/DefaultIsbnValidator.javapackage example.micronaut;import io.micronaut.validation.Validated; import javax.inject.Singleton; import javax.validation.constraints.Pattern;@Singleton @Validated public class DefaultIsbnValidator implements IsbnValidator {    /**      * must range from 0 to 10 (the symbol X is used for 10), and must be such that the sum of all the ten digits, each multiplied by its (integer) weight, descending from 10 to 1, is a multiple of 11.   * @param isbn 10 Digit ISBN   * @return whether the ISBN is valid or not.   */   @Override   public boolean isValid(@Pattern(regexp = \u0026quot;\\\\d{10}\u0026quot;) String isbn) {        char[] digits = isbn.toCharArray();        int accumulator = 0;        int multiplier = 10;        for (int i = 0; i \u0026lt; digits.length; i++) {            char c = digits[i];            accumulator += Character.getNumericValue(c) * multiplier;            multiplier--;        }        return (accumulator % 11 == 0);   }}

与之前的代码清单一样,你要为需要验证的类添加@Validated注解。

创建单元测试:

isbn-validator/src/test/java/example/micronaut/IsbnValidatorTest.javapackage example.micronaut;import io.micronaut.context.ApplicationContext; import io.micronaut.context.DefaultApplicationContext; import io.micronaut.context.env.Environment; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException;import javax.validation.ConstraintViolationException;import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue;public class IsbnValidatorTest {    private static ApplicationContext applicationContext;    @BeforeClass     public static void setupContext() {         applicationContext = new DefaultApplicationContext(Environment.TEST).start();     }        @AfterClass     public static void stopContext() {        if (applicationContext!=null) {                 applicationContext.stop();        }    }    @Rule public ExpectedException thrown = ExpectedException.none();    @Test public void testTenDigitValidation() {       thrown.expect(ConstraintViolationException.class);       IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);       isbnValidator.isValid(\u0026quot;01234567891\u0026quot;);     }    @Test     public void testControlDigitValidationWorks() {          IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class);        assertTrue(isbnValidator.isValid(\u0026quot;1491950358\u0026quot;));        assertTrue(isbnValidator.isValid(\u0026quot;1680502395\u0026quot;));        assertFalse(isbnValidator.isValid(\u0026quot;0000502395\u0026quot;));    }}

如果我们尝试使用十一位数字字符串调用该方法,就会抛出javax.validation.ConstraintViolationException。

函数的输入和输出

这个函数将接受单个参数(ValidationRequest,它是一个封装了ISBN的POJO)。

isbn-validator/src/main/java/example/micronaut/IsbnValidationRequest.javapackage example.micronaut;public class IsbnValidationRequest {     private String isbn;    public IsbnValidationRequest() {    }    public IsbnValidationRequest(String isbn) {         this.isbn = isbn;     }    public String getIsbn() { return isbn; }    public void setIsbn(String isbn) { this.isbn = isbn; }}

并返回单个结果(ValidationResponse,一个封装了ISBN和一个指示ISBN是否有效的布尔值的POJO)。

isbn-validator/src/main/java/example/micronaut/IsbnValidationResponse.javapackage example.micronaut;public class IsbnValidationResponse {     private String isbn;     private Boolean valid;        public IsbnValidationResponse() {    }    public IsbnValidationResponse(String isbn, boolean valid) {        this.isbn = isbn;         this.valid = valid;     }    public String getIsbn() {         return isbn;     }    public void setIsbn(String isbn) {         this.isbn = isbn;     }    public Boolean getValid() {         return valid;     }    public void setValid(Boolean valid) {         this.valid = valid;     }}

函数测试

当我们运行create-function命令时,Micronaut会在src/main/java/example/micronaut目录创建一个IsbnValidatorFunction类。修改它,让它实现java.util.Function接口。

isbn-validator/src/main/java/example/micronaut/IsbnValidatorFunction.javapackage example.micronaut;import io.micronaut.function.FunctionBean;import java.util.function.Function; import javax.validation.ConstraintViolationException;@FunctionBean(\u0026quot;isbn-validator\u0026quot;) public class IsbnValidatorFunction implements Function\u0026lt;IsbnValidationRequest, IsbnValidationResponse\u0026gt; {    private final IsbnValidator isbnValidator;    public IsbnValidatorFunction(IsbnValidator isbnValidator) {        this.isbnValidator = isbnValidator;     }    @Override     public IsbnValidationResponse apply(IsbnValidationRequest req) {       try {            return new IsbnValidationResponse(req.getIsbn(), isbnValidator.isValid(req.getIsbn()));        } catch(ConstraintViolationException e) {             return new IsbnValidationResponse(req.getIsbn(),false);        }    }}

上面的代码做了几件事:

  • 使用@FunctionBean注解了一个返回函数的方法。
  • 你可以在函数中使用Micronaut的编译时依赖注入。我们通过构造函数注入了IsbnValidator。

函数也可以作为Micronaut应用程序上下文的一部分运行,这样方便进行测试。应用程序已经在类路径中包含了用于测试的function-web和HTTP服务器依赖项:

isbn-validator/build.gradle     testRuntime \u0026quot;io.micronaut:micronaut-http-server-netty\u0026quot;      testRuntime \u0026quot;io.micronaut:micronaut-function-web\u0026quot;

要在测试中调用函数,需要修改IsbnValidatorClient.java

isbn-validator/src/test/java/example/micronaut/IsbnValidatorClient.javapackage example.micronaut;import io.micronaut.function.client.FunctionClient; import io.micronaut.http.annotation.Body; import io.reactivex.Single;import javax.inject.Named;@FunctionClient public interface IsbnValidatorClient {    @Named(\u0026quot;isbn-validator\u0026quot;)     Single\u0026lt;IsbnValidationResponse\u0026gt; isValid(@Body IsbnValidationRequest isbn);}

同时修改IsbnValidatorFunctionTest.java。我们需要测试不同的场景(有效的ISBN、无效的ISBN、超过10位的ISBN和少于10位的ISBN)。

isbn-validator/src/test/java/example/micronaut/IsbnValidatorFunctionTest.javapackage example.micronaut;import io.micronaut.context.ApplicationContext; import io.micronaut.runtime.server.EmbeddedServer; import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue;public class IsbnValidatorFunctionTest {    @Test public void testFunction() {         EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);        IsbnValidatorClient client = server.getApplicationContext().getBean(IsbnValidatorClient.class);        assertTrue(client.isValid(new IsbnValidationRequest(\u0026quot;1491950358\u0026quot;)).blockingGet().getValid());        assertTrue(client.isValid(new    IsbnValidationRequest(\u0026quot;1680502395\u0026quot;)).blockingGet().getValid());        assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;0000502395\u0026quot;)).blockingGet().getValid());        assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;01234567891\u0026quot;)).blockingGet().getValid());        assertFalse(client.isValid(new IsbnValidationRequest(\u0026quot;012345678\u0026quot;)).blockingGet().getValid());        server.close();}}

部署到AWS Lambda

假设你拥有Amazon Web Services(AWS)帐户,那么就可以转到AWS Lambda并创建一个新功能。

选择Java 8运行时。名称为isbn-validator,并创建一个新的角色表单模板。角色名称为lambda_basic_execution。

\"image\"

运行./gradlew shadowJar生成一个Jar包。

shadowJar是Gradle ShadowJar插件提供的一个任务。

$ du -h isbn-validator/build/libs/isbn-validator-0.1-all.jar 11M isbn-validator/build/libs/isbn-validator-0.1-all.jar

上传JAR,并指定Handler。

io.micronaut.function.aws.MicronautRequestStreamHandler 

我只分配了256Mb内存,超时时间为25秒。

\"image\"

从另一个微服务中调用函数

我们将在gateway微服务中使用这个lambda。修改gateway微服务中的build.gradle,添加micronaut-function-client:

com.amazonaws:aws-java-sdk-lambda dependencies:build.gradle     compile \u0026quot;io.micronaut:micronaut-function-client\u0026quot;      runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'

修改src/main/resources/application.yml:

src/main/resources/application.ymlaws:    lambda:        functions:            vat:                functionName: isbn-validator                 qualifer: isbn         region: eu-west-3 # Paris Region

创建一个接口:

src/main/java/example/micronaut/IsbnValidator.javapackage example.micronaut;import io.micronaut.http.annotation.Body;import io.reactivex.Single;public interface IsbnValidator {     Single\u0026lt;IsbnValidationResponse\u0026gt; validateIsbn(@Body IsbnValidationRequest req); }

创建一个@FunctionClient:

src/main/java/example/micronaut/FunctionIsbnValidator.javapackage example.micronaut;import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.function.client.FunctionClient; import io.micronaut.http.annotation.Body; import io.reactivex.Single;import javax.inject.Named;@FunctionClient @Requires(notEnv = Environment.TEST) public interface FunctionIsbnValidator extends IsbnValidator {    @Override     @Named(\u0026quot;isbn-validator\u0026quot;)     Single\u0026lt;IsbnValidationResponse\u0026gt; validateIsbn(@Body IsbnValidationRequest req);}

关于上面这些代码有几点值得注意:

  • FunctionClient注解可以在接口上应用引入通知(introduction advice),这样接口定义的方法就会成为远程函数的调用者。
  • 使用函数名isbn-validator,与application.yml定义的一样。

最后一步是修改gateway的BookController,让它调用函数。

src/main/java/example/micronaut/BooksController.javapackage example.micronaut;import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.security.annotation.Secured; import io.reactivex.Flowable;import java.util.List;@Secured(\u0026quot;isAuthenticated()\u0026quot;)@Controller(\u0026quot;/api\u0026quot;) public class BooksController {    private final BooksFetcher booksFetcher;     private final InventoryFetcher inventoryFetcher;     private final IsbnValidator isbnValidator;    public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher, IsbnValidator isbnValidator) {        this.booksFetcher = booksFetcher;         this.inventoryFetcher = inventoryFetcher;         this.isbnValidator = isbnValidator;    }    @Get(\u0026quot;/books\u0026quot;)     Flowable\u0026lt;Book\u0026gt; findAll() {         return booksFetcher.fetchBooks()             .flatMapMaybe(b -\u0026gt; isbnValidator.validateIsbn(new IsbnValidationRequest(b.getIsbn()))                 .filter(IsbnValidationResponse::getValid)                 .map(isbnValidationResponse -\u0026gt; b)               )              .flatMapMaybe(b -\u0026gt; inventoryFetcher.inventory(b.getIsbn())                  .filter(stock -\u0026gt; stock \u0026gt; 0)                  .map(stock -\u0026gt; {                       b.setStock(stock);                       return b;                   })              );   }}

我们通过构造函数注入了IsbnValidator。调用远程函数对程序员来说是透明的。

结论

下面的图片说明了我们在这一系列文章中开发的应用程序:

  • 我们有三个微服务(一个Java服务、一个Groovy服务和一个Kotlin服务)。
  • 这些微服务使用Consul进行服务发现。
  • 这些微服务使用Zipkin作为分布式跟踪服务。
  • 我们添加了第四个微服务,一个部署到AWS Lambda的功能。
  • 微服务之间的通信是安全的。每个请求在Authorization Http标头中包含一个JWT令牌就可以通过网络。JWT令牌通过内部请求自动传播。

关于Micronaut的更多内容,请访问官方网站

关于作者

\"image\"

Sergio del AmoCaballero是一名手机应用程序(iOS、Android,后端由Grails/Micronaut驱动)开发者。自2015年起,Sergio del Amo为Groovy生态系统和微服务维护着一个新闻源Groovy Calamari

查看英文原文Micronaut Tutorial: Part 2: Easy Distributed Tracing, JWT Security and AWS Lambda Deployment

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值