小架構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.pswp.cn/web/87726.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/87726.shtml
英文地址,請注明出處:http://en.pswp.cn/web/87726.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

LoRaWAN的設備類型有哪幾種?

LoRaWAN&#xff08;Long Range Wide Area Network&#xff09;是一種專為物聯網&#xff08;IoT&#xff09;設備設計的低功耗、長距離通信協議。它根據設備的功能和功耗需求&#xff0c;將設備分為三種類型&#xff1a;Class A、Class B 和 Class C。每種設備類型都有其獨特的…

三維目標檢測|Iou3D 代碼解讀一

本文對OpenPCDet項目中的iou3d模塊中的iou3d_nms_kernel.cu代碼進行解讀&#xff0c;本次解決的函數是box_overlap&#xff0c;它的輸入是兩個包圍盒&#xff0c;輸出是兩個包圍盒在bev下的重疊面積&#xff0c;計算流程是 確定box_a和box_b的四個角落坐標 從包圍盒中提取坐標值…

探索實現C++ STL容器適配器:優先隊列priority_queue

前引&#xff1a; 在算法競賽中&#xff0c;選手們常常能在0.01秒內分出勝負&#xff1b;在實時交易系統中&#xff0c;毫秒級的延遲可能意味著數百萬的盈虧&#xff1b;在高并發服務器中&#xff0c;每秒需要處理數萬條不同優先級的請求——這些系統背后&#xff0c;都隱藏著同…

一、Dify 私有部署、本地安裝教程(LInux-openeuler)

官網&#xff1a;Dify AI Plans and Pricing 1.找到下載的位置。 2.可以切換文檔為中午文檔。 3.本次安裝使用Docker Compose 安裝&#xff0c;可以大致看一下文檔描述的配置信息要求。 4.各個版本信息&#xff0c;本次下載1.5.1版本&#xff0c;你也可以選擇安裝其他版本。 …

GASVM+PSOSVM+CNN+PSOBPNN+BPNN軸承故障診斷

一、各算法基本原理與技術特點 1. GASVM&#xff08;遺傳算法優化支持向量機&#xff09; 原理&#xff1a; 利用遺傳算法&#xff08;GA&#xff09;優化SVM的超參數&#xff08;如懲罰因子 C C C 和核函數參數 g g g&#xff09;。遺傳算法通過模擬自然選擇機制&#xff…

Python實例練習---魔法方法

&#xff08;主頁有對應知識點^V^&#xff09; 【練習要求】 針對知識點Python面向對象的魔法方法安排的本實例。要求實現&#xff1a;用__init__魔法方法定義書的長&#xff0c;寬&#xff0c;高&#xff0c;最后用__str__輸出返回值 【重要步驟提示】 定義class書類 2、使…

【從0-1的CSS】第3篇:盒子模型與彈性布局

文章目錄 盒子模型內容區content內邊距padding邊框border外邊距margin元素的寬度高度box-sizing屬性content-box&#xff1a;設置的width和height就是內容區的width和heightborder-box:設置的width和height是context padding border的width和height 彈性布局Flex容器的屬性fl…

設置LInux環境變量的方法和區別_Ubuntu/Centos

Linux環境變量可以通過export實現&#xff0c;也可以通過修改幾個文件來實現 1 通過文件設置LInux環境變量 首先是設置全局環境變量&#xff0c;對所有用戶都會生效 /etc/profile&#xff1a;該文件為系統的每個用戶設置環境信息&#xff0c;當用戶登錄時&#xff0c;該文件…

python緩存裝飾器實現方案

