Redian新闻
>
只会用 Spring Boot 创建微服务?那你就 OUT 了,还有这 4 种替代方案!

只会用 Spring Boot 创建微服务?那你就 OUT 了,还有这 4 种替代方案!

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:www.kubernetes.org.
cn/9526.html


前言

在 Java 和 Kotlin 中, 除了使用Spring Boot创建微服务外,还有很多其他的替代方案。

本文,基于这些微服务框架,创建了五个服务,并使用Consul的服务发现模式实现服务间的 相互通信。因此,它们形成了异构微服务架构(Heterogeneous Microservice Architecture, 以下简称 MSA):

本文简要考虑了微服务在各个框架上的实现(更多细节请查看源代码:https : //github.com/rkudryashov/heterogeneous-microservices

  • 技术栈:
  • JDK 13
  • Kotlin
  • Gradle (Kotlin DSL)
  • JUnit 5
  • 功能接口(HTTP API):
  • GET /application-info{?request-to=some-service-name}
  • GET /application-info/logo
  • 实现方式:
  • 使用文本文件的配置方式
  • 使用依赖注入
  • HTTP API
  • MSA:
  • 使用服务发现模式(在Consul中注册,通过客户端负载均衡的名称请求另一个微服务的HTTP API)
  • 构建一个 uber-JAR

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

先决条件

  • JDK 13
  • Consul

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

从头开始创建应用程序

要基于其中一个框架上生成新项目,你可以使用web starter 或其他选项(例如,构建工具或 IDE):

Helidon服务

该框架是在 Oracle 中创建以供内部使用,随后成为开源。Helidon 非常简单和快捷,它提供了两个版本:标准版(SE)和MicroProfile(MP)。在这两种情况下,服务都是一个常规的 Java SE 程序。(在Helidon上了解更多信息)

Helidon MP 是 Eclipse MicroProfile的实现之一,这使得使用许多 API 成为可能,包括 Java EE 开发人员已知的(例如 JAX-RS、CDI等)和新的 API(健康检查、指标、容错等)。在 Helidon SE 模型中,开发人员遵循“没有魔法”的原则,例如,创建应用程序所需的注解数量较少或完全没有。

Helidon SE 被选中用于微服务的开发。因为Helidon SE 缺乏依赖注入的手段,因此为此使用了Koin。

以下代码示例,是包含 main 方法的类。为了实现依赖注入,该类继承自KoinComponent

首先,Koin 启动,然后初始化所需的依赖并调用startServer()方法—-其中创建了一个WebServer类型的对象,应用程序配置和路由设置传递到该对象;

启动应用程序后在Consul注册:

object HelidonServiceApplication : KoinComponent {  
  
    @JvmStatic  
    fun main(args: Array<String>) {  
        val startTime = System.currentTimeMillis()  
        startKoin {  
            modules(koinModule)  
        }  
  
        val applicationInfoService: ApplicationInfoService by inject()  
        val consulClient: Consul by inject()  
        val applicationInfoProperties: ApplicationInfoProperties by inject()  
        val serviceName 
= applicationInfoProperties.name  
  
        startServer(applicationInfoService, consulClient, serviceName, startTime)  
    }  
}  
  
fun startServer(  
    applicationInfoService: ApplicationInfoService,  
    consulClient: Consul,  
    serviceName: String,  
    startTime: Long  
)
: WebServer 
{  
    val serverConfig = ServerConfiguration.create(Config.create().get("webserver"))  
  
    val server: WebServer = WebServer  
        .builder(createRouting(applicationInfoService))  
        .config(serverConfig)  
        .build()  
  
    server.start().thenAccept { ws ->  
        val durationInMillis = System.currentTimeMillis() - startTime  
        log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port())  
        // register in Consul  
        consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port()))  
    }  
  
    return server  
}  

路由配置如下:

