3年前 (2021-07-07)  相关技术 |   抢沙发  1003 
文章评分 0 次,平均分 0.0

18年5月,OCI的一个开发团队发布了新开源框架的第一个里程碑:Micronaut

Micronaut是JVM的应用程序框架,特别强调微服务云原生应用程序。

可以理解的是,在一个似乎充斥着框架选项的行业中,开发人员通常希望提前知道新框架带来了什么,以及它提供了什么独特的特性或功能。本文的目标是:

  • 介绍Micronaut背后的一些基本原理
  • 强调使用该框架的一些关键优势
  • 带您浏览一个简单的应用程序,全面了解框架的结构和编程风格

Micronaut是一个JVM框架,用于使用Java、Groovy或Kotlin构建可伸缩、高性能的应用程序。

它提供(除其他外)以下所有功能:

  • 一种高效的编译时依赖注入容器
  • 基于Netty的反应式HTTP服务器与客户端
  • 在构建微服务系统时提高开发人员生产力的一套云原生功能。

该框架的灵感来自Spring和Grails,它提供了一个熟悉的开发工作流,但启动时间和内存使用量都很小。因此,Micronaut可用于传统MVC框架不可行的场景,包括Android应用程序、无服务器功能、物联网部署和CLI应用程序。

巨石崛起

目前开发的大多数jvmweb应用程序都基于一些框架,这些框架促进MVC(Model/View/Controller)模式,并提供依赖注入、AOP(Aspect-Oriented Programming)支持和易于配置。

springboot和Grails等框架依赖于springioc(inversionofcontrol)容器,它使用反射在运行时分析应用程序类,然后将它们连接在一起,为应用程序构建依赖关系图。反射元数据还用于为事务管理等功能生成代理。

这些框架为开发人员带来了许多好处,包括提高了生产效率、简化了样板文件和更具表现力的应用程序代码。

其中许多框架都是围绕(现在称为)一个整体式应用程序而设计的,这个应用程序是一个独立的程序,管理从数据库到UI的整个应用程序堆栈。然后将这些应用程序打包为二进制文件并部署到服务器上,通常是servlet容器(Tomcat、Glassfish等)。对于更完整的工作流,框架可以包含一个嵌入式容器,使应用程序更具可移植性。

云服务

今天,这些传统的应用程序架构正被新的模式和技术所取代。

许多组织正在将所谓的单片应用程序分解为更小的、面向服务的应用程序,这些应用程序在分布式系统中协同工作。

新的体系结构模式要求通过众多范围有限的独立应用程序(microservices)的交互来满足业务需求。

跨服务边界的通信(通常通过restfulhttp调用)是这种设计转变的关键。

MICRONAUT:面向未来的Java框架!

然而,现代框架不仅需要简化开发,还需要简化操作。

现代应用程序比以往任何时候都更加依赖云计算技术。

与管理服务器和数据中心的健康状况不同,组织越来越多地将其应用程序部署到平台上,在这些平台上,服务器的细节被抽象出来,并且可以使用复杂的工具和自动化来扩展、重新部署和监控服务。

当然,传统框架在很大程度上跟上了行业的变化,许多开发人员已经成功地构建了微服务,并将其部署到使用微服务的云提供商。

然而,新架构和云环境的需求揭示了使用这些工具时的一些潜在痛点。依赖运行时反射(用于DI和代理生成)会带来一些性能问题,包括启动、分析和连接应用程序所需的时间,以及加载和缓存此元数据所需的内存。

不幸的是,在给定的应用程序中,这些不是固定的度量标准;随着代码库规模的增长,资源需求也随之增长。

在云平台中,时间和内存都是实际成本。服务需要回收利用,并以最小的延迟重新上线。服务的数量也在增长(在大型系统中可能会增加到数百个)。对于每个服务的多个实例,很快就会发现,为了这些框架的方便,需要付出实际的代价。

此外,许多云提供商正在提供无服务器平台,如AWS Lambda,其中应用程序被简化为单一用途的功能,这些功能可以组合和编排以执行复杂的业务逻辑。

无服务器计算为应用程序提供了额外的激励,使其具有轻量级和响应性,并消耗最少的内存,从而加剧了传统的基于反射的框架的问题。

