当前位置: 首页 > news >正文

小架构step系列05:Springboot三种运行模式

1 概述

前面搭建工程的例子,运行的是一个桌面程序,并不是一个Web程序,在这篇中我们把它改为Web程序,同时从启动角度看看它们的区别。

2 Web模式

2.1 桌面例子

回顾一下前面的例子,其pom.xml的配置如下:

// pom.xml
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.18</version><relativePath/>
</parent>
<groupId>com.qqian.stepfmk</groupId>
<artifactId>srvpro</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>

代码如下:

@SpringBootApplication
public class SrvproApplication {public static void main(String[] args) {SpringApplication.run(SrvproApplication.class, args);}@Beanpublic CommandLineRunner commandLineRunner(ApplicationContext ctx) {return args -> {System.out.println("Hello World");};}
}

2.2 Web例子

(1) 之前看<parent>节点上一级的parent所用的spring-boot-dependencies的时候,pom文件见 https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/2.7.18/spring-boot-dependencies-2.7.18.pom ,里面比较多starter,其中spring-boot-starter-web就是和web有关的starter。在pom.xml中,用spring-boot-starter-web代替spring-boot-starter即可转换为web程序。

// pom.xml
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.18</version><relativePath/>
</parent>
<groupId>com.qqian.stepfmk</groupId>
<artifactId>srvpro</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>

查看一下spring-boot-starter-web里的依赖:https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-web/2.7.18/spring-boot-starter-web-2.7.18.pom

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>5.3.31</version><scope>compile</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.3.31</version><scope>compile</scope></dependency>
</dependencies>

从里面看到,也引用了spring-boot-starter,由这个starter提供springboot的基础功能;另外引用的spring-boot-starter-tomcat、spring-web和spring-webmvc,则提供了web相关的基础功能。

(2) 在入口代码中去掉CommandLineRunner这个bean,在web程序中一般不需要用到它(留着也可以运行)。

// 只留main()方法的运行
@SpringBootApplication
public class SrvproApplication {public static void main(String[] args) {SpringApplication.run(SrvproApplication.class, args);}
}

(3) 新建一个Controller类,提供一个接口方法:

// com.qqian.stepfmk.srvpro.hello.HelloController
@RestController
public class HelloController {@RequestMapping("sayHello")public String say(@RequestParam("message") String messge) {return "Hello world: " + messge;}
}

(4) 运行程序,在控制台上打印的日志

Starting SrvproApplication using Java 1.8.0_60 on DESKTOP-1 with PID 21336
No active profile set, falling back to 1 default profile: "default"
Tomcat initialized with port(s): 8080 (http)
Starting service [Tomcat]
Starting Servlet engine: [Apache Tomcat/9.0.83]
Initializing Spring embedded WebApplicationContext
Root WebApplicationContext: initialization completed in 1602 ms
Tomcat started on port(s): 8080 (http) with context path ''
Started SrvproApplication in 2.673 seconds (JVM running for 3.208)

从日志可以看出,web程序运行在8080端口上,context path是空字符串。

5、从浏览器上访问:http://localhost:8080/sayHello?message=zhangsan,返回以下结果:

Hello world: zhangsan

3 原理

从上面例子看,就更换了一个依赖,再增加Controller接口,就可以用浏览器的方式访问了,main函数里还是只有一行代码这么整洁,传统的tomcat和把war发布到tomcat里等操作都不需要了,简单了很多。如此简洁的代码,是如何实现web功能的?

3.1 run()方法的主流程

// 1. 通过SpringApplication.run运行程序
// 源码位置:com.qqian.stepfmk.srvpro.SrvproApplication
public static void main(String[] args) {SpringApplication.run(SrvproApplication.class, args);
}// 2. 在SpringApplication提供了两个静态方法run,和一个对象方法run,在第二个静态run方法中new了一个SpringApplication,执行对象方法run()
// 源码位置:org.springframework.boot.SpringApplication
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args);
}
public ConfigurableApplicationContext run(String... args) {// 省略部分代码...try {// 3. 创建上下文类,通过上下文类区分是否是web程序context = createApplicationContext();// 4. 初始化程序refreshContext(context);// 5. 执行RunnercallRunners(context, applicationArguments);}// 省略部分代码...return context;
}