private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder()  
    .register(JacksonSupport.create())  
    .get("/application-info", Handler { req, res ->  
        val requestTo: String? = req.queryParams()  
            .first("request-to")  
            .orElse(null)  
  
        res  
            .status(Http.ResponseStatus.create(200))  
            .send(applicationInfoService.get(requestTo))  
    })  
    .get("/application-info/logo", Handler { req, res ->  
        res.headers().contentType(MediaType.create("image""png"))  
        res  
            .status(Http.ResponseStatus.create(200))  
            .send(applicationInfoService.getLogo())  
    })  
    .error(Exception::class.java{ req, res, ex ->  
        log.error("Exception:", ex)  
        res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send()  
    }  
    .build()  

该应用程序使用HOCON格式的配置文件:

webserver {  
  port: 8081  
}  
  
application-info {  
  name: "helidon-service"  
  framework {  
    name: "Helidon SE"  
    release-year: 2019  
  }  
}  

还可以使用 JSON、YAML 和properties 格式的文件进行配置(在Helidon 配置文档中了解更多信息)。

Ktor服务

该框架是为 Kotlin 编写和设计的。和 Helidon SE 一样,Ktor 没有开箱即用的 DI,所以在启动服务器依赖项之前应该使用 Koin 注入:

val koinModule = module {  
    single { ApplicationInfoService(get(), get()) }  
    single { ApplicationInfoProperties() }  
    single { ServiceClient(get()) }  
    single { Consul.builder().withUrl("https://localhost:8500").build() }  
}  
  
fun main(args: Array<String>) {  
    startKoin {  
        modules(koinModule)  
    }  
    val server = embeddedServer(Netty, commandLineEnvironment(args))  
    server.start(wait = true)  
}  

应用程序需要的模块在配置文件中指定(HOCON格式;更多配置信息参考Ktor配置文档 ),其内容如下:

ktor {  
  deployment {  
    host = localhost  
    port = 8082  
    environment = prod  
    // for dev purpose  
    autoreload = true  
    watch = [io.heterogeneousmicroservices.ktorservice]  
  }  
  application {  
    modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module]  
  }  
}  
  
application-info {  
  name: "ktor-service"  
  framework {  
    name: "Ktor"  
    release-year: 2018  
  }  
}  

在 Ktor 和 Koin 中,术语“模块”具有不同的含义。

在 Koin 中,模块类似于 Spring 框架中的应用程序上下文。Ktor的模块是一个用户定义的函数,它接受一个 Application类型的对象,可以配置流水线、注册路由、处理请求等:

fun Application.module() {  
    val applicationInfoService: ApplicationInfoService by inject()  
  
    if (!isTest()
{  
        val consulClient: Consul by inject()  
        registerInConsul(applicationInfoService.get(null).name, consulClient)  
    }  
  
    install(DefaultHeaders)  
    install(Compression)  
    install(CallLogging)  
    install(ContentNegotiation) 
{  
        jackson {}  
    }  
  
    routing {  
        route("application-info") {  
            get {  
                val requestTo: String? = call.parameters["request-to"]  
                call.respond(applicationInfoService.get(requestTo))  
            }  
            static {  
                resource("/logo""logo.png")  
            }  
        }  
    }  
}  

此代码是配置请求的路由,特别是静态资源logo.png。

下面是基于Round-robin算法结合客户端负载均衡实现服务发现模式的代码:

class ConsulFeature(private val consulClientConsul{  
  
    class Config {  
        lateinit var consulClient: Consul  
    }  
  
    companion object Feature : HttpClientFeature<Config, ConsulFeature> {  
        var serviceInstanceIndex: Int = 0  
  
        override val key = AttributeKey<ConsulFeature>("ConsulFeature")  
  
        override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient)  
  
        override fun install(feature: ConsulFeature, scope: HttpClient) {  
            scope.requestPipeline.intercept(HttpRequestPipeline.Render) {  
                val serviceName = context.url.host  
                val serviceInstances =  
                    feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response  
                val selectedInstance = serviceInstances[serviceInstanceIndex]  
                context.url.apply {  
                    host = selectedInstance.service.address  
                    port = selectedInstance.service.port  
                }  
                serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size  
            }  
        }  
    }  
}  

主要逻辑在install方法中:在Render请求阶段(在Send阶段之前执行)首先确定被调用服务的名称,然后consulClient请求服务的实例列表,然后通过循环算法定义一个实例正在调用。因此,以下调用成为可能:

fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {  
    httpClient.get<ApplicationInfo>("http://$serviceName/application-info")  
}  

Micronaut 服务

Micronaut 由Grails框架的创建者开发,灵感来自使用 Spring、Spring Boot 和 Grails 构建服务的经验。该框架目前支持 Java、Kotlin 和 Groovy 语言。依赖是在编译时注入的,与 Spring Boot 相比,这会导致更少的内存消耗和更快的应用程序启动。

主类如下所示:

object MicronautServiceApplication {  
  
    @JvmStatic  
    fun main(args: Array<String>) {  
        Micronaut.build()  
            .packages("io.heterogeneousmicroservices.micronautservice")  
            .mainClass(MicronautServiceApplication.javaClass)  
            .start()  
    }  
}  

基于 Micronaut 的应用程序的某些组件与它们在 Spring Boot 应用程序中的对应组件类似,例如,以下是控制器代码:

@Controller(  
    value = "/application-info",  
    consumes = [MediaType.APPLICATION_JSON],  
    produces = [MediaType.APPLICATION_JSON]  
)  
class ApplicationInfoController(  
    private val applicationInfoServiceApplicationInfoService  
{  
  
    @Get  
    fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)  
  
    @Get("/logo", produces = [MediaType.IMAGE_PNG])  
    fun getLogo(): ByteArray = applicationInfoService.getLogo()  
}  

Micronaut 中对 Kotlin 的支持建立在kapt编译器插件的基础上(参考Micronaut Kotlin 指南了解更多详细信息)。

构建脚本配置如下:

plugins {  
    ...  
    kotlin("kapt")  
    ...  
}  
  
dependencies {  
    kapt("io.micronaut:micronaut-inject-java:$micronautVersion")  
    ...  
    kaptTest("io.micronaut:micronaut-inject-java:$micronautVersion")  
    ...  
}  

以下是配置文件的内容:

micronaut:  
  application:  
    name: micronaut-service  
  server:  
    port: 8083  
  
consul:  
  client:  
    registration:  
      enabled: true  
  
application-info:  
  name: ${micronaut.application.name}  
  framework:  
    name: Micronaut  
    release-year: 2018  

JSON、properties和 Groovy 文件格式也可用于配置(参考Micronaut 配置指南查看更多详细信息)。

Quarkus服务

Quarkus是作为一种应对新部署环境和应用程序架构等挑战的工具而引入的,在框架上编写的应用程序将具有低内存消耗和更快的启动时间。此外,对开发人员也很友好,例如,开箱即用的实时重新加载。

Quarkus 应用程序目前没有 main 方法,但也许未来会出现(GitHub 上的问题)。

对于熟悉 Spring 或 Java EE 的人来说,Controller 看起来非常熟悉:

@Path("/application-info")  
@Produces(MediaType.APPLICATION_JSON)  
@Consumes(MediaType.APPLICATION_JSON)  
class ApplicationInfoResource(  
    @Inject private val applicationInfoServiceApplicationInfoService  
{  
  
    @GET  
    fun get(@QueryParam("request-to") requestTo: String?): Response =  
        Response.ok(applicationInfoService.get(requestTo)).build()  
  
    @GET  
    @Path("/logo")  
    @Produces("image/png")  
    fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build()  
}  

如你所见,bean 是通过@Inject注解注入的,对于注入的 bean,你可以指定一个范围,例如:

@ApplicationScoped  
class ApplicationInfoService(  
    ...  
{  
...  
}  

为其他服务创建 REST 接口,就像使用 JAX-RS 和 MicroProfile 创建接口一样简单:

@ApplicationScoped  
@Path("/")  
interface ExternalServiceClient {  
    @GET  
    @Path("/application-info")  
    @Produces("application/json")  
    fun getApplicationInfo(): ApplicationInfo  
}  
  
@RegisterRestClient(baseUri = "http://helidon-service")  
interface HelidonServiceClient : ExternalServiceClient  
  
@RegisterRestClient(baseUri = "http://ktor-service")  
interface KtorServiceClient : ExternalServiceClient  
  
@RegisterRestClient(baseUri = "http://micronaut-service")  
interface MicronautServiceClient : ExternalServiceClient  
  
@RegisterRestClient(baseUri = "http://quarkus-service")  
interface QuarkusServiceClient : ExternalServiceClient  
  
@RegisterRestClient(baseUri = "http://spring-boot-service")  
interface SpringBootServiceClient : ExternalServiceClient  

但是它现在缺乏对服务发现 ( Eureka和Consul ) 的内置支持,因为该框架主要针对云环境。因此,在 Helidon 和 Ktor 服务中, 我使用了Java类库方式的Consul 客户端。

首先,需要注册应用程序:

@ApplicationScoped  
class ConsulRegistrationBean(  
    @Inject private val consulClientConsulClient  
{  
  
    fun onStart(@Observes event: StartupEvent) {  
        consulClient.register()  
    }  
}  

然后需要将服务的名称解析到其特定位置;

解析是通过从 Consul 客户端获得的服务的位置替换 requestContext的URI 来实现的:

@Provider  
@ApplicationScoped  
class ConsulFilter(  
    @Inject private val consulClientConsulClient  
) : ClientRequestFilter 
{  
  
    override fun filter(requestContext: ClientRequestContext) {  
        val serviceName = requestContext.uri.host  
        val serviceInstance = consulClient.getServiceInstance(serviceName)  
        val newUri: URI = URIBuilder(URI.create(requestContext.uri.toString()))  
            .setHost(serviceInstance.address)  
            .setPort(serviceInstance.port)  
            .build()  
  
        requestContext.uri = newUri  
    }  
}  

Quarkus也支持通过properties 或 YAML 文件进行配置(参考Quarkus 配置指南了解更多详细信息)。

Spring Boot服务

创建该框架是为了使用 Spring Framework 生态系统,同时有利于简化应用程序的开发。这是通过auto-configuration实现的。

以下是控制器代码:

@RestController  
@RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_VALUE])  
class ApplicationInfoController(  
    private val applicationInfoServiceApplicationInfoService  
{  
  
    @GetMapping  
    fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)  
  
    @GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE])  
    fun getLogo(): ByteArray = applicationInfoService.getLogo()  
}  