更好的方法

Micronaut的设计考虑到了微服务和云,同时保留了MVC编程模型和传统框架的其他特性。这主要是通过一个全新的DI/AOP容器实现的,它在编译时而不是运行时执行依赖注入。

通过在代码中注释类和类成员,可以使用与Spring非常相似的约定来表达应用程序的依赖关系和AOP行为;但是,在编译应用程序时会对此元数据进行分析。届时,Micronaut将在您自己的代码旁边生成其他类,创建bean定义、拦截器和其他工件,这些工件将在应用程序运行时启用DI/AOP行为。

提示:从技术角度来说,这种编译时处理是通过使用Java注释处理器实现的,Micronaut使用Java注释处理器分析类并创建相关的bean定义类。为了支持Groovy,框架使用AST转换来执行相同的处理。

Micronaut为Java依赖注入实现了jsr330规范,它在javax.inject包(比如@inject@Singleton)下提供了一组语义注释,以表示DI容器中类之间的关系。

Micronaut的DI的一个简单示例如下所示。

import javax.inject.*;
 
interface Engine {
    int getCylinders();
    String start();
}
 
@Singleton
public class V8Engine implements Engine {
    int cylinders = 8;
 
    String start() {
        return "Starting V8";
    }
}
 
@Singleton
public class Vehicle {
    final Engine engine
 
    public Vehicle(Engine engine) {
        this.engine = engine;
    }
 
    String public start() {
        return engine.start();
    }
}

当应用程序运行时,一个新的Vehicle实例将提供一个Engine接口的实例—在本例中是V8Engine。

import io.micronaut.context.*;
 
Vehicle vehicle = BeanContext.run().getBean(Vehicle);
System.out.println( vehicle.start() );

通过将DI容器的工作转移到编译阶段,代码库的大小与启动应用程序所需的时间(或存储反射元数据所需的内存)之间不再存在联系。

因此,用Java编写的Micronaut应用程序通常在一秒钟内启动。

Gradle

> ./gradlew run
 
Task :run
16:21:32.511 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 842ms. Server Running: http://localhost:8080

Maven

> ./mvnw compile exec:exec
[INFO] Scanning for projects...
[INFO] --- exec-maven-plugin:1.6.0:exec (default-cli) @ my-java-app ---
16:22:49.833 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 796ms. Server Running: http://localhost:8080

由于Groovy和Kotlin语言的开销,用Groovy和Kotlin编写的应用程序可能需要一秒钟左右的时间,使用第三方库(比如Hibernate)也会增加它们自己的启动和内存需求。然而,代码库的大小不再是启动时间或内存使用的重要因素;编译的字节码已经包含了在应用程序中运行和管理DI感知类所需的一切。

HTTP层

Micronaut的DI核心是框架的重要部分,但是通过HTTP公开服务(并使用其他服务)是微服务体系结构的另一个组成部分。

Micronaut的HTTP功能是建立在Netty之上的,Netty是一个异步网络框架,提供高性能、反应式事件驱动编程模型,并支持构建服务器和客户端应用程序。

在微服务系统中,许多应用程序将同时扮演这两个角色;通过网络公开数据的服务器,以及对系统中的其他服务发出请求的客户端。

与传统框架一样,Micronaut包含了一个用于服务请求的控制器的概念。一个简单的Micronaut控制器如下所示。

HelloController.java

import io.micronaut.http.annotation.*;
 
@Controller("/hello")
public class HelloController {
 
    @Get("/{name}")
    public String hello(String name) {
        return "Hello, " + name;
    }
}

这是一个微不足道的示例,但它演示了许多JavaMVC框架所使用的熟悉的编程模型。控制器只是带有方法的类,每个类都带有有意义的注释,Micronaut使用这些注释在编译时创建必要的HTTP处理代码。

在微服务环境中同样重要的是作为客户机与其他服务交互。

Micronaut在使其HTTP客户机功能等同于服务器功能方面付出了额外的努力,这意味着使用服务的代码与创建服务所需的代码惊人地相似。

下面是一个简单的Micronaut客户机,它将使用上面表示的控制器端点。