3.2 初始化Web应用类型标记

在主流程步骤2里new了一个SpringApplication,在里面初始化了webApplicationType这个Web应用类型标识,应用类型大致分为Servlet Web应用、响应式Web应用、普通应用,这里把Application翻译为“应用”:

// 源码位置:org.springframework.boot.SpringApplication
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 1. 创建SpringApplication对象,并运行其run()方法return new SpringApplication(primarySources).run(args);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 2. 推演Web应用类型标识,因为其是根据依赖的类来确定的,而不是在哪里有对应的配置,所以是推演来的this.webApplicationType = WebApplicationType.deduceFromClasspath();this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));this.mainApplicationClass = deduceMainApplicationClass();
}// 源码位置:org.springframework.boot.WebApplicationType
public enum WebApplicationType {// 3. 在枚举中定义三种Web应用类型,NONE代表不是Web应用(普通应用),另外两种分别代表Servlet Web应用、响应式Web应用NONE, SERVLET, REACTIVE;// 4. 预先初始化一些帮助推演的常量,大概是当引用的包里面有哪些类的时候,就认为是哪种应用类型// javax.servlet.Servlet在tomcat-embed-core包里,ConfigurableWebApplicationContext在spring-web包里private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" };// DispatcherServlet在spring-webmvc包里private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";// DispatcherHandler在spring-webflux包里private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";// ServletContainer在org.glassfish.jersey.containers:jersey-container-servlet-core包里,Jersey是一个Web框架private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";static WebApplicationType deduceFromClasspath() {// 5. 只有引了spring-webflux包且没引另外两个包的任意一个,才是响应式Web模式if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {return WebApplicationType.REACTIVE;}// 6. 没有引tomcat-embed-core包和spring-web包中的任意一个则不是Web模式for (String className : SERVLET_INDICATOR_CLASSES) {if (!ClassUtils.isPresent(className, null)) {return WebApplicationType.NONE;}}// 7. 引了tomcat-embed-core包和spring-web包中的任意一个则是Web模式return WebApplicationType.SERVLET;}
}

3.3 创建上下文

主流程步骤3中的创建上下文SpringApplication.createApplicationContext():

