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>