微服务由 YAML 文件配置:

spring:  
  application:  
    name: spring-boot-service  
  
server:  
  port: 8085  
  
application-info:  
  name: ${spring.application.name}  
  framework:  
    name: Spring Boot  
    release-year: 2014  

也可以使用properties文件进行配置(更多信息参考Spring Boot 配置文档)。

启动微服务

在启动微服务之前,你需要安装Consul和 启动代理-例如,像这样:consul agent -dev。

你可以从以下位置启动微服务:

IDE中启动微服务IntelliJ IDEA 的用户可能会看到如下内容:

要启动 Quarkus 服务,你需要启动quarkusDev的Gradle 任务。

console中启动微服务在项目的根文件夹中执行:

java -jar helidon-service/build/libs/helidon-service-all.jar

java -jar ktor-service/build/libs/ktor-service-all.jar

java -jar micronaut-service/build/libs/micronaut-service-all.jar

java -jar quarkus-service/build/quarkus-service-1.0.0-runner.jar

java -jar spring-boot-service/build/libs/spring-boot-service.jar

启动所有微服务后,访问http://localhost:8500/ui/dc1/services,你将看到:

API测试

以Helidon服务的API测试结果为例:

GET http://localhost:8081/application-info

{  
  "name""helidon-service",  
  "framework": {  
    "name""Helidon SE",  
    "releaseYear"2019  
  },  
  "requestedService"null  
}  

GET http://localhost:8081/application-info?request-to=ktor-service

{  
  "name""helidon-service",  
  "framework": {  
    "name""Helidon SE",  
    "releaseYear"2019  
  },  
  "requestedService": {  
    "name""ktor-service",  
    "framework": {  
          "name""Ktor",  
          "releaseYear"2018  
    },  
    "requestedService"null  
  }  
}  

GET http://localhost:8081/application-info/logo返回logo信息

你可以使用Postman 、IntelliJ IDEA HTTP 客户端 、浏览器或其他工具测试微服务的 API接口 。

不同微服务框架对比

