第3章 SpringBoot 目前主流的互联网公司在开发Java项目时基本会使用SpringBoot快速构建应用, 相 比以前的项目,使用Spring开发需要一系列的配置,SpringBoot提供了Spring运行的默 认 配置,相当于将汽车手动挡改为自动挡,这种方式旨在让开发者能够更专注于业务实现。 本 7min 章从SpringBoot的实战集成入手,介绍集成SpringBoot的3种方式及SpringBoot底 层 运行的工作原理。希望读者能够对SpringBoot框架有一个清楚的认识,深入理解它的 底 层运行机制 。 3.自动配置/依赖管理 1 在古代的某个“Spring村(春风村)”,居民们过着平静的生活。随着时代的进步,人 们 对方便的生活方式的要求越来越高,为此,春风村推出了一种名为“春风吹又生”的新科技 , 让人们可以更轻松地完成每件事。SpringBoot是一个开放源码的架构,旨在为Spring程 序 的创建、配置和运行提供更简单、更高效的方法。它可以让Spring程序更容易创建、配置 和 运行,同时也让Spring程序更加容易管理。有了SpringBoot的“冲锋枪”,开发人员可以 更 快、更容易地完成事情。汤姆就是这样一个例子,他决定为自己的咖啡馆开发一款网络 软 件。他开始建立一个简单的Maven工程,很快就建立起了这个软件的基础结构,然后汤 姆 使用SpringBoot提供的自动设置功能设置应用中缺省的数据库连接和网络MVC,实现 了 一个完整的网络应用。SpringBoot也被称为Starters,它可以使从属关系管理变得更简单 。 有了Starters,Tom 可以简单地增加一个依赖性获取自己想要的类库和能力。SpringBoot 让汤姆在他的“咖啡屋”网络应用软件中变得更加容易工作,而不用去考虑那些烦琐的组 态 和从属关系管理。另外,“汤姆咖啡”网站的成功例子也使SpringBoot在春风村中变得 非 常有影响力。现在,SpringBoot已成为开发人员最喜欢的框架之一,它可以让开发人员 在 不同的应用中更容易地进行开发和部署 。 3.实战集成 2 快速构建一个SpringBoot项目,通常只需往pom 文件中添加依赖。本节将介绍如何 使用sprngbo-tre-prnspig-otdpednis、o.prng.tfrm 这3种方式 i-otsatraet、rnbo-eneceisiplao 1 08 快速集成SpringBoot项目。 3.2.1 使用spring-boot-starter-parent SpringBoot官方提供的示例项目依赖于spring-boot-starter-parent进行依赖管理,但 在企业级微服务架构中,每个模块只能有一个parent,多个微服务间的继承关系可能导致扩 展性问题,开发者需要采取其他手段(如修改父类依赖项或通过import导入)实现。在 SpringBoot的各个发布版本中包含了许多默认版本的依赖项,对开发者而言是一个便利, 只需专注于核心功能的开发。 下面去证实这一说法:如何在SpringBoot项目中添加依赖。具体方法是新建一个继 承SpringBoot的项目,并在pom.xml文件中添加依赖,代码如下: //第3 章/3.2.1 继承spring-boot-starter-parent 代码 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.11</version> <relativePath/><!--lookup parent from repository --> </parent> 在IDEA 开发环境中,通过按住Ctrl键并同时单击spring-boot-starter-parent,可以进 入其对应的pom.xml文件进行查看。通过查看可知,spring-boot-starter-parent作为一个 父项目,它继承了spring-boot-dependencies作为其父依赖管理,代码如下: //第3 章/3.2.1 继承spring-boot-dependencies 代码 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.6.11</version> </parent> 在IDEA 开发环境中,按住Ctrl键并单击spring-boot-dependencies,即可进入其对应 的pom.xml文件进行查看。在该文件的properties部分,开发者可以发现许多与依赖管理 相关的版本号配置,spring-boot-dependencies管理版本号如图3-1所示。 这也是所有SpringBoot的Starter无须指定版本号的原因,但如果开发者不想使用默 认版本号,则可以在项目中使用property的方式覆盖原有依赖项。 本节提到,企业级开发很少使用spring-boot-starter-parent作为依赖管理,因为企业通 常定义自己的parent,因此,在这种情况下,继承spring-boot-starter-parent并不适用。为了 解决这个问题,开发者可以在dependencyManagement部分使用<scope>import</scope>的 方式进行依赖管理,代码如下: 1 09 图3-1 spring-boot-dependencies管理版本号 //第3 章/3.2.1 在dependencyManagement 里面导入import 代码 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.11</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 然而,这种方法也存在一定的限制,因为它无法直接覆盖原始的依赖项配置。为了解决 这个问题,开发者可以采用一种相对复杂的策略:将之前引入的依赖项移至自己的 dependencyManagement部分,并使用spring-boot-dependencies进行替换,然而,这种方法 在企业级开发中并不常见。 除了继承spring-boot-dependencies之外,spring-boot-starter-parent还添加了一些默 认配置,例如设置JDK版本、使用占位符@、指定编译和打包时使用的JDK 版本及将编码 设置为UTF-8。具体示例,代码如下: //第3 章/3.2.1 spring-boot-starter-parent 默认配置代码 <properties> <java.version>1.8</java.version> <resource.delimiter>@</resource.delimiter> <maven.compiler.source>${java.version}</maven.compiler.source> 1 10 <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> < project. reporting. outputEncoding > UTF - 8 </project. reporting. outputEncoding> </properties> 此外,还设置了默认读取的配置文件目录和文件,以减少每个微服务都需要开发者自行 设置配置文件目录和文件的工作量,代码如下: //第3 章/3.2.1 读取配置文件目录和文件代码 <resources> <resource> <directory>${basedir}/src/main/resources</directory> <filtering>true</filtering> <includes> <include>**/application*.yml</include> <include>**/application*.yaml</include> <include>**/application*.properties</include> </includes> </resource> <resource> <directory>${basedir}/src/main/resources</directory> <Excludes> <Exclude>**/application*.yml</Exclude> <Exclude>**/application*.yaml</Exclude> <Exclude>**/application*.properties</Exclude> </Excludes> </resource> </resources> spring-boot-starter-parent还覆盖了spring-boot-dependencies中的某些插件。 3.2.2 使用spring-boot-dependencies spring-boot-dependencies同样是通过继承parent和导入(import)实现的。 第1种方式是通过继承Parent实现,代码如下: //第3 章/3.2.2 继承parent 代码 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.6.11</version> <relativePath/><!--lookup parent from repository --> </parent> 第2种方式是通过导入(import)的方式实现,代码如下: //第3 章/3.2.2 导入import 代码 <dependencyManagement> <dependencies> 1 11 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.6.11</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 在引用spring-boot-starter-web时,可以省略版本号,具体示例,代码如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 以上所述的依赖项涉及Web模块,该模块包含大量相关依赖,例如Spring相关库和内 置的Tomcat服务器等。有了这些依赖,开发人员就能够使用SpringBoot进行Web开发。 SpringBoot的spring-boot-dependencies引入了许多插件。在此,将简要介绍3个关键 插件:maven-help-plugin插件用于获取帮助信息;maven-resources-plugin插件用于处理资 源文件;maven-compiler-plugin插件用于编译Java代码,具体示例,代码如下: //第3 章/3.2.2 maven-help-plugin 插件代码 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-help-plugin</artifactId> <version>${maven-help-plugin.version}</version> </plugin> xml-maven-plugin插件是用于处理XML的Maven插件,代码如下: //第3 章/3.2.2 xml-maven-plugin 插件代码 <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>xml-maven-plugin</artifactId> <version>${xml-maven-plugin.version}</version> </plugin> build-helper-maven-plugin插件可以用来设置主源代码、测试源代码、主资源文件、测 试资源文件等目录,代码如下: //第3 章/3.2.2 build-helper-maven-plugin 插件代码 <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>${build-helper-maven-plugin.version}</version> </plugin> 综上可得出结论,spring-boot-dependencies插件的主要作用是管理依赖项的版本号、 1 12 管理插件的版本号及引入辅助插件。 3.2.3 使用io.spring.platform io.spring.platform 作为SpringBoot的基础平台,承担着继承spring-boot-starterparent 的角色。同时,spring-boot-starter-parent继承了spring-boot-dependencies,共同构 成了SpringBoot项目的根依赖管理。在日常开发中,开发者经常需要处理多个依赖项的 集成,很可能会遇到版本冲突或不兼容的问题。 为满足这种需求,一个重要目标是将已经过集成测试的依赖项整合在一起。由于这些 依赖项都经过了全面的集成测试,因此在使用过程中出现问题的概率相对较低。这也是 io.spring.platform 诞生的背景。 实现这一目标的方式是通过继承parent和使用import,以确保项目的依赖管理得到统 一和优化。这种方法有助于简化开发者的工作流程,减少潜在的问题,并提高应用程序的稳 定性和性能。 第1种方式是继承parent,代码如下: //第3 章/3.2.3 继承parent 代码 <parent> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR7</version> </parent> 这种方式的缺点在于,需要明确地添加插件,因为它需要继承一些插件管理。以Spring Boot为例,需要显式地添加插件,代码如下: //第3 章/3.2.3 显式地添加plugin 代码 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> 第2种方式是通过导入(import)实现,代码如下: //第3 章/3.2.3 导入import 代码 <dependencyManagement> <dependencies> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR6</version> <type>pom</type> 1 13 <scope>import</scope> </dependency> </dependencies> </dependencyManagement> SpringBoot已集成了许多开源框架,旨在帮助开发者简化第三方依赖管理,然而,在实 际开发过程中,仍有很多依赖未包含在内。在大型互联网项目中,各个模块之间的关系往往 错综复杂,维护工作可能变得枯燥乏味且具有较高的工作量。 为解决这一问题,io.spring.platform 应运而生,它有助于连接各个依赖。例如,假设开 发者需要升级某个依赖,只需更新相应的版本,无须担心版本兼容性问题。如今,一些大型 互联网项目会维护自己的基础项目platform。 3.3 手写一个简易版的SpringBoot 许多开发者渴望了解SpringBoot框架的内部运行机制,但由于阅读源码能力有限,难 以深入理解其底层工作原理。为帮助读者更好地理解SpringBoot框架,本节将通过手写 一个简易版本的SpringBoot来阐述其底层运行原理。在学习本节内容之前,建议读者先 熟悉SpringMVC的工作流程及SpringIOC的控制反转概念。 3.3.1 Java代码直接启动Tomcat SpringBoot框架以内置的Tomcat作为其Web容器,为Web应用提供服务,这是 SpringBoot的一个显著特点。本节将通过一个简化的示例工程展示如何启动Tomcat。 1.工程介绍 先创建一个名为simple-springboot的父工程,然后构建两个模块:springboot-module和 user-module。springboot-module模块定义了一个自定义注解ExampleSpringBootApplication, 并创建了启动类ExampleSpringApplication。user-module模块在UserApplication中引入 了springboot-module模块的注解和启动类。最后,通过main方法运行user-module模块。 项目结构图如图3-2所示。 simple-springboot的父工程需要将springboot-module模块和user-module模块添加 到其依赖中。父工程的pom.xml文件示例,代码如下: //第3 章/3.3.1 在simple-springboot 父工程将springboot-module 模块和user-module //模块的依赖添加进来 <? xml version="1.0" encoding="UTF-8"? > <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://maven. apache. org/POM/4. 0. 0 http://maven. apache. org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> 1 14 图3-2 项目结构图 <artifactId>simple-springboot</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>springboot-module</module> <module>user-module</module> </modules> </project> user-module模块的pom.xml文件需引入springboot-module模块的依赖,代码如下: //第3 章/3.3.1 在user-module 模块的pom.xml 文件中引入springboot-module 模块的依赖 <? xml version="1.0" encoding="UTF-8"? > <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://maven. apache. org/POM/4. 0. 0 http://maven. apache. org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>simple-springboot</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>user-module</artifactId> 1 15 <dependencies> <dependency> <groupId>org.example</groupId> <artifactId>springboot-module</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project> springboot-module模块引入的是Spring、Servlet及与Tomcat相关的依赖。其pom.xml 文件示例,代码如下: //第3 章/3.3.1 springboot-module 模块引入了Spring、Servlet 及与Tomcat 相关的依赖 <? xml version="1.0" encoding="UTF-8"? > <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://maven. apache. org/POM/4. 0. 0 http://maven. apache. org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>simple-springboot</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springboot-module</artifactId> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.18</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> 1 16 <version>5.3.18</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.60</version> </dependency> </dependencies> </project> 2.自定义注解 参考SpringBoot框架,可以自定义一个注解,该注解作用于启动类上。以下是该注解 的示例,代码如下: //第3 章/3.3.1 向启动类添加自定义注解 package com.example.springboot; import java.lang.annotation.*; /** * 自定义注解:启动类注解 */ @Target(ElementType.TYPE)//作用于类上面 @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited//一个类用上了@Inherited 修饰的注解子类继承这个注解 public @interface ExampleSpringBootApplication { } 3.自定义启动类 当浏览器发送请求时,需要启动Tomcat才能接受请求。以下示例用于展示如何启动 Tomcat,代码如下: //第3 章/3.3.1 自定义启动类 package com.example.springboot; import org.apache.catalina.*; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat; import org.springframework.context.annotation.Bean; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; 1 17 import java.util.Map; /** * 自定义启动类 */ public class ExampleSpringApplication { public static void run(Class clazz){ startTomcat(); } private static void startTomcat(){ Tomcat tomcat =new Tomcat(); Server server =tomcat.getServer(); Service service =server.findService("Tomcat"); Connector connector =new Connector(); connector.setPort(8080); Engine engine =new StandardEngine(); engine.setDefaultHost("localhost"); Host host =new StandardHost(); host.setName("localhost"); String contextPath =""; Context context =new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } } 4.自定义启动类 user-module模块的启动类应该使用自定义的注解和自定义的启动类,在main方法中 运行run方法。以下是该启动类的示例,代码如下: //第3 章/3.3.1 user-module 模块的启动类 package com.example.user; import com.example.springboot.ExampleSpringApplication; import com.example.springboot.ExampleSpringBootApplication; @ExampleSpringBootApplication public class UserApplication { public static void main(String[] args) { ExampleSpringApplication.run(UserApplication.class); } } 1 18 5.运行项目 启动项目后,可以在控制台查看日志,如图3-3所示。 图3-3 控制台日志 6.请求处理流程 既然Tomcat已经成功启动,那么接下来就需要处理请求了。以下是处理请求的示例, 代码如下: tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet(webApplicationContext)); context.addServletMappingDecoded("/*","dispatcher");//拦截所有请求给 //DispatcherServlet 处理 对于熟悉Spring MVC 工作流程的开发人员可能会立即联想到前端控制器 DispatcherServlet。在该框架中,所有的请求都会先经过DispatcherServlet进行处理。 注意:为了防止一些读者不熟悉SpringMVC的工作流程,本节通过一个小故事进行阐述。 在丰富多彩的互联网世界中,有一个名叫SpringMVC的小镇,其居民热情好客。这个小镇 充满生机和活力,居民和谐相处,共同分享美好生活。某一天,一位名叫“请求”的游客慕名 而来,想要参加一场特别的盛会。在这场盛会中,SpringMVC小镇的居民需要设计出最美 1 19 的舞蹈和最华丽的舞池,以期望能够迎接这场难得的盛会。 作为小镇的前端控制,DispatcherServlet以谨慎的态度接待请求。当收到请求时, DispatcherServlet认识到其重要性,首先对请求进行详尽解析,将URL转换为URI,并调用 HandlerMapping将请求与相关的控制器和拦截器联系在一起。DispatcherServlet将联系 在一起的对象封装为HandlerExecutionChain对象,并选择一个适宜的HandlerAdapter处 理请求。这位HandlerAdapter犹如专业的舞蹈教练,娴熟地将请求转换为优美的舞蹈。 在处理请求的过程中,HandlerAdapter承担着关键的辅助工作,如数据转换、数据验证 和消息转换,确保游客的请求以最佳形式呈现。经过一番努力,Handler完成任务,向 DispatcherServlet返回一个包含视图名的ModelAndView 对象,表明舞池和舞蹈已经准备 就绪。DispatcherServlet根据ModelAndView对象中的视图名,选择合适的ViewResolver 解析视图。这位ViewResolver犹如熟练掌握各种舞蹈的舞蹈指导,能够将游客的请求转换 为各种美丽的舞蹈画面。 在解析视图名的过程中,ViewResolver找到了一个最匹配的视图———一个名为View 的小镇居民。View利用模型数据(如舞池和舞蹈),渲染出美丽的舞蹈画面。在渲染过程 中,View与模型数据完美结合,精心布置了舞池,编排了一支优雅的舞蹈。游客们被这美丽 的舞蹈所吸引,纷纷拿出相机记录下这难忘的时刻。渲染结束后,View 将渲染后的舞蹈画 面传递给DispatcherServlet。DispatcherServlet将这些画面呈现在游客们的眼前,引发他 们的欢呼雀跃,感叹小镇居民们的才华与热情。舞会结束后,游客们与SpringMVC小镇的 居民们建立了深厚的友谊,这段美好的回忆成为永恒的佳话。从此,SpringMVC小镇名声 远扬,吸引了越来越多的游客,SpringMVC的故事,也成了互联网世界中一段不朽的传奇。 在本流程中,DispatcherServlet会对请求的URL 进行解析,并查找对应的Controller 方法。事实上,每个Controller都是Spring容器中的一个Bean,因此,为了处理请求,需要 将一个Spring容器传递给DispatcherServlet。那么,这个容器从何而来呢? 通过查阅SpringMVC源代码,找到DispatcherServlet的有参数构造方法,代码如下: public DispatcherServlet(WebApplicationContext webApplicationContext) { super(webApplicationContext); this.setDispatchOptionsRequest(true); } 在这种方法中,需要使用WebApplicationContext容器。了解应该使用哪种容器后,便 可以直接创建一个Spring容器,并将UserApplication启动类注册进来,代码如下: //第3 章/3.3.1 自定义启动类 package com.example.springboot; import org.apache.catalina.*; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; 1 20 import org.apache.catalina.startup.Tomcat; import org.springframework.context.annotation.Bean; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import java.util.Map; /** * 自定义启动类 */ public class ExampleSpringApplication { public static void run(Class clazz){ //创建一个Spring 容器 AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); //注册启动类 webApplicationContext.register(clazz); webApplicationContext.refresh(); //启动Tomcat startTomcat(webApplicationContext); } private static void startTomcat(WebApplicationContext webApplicationContext){ Tomcat tomcat =new Tomcat(); Server server =tomcat.getServer(); Service service =server.findService("Tomcat"); Connector connector =new Connector(); connector.setPort(8080); Engine engine =new StandardEngine(); engine.setDefaultHost("localhost"); Host host =new StandardHost(); host.setName("localhost"); String contextPath =""; Context context =new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet (webApplicationContext)); //拦截所有请求,交给DispatcherServlet 处理 context.addServletMappingDecoded("/*","dispatcher"); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } } 1 21 这样,在启动Tomcat时,容器会解析UserApplication类,并解析其上的自定义注解 ExampleSpringBootApplication,然后开发者将@ ComponentScan 注解添加到 ExampleSpringBootApplication中,代码如下: //第3 章/3.3.1 自定义注解 package com.example.springboot; import org.springframework.context.annotation.ComponentScan; import java.lang.annotation.*; /** * 自定义注解:启动类注解 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented //@Inherited 是一个标识,用来修饰注解 @Inherited //扫描UserApplication 类所在的包路径 @ComponentScan public @interface ExampleSpringBootApplication { } 由于没有指定具体的扫描路径,所以容器会扫描ExampleSpringBootApplication注解 作用的类UserApplication,并解析其包路径com.example.user。进一步将扫描范围扩大到 该包下的所有Controller。 为了测试上述处理过程,在user-module模块下创建一个TestController,并编写一个 简单的接口,代码如下: //第3 章/3.3.1 TestController 类 package com.example.user.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @GetMapping("/test") public String test(){ return "test"; } } 运行项目,通过浏览器访问接口校验代码的正确性,如图3-4所示。 为了更好地理解整个流程,下面通过脑图进行总结,如图3-5所示。 3.3.2 多态实现WebServer 在3.3.1节中,SpringBoot使用的是一种固定的方式来启动Tomcat,无法切换到其他 Web容器,例如Jetty。假设项目需要实现切换到Jetty容器的功能,应该如何实现呢? 首先,提供Tomcat和Jetty 的依赖,然后根据依赖情况来确定项目中使用的是 1 22 图3-4 浏览器请求接口 图3-5 请求处理流程脑图 TomcatWebServer还是JettyWebServer的Bean,进而决定使用哪种Web容器进行执行。 在确保项目可以正常切换Web容器之后,再进行代码优化。这就是实现这一功能的基本思 路。下面对3.3.1节的代码进行修改。 1.引入Jetty依赖 在引入Tomcat依赖的springboot-module模块中,若要引入Jetty依赖,则需要在依赖 中添加配置<optional>true</optional>,表示该依赖不会被传递给调用服务,即usermodule 服务。由于springboot-module模块需要支持多种Web容器(Tomcat/Jetty),所以 调用端只能使用其中一种,否则会出现错误,代码如下: //第3 章/3.3.2 引入Tomcat 和Jetty 依赖 <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.60</version> 1 23 </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>9.4.48.v20220622</version> <optional>true</optional> </dependency> Jetty依赖应添加至springboot-module模块中,而user-module模块仅依赖于springbootmodule 模块,并默认使用Tomcat依赖,因此,Jetty依赖无法传递至user-module模块。如 果user-module模块需要使用Jetty依赖,就需要在springboot-module模块中排除Tomcat 依赖,并添加Jetty依赖,代码如下: //第3 章/3.3.2 排除Tomcat 依赖 <dependency> <groupId>org.example</groupId> <artifactId>springboot-module</artifactId> <version>1.0-SNAPSHOT</version> <!--排除Tomcat 依赖--> <Excelusions> <Excelusion> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> </Excelusion> </Excelusions> </dependency> <!--引入Jetty 依赖--> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>9.4.48.v20220622</version> </dependency> 2.创建WebServer接口 已知该项目需要同时使用Tomcat和Jetty容器,为了避免在后续引入其他Web容器 时产生混淆,开发者定义了一个WebServer接口,用于抽象出Web容器的启动功能,代码 如下: //第3 章/3.3.2 创建WebServer 接口 package com.example.springboot; import org.springframework.web.context.WebApplicationContext; /** * Web 服务接口 */ public interface WebServer { public void start(WebApplicationContext applicationContext); } 1 24 3.创建TomcatWebServer实现类 将startTomcat方法的实现移至实现类的start方法中,代码如下: //第3 章/3.3.2 创建TomcatWebServer 实现类 package com.example.springboot; import org.apache.catalina.*; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; /** * Web 服务:启动Tomcat 相关代码 */ public class TomcatWebServer implements WebServer{ @Override public void start(WebApplicationContext applicationContext) { System.out.println("============启动Tomcat============="); Tomcat tomcat =new Tomcat(); Server server =tomcat.getServer(); Service service =server.findService("Tomcat"); Connector connector =new Connector(); connector.setPort(9081); Engine engine =new StandardEngine(); engine.setDefaultHost("localhost"); Host host =new StandardHost(); host.setName("localhost"); String contextPath =""; Context context =new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext)); context.addServletMappingDecoded("/*", "dispatcher"); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } } 1 25 4.创建JettyWebServer实现类 Jetty启动过程,代码如下: //第3 章/3.3.2 创建JettyWebServer 实现类 package com.example.springboot; import org.springframework.web.context.WebApplicationContext; /** * Web 服务:启动Jetty 相关代码 */ public class JettyWebServer implements WebServer{ @Override public void start(WebApplicationContext applicationContext) { System.out.println("启动Jetty"); //省略Jetty 启动过程的代码,不重点讲解 } } 5.改造ExampleSpringApplication类 获取Tomcat或者Jetter容器,代码如下: //第3 章/3.3.2 改造ExampleSpringApplication 类 package com.example.springboot; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import java.util.Map; /** * 自定义启动类 */ public class ExampleSpringApplication { public static void run(Class clazz){ //创建一个Spring 容器AnnotationConfigWebApplicationContext 支持SpringMVC AnnotationConfigWebApplicationContext applicationContext =new AnnotationConfigWebApplicationContext(); applicationContext.register(clazz);//注册一个类进来 applicationContext.refresh(); //启动Web 服务器(Tomcat、Jetty) WebServer webServer =getWebServer(applicationContext); webServer.start(applicationContext); } /** * 获取Web 服务:获取Tomcat 或者Jetty 容器 * @param applicationContext * @return */ private static WebServer getWebServer(WebApplicationContext applicationContext) { Map< String, WebServer> beansOfType = applicationContext.getBeansOfType (WebServer.class); //两个都没有定义: UserApplication 类中没有定义TomcatWebServer 或者 //JettyWebServer 1 26 if (beansOfType.size() ==0) { throw new NullPointerException(); } //定义了两个: UserApplication 类中有定义TomcatWebServer 和JettyWebServer //会报错 if (beansOfType.size() >1) { throw new IllegalStateException(); } //定义第1 个: UserApplication 类中有定义TomcatWebServer 或者JettyWebServer //其中一个 return beansOfType.values().stream().findFirst().get(); } } 6.改造UserApplication类 在UserApplication类中只能定义TomcatWebServer或JettyWebServer其中之一,代 码如下: //第3 章/3.3.2 改造UserApplication 类 package com.example.user; import com.example.springboot.ExampleSpringApplication; import com.example.springboot.ExampleSpringBootApplication; import com.example.springboot.JettyWebServer; import com.example.springboot.TomcatWebServer; import org.springframework.context.annotation.Bean; /** * TomcatWebServer 和JettyWebServer 只能定义其中一个。弊端:比较麻烦,需要有一个自动 的配置类识别我要用什么类型的Web 服务容器。解决方案:WebServerAutoConfiguration */ @ExampleSpringBootApplication public class UserApplication { //@Bean //public TomcatWebServer tomcatWebServer(){ //return new TomcatWebServer(); //} @Bean public JettyWebServer jettyWebServer(){ return new JettyWebServer(); } public static void main(String[] args) { ExampleSpringApplication.run(UserApplication.class); } } 7.运行项目 启动项目并查看日志,如图3-6所示。 读者可以注释JettyWebServer或TomcatWebServer,或同时使用它们,以检查功能是 否可实现。