HelloClient.java

import io.micronaut.http.client.Client;
import io.micronaut.http.annotation.*;
 
@Client("/hello")
public interface HelloClient {
 
    @Get("/{name}")
    public String hello(String name);
}

HelloClient现在可以用来与在/hello URI上运行的服务交互。在DI容器中创建客户机bean、执行HTTP请求、绑定参数甚至解析响应所需的所有代码都是在编译时生成的。

此客户机可以在示例应用程序、单独的服务(假设URL设置正确或服务发现已启用)中使用,也可以在测试类中使用,如下所示。

HelloClientSpec.groovy

import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.*
 
class HelloClientSpec extends Specification {
    //start the application
    @Shared
    @AutoCleanup
    EmbeddedServer embeddedServer =  ApplicationContext.run(EmbeddedServer)
 
    //get a reference to HelloClient from the DI container
    @Shared
    HelloClient client = embeddedServer.applicationContext.getBean(HelloClient)
 
    void "test hello response"() {
        expect:
        client.hello("Bob") == "Hello, Bob"
    }
}

因为客户机和服务器方法共享相同的签名,所以通过实现共享接口,可以很容易地在请求的两端执行协议,共享接口可以存储在跨微服务系统使用的共享库中。

在我们的示例中,HelloControllerHelloClient都可能实现/扩展一个共享的HelloOperations接口。

HelloOperations.java

import io.micronaut.http.annotation.*;
 
public interface HelloOperations {
 
    @Get("/{name}")
    public String hello(String name);
}

HelloClient.java

@Client("/hello")
public interface HelloClient extends HelloOperations {/*..*/ }

HelloController.java

@Controller("/hello")
public class HelloController implements HelloOperations {/*..*/ }

反应式编程

反应式编程在Netty和Micronaut中都是一流的公民。

上面的控制器和客户机可以使用任何反应流实现(如rxjava2.0)轻松地重写。这允许您以完全非阻塞的方式编写所有HTTP逻辑,使用可观察的、订户的和单一的等反应性结构。

RxHelloController.java

import io.reactivex.*;
 
@Controller("/hello")
public class RxHelloController {
 
    @Get("/{name}")
    public Single<String> hello(String name) {
        return Single.just("Hello, " + name);
    }
}

RxHelloClient.java

import io.reactivex.*;
 
@Client("/hello")
public interface RxHelloClient {
 
    @Get("/{name}")
    public Single<String> hello(String name);
}

云原生

云原生Cloud-native应用程序专门设计为在云计算环境中运行,与系统中的其他服务交互,并在其他服务不可用或无响应时优雅地降级。

Micronaut包含了一系列的特性,使得构建这些类型的应用程序非常令人愉快。

Micronaut不依赖第三方工具或服务,而是为许多最常见的需求提供本机解决方案。

我们来看看其中的几个。

1.服务发现

服务发现意味着应用程序能够在一个中心注册中心上找到彼此(并使自己可以找到),而无需在配置中查找url或硬编码服务器地址。

Micronaut将服务发现支持直接构建到@Client注释中,这意味着执行服务发现就像提供正确的配置,然后使用所需服务的“服务ID”一样简单。

例如,以下配置将使用helloworld的服务ID向consur实例注册Micronaut应用程序。

src/main/resources/application.yml

micronaut:
      application:
          name: hello-world
  consul:
    client:
      registration:
        enabled: true
      defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

一旦应用程序启动并向Consul注册,客户端就可以通过在@Client注释中指定服务ID来查找服务。

@Client(id = "hello-world")
public interface HelloClient{
        //...
}

目前可用的服务发现提供商包括Consul和Kubernetes,并计划支持其他提供商。

2.负载均衡

当注册同一服务的多个实例时,Micronaut提供了一种形式的“循环”负载平衡,通过可用实例循环请求,以确保没有一个实例被淹没或未充分利用。

这是客户端负载平衡的一种形式,其中每个实例要么接受请求,要么将其传递给服务的下一个实例,从而自动将负载分布到可用实例上。

这种负载平衡基本上是“免费”进行的,但是,也可以提供一种替代实现。例如,Netflix的功能区库可以安装和配置为支持备用负载平衡策略。