不同微服务框架的新版本发布后,下面的结果可能会有变化;你可以使用此GitHub项目自行检查最新的对比结果 。

另外,微服务系列面试题和答案全部整理好了,微信搜索互联网架构师,在后台发送:面试,可以在线阅读。

程序大小

为了保证设置应用程序的简单性,构建脚本中没有排除传递依赖项,因此 Spring Boot 服务 uber-JAR 的大小大大超过了其他框架上的类似物的大小(因为使用 starters 不仅导入了必要的依赖项;如果需要,可以通过排除指定依赖来减小大小):

备注:什么是 maven的uber-jar

在maven的一些文档中我们会发现 “uber-jar”这个术语,许多人看到后感到困惑。其实在很多编程语言中会把super叫做uber (因为super可能是关键字), 这是上世纪80年代开始流行的,比如管superman叫uberman。所以uber-jar从字面上理解就是super-jar,这样的jar不但包含自己代码中的class ,也会包含一些第三方依赖的jar,也就是把自身的代码和其依赖的jar全打包在一个jar里面了,所以就很形象的称其为super-jar ,uber-jar来历就是这样的。

启动时长

每个应用程序的启动时长都是不固定的:

值得注意的是,如果你将 Spring Boot 中不必要的依赖排除,并注意设置应用的启动参数(例如,只扫描必要的包并使用 bean 的延迟初始化),那么你可以显著地减少启动时间。

内存使用情况

对于每个微服务,确定了以下内容:

  • 通过-Xmx参数,指定微服务所需的堆内存大小
  • 通过负载测试服务健康的请求(能够响应不同的请求)
  • 通过负载测试50 个用户 * 1000 个的请求
  • 通过负载测试500 个用户 * 1000 个的请求

堆内存只是为应用程序分配的总内存的一部分。例如,如果要测量总体内存使用情况,可以参考本指南。

对于负载测试,使用了Gatling和Scala脚本 。

1、负载生成器和被测试的服务在同一台机器上运行(Windows 10、3.2 GHz 四核处理器、24 GB RAM、SSD)。

2、服务的端口在 Scala 脚本中指定。

3、通过负载测试意味着微服务已经响应了所有时间的所有请求。

需要注意的是,所有微服务都使用 Netty HTTP 服务器。

结论

通过上文,我们所需的功能——一个带有 HTTP API 的简单服务和在 MSA 中运行的能力——在所有考虑的框架中都取得了成功。

是时候开始盘点并考虑他们的利弊了。

Helidon标准版

优点

创建的应用程序,只需要一个注释(@JvmStatic)

缺点

开发所需的一些组件缺少开箱即用(例如,依赖注入和与服务发现服务器的交互)

Helidon MicroProfile

微服务还没有在这个框架上实现,所以这里简单说明一下。

优点

1、Eclipse MicroProfile 实现

2、本质上,MicroProfile 是针对 MSA 优化的 Java EE。因此,首先你可以访问各种 Java EE API,包括专门为 MSA 开发的 API,其次,你可以将 MicroProfile 的实现更改为任何其他实现(例如:Open Liberty、WildFly Swarm 等)

Ktor

优点

1、轻量级的允许你仅添加执行任务直接需要的那些功能

2、应用参数所有参数的良好结果

缺点

1、依赖于Kotlin,即用其他语言开发可能是不可能的或不值得的

2、微框架:参考Helidon SE

3、目前最流行的两种 Java 开发模型(Spring Boot/Micronaut)和 Java EE/MicroProfile)

4、中没有包含该框架,这会导致:

  • 难以寻找专家
  • 由于需要显式配置所需的功能,因此与 Spring Boot 相比,执行任务的时间有所增加

Micronaut

优点

1、AOT如前所述,与 Spring Boot 上的模拟相比,AOT 可以减少应用程序的启动时间和内存消耗

2、类Spring开发模式有 Spring 框架经验的程序员不会花太多时间来掌握这个框架

3、Micronaut for Spring可以改变现有的Spring Boot应用程序的执行环境到Micronaut中(有限制)

Quarkus

优点

1、Eclipse MicroProfile 的实现

2、该框架为多种 Spring 技术提供了兼容层:DI、 Web、Security、Data JPA