// 源码位置:org.springframework.boot.SpringApplication
protected ConfigurableApplicationContext createApplicationContext() {// 1. 调用工厂的create()方法创建上下文,这里以DefaultApplicationContextFactory工厂为例//    applicationContextFactory为org.springframework.boot.DefaultApplicationContextFactoryreturn this.applicationContextFactory.create(this.webApplicationType);
}
// 源码位置:org.springframework.boot.DefaultApplicationContextFactory
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {try {// 2. 调用getFromSpringFactories方法创建Context//    提供的webApplicationType这个Web应用类型标识作为参数,参考前面推演这个值的说明//    另外两个是方法的引用,类似函数式编程的函数,前一个是预期用来场景Context的,后一个是在没有创建到Context的时候作为默认补救的return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create, AnnotationConfigApplicationContext::new);} catch (Exception ex) {throw new IllegalStateException("Unable create a default ApplicationContext instance, "+ "you may need a custom ApplicationContextFactory", ex);}
}
private <T> T getFromSpringFactories(WebApplicationType webApplicationType, BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {// 3. SpringFactoriesLoader.loadFactories()加载到工厂有两个,遍历工厂去调用工厂创建Context对象://     org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext.Factory//     org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext.Factoryfor (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, getClass().getClassLoader())) {// 4. action为ApplicationContextFactory::create,尝试根据Web应用类型标识创建Context对象T result = action.apply(candidate, webApplicationType);if (result != null) {return result;}}return (defaultResult != null) ? defaultResult.get() : null;
}// 源码位置:org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext.Factory
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {// 5. 如果webApplicationType类型为REACTIVE则创建AnnotationConfigReactiveWebServerApplicationContext,否则为nullreturn (webApplicationType != WebApplicationType.REACTIVE) ? null : new AnnotationConfigReactiveWebServerApplicationContext();
}
// 源码位置:org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext.Factory
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {// 6. 如果webApplicationType类型为SERVLET则创建AnnotationConfigServletWebServerApplicationContext,否则为nullreturn (webApplicationType != WebApplicationType.SERVLET) ? null : new AnnotationConfigServletWebServerApplicationContext();
}// 回到DefaultApplicationContextFactory的getFromSpringFactories()
// 源码位置:org.springframework.boot.DefaultApplicationContextFactory
private <T> T getFromSpringFactories(WebApplicationType webApplicationType, BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {// 3. SpringFactoriesLoader.loadFactories()加载到工厂有两个,遍历工厂去调用工厂创建Context对象for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class, getClass().getClassLoader())) {// 4. action为ApplicationContextFactory::create,尝试根据Web应用类型标识创建Context对象T result = action.apply(candidate, webApplicationType);// 7. AnnotationConfigReactiveWebApplicationContext.Factory只能创建webApplicationType=REACTIVE的Context对象,两者不匹配时result为AnnotationConfigReactiveWebServerApplicationContext对象//    AnnotationConfigServletWebServerApplicationContext.Factory只能创建webApplicationType=SERVLET的Context对象,两者匹配时result为AnnotationConfigServletWebServerApplicationContext对象//    匹配到一个就返回,REACTIVE的工厂排在前面,优先级更高if (result != null) {return result;}}// 8. 不是web相关的模式则使用默认的org.springframework.context.annotation.AnnotationConfigApplicationContext//    defaultResult为AnnotationConfigApplicationContext::new,defaultResult.get()就是执行new AnnotationConfigApplicationContext()的结果return (defaultResult != null) ? defaultResult.get() : null;
}

3.4 初始化

在主流程4进行初始化refreshContext(context),这个方法名称起得不太表意,就当是刷新吧。

// 源码位置:org.springframework.boot.SpringApplication#refreshContext
private void refreshContext(ConfigurableApplicationContext context) {if (this.registerShutdownHook) {shutdownHook.registerApplicationContext(context);}// 1. 调私refresh()方法刷新refresh(context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {// 2. 调用context的refresh()刷新方法//    从上面看这个context可能有三种,需分别大致看一下各个context的刷新://    AnnotationConfigReactiveWebServerApplicationContext//    AnnotationConfigServletWebServerApplicationContext//    AnnotationConfigApplicationContextapplicationContext.refresh();
}

3.4.1 AnnotationConfigReactiveWebServerApplicationContext刷新

AnnotationConfigReactiveWebServerApplicationContext本身并没有refresh()刷新方法,刷新方法来自于父类:

// 源码位置:org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext
// 1. 继承关系:AnnotationConfigReactiveWebServerApplicationContext < ReactiveWebServerApplicationContext < GenericReactiveWebApplicationContext < GenericApplicationContext < AbstractApplicationContext
public class ReactiveWebServerApplicationContext extends GenericReactiveWebApplicationContext implements ConfigurableWebServerApplicationContext {public final void refresh() throws BeansException, IllegalStateException {try {// 2. 调用父类refresh()方法刷新,直接父类GenericReactiveWebApplicationContext、GenericApplicationContext没有重载refresh()方法,//    调的是AbstractApplicationContext的refresh()方法super.refresh();}catch (RuntimeException ex) {WebServerManager serverManager = this.serverManager;if (serverManager != null) {serverManager.getWebServer().stop();}throw ex;}}
}// 源码位置:org.springframework.context.support.AbstractApplicationContext
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {@Overridepublic void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {            // 省略部分代码try {postProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");invokeBeanFactoryPostProcessors(beanFactory);registerBeanPostProcessors(beanFactory);beanPostProcess.end();initMessageSource();initApplicationEventMulticaster();// 3. 不同子类有不同的的初始化,Web应用的体现就在此方法onRefresh();registerListeners();finishBeanFactoryInitialization(beanFactory);finishRefresh();}// 省略部分代码}}
}
protected void onRefresh() throws BeansException {// 4. AbstractApplicationContext没有实现此方法,实际实现要回到子类当中
}// 5. AnnotationConfigReactiveWebServerApplicationContext没有重载onRefresh()方法
//    在其父类ReactiveWebServerApplicationContext(为AbstractApplicationContext子类)重载了onRefresh()方法
// 源码位置:org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext
protected void onRefresh() {super.onRefresh();try {// 6. 创建web server对象createWebServer();}catch (Throwable ex) {throw new ApplicationContextException("Unable to start reactive web server", ex);}
}
private void createWebServer() {WebServerManager serverManager = this.serverManager;if (serverManager == null) {StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");String webServerFactoryBeanName = getWebServerFactoryBeanName();ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);createWebServer.tag("factory", webServerFactory.getClass().toString());boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();// 7. 在此创建web server对象this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.serverManager.getWebServer()));getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.serverManager));createWebServer.end();}initPropertySources();
}

3.4.2 AnnotationConfigServletWebServerApplicationContext刷新

AnnotationConfigServletWebServerApplicationContext本身并没有refresh()刷新方法,刷新方法来自于父类,整个过程和AnnotationConfigReactiveWebServerApplicationContext的创建基本相似,只有最后用来创建Web Server的工厂ServletWebServerFactory不一样,创建出来的Web Server就不一样:

// 源码位置:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
// 1. 继承关系:AnnotationConfigServletWebServerApplicationContext < ServletWebServerApplicationContext < GenericApplicationContext < AbstractApplicationContext
public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {public final void refresh() throws BeansException, IllegalStateException {try {// 2. 调用父类refresh()方法刷新,调的是AbstractApplicationContext的refresh()方法super.refresh();}catch (RuntimeException ex) {WebServer webServer = this.webServer;if (webServer != null) {webServer.stop();}throw ex;}}
}// 源码位置:org.springframework.context.support.AbstractApplicationContext
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {            // 省略部分代码try {postProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");invokeBeanFactoryPostProcessors(beanFactory);registerBeanPostProcessors(beanFactory);beanPostProcess.end();initMessageSource();initApplicationEventMulticaster();// 3. 调用子类刷新onRefresh();registerListeners();finishBeanFactoryInitialization(beanFactory);finishRefresh();}// 省略部分代码}}
}// 源码位置:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
protected void onRefresh() {super.onRefresh();try {createWebServer();}catch (Throwable ex) {throw new ApplicationContextException("Unable to start web server", ex);}
}
private void createWebServer() {WebServer webServer = this.webServer;ServletContext servletContext = getServletContext();if (webServer == null && servletContext == null) {StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");ServletWebServerFactory factory = getWebServerFactory();createWebServer.tag("factory", factory.getClass().toString());// 4. 创建web server对象this.webServer = factory.getWebServer(getSelfInitializer());createWebServer.end();getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer));getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer));}else if (servletContext != null) {try {getSelfInitializer().onStartup(servletContext);}catch (ServletException ex) {throw new ApplicationContextException("Cannot initialize servlet context", ex);}}initPropertySources();
}

3.4.3 AnnotationConfigApplicationContext刷新

AnnotationConfigApplicationContext本身没有refresh()方法,需要找到父类AbstractApplicationContext,最终会调onRefresh()方法,由于这几层类都没有重载该方法,所以此onRefresh()没有做什么,跟之前两个Context比,最大的区别在于没有创建Web Server。

// 源码位置:org.springframework.context.support.AbstractApplicationContext
// 1. 继承关系:AnnotationConfigApplicationContext < GenericApplicationContext < AbstractApplicationContext
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// 省略部分代码try {postProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");invokeBeanFactoryPostProcessors(beanFactory);registerBeanPostProcessors(beanFactory);beanPostProcess.end();initMessageSource();initApplicationEventMulticaster();// 2. 调用子类的onRefresh()刷新onRefresh();registerListeners();finishBeanFactoryInitialization(beanFactory);finishRefresh();}// 省略部分代码}}
}
protected void onRefresh() throws BeansException {// 3. 由于子类GenericApplicationContext和AnnotationConfigApplicationContext都没有重载此方法,所以执行了空方法
}

3.5 执行Runner

在主流程步骤5执行runner:

// 源码位置:org.springframework.boot.SpringApplication
private void callRunners(ApplicationContext context, ApplicationArguments args) {// 1. 用context.getBeanProvider()找所有实现了Runner接口的类,并遍历这些类context.getBeanProvider(Runner.class).orderedStream().forEach((runner) -> {// 2. 支持两种Runner:ApplicationRunner、CommandLineRunner,分别都执行if (runner instanceof ApplicationRunner) {callRunner((ApplicationRunner) runner, args);}if (runner instanceof CommandLineRunner) {callRunner((CommandLineRunner) runner, args);}});
}
private void callRunner(ApplicationRunner runner, ApplicationArguments args) {try {// 3. 执行Runner,传的参数是ApplicationArguments(runner).run(args);}catch (Exception ex) {throw new IllegalStateException("Failed to execute ApplicationRunner", ex);}
}
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {try {// 4. 执行Runner,传的参数是原始数组类型(main方法的参数类型)(runner).run(args.getSourceArgs());}catch (Exception ex) {throw new IllegalStateException("Failed to execute CommandLineRunner", ex);}
}

3.6 小结

概括地看,SpringApplication的启动就是根据导入的包情况,分三种情况创建不同的Context:响应式流web、普通web、非web,然后执行Context的refresh进行初始化,下图为Context体系的继承情况,对于响应式流web、普通web这两种Context,分别在子类实现了不同的onRefresh(),用来创建不同的web server。

最后,还执行了runner(如果有实现runner的话)。注意,runner的执行与context种类无关,也就是不管哪种context都会执行。如果没有引任何web相关的包,那么就不会有web server的执行,只执行了runner,就变成了一个普通的桌面程序。runner的继承情况如下,两种Runner的差别仅在于参数的类型:

注:上面并没有把启动流程的每个细节都进行解析,这算看源码的一个小技巧,先看自己关心的部分(或者重点部分),如这次只想了解Web程序和普通程序的区别,以及SpringBoot用什么方式来区分的。

5 架构一小步

依赖spring-boot-starter-web,开启Web应用模式。

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.18</version><relativePath/>
</parent>
<groupId>com.qqian.stepfmk</groupId>
<artifactId>srvpro</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>

http://www.dtcms.com/a/267585.html

相关文章:

  • 理想汽车6月交付36279辆 第二季度共交付111074辆
  • 基于微信小程序的校园跑腿系统
  • MySQL——9、事务管理
  • Java-继承
  • 远程协助软件:Git的用法
  • STM32第15天串口中断接收
  • 数据结构:数组抽象数据类型(Array ADT)
  • oracle的内存架构学习
  • Hashcat 最快密码恢复工具实践指南
  • jvm架构原理剖析篇
  • C++ Qt 基础教程:信号与槽机制详解及 QPushButton 实战
  • virtualbox+vagrant私有网络宿主机无法ping通虚拟机问题请教
  • Apache 配置文件提权的实战思考
  • 数据库-元数据表
  • docker容器中Mysql数据库的备份与恢复
  • Java的AI新纪元:Embabel如何引领智能应用开发浪潮
  • 一文讲清楚React中setState的使用方法和机制
  • 应用标签思路参考
  • wsl查看磁盘文件并清理空间
  • Django跨域
  • 什么是单点登录SSO?有哪些常用的实现方式?
  • Android PNG/JPG图ARGB_8888/RGB_565‌解码形成Bitmap在物理内存占用大小的简单计算
  • SpringBoot系列—入门
  • ffplay6 播放器关键技术点分析 1/2
  • NumPy-核心函数np.matmul()深入解析
  • UI前端与数字孪生融合:为智能制造提供可视化生产调度方案
  • 分享一些服务端请求伪造SSRF的笔记
  • RAL-2025 | 触觉助力无人机空中探索!基于柔顺机器人手指的无人机触觉导航
  • 快讯|美团即时零售日订单已突破1.2亿,餐饮订单占比过亿
  • 【第五章】 工程测量学