src/main/resources/application.yml

ribbon:
      VipAddress: test
      ServerListRefreshInterval: 2000

3.可回收和断路器

在分布式系统中,当与其他服务交互时,不可避免地会出现某些情况,事情不会按计划进行;也许某个服务会暂时停止,或者干脆放弃一个请求。Micronaut提供了许多工具来优雅地处理这些灾难。

例如,Micronaut中的任何方法都可以用@Retryable注释,以便对该方法应用可定制的重试策略。当注释应用于@Client接口时,重试策略将应用于客户端中的每个请求方法。

@Retryable
@Client("/hello")
public interface HelloClient { /*...*/ }

默认情况下,@Retryable将尝试调用该方法三次,每次尝试之间有1秒的延迟。

当然,可以覆盖这些值,例如:

@Retryable( attempts = "5", delay = "2s" )
@Client("/hello")
public interface HelloClient { /*...*/ }

如果硬编码值给您留下了不好的印象,您可以从配置中注入值,如果没有提供配置,可以选择提供默认值。

@Retryable( attempts = "${book.retry.attempts:3}",
            delay = "${book.retry.delay:1s}" )
@Client("/hello")
public interface HelloClient { /*...*/ }

@Retryable更复杂的形式是@circuitbruker注释。它的行为稍有不同,它将允许在给定的重置周期(默认情况下为30秒)内“打开”电路之前,进行指定次数的尝试失败,从而导致该方法在不执行代码的情况下立即失败。

这有助于防止陷入困境的服务或其他下游资源被请求压得喘不过气来,给它们一个恢复的机会。

@CircuitBreaker( attempts = "3", reset = "20s")
@Client("/hello")
public interface HelloClient { /*...*/ }

构建MICRONAUT应用程序

真正学习一个框架的最好方法是自己开始使用它,因此我们将用构建第一个应用程序的分步指南来结束对Micronaut的概述。

作为奖励,我们还将更进一步,将我们的“微服务”作为一个容器部署到云提供商——在本例中是Google计算引擎。

步骤1:安装MICRONAUT

Micronaut可以从Github上的源代码构建:https://github.com/micronaut-projects/micronaut-core,也可以作为二进制文件下载并安装在shell路径上。但是,建议通过sdkman安装Micronaut。

如果尚未安装sdkman,则可以在任何基于Unix的shell中使用以下命令进行安装:

> curl -s "https://get.sdkman.io" | bash
> source "$HOME/.sdkman/bin/sdkman-init.sh"
> sdk version
 
SDKMAN 5.6.4+305

现在可以使用以下sdkman命令安装Micronaut本身。

(使用sdk list micronaut查看可用版本。目前最新版本是1.0.0.M2。)

> sdk install micronaut 1.0.0.M2

通过运行mn-v确认已安装Micronaut。

mn -v
| Micronaut Version: 1.0.0.M2
| JVM Version: 1.8.0_171

步骤2:创建项目

mn命令用作Micronaut的CLI。您可以使用此命令创建新的Micronaut项目。

对于本练习,我们将创建一个普通Java应用程序,但是您也可以通过提供-lang标志(-lang Groovy或-lang Kotlin)来选择Groovy或Kotlin作为首选语言。

mn命令接受一个features标志,您可以在其中指定为项目中的各种库和配置添加支持的功能。您可以通过运行mn profile info services来查看可用功能。

我们将使用spock特性为Java项目添加对spock测试框架的支持。运行以下命令:

mn create-app example.greetings -features spock
| Application created at /Users/dev/greetings

请注意,我们可以为项目名称(问候语)提供默认的包前缀(示例)。

如果不这样做,项目名称将用作默认包。这个包将包含Application类和使用CLI命令生成的任何类(我们稍后会做)。

默认情况下,createapp命令将生成Gradle构建。如果您喜欢Maven作为构建工具,那么可以使用-build标志

此时,可以使用gradlerun任务运行应用程序。

./gradlew run
Starting a Gradle Daemon (subsequent builds will be faster)
 
> Task :run
03:00:04.807 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 1109ms. Server Running: http://localhost:37619