Spring Boot

优点

1、平台成熟度和生态系统对于大多数日常任务,Spring的编程范式已经有了解决方案,也是很多程序员习惯的方式。此外,starter和auto-configuration的概念简化了开发

2、专家多,文档详细

我想很多人都会同意 Spring 在不久的将来仍将是 Java/Kotlin开发领域领先的框架。

缺点

  • 应用参数多且复杂但是,有些参数,如前所述,你可以自己优化。还有一个Spring Fu项目的存在,该项目正在积极开发中,使用它可以减少参数。

Helidon SE 和 Ktor 是 微框架,Spring Boot 和 Micronaut 是全栈框架,Quarkus 和 Helidon MP 是 MicroProfile 框架。微框架的功能有限,这会减慢开发速度。

我不敢判断这个或那个框架会不会在近期“大更新”,所以在我看来,目前最好继续观察,使用熟悉的框架解决工作问题。

同时,如本文所示,新框架在应用程序参数设置方面赢得了 Spring Boot。如果这些参数中的任何一个对你的某个微服务至关重要,那么也许值得关注。

但是,我们不要忘记,Spring Boot 一是在不断改进,二是它拥有庞大的生态系统,并且有相当多的 Java 程序员熟悉它。此外,还有未涉及的其他框架:Vert.x、Javalin 等,也值得关注。

原文:https://dzone.com/articles/not-only-spring-boot-a-review-of-alternatives

译文:https://www.kubernetes.org.cn/9526.html



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
MacBook Pro 13in 2018 A1989 keep rebooting ( screen/battery/otheSpringBoot项目中使用缓存的正确姿势,太优雅了!RLAIF:一个不依赖人工的RLHF替代方案专访日本原子力资料情报室专家:明知有可替代方案,核污水排海不负责任美国的问题根源(this guy figured out)[歪解] I thought I told you about it already.治疗稳定性心绞痛,硝酸盐类药物或有新替代方案?非常容易被负面情绪影响,很容易stress out 有没有什么解SpringBoot 通用限流方案(VIP珍藏版)买机票只会用Google?这些比价网站能买到更便宜的回国机票SpringBoot+Mybatis 如何实现流式查询,你知道吗?TCR-T细胞治疗公司停止临床试验,裁员60%,正在寻找替代方案5078 血壮山河之武汉会战 黄广战役 10数据可视化:基于 Echarts + SpringBoot 的动态实时大屏银行监管系统【源码】China Is Getting Seriously Worried About Student Anxiety亚裔因天资聪颖却在录取中吃亏?逆境得分或成为替代方案?买机票只会用Google?这些比价网站让你买到更便宜的回国机票!Spring容器原始Bean是如何创建的?“没有‘替代方案’”,土外长要访俄基于 SpringBoot 实现多租户架构:支持应用多租户部署和管理别再用 kill -9 了,这才是 Spring Boot 停机的正确方式!!!中国正在背离邓小平模式推荐35款 SpringBoot/SpringCloud 开源项目,附源码SpringBoot 插件化开发模式,强烈推荐!公司入职一大佬,把Spring Boot系统启动时间从 7分钟降到了 40秒!大公司为什么禁止在 Spring Boot 项目中使用 @Autowired 注解?违反国际公约!墨尔本大学教授发长文反对日本排放核废水,现在停下还来得及!寻找其它替代方案!公司新入职一位大佬,把SpringBoot项目启动时间从7分钟降到了40秒!请问大家,在做一个小出租房的cash out refi,要求签4506C,现诵《SPRING HOLIDAY偶题 》by AP/盈盈/K2组合咀外文嚼汉字(243)“九轮草”与“报春花”你还在付1万3申请政治庇护?那你就OUT了!China’s Youth Are Hooked on a New Outdoor Sport: Lure FishingFamous Taiwan Rapper Admits Plagiarizing Comic About Depression晨游杏林春暖SpringBoot 中的自带工具类,开发效率倍增!Spring Boot 3.1.0 发布,添加大量新功能和改进美国国会大厦升国旗 向李洪志先生致敬SpringBoot + 规则引擎 URule,真的很强!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。