寫python的時候突然想著能不能用注解于是就寫了個這個 文章目錄 原始版改進點 原始版 import os import pickle import hashlib import inspect import functoolsdef _generate_cache_filename(func, *args, **kwargs):"""生成緩存文件名的內部函數""…

使用 java -jar xxxx.jar 運行 jar 包報錯: no main manifest attribute

1、問題描述 在Linux服務器上本想運行一下自己寫的一個JAR&#xff0c;但是報錯了&#xff01; no main manifest attribute, in first-real-server-1.0-SNAPSHOT.jar 2、解決辦法 在自己的Spring項目的啟動類&#xff08;xxx.xxx.xxx.XXXXApplication&#xff09;所在的Mo…

信號與槽的總結

信號與槽的總結 QT中的信號與Linux的信號對比 1&#xff09;信號源 2&#xff09;信號的類型 3&#xff09;信號的處理方式 QT信號與Linux信號的深度對比分析 一、信號源對比 QT信號 用戶定義信號 &#xff1a;由開發者通過 signals:關鍵字在QObject派生類中顯式聲明 cl…

Python Mitmproxy詳解:從入門到實戰

一、Mitmproxy簡介 Mitmproxy是一款開源的交互式HTTPS代理工具&#xff0c;支持攔截、修改和重放HTTP/HTTPS流量。其核心優勢在于&#xff1a; 多平臺支持&#xff1a;兼容Windows、macOS、Linux三端工具&#xff1a;提供命令行(mitmproxy)、Web界面(mitmweb)、數據流處理(mi…

刷題筆記--串聯所有單詞的子串

題目&#xff1a;1、我的寫法&#xff08;超時&#xff09;從題面自然想到先用回溯算法把words的全排列先算出來&#xff0c;然后遍歷字符串s一次將符合條件的位置加入結果全排列計算所有可能字符串算法寫法&#xff1a;這是一個模板用于所有全排列算法的情況&#xff0c;本質思…

操作系統【1】【硬件結構】【操作系統結構】

一、CPU如何執行程序&#xff1f; 提綱 圖靈機工作方式馮諾依曼模型線路位寬CPU位寬程序執行基本過程執行具體過程 1. 圖靈機工作方式 圖靈機可以視作“一臺帶規則的自動草稿機” 圖靈機基本組成&#xff1a; 紙帶&#xff08;內存&#xff09;&#xff1a;連續格子組成&…

SQLite與MySQL:嵌入式與客戶端-服務器數據庫的權衡

SQLite與MySQL&#xff1a;嵌入式與客戶端-服務器數據庫的權衡 在開發應用程序時&#xff0c;數據庫選擇是一個至關重要的決策&#xff0c;它會影響應用的性能、可擴展性、部署難度和維護成本。SQLite和MySQL是兩種廣泛使用的關系型數據庫管理系統&#xff0c;它們各自針對不同…

CppCon 2018 學習:Smart References

“強類型別名”&#xff08;strong typedefs&#xff09; 的動機和實現&#xff0c;配合一個簡單例子說明&#xff1a; 動機&#xff08;Motivation&#xff09; 用 using filename_t string; 和 using url_t string; 來區分不同的字符串類型&#xff08;比如文件名和網址&…

高性能高準確度的CPU電壓與溫度監測軟件HWInfo

&#x1f5a5;? 一、軟件概述 Windows版&#xff1a;圖形化界面&#xff0c;支持實時監控&#xff08;溫度、電壓、風扇轉速等&#xff09;、基準測試及報告生成&#xff0c;兼容Windows XP至Windows 11系統。Linux版&#xff1a;命令行工具&#xff0c;由openSUSE社區維護&a…

H3C WA6322 AP版本升級

1、查看當前版本&#xff1a;R2444P01 2、官網下載升級文件&#xff1a; WA6300系列版本說明H3C WA6300系列(適用于WA6330、 WA6322、WA6320H、WA6320、 WTU630H、WTU630、WA6330-LI、WA6320-C、WA6320-D、WA6320H-LI、WA6338、WA6322H、WTU632H-IOT、WAP922E、WAP923、WA6320…

用 YOLOv8 + DeepSORT 實現目標檢測、追蹤與速度估算

【導讀】 目標檢測與追蹤技術是計算機視覺領域最熱門的應用之一&#xff0c;廣泛應用于自動駕駛、交通監控、安全防護等場景。今天我們將帶你一步步實現一個完整的項目&#xff0c;使用YOLOv8 DeepSORT實現目標檢測、追蹤與速度估算。>>更多資訊可加入CV技術群獲取了解…

Python實例題:基于 Python 的簡單聊天機器人

Python實例題 題目 基于 Python 的簡單聊天機器人 要求&#xff1a; 使用 Python 構建一個聊天機器人&#xff0c;支持以下功能&#xff1a; 基于規則的簡單問答系統關鍵詞匹配和意圖識別上下文記憶功能支持多輪對話可擴展的知識庫 使用tkinter構建圖形用戶界面。實現至少 …