请注意,每次运行应用程序时都会随机选择服务器端口。这在使用服务发现解决方案定位实例时是有意义的,但是对于我们的练习来说,将端口号设置为一个已知值(如8080)会很方便。

提示:如果您想使用IDE运行Micronaut项目,请确保IDE支持Java注释处理器,并且已为您的项目启用此支持。在IntelliJ IDEA中,可以在Preferences下找到相关的设置→ 构建、执行、部署→ 编译程序→ 批注处理器→ 启用。

第3步:配置

Micronaut中的默认配置格式是YAML,但支持其他格式,包括Java属性文件、Groovy配置和JSON。

默认配置文件位于src/main/resources/application.yml。让我们编辑这个文件来设置我们的服务器端口号。

src/main/resources/application.yml文件

micronaut:
      application:
          name: greetings
      server:
          port: 8080

如果您重新启动应用程序,您将看到它在上运行http://localhost:8080.

第四步:编写代码

在项目目录中,自行运行mn命令以在交互模式下启动Micronaut CLI。

> mn
| Starting interactive mode...
| Enter a command name to run. Use TAB for completion:
mn>

运行以下两个命令来生成控制器、客户机和服务bean。

mn> create-controller GreetingController
| Rendered template Controller.java to destination src/main/java/greetings/GreetingController.java
| Rendered template ControllerSpec.groovy to destination src/test/groovy/greetings/GreetingControllerSpec.groovy
 
mn> create-client GreetingClient
| Rendered template Client.java to destination src/main/java/greetings/GreetingClient.java
 
mn> create-bean GreetingService
| Rendered template Bean.java to destination src/main/java/demo/GreetingService.java

编辑生成的文件,如下面三个清单所示。

src/main/java/example/GreetingController.java

package example;
 
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.Single;
import javax.inject.Inject;
 
@Controller("/greeting")
public class GreetingController {
 
    @Inject
    GreetingService greetingService;
 
    @Get("/{name}")
    public Single<String> greeting(String name) {
        return greetingService.message(name);
    }
}

.src/main/java/example/GreetingClient.java

package example;
 
import io.micronaut.http.client.Client;
import io.micronaut.http.annotation.Get;
import io.reactivex.Single;
 
@Client("greeting")
public interface GreetingClient {
 
    @Get("/{name}")
    Single<String> greeting(String name);
}

.src/main/java/example/GreetingService.java

package example;
 
import io.reactivex.Single;
import javax.inject.Singleton;
 
@Singleton
public class GreetingService {
 
    public Single<String> message(String name) {
        return Single.just("Hello, " + name);
    }
}

编写了控制器、客户机和服务类之后,如果再次运行应用程序,我们应该能够发出请求,如下面显示的CURL命令。

> curl http://localhost:8080/greeting/Beth
Hello, Beth

让我们编辑生成的GreetingControllerSpec以利用我们的客户机接口。

.src/test/groovy/example/GreetingControllerSpec.groovy

package example
 
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
 
class GreetingControllerSpec extends Specification {
 
    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
 
    void "test greeting"() {
        given:
        GreetingClient client = embeddedServer.applicationContext.getBean(GreetingClient)
 
        expect:
        client.greeting("Beth").blockingGet() == "Hello, Beth"
    }
}

运行./gradlew test以执行测试(如果启用了注释处理,则在IDE中执行它们)。

./gradlew test
 
BUILD SUCCESSFUL in 6s

第五步:进入云端

为了部署我们的应用程序,我们需要生成一个可运行的构建工件。运行shadowJar Gradle任务创建一个可执行的“fat”JAR文件。

> ./gradlew shadowJar
 
BUILD SUCCESSFUL in 6s
3 actionable tasks: 3 executed

使用java-JAR命令测试JAR文件是否按预期运行。

java -jar build/libs/greetings-0.1-all.jar
03:44:50.120 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 847ms. Server Running: http://localhost:8080

接下来的几个步骤是从googlecloud网站上的文档中获取的。为了遵循这些步骤,您将需要一个启用计费的Google云帐户。

谷歌云设置

  • 从Google云控制台创建一个项目。
  • 确保在API库中启用了计算引擎和云存储API。
  • 安装Google云SDK。运行gcloud init初始化SDK并选择在步骤1中创建的新项目。

上传JAR

1. 创建一个新的Google存储桶来存储JAR文件。记下bucket的名称:本例中的问候语。

> gsutil mb gs://greetings

2. 将greetings-all.jar文件上传到新的bucket。

gsutil cp build/libs/greetings-0.1-all.jar gs://greetings/greetings.jar

创建实例启动脚本

googlecompute允许您使用Bash脚本提供一个新实例。在项目目录中创建一个名为instance-startup.sh的新文件。添加以下内容:

#!/bin/sh
 
# Set up instance metadata
PROJECTID=$(curl -s "http://metadata.google.internal/computeMetadata/v1/project/project-id" -H "Metadata-Flavor: Google")
BUCKET=$(curl -s "http://metadata.google.internal/computeMetadata/v1/instance/attributes/BUCKET" -H "Metadata-Flavor: Google")
 
echo "Project ID: ${PROJECTID} Bucket: ${BUCKET}"
 
# Copy the JAR file from the bucket
gsutil cp gs://${BUCKET}/greetings.jar .
 
# Update and install/configure dependencies
apt-get update
apt-get -y --force-yes install openjdk-8-jdk
update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
 
# Start the application
java -jar greetings.jar

配置计算引擎

使用instance-startup.sh脚本和前面步骤中使用的bucket名称,运行以下命令来创建Compute实例。

gcloud compute instances create greetings-instance \
--image-family debian-9 --image-project debian-cloud \
--machine-type g1-small --scopes "userinfo-email,cloud-platform" \
--metadata-from-file startup-script=instance-startup.sh \
--metadata BUCKET=greetings --zone us-east1-b --tags http-server

2. 实例将被初始化并立即开始启动。这可能需要几分钟。

3. 定期运行以下命令以在启动过程中查看实例日志。如果一切顺利,在这个过程完成后,您应该会看到“Finished running startup scripts”消息。

> gcloud compute instances get-serial-port-output greetings-instance --zone us-east1-b

4. 运行以下命令打开端口8080的HTTP通信。

gcloud compute firewall-rules create default-allow-http-8080 \
--allow tcp:8080 --source-ranges 0.0.0.0/0  \
--target-tags http-server --description "Allow port 8080 access to http-server"

5. 使用以下命令获取计算实例的外部IP:

gcloud compute instances list
NAME                ZONE        MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
greetings-instance  us-east1-b  g1-small                   10.142.0.3   35.231.160.118  RUNNING

您现在应该可以使用外部IP访问您的应用程序。

> curl 35.231.160.118:8080/greeting/World
Hello, World

结论

在撰写本文时,Micronaut仍处于早期开发阶段,仍有大量工作要做。然而,在当前的里程碑版本中,已经有大量的功能可用。

除了本文讨论的功能外,还支持以下功能:

  • 安全性(使用JWT、sessions或basic auth)
  • 管理终结点
  • 使用Hibernate、JPA和GORM自动配置数据访问
  • 支持使用@Scheduled
  • 配置共享

使用Micronaut开发的最终参考是http://docs.micronaut.io.

一个小的,但越来越多的选择一步一步的教程可在http://guides.micronaut.io as 包括Micronaut支持的三种语言的指南:Java、Groovy和Kotlin。

Gitter上的Micronaut社区频道是一个很好的地方,可以与其他已经使用该框架构建应用程序的开发人员见面,并与核心开发团队进行互动。

时间会告诉我们Micronaut将对微服务开发和整个行业产生什么样的影响,但很明显,该框架已经为将来如何构建应用程序做出了重大贡献。

云本地开发肯定会继续存在,Micronaut是一个考虑到这种情况而构建的工具的最新示例。与推动其创建的体系结构一样,Micronaut的灵活性和模块化将允许开发人员创建连其设计者都无法预见的系统。

JVM的发展前景是光明的,如果有点阴暗的话,Micronaut肯定会发挥重要作用。

原文地址:https://objectcomputing.com/resources/publications/sett/july-2018-micronaut-framework-for-the-future

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2091.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册