解决传统SSM的问题
依赖太多了, 且存在版本问题
配置太多了且每次都一样, 大部分工程, 配置每次都是一样的, 从一个地方拷贝到另外一个地方. 且Spring发展10多年, 各种配置版本太多, 对于很多程序员来说, 分不清哪个是有效, 哪个无效.
部署太麻烦. 需要tomcat部署, 项目结构也需要照着Java EE的目录结构来写.SpringBoot特点
创建独立的Spring应用程序
嵌入的Tomcat,无需部署WAR文件
简化Maven配置
自动配置Spring
提供生产就绪型功能,如指标,健康检查和外部配置
绝对没有代码生成和对XML没有要求配置
几乎可以全注解方式SpringBoot的功能
自动配置(auto-configuration)
一项简化配置的功能,比如在classpath中发现有spring security的jar包,则自动创建相关的bean等
starters(简化依赖)
这个比较关键,方便spring去集成各类组件,比如redis、mongodb等等。
SpringBoot项目搭建
说明:开发工具使用IDEA ,版本为2.1.3
在IDEA 中新建module ,选择Spring Initializr 按next一步一步创建即可;或者直接创建普通maven项目
修改pom.xml 文件:
1 2 3 4 5 6 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
继承spring-boot-starter-parent项目来获取合适的默认设置。
注意:只需要在该依赖上指定Spring Boot版本。如果导入其他的starters,可以放心的省略版本号。
聚合项目pom.xml修改:
不是每个人都喜欢继承spring-boot-starter-parent POM。你可能需要使用公司标准parent,或你可能倾向于显式声明所有Maven配置。 如果你不使用spring-boot-starter-parent,通过使用一个scope=import的依赖,你仍能获取到依赖管理的好处:
1 2 3 4 5 6 7 8 9 10 11 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.3.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
指定JDK版本需要在pom.xml添加:
1 2 3 4 5 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
或者简单的添加:
1 2 3 <properties> <java.version>1.8</java.version> </properties>
SpringBoot需要Web支持,所以需要导入web组件:
1 2 3 4 5 6 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
现在可以写一个简单SpringBoot程序了,我们知道maven会默认编译src/main/java下的源码;
所以我们在该目录下新建java文件:com.ccsu.App
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.ccsu; import org.springframework.boot.*; import org.springframework.boot.autoconfigure.*; import org.springframework.stereotype.*; import org.springframework.web.bind.annotation.*; @RestController @EnableAutoConfiguration public class App { @RequestMapping("/") String home() { return "Hello World!"; } public static void main(String[] args) throws Exception { SpringApplication.run(App.class, args); } }
执行main方法,使用浏览器打开http://localhost:8080,即可看到**Hello World!**字样。
上述SpringBoot特点已说明内部嵌入Tomcat,所以该处默认端口为8080
注解说明:
@RestController。这被称为一个构造型(stereotype)注解。它为阅读代码的人们提供建议。对于Spring,该类扮演了一个特殊角色。在本示例中,我们的类是一个web @Controller,所以当处理进来的web请求时,Spring会询问它。
@RequestMapping。注解提供路由信息。它告诉Spring任何来自”/“路径的HTTP请求都应该被映射到home方法。@RestController注解告诉Spring以字符串的形式渲染结果,并直接返回给调用者。
@EnableAutoConfiguration。这个注解告诉Spring Boot根据添加的jar依赖猜测你想如何配置Spring。由于spring-boot-starter-web添加了Tomcat和Spring MVC,所以auto-configuration将假定你正在开发一个web应用并相应地对Spring进行设置。该注解会自动扫描App.java所在的所有文件。
main方法。这只是一个标准的方法,它遵循Java对于一个应用程序入口点的约定。我们的main方法通过调用run,将业务委托给了Spring Boot的SpringApplication类。SpringApplication将引导我们的应用,启动Spring,相应地启动被自动配置的Tomcat web服务器。我们需要将App.class作为参数传递给run方法来告诉SpringApplication谁是主要的Spring组件。
注意事项:
定位main应用类
通常建议你将main应用类放在位于其他类上面的根包(root package)中。通常使用@EnableAutoConfiguration注解你的main类,并且暗地里为某些项定义了一个基础“search package”。例如,如果你正在编写一个JPA应用,被@EnableAutoConfiguration注解的类所在包将被用来搜索@Entity项。 使用根包允许你使用@ComponentScan注解而不需要定义一个basePackage属性。如果main类位于根包中,你也可以使用@SpringBootApplication注解。
比较典型的项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 cn +- ccsu +- myproject +- Application.java | +- domain | +- Customer.java | +- CustomerRepository.java | +- service | +- CustomerService.java | +- web +- CustomerController.java
实现简单注册登录功能
使用数据库mysql,创建用户表:
1 2 3 4 5 6 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT , `passwd` varchar(255) NULL , `username` varchar(255) NULL , PRIMARY KEY (`id`) );
2.集成mybatis。修改pom.xml
1 2 3 4 5 6 7 8 9 10 11 <!--mybatis整合,需要mybatis-spring-boot-starter, 和mysql连接,即:mysql-connector-java--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <!--JDBC导入,此处用mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
3.配置mysql数据源
在resources目录,新建application.properties或application.yml文件(application.properties或application.yml是SpringBoot默认支持的两种配置文件,application是默认约定的文件名)。
配置数据源,需要在该配置文件中添加:
application.properties
1 2 3 4 5 6 7 8 #mysql数据源配置 spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/spingboot?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=password #配置需要扫描加载的mapper文件,此处即:mapping文件夹下的xml文件 mybatis.mapperLocations=classpath:mapping/*.xml
4.使用mybatis逆向工程自动生成相关文件。
自动生成代码需要相关插件,此处使用mybatis-generator-maven-plugin ,比较好用的代码生成工具还有:rapid-generator 等。
修改pom.xml引入插件:
1 2 3 4 5 6 7 <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <configuration> <overwrite>true</overwrite> </configuration> </plugin>
创建项目目录结构,以便指定生成文件存放目录,我的项目项目结构为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 com +- ccsu +- App.java | +- controller | +- UserController.java | +- service | +-impl | +- IBaseServiceImpl.java | +- UserServiceImpl.java | +-IBaseService.java | +- UserService.java | +- dao | +- IBaseMapper.java | +- UserMapper.java | +- model | +- User.java
同时需要在resources 目录下创建mapping 文件夹,以便存放生成相关的mapper.xml 文件
准备自动生成代码的配置文件,mybatis-generator-maven-plugin插件使用名为generatorConfig.xml 的配置文件。
generatorConfig.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 数据库驱动:选择你的本地硬盘上面的数据库驱动包--> <classPathEntry location="D:\JavaSpace\software\maven\repo\mysql\mysql-connector-java\5.1.34\mysql-connector-java-5.1.34.jar"/> <context id="DB2Tables" targetRuntime="MyBatis3"> <commentGenerator> <property name="suppressDate" value="true"/> <!-- 是否去除自动生成的注释 true:是 : false:否 --> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--数据库链接URL,用户名、密码 --> <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/spingboot" userId="root" password=""> </jdbcConnection> <javaTypeResolver> <property name="forceBigDecimals" value="false"/> </javaTypeResolver> <!-- 生成模型的包名和位置--> <javaModelGenerator targetPackage="com.ccsu.model" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- 生成映射文件的包名和位置--> <sqlMapGenerator targetPackage="mapping" targetProject="src/main/resources"> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!-- 生成DAO的包名和位置--> <javaClientGenerator type="XMLMAPPER" targetPackage="com.ccsu.dao" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 要生成的表 tableName是数据库中的表名或视图名 domainObjectName是实体类名--> <table tableName="user" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table> </context> </generatorConfiguration>
找到该项目下的这个maven插件,点击run即可。生成的结果如上述目录结构中dao、model下的文件。生成了数据库表对应的Bean,以及mapper类。
5.在App启动类添加自动扫描mapper,使其能自动生成代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.ccsu; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.ccsu.dao") //此处添加mapper扫描,mapper接口能自动生成代理类 public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
接下来在相关地方编写登录注册逻辑即可。
项目热部署支持
spring为开发者提供了一个名为spring-boot-devtools的模块来使Spring Boot应用支持热部署,提高开发者的开发效率,无需手动重启Spring Boot应用。
修改pom.xml引入spring-boot-devtools。
1 2 3 4 5 6 <!--引入spring-boot-devtools--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
同时需要spring-boot-maven-plugin插件支持
1 2 3 4 5 6 7 <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin>
还需要修改application.properties文件,开启热部署支持,默认是不开启的。
application.properties
1 2 3 #热部署 spring.devtools.livereload.enabled=true spring.devtools.remote.restart.enabled=true //默认false
同时开发工具IDEA默认也不支持自动编译,需要进行修改;
File->Settings->Compiler
勾选 Build Project automatically
快捷键:ctrl + shift + alt + /,选择Registry
勾上 Compiler autoMake allow when app running
SpringBoot单元测试
进行单元测试,SpringBoot有专门的组件来支持,需要修改pom.xml,增加依赖:
1 2 3 4 5 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
test目录下新建UserTests.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.ccsu; import com.ccsu.dao.UserMapper; import com.ccsu.model.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; @RunWith(SpringRunner.class)//指定运行的单元测试组件,SpringRunner.class继承了SpringJUnit4ClassRunner,SpringBoot内置了Junit4,此处表示使用Junit4测试组件 @SpringBootTest(classes = App.class) //@SpringBootTest标记为该类为springboot单元测试类,参数:classes = App.class指定启动类为App.class,及对应上述App.java public class UserTests { @Resource private UserMapper userMapper; @Test public void testUserAdd(){ User user = new User(); user.setUsername("test"); user.setPasswd("test"); int userInsert = userMapper.insertSelective(user); System.out.println(userInsert); } @Test public void testUserLogin(){ User user = userMapper.findByUsernameAndPasswd("test", "test"); System.out.println(user); } }
SpringBoot事务支持
测试事务,在UserService中添加方法batchAdd,在方法中故意产生一个被0整除的异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * 测试事务 * 此处插入了两个User,在没有事务支持的情况下,在10/0发生异常的时候,第一个User将被插入到数据库中,第二个不会 * 在事务支持的情况下,在10/0发生异常的时候,第一个插入数据没有进行提交,会被回滚,即两个User都不会插入。在业务场景下,这种才是正确的 * * @param username * @param passwd */ @Override @Transactional public void batchAdd(String username, String passwd) { User user1 = new User(); user1.setUsername(username); user1.setPasswd(passwd); userMapper.insertSelective(user1); int i = 10 / 0; User user2 = new User(); user2.setUsername(username); user2.setPasswd(passwd); userMapper.insertSelective(user2); }
在方式上方添加@Transactional注解,即加入事务支持,解决了事务问题;如果没有得到解决可在启动类上加入@EnableTransactionManagement注解。当然一般方法上加入@Transactional即可生效
全局异常处理
通过上面的步骤,已经解决事务问题,但是500的错误展示给用户看,对于用户来说极其不友好。
一般在企业里,对于这些异常一般都会统一捕获,由专门的异常处理类来统一处理。
新建目录utils,在该目录下新建GlobalExceptionHandler.java全局异常处理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.ccsu.utils; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; /** * 全局异常处理类 * 注解@ControllerAdvice会管理所有的@RestController * 注解@ExceptionHandler表示异常处理 */ @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = RuntimeException.class) @ResponseBody public Object defaultErrorHandler(HttpServletRequest request, Exception ex) throws Exception{ ex.printStackTrace(); return "我是一个异常处理类"; } }
这样,用户就不会再看到系统直接返回的错误了。而是更友好的定义的提示。但是,当用户输入一个不存在的路由的时候,系统还是会显示404的糟糕提示。此时,我们需要处理404的提示,让它看上去更友好,一般企业开发中会定义好这种404页面,专门处理404。所以我们需要当发生404时,让其路由到404页面上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.ccsu.utils; import org.springframework.boot.web.server.ConfigurableWebServerFactory; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; /** * 全局异常处理类 * 注解@ControllerAdvice会管理所有的@RestController * 注解@ExceptionHandler表示异常处理 */ @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = RuntimeException.class) @ResponseBody public Object defaultErrorHandler(HttpServletRequest request, Exception ex) throws Exception{ ex.printStackTrace(); return "我是一个异常处理类"; } @Bean public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){ return (factory -> { ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404"); factory.addErrorPages(error404Page); }); /* return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() { @Override public void customize(ConfigurableWebServerFactory factory) { ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404"); factory.addErrorPages(error404Page); } };*/ } }
以上我们写了一个处理方法,让其定位到404的路由路径下,同时我们在Controller目录下新建一个BaseController.java来处理展示404。
1 2 3 4 5 6 7 8 9 10 11 12 package com.ccsu.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BaseController { @RequestMapping("/404") public Object error_404(){ return "抱歉,你要找到页面被别人偷吃了!"; } }
这样我们就能在页面上看到更友好的处理结果了。
注意:WebServerFactoryCustomizer这种配置方式是在SpringBoot2之后才这样配置的,在1.x的版本需要用到的是EmbeddedServletContainerCustomizer
静态资源的访问
静态资源:js, css, html, 图片, 音视频等
静态资源路径:是指系统可以直接访问的路径,且路径下的所有文件均可被用户直接读取。
SpringBoot默认提供静态资源目录位置需要在classpath下,并且目录名需要符合规范: /static 或者 /public
比如我们在resources目录下新建static目录,并任意放一个图片test.jpg。在浏览器地址栏输入localhost:8080/test.jpg,就可以直接访问。
前端界面
SpringBoot推荐使用模板引擎来渲染html,如果不是历史遗留项目,一定不要使用JSP,常用的模板引擎很多,有freemark,thymeleaf等;其实都大同小异,其中SpringBoot强烈推荐使用thymeleaf。
修改pom.xml添加thymeleaf的支持。
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
新建ThymeleafController.java,此处使用@Controller注解,而不是@RestController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.ccsu.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/tpl") public class ThymeleafController { @RequestMapping("testThymeleaf") public String testThymeleaf(ModelMap map) { //设置属性 map.addAttribute("name", "lisl"); //testThymeleaf:即模板文件名称 //对应src/main/resources/templates/testThymeleaf.html return "testThymeleaf"; } }
SpringBoot默认模板配置路径为:src/mian/resources/templates
所以我们在templates新建testThymeleaf.html文件:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html> <html xmlns:th="http://www.w3.org/1999/xhtml"> <head lang="en"> <meta charset="UTF-8" /> <title>testThymeleaf</title> </head> <body> <h1 th:text="${name}"/> </body> </html>
在浏览器输入:localhot:8080/tpl/testThymeleaf即可看到结果。
集成Swagger2构建API文档
Swagger2作用:
随项目自动生成强大的RESTful API文档,减少工作量
API文档与代码整合在一起,便于同步更新API说明
页面测试功能来调试每个RESTful API
修改pom.xml,添加Swagger2的相关依赖:
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
现在Swagger2配置类,在utils目录下新建SwaggerConfig.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.ccsu.utils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * Swagger2配置类 */ @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.ccsu"))//指定扫描包下面的注解 .paths(PathSelectors.any()) .build(); } /*创建api的基本信息*/ private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("集成Swagger2构建RESRful APIs") .description("集成Swagger2构建RESTful APIs") .termsOfServiceUrl("https://baidu.com") .contact(new Contact("lisl", "cn.lisl","lisl@lisl.cn")) .version("1.0.0") .build(); } }
在Controller接口上加上Swagger描述注解,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ApiOperation(value = "注册",notes = "填写用户名密码进行注册") @ApiImplicitParams({ @ApiImplicitParam(name = "username", value = "用户名" ,required = true, dataType = "String"), @ApiImplicitParam(name = "passwd", value = "用户密码",required = true, dataType = "String") }) @RequestMapping(value = "/register", method = RequestMethod.POST) public String register(String username, String passwd){ return userService.register(username, passwd)?"注册成功":"注册失败"; } @ApiOperation(value = "登录", notes = "根据用户名和密码登录系统") @ApiImplicitParams({ @ApiImplicitParam(name = "username", value = "用户名",required = true, dataType = "String"), @ApiImplicitParam(name = "passwd", value = "用户密码",required = true, dataType = "String") }) @RequestMapping(value = "/login", method = RequestMethod.GET) public String login(String username, String passwd){ return userService.Login(username, passwd)?"登录成功":"登录失败"; }
访问:localhost:8080/swagger-ui.html即可看到最终结果。
日志集成
java有许多的日志组件,比如:log4j,log4j2,logback还有java自身提供的Java Util Logging,其实在SpringBoot中对这些组件都提供了支持,log4j,log4j2和logback都提供了支持。
Logback:
在SpringBoot中默认使用的日志工具是logback;所以我们可以直接使用。
修改UserController.java进行测试:
1 2 3 4 5 6 7 private final Logger logger = LoggerFactory.getLogger(UserController.class); @RequestMapping(value = "/hello", method = RequestMethod.GET) public String sayHello() { logger.info("这是一个Hello日志!"); return "Hello World! 我的世界,我是世界的主宰!111"; }
这是访问hello时就会看到控制台输出日志。
修改UserController把日志输出改为:
logger.debug(“这是Hello debug日志!”)
控制台不会输出日志,这是因为日志级别不够,日志级别等级为:
debug<info<warn<Error<Fatal;
此时如果要想控制台输出日志,则需要修改application.properties进行配置。
1 2 3 4 5 #配置logback日志 #指定哪些包的日志级别 logging.level.root = INFO logging.level.org.springframework.web=debug logging.level.com.ccsu.controller=debug
这个时候org.springframework.web和com.ccsu.controller的debug日志就可以输出来
日志文件
一般情况下,SpringBoot日志只会输出到控制台,不会输出到日志文件,但是,正式环境我们都需要写到日志文件中。这个时候需要修改application.properties,在文件中配置logging.file文件名称和logging.path路径。
注意:如果只配置loggin.path,在/var/tmp文件夹生成日志文件为spring.log.如果只配置了loggin.file,会在项目的当前路径下生成一个xxx.log日志文件。
如果logging.path和logging.file都配置具体路径和文件,则只会logging.file生效,所以要指定日志生成的具体位置只使用logging.file写好路径和文件名即可;
1 2 #指定日志输出文件 logging.file=D:\\java\\log\\spingboot.log
这样就可输出日志到指定的日志文件了。
log4j2
修改pom.xml,添加log4j2的依赖:
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
同时需要注意的是由于默认使用的是logback,在扩展之前需要先把logback移除:
1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency>
使用跟logback一样。如上logback所述。
使用AOP统一日志处理
修改pom.xml,添加aop依赖。
1 2 3 4 5 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.2.RELEASE</version> </dependency>
在utils下新增AOP日志处理类:WebLogAspect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.ccsu.utils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; @Component @Aspect public class WebLogAspect { private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class); @Pointcut("execution(public * com.ccsu.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 接收到请求,记录请求内容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录下请求内容 logger.info("URL : " + request.getRequestURL().toString()); logger.info("HTTP_METHOD : " + request.getMethod()); logger.info("IP : " + request.getRemoteAddr()); Enumeration<String> enu = request.getParameterNames(); while (enu.hasMoreElements()) { String name = (String) enu.nextElement(); logger.info("name:{},value:{}", name, request.getParameter(name)); } } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { // 处理完请求,返回内容 logger.info("RESPONSE : " + ret); } }
这样,每次请求接口时,就都会有基本的日志打印。
编译打包 快速入门进行到这里,其实已经差不多了,能应对绝大多数开发常见,接下来就是导包部署。
修改pom.xml,引入maven的编译插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- 如果不设置fork,那么不会restart,devtools热部署不会起作用--> <fork>true</fork> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
使用命令:mvn clean package 即可打包。
使用 java -jar xxxx.jar 即可运行。
war部署
如果并不希望使用内置的tomcat,希望部署到其他tomcat服务器,那么就需要使用war包部署了。
修改pom.xml,打包方式改成war:
1 2 3 4 5 6 <groupId>com.ccsu</groupId> <artifactId>study</artifactId> <version>0.0.1-SNAPSHOT</version> <name>study</name> <packaging>war</packaging> //修改此处为war <description>Demo project for Spring Boot</description>
修改在pom文件,剔除内置tomcat的支持,否则会和外面的tomcat冲突:
1 2 3 4 5 6 7 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <!--打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。 相当于compile,但是打包阶段做了exclude操作--> <scope>provided</scope> </dependency>
修改启动类,使其继承 org.springframework.boot.web.servlet.support.SpringBootServletInitializer,并重写configure方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.ccsu; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @MapperScan("com.ccsu.dao") @EnableTransactionManagement public class App extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(App.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(App.class); } }
使用mvn clean package 打包
把war包拷贝到tomcat webapps中,即可访问。
集成redis 修改pom.xml,添加依赖:
1 2 3 4 5 <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
修改application.properties,添加redis配置
1 2 3 4 5 6 7 8 9 10 11 #redis #redis数据索引,默认为0 spring.redis.database=0 #Redis服务器地址 spring.redis.host=localhost #redis服务器连接端口 spring.redis.port=6379 #redis连接密码,默认为空 spring.redis.password= #redis连接超时时间 spring.redis.timeout=5000
新增单元测试类,进行测试;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.ccsu; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; @SpringBootTest(classes = App.class) @RunWith(SpringRunner.class) public class SpringRedisTests { @Resource private RedisTemplate<String,String> redisTemplate; @Test public void testRedis(){ ValueOperations<String, String> ops = redisTemplate.opsForValue(); ops.set("name","lisl"); System.out.println(ops.get("name")); } }
运行,即可看到输出结果。
集成RabbitMQ 修改pom.xml,添加rabbitmq依赖:
1 2 3 4 5 <!--rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
修改application.properties,添加rabbitmq的配置:
1 2 3 4 5 #rabbitmq spring.rabbitmq.host=192.168.80.10 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest
创建Rabbit配置类,用来配置队列,交换器,路由等高级信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.ccsu.utils; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitConfig { @Bean public Queue firstQueue(){ //创建一个名为lisl的队列 return new Queue("lisl"); } }
创建消息的生产者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.ccsu.mq; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; @Component public class Producer { @Resource private AmqpTemplate rabbitAmqpTemplate; public void send(){ rabbitAmqpTemplate.convertAndSend("lisl","my name is lisl!"); } }
创建消息的消费者,消费者监听队列来消费消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.ccsu.mq; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component //定义需要监听的队列 @RabbitListener(queues = "lisl") public class Consumer { //指定对消息的处理 @RabbitHandler public void process(String message){ System.out.println("receive message:"+message); } }
创建单元测试类,进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.ccsu; import com.ccsu.mq.Producer; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; @SpringBootTest(classes = App.class) @RunWith(SpringRunner.class) public class RabbitMqTests { @Resource private Producer producer; @Test public void testRabbitmq(){ producer.send(); } }
运行之后,即可看到消费者消费的消息。
Actuator监控管理 Actuator是spring boot的一个附加功能,可帮助你在应用程序生产环境时监视和管理应用程序。可以使用HTTP的各种请求来监管,审计,收集应用的运行情况.特别对于微服务管理十分有意义。
缺点:没有可视化界面
修改pom.xml添加依赖:
1 2 3 4 5 <!--actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
修改application.properties,启动监控端点:
1 2 3 4 5 6 7 #actuator # 加载所有的端点/默认只加载了 info / health management.endpoints.web.exposure.include=* # 描述信息 info.blog-url=http://lisl.cn info.author=lisl info.version=@project.version@
重新启动,在地址栏输入http://localhost:8080/actuator/info即可看到监控信息。
Actuator访问路径
通过actuator/+端点名就可以获取相应的信息。
路径
作用
/actuator/beans
显示应用程序中所有Spring bean的完整列表。
/actuator/configprops
显示所有配置信息。
/actuator/env
陈列所有的环境变量。
/actuator/mappings
显示所有@RequestMapping的url整理列表。
/actuator/health
显示应用程序运行状况信息 up表示成功 down失败
/actuator/info
查看自定义应用信息
自定义starter 在学习SpringBoot的过程中,不管是集成redis还是RabbitMQ,甚至是前面集成mybatis已经学习了很多starter,这些starter都是springboot为我们提供的一些封装,这些starter能非常方便快捷的增加功能,并不需要很多配置,即使需要配置也就在application.properties稍微配置下就可以了。
那么接下来就初步认识怎么创建属于自己的starter
自己集成redis,创建redis-starter插件:
自定义starter,我们可以根据查看其他的starter,可以理清楚基本的思路;
自定义starter:
首先,需要一个属性配置类;即:xxxxProperties.java
该配置类即定义application.properties需要的属性
其次,我们需要一个自动配置类,让前面定义的属性能够生效,即xxxxAutoConfiguration.java
最后,我们需要在META-INF下定义spring.factories文件;这个文件我们查看其它starter可发现,SpringBoot会去扫描这个文件,查找自动化配置;让SpringBoot能够找到自动化配置类xxxAutoConfiguration.java。
根据以上思路,我们开始来自定义一个starter,及上述所说的redis-starter。
首先 ,我们创建一个简单的maven项目。这个项目不需要web,就是一个简单的maven项目。
修改pom.xml,仅仅引入SpringBoot依赖。当然,同时我们需要引入redis包依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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>learn</groupId> <artifactId>redis-starter</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> </dependencies> </project>
然后,根据我们之前的思路,第一步,创建属性配置定义类,定义配置属性,用于加载redis需要的配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package cn.lisl.redis; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "redis") public class RedisProperties { private String host; private int port; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } }
注解@ConfigurationProperties,用于定义application.properties中属性的前缀,即:redis.host、redis.port
第二步,创建一个配置类,这个配置类用于加载配置,并实例化Jedis客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package cn.lisl.redis; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.Jedis; @Configuration//开启配置 @ConditionalOnClass(Jedis.class) @EnableConfigurationProperties({RedisProperties.class})//开启使用映射实体对象 @ConditionalOnProperty//存在对应配置信息时初始化该配置类 ( prefix = "redis",//存在配置前缀redis value = "enabled",//开启 matchIfMissing = true//缺失检查 ) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean public Jedis jedis(RedisProperties redisProperties){ return new Jedis(redisProperties.getHost(),redisProperties.getPort()); } }
自动化配置代码中有很多我们之前没有用到的注解配置,我们从上开始讲解
@Configuration:这个配置就不用多做解释了,我们一直在使用,用于定义配置,开启配置的功能
@EnableConfigurationProperties:这是一个开启使用配置参数的注解,value值就是我们配置实体参数映射的ClassType,将配置实体作为配置来源。
SpringBoot内置条件注解 有关@ConditionalOnXxx相关的 注解这里要系统的说下,因为这个是我们配置的关键,根据名称我们可以理解为具有Xxx条件,当然它实际的意义也是如此,条件注解是一个系列,下面我们详细做出解释
@ConditionalOnBean:当SpringIoc容器内存在指定Bean的条件
@ConditionalOnClass:当SpringIoc容器内存在指定Class的条件
@ConditionalOnExpression:基于SpEL表达式作为判断条件
@ConditionalOnJava:基于JVM版本作为判断条件
@ConditionalOnMissingBean:当SpringIoc容器内不存在指定Bean的条件
@ConditionalOnMissingClass:当SpringIoc容器内不存在指定Class的条件
@ConditionalOnNotWebApplication:当前项目不是Web项目的条件
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnSingleCandidate:当指定Bean在SpringIoc容器内只有一个,或者虽然有多个但是指定首选的Bean
@ConditionalOnWebApplication:当前项目是Web项目的条件 以上注解都是元注解@Conditional演变而来的,根据不用的条件对应创建以上的具体条件注解。
上述我们已经说了思路,但思路是怎么来的,我们其实是通过在查看注解@SpringBootApplication时发现,注解@SpringBootApplication上存在一个开启自动化配置的注解@EnableAutoConfiguration来完成自动化配置,注解源码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; /** * Exclude specific auto-configuration classes such that they will never be applied. * @return the classes to exclude */ Class<?>[] exclude() default {}; /** * Exclude specific auto-configuration class names such that they will never be * applied. * @return the class names to exclude * @since 1.3.0 */ String[] excludeName() default {}; }
在@EnableAutoConfiguration注解内使用到了@import注解来完成导入配置的功能,而EnableAutoConfigurationImportSelector内部则是使用了SpringFactoriesLoader.loadFactoryNames方法进行扫描具有META-INF/spring.factories文件的jar包。源码部分如下:
1 2 3 4 5 6 7 8 9 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; }
我们可以先看下spring-boot-autoconfigure包内的spring.factories文件内容,可以看到配置的结构形式是Key=>Value形式,多个Value时使用,隔开,那我们在自定义starter内也可以使用这种形式来完成,我们的目的是为了完成自动化配置,所以我们这里Key则是需要使用org.springframework.boot.autoconfigure.EnableAutoConfiguration
所以,第三步,我需要自定义spring.factories ;
在src/main/resource目录下创建META-INF目录,并在目录内添加文件spring.factories
1 2 #配置自定义Starter的自动化配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.lisl.redis.RedisAutoConfiguration
至此,我们自定义starter已经完成。
最后,我们新建一个SpringBoot项目来测试它。
新建项目如:testRedisStarter
修改pom.xml,引入自定义的starter依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?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>lisl</groupId> <artifactId>testRedisStarter</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <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> </dependency> <!--此处引入自定义starter--> <dependency> <groupId>learn</groupId> <artifactId>redis-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
新建启动类,App.java:
1 2 3 4 5 6 7 8 9 10 11 package cn.lisl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
新建application.properties在里面配置redis连接相关信息
1 2 3 #即自定义的属性,以redis为前缀 redis.host=localhost redis.port=6379
新建单元测试类,进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package cn.lisl.test; import cn.lisl.App; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import redis.clients.jedis.Jedis; import javax.annotation.Resource; @SpringBootTest(classes = App.class) @RunWith(SpringRunner.class) public class RedisTests { @Resource private Jedis jedis; @Test public void testRedis(){ jedis.set("lisl", "99"); System.out.println(jedis.get("lisl")); } }
运行之后,我们可以看到已经可以使用redis,并进行操作了。但我们其实并没有引入其他redis相关的东西,只引入了我们自定义的starter,说明我们自定义的starter已经成功生效了。
到这一步,自定义redis-stater搞定。
SpringBoot CLI Spring Boot CLI是一个命令行工具,如果想使用Spring进行快速开发可以使用它。它允许你运行Groovy脚本,这意味着你可以使用熟悉的类Java语法,并且没有那么多的模板代码。你可以通过Spring Boot CLI启动新项目,或为它编写命令。
Groovy是个基于JVM(Java虚拟机)的敏捷开发语音,既然是基于jvm,那么在groovy里面使用任何java的组件他都是可以支持识别的.
解压安装SpringBoot CLIhttps://repo.spring.io/release/org/springframework/boot/spring-boot-cli/2.1.2.RELEASE/spring-boot-cli-2.1.2.RELEASE-bin.zip
新建一个groovy的文件 hello.groovy:
1 2 3 4 5 6 7 @RestController class WebApplication { @RequestMapping("/") String home() { "Hello World!" } }
使用命令即可以启动
.\spring.bat run .\hello.groovy
此时,它会自动下载依赖,并运行项目。
快速构建项目
刚开始学习springboot的时候经常使用http://start.spring.io/ 构建项目;
其实这个功能你完全也可以使用 SpringBoot CLI,完成构建 使用命令
1 ./spring init --build=maven --java-version=1.8 --dependencies=web --packaging=jar --boot-version=1.5.3.RELEASE --groupId=enjoy --artifactId=demo
即会在当前目录下,生成SpringBoot项目。
SpringBoot性能优化 在默认情况下,我们会使用@SpringBootApplication注解来自动获取应用的配置信息,但这样也会带来一些副作用。使用这个注解后,会触发自动配置(auto-configuration)和组件扫描(component scanning),这跟使用@Configuration、@EnableAutoConfiguration和@ComponentScan三个注解的作用是一样的。这样做给开发带来方便的同时,会有以下的一些影响:
会导致项目启动时间变长(原因:加载了我们不需要使用的组件,浪费了cpu资源和内存资源)。当启动一个大的应用程序,或将做大量的集成测试启动应用程序时,影响会特别明显。
会加载一些不需要的多余的实例(beans)
会增加CPU消耗和内存的占用。
针对以上的问题,我们要怎么解决呢?很明显,既然@SpringBootApplication加载了一些不必要的配置,那么我们想是否可以就加载我们自己指定的配置呢?
我们的思路是不使用@SpringBootApplication,并且不使用@ComponentScan注解(此注解会自动扫描我们注解了@Controller,@Service的注解的类,加载到Spring IOC容器中),然后我们使用@Configuration和@EnableAutoConfiguration进行配置启动类
新建项目,只引入web:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?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>com.lisl</groupId> <artifactId>xnspringboot</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>
新建Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.lisl.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @RequestMapping("/hello") public String sayHello(){ return "Hello World!"; } }
新建启动类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.lisl; import org.springframework.boot.SpringApplication; //import org.springframework.boot.autoconfigure.EnableAutoConfiguration; //import org.springframework.boot.autoconfigure.SpringBootApplication; //@SpringBootApplication @EnableAutoConfiguration @Configuration @ComponentScan("com.lisl.controller") public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
我们用三个注解代替:@SpringBootApplication 注解
@EnableAutoConfiguration
@Configuration
@ComponentScan(“com.lisl.controller”)
这样就不会去扫描所有目录,而只扫描我们指定的com.lisl.controller 这样,我们可以减少一点启动时间。
但前面我们知道@EnableAutoConfiguration注解会导入META-INF/spring.factories里面配置的很多Configuration,这些Configuration他都会去扫描
在启动VM参数里面加入 -Ddebug
控制台会输出一些日志信息,主要是这几大类:
Positive matches: 匹配(以及匹配的原因)
Negative matches: 忽略匹配(以及忽略的原因)
Exclusions: 排除的配置类
Unconditional classes: 没有带任何条件,肯定要扫描的类
根据上面的理论知识,我们只需要在启动的时候,显式地引入这些组件 需要的组件=Positive matches+Unconditional classes
我们可以不使用@EnableAutoConfiguration,转而显示的使用@Import来导入需要的配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.lisl; import org.springframework.boot.SpringApplication; //import org.springframework.boot.autoconfigure.EnableAutoConfiguration; //import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.*; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; //@SpringBootApplication //@EnableAutoConfiguration @Configuration @ComponentScan("com.lisl.controller") @Import({ CodecsAutoConfiguration.class, DispatcherServletAutoConfiguration.class, EmbeddedWebServerFactoryCustomizerAutoConfiguration.class, ErrorMvcAutoConfiguration.class, HttpEncodingAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, WebMvcAutoConfiguration.class, ValidationAutoConfiguration.class, MultipartAutoConfiguration.class, JmxAutoConfiguration.class, RestTemplateAutoConfiguration.class, WebSocketServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class, ConfigurationPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, ProjectInfoAutoConfiguration.class }) public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
另外也可以删除一些虽然匹配到了,但是在项目中目前并没有使用到的配置,比如 任务调度:TaskExecutionAutoConfiguration,TaskSchedulingAutoConfiguration WebSocket:WebSocketServletAutoConfiguration 附件上传:MultipartAutoConfiguration JMX:JmxAutoConfiguration 等等
JVM参数调优 启动App,使用jdk里面jvisualvm.exe 查看当前进程。
这个时候内存分配了2个G,可以根据需要,判断是否需要这么大,一般来说1个G足够,尤其是微服务
另外还发现最大值和最小值两个设置的并不一样,来看下会有什么问题
设置JVM参数
-XX:+PrintGCDetails -Xmx32M -Xms1M
这样设置后发现有大量的GC,更可怕的还有大量的FULL GC存在
频繁的GC对性能影响是很大的。
频繁调用http://localhost:8080/hello,发现垃圾回收特别频繁
设置JVM参数,把最大的内存数设置成1024
-XX:+PrintGCDetails -Xmx1024M -Xms1M
GC次数明显减少,但既然还会出现几个full GC
频繁调用http://localhost:8080/hello,发现垃圾回收依然特别频繁,这不断的申请内存,释放内存对性能是有不小的影响
置JVM参数,把最大的内存数设置成1024,最小内存也设置成1024
-XX:+PrintGCDetails -Xmx1024M -Xms1024M
这个时候再重启,发现不管是gc还是full gc 都明显的减少了次数
这个时候再来频繁调用http://localhost:8080/hello,发现内存的使用比较均匀了
Undertow容器 默认情况下,Spring Boot 使用 Tomcat 来作为内嵌的 Servlet 容器 可以将 Web 服务器切换到 Undertow 来提高应用性能。
Undertow 是一个采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。Undertow 是红帽公司的开源产品
首先使用tomcat测试,使用Jmeter,使用1个线程跑10000次,发现tomcat的吞吐量没有超过2000
然后,我们换成undertow容器,修改pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?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>com.lisl</groupId> <artifactId>xnspringboot</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> </dependencies> </project>
一样用之前的测试进行测试,发现undertow容器的吞吐量有超过2000的,可以看出性能还是提升比较明显的。
多数据源与jta+atomikos分布式事务 新建springboot项目:
第一步:创建两个数据库
1.spingboot
1 2 3 4 5 6 CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT , `passwd` varchar(255) NULL , `username` varchar(255) NULL , PRIMARY KEY (`id`) );
spring
1 2 3 4 5 6 7 CREATE TABLE `order` ( `id` int(10) NOT NULL AUTO_INCREMENT , `name` varchar(255) , `user_id` int(11) NOT NULL , `account` int(255) NULL DEFAULT NULL , PRIMARY KEY (`id`) )
如上述,新建了两个数据库spingboot和spring,spingboot有一个user表,spring数据库中有一个order表。
第二步:生产相关mapper和model等。
1.修改pom.xml,引入mybatis依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.lisl</groupId> <artifactId>atomikospringboot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>atomikospringboot</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <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> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <configuration> <overwrite>true</overwrite> </configuration> </plugin> </plugins> </build> </project>
生成model、mapper和dao。如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 com +- ccsu +- App.java | +- service | +-impl | +- OrderServiceImpl.java | +- IOrderService.java | +- dao | +-user | +- UserMapper.java | +-order | +- OrderMapper.java | +- model | +- user | +- User.java | +- order | +- Order.java ...... resouces +- mapping +- user | +- UserMapper.xml | +- Order | +- OrderMapper.xml
为了区分,分别创建user、order目录,存放相应的数据。
3.配置两个数据源,修改application.properties:
1 2 3 4 5 6 7 8 9 10 spring.datasource.spring.driverClassName=com.mysql.jdbc.Driver spring.datasource.spring.jdbcUrl=jdbc:mysql://localhost:3306/spingboot?serverTimezone=GMT%2B8 spring.datasource.spring.username=root spring.datasource.spring.password=password spring.datasource.spring2.driverClassName=com.mysql.jdbc.Driver spring.datasource.spring2.jdbcUrl=jdbc:mysql://localhost:3306/spring?serverTimezone=GMT%2B8 spring.datasource.spring2.username=root spring.datasource.spring2.password=password
4.新建IOrderService.java接口,以及实现类OrderServiceImpl.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.ccsu.service.impl; import com.ccsu.dao.order.OrderMapper; import com.ccsu.dao.user.UserMapper; import com.ccsu.model.order.Order; import com.ccsu.model.user.User; import com.ccsu.service.IOrderService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class OrderServiceImpl implements IOrderService { @Resource private OrderMapper orderMapper; @Resource private UserMapper userMapper; @Override public void addOrder(Order order, User user) { userMapper.insertSelective(user); orderMapper.insertSelective(order); } }
4.新建配置类,配置数据源:
DataSource1Config.java、DataSource2Config.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.ccsu.config; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan(basePackages = "com.ccsu.dao.user", sqlSessionFactoryRef = "test1SqlSessionFactory") public class DataSource1Config { @Bean(name = "test1DataSource") @ConfigurationProperties(prefix = "spring.datasource.spring") public DataSource testDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "test1TransactionManager") @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource")DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } @Bean(name = "test1SqlSessionFactory") @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/user/*.xml")); return bean.getObject(); } @Bean(name = "test1SqlSessionTemplate") @Primary public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.ccsu.config; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan(basePackages = "com.ccsu.dao.order", sqlSessionFactoryRef = "test2SqlSessionFactory") public class DataSource2Config { @Bean(name = "test2DataSource") @ConfigurationProperties(prefix = "spring.datasource.spring2") public DataSource testDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "test2TransactionManager") public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource")DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } @Bean(name = "test2SqlSessionFactory") public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/order/*.xml")); return bean.getObject(); } @Bean(name = "test2SqlSessionTemplate") public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
这两个配置文件是重中之重,配置了数据源,连接工厂,Mapper扫描的包, mapper xml配置的位置等
5.新建单元测试进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package atomikospringboot; import com.ccsu.AtomikospringbootApplication; import com.ccsu.model.order.Order; import com.ccsu.model.user.User; import com.ccsu.service.IOrderService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; @RunWith(SpringRunner.class) @SpringBootTest(classes = AtomikospringbootApplication.class) public class AtomikospringbootApplicationTests { @Test public void contextLoads() { } @Resource private IOrderService iOrderService; @Test public void testOrderAdd(){ User user = new User(); user.setId(1); user.setPasswd("test111"); user.setUsername("testUser"); Order order = new Order(); order.setUserId(1); order.setName("testOrder"); order.setAccount(100); iOrderService.addOrder(order, user); } }
运行可以发现,已经配置了数据源。
现在修改OrderServiceImpl.java,让其发生事务问题。
1 2 3 4 5 6 7 @Override @Transactional public void addOrder(Order order, User user) { userMapper.insertSelective(user); int i = 10/0; orderMapper.insertSelective(order); }
我们会发现,即使使用了注解@Transactional,事务问题还是发生了,即:user插入了数据,order没插入数据。我们需要的是,发生异常,应该是都不插入数据才是解决事务问题。
所以,我们需要使用jta+atomikos分布式事务:
修改pom.xml,添加依赖:
1 2 3 4 5 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> <version>2.1.2.RELEASE</version> </dependency>
新增配置类,DBConfig1.java、DBConfig2.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.ccsu.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "spring.datasource.spring") @Component public class DBConfig1 { private String driverClassName; private String jdbcUrl; private String username; private String password; public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getJdbcUrl() { return jdbcUrl; } public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.ccsu.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "spring.datasource.spring2") @Component public class DBConfig2 { private String driverClassName; private String jdbcUrl; private String username; private String password; public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getJdbcUrl() { return jdbcUrl; } public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
修改数据源配置类,使用全局数据源,XADataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package com.ccsu.config; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan(basePackages = "com.ccsu.dao.user", sqlSessionFactoryRef = "test1SqlSessionFactory") public class DataSource1Config { // @Bean(name = "test1DataSource") // @ConfigurationProperties(prefix = "spring.datasource.spring") // public DataSource testDataSource() { // return DataSourceBuilder.create().build(); // } @Bean(name = "test1DataSource") @Primary public DataSource testDataSource(DBConfig1 config1) { MysqlXADataSource mysqlXADataSource = new MysqlXADataSource(); mysqlXADataSource.setUrl(config1.getJdbcUrl()); mysqlXADataSource.setPassword(config1.getPassword()); mysqlXADataSource.setUser(config1.getUsername()); AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); atomikosDataSourceBean.setXaDataSource(mysqlXADataSource); atomikosDataSourceBean.setUniqueResourceName("test1Datasource"); return atomikosDataSourceBean; } @Bean(name = "test1TransactionManager") @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource")DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } @Bean(name = "test1SqlSessionFactory") @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/user/*.xml")); return bean.getObject(); } @Bean(name = "test1SqlSessionTemplate") @Primary public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package com.ccsu.config; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; @Configuration @MapperScan(basePackages = "com.ccsu.dao.order", sqlSessionFactoryRef = "test2SqlSessionFactory") public class DataSource2Config { // @Bean(name = "test2DataSource") // @ConfigurationProperties(prefix = "spring.datasource.spring2") // public DataSource testDataSource() { // return DataSourceBuilder.create().build(); // } @Bean(name = "test2DataSource") public DataSource testDataSource(DBConfig2 dbConfig2) { MysqlXADataSource mysqlXADataSource=new MysqlXADataSource(); mysqlXADataSource.setUrl(dbConfig2.getJdbcUrl()); mysqlXADataSource.setPassword(dbConfig2.getPassword()); mysqlXADataSource.setUser(dbConfig2.getUsername()); AtomikosDataSourceBean atomikosDataSourceBean=new AtomikosDataSourceBean(); atomikosDataSourceBean.setXaDataSource(mysqlXADataSource); atomikosDataSourceBean.setUniqueResourceName("test2Datasource"); return atomikosDataSourceBean; } @Bean(name = "test2TransactionManager") public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource")DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } @Bean(name = "test2SqlSessionFactory") public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/order/*.xml")); return bean.getObject(); } @Bean(name = "test2SqlSessionTemplate") public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
修改OrderServiceImpl.java,加入注解:@Transactional
1 2 3 4 5 6 7 @Override @Transactional public void addOrder(Order order, User user) { userMapper.insertSelective(user); int i = 10/0; orderMapper.insertSelective(order); }
再次,运行单元测试,发现事务问题已经得到解决。
SpringBoot源码分析 对于SpringBoot, 我们有意思的是他可以直接main方法启动,以前我们通常的web项目,都是需要在web容器才能启动。为什么SpingBoot可以直接启动呢?
是因为SpringBoot使用了内嵌的Tomcat,还有就是他对springmvc进行了无缝整合。
内嵌Tomcat
新建项目,修改pom.xml,引入内嵌tomcat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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>com.lisl</groupId> <artifactId>embedtomcat</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.16</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>8.5.16</version> </dependency> </dependencies> </project>
新建servlet:
IndexServlet.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.lisl.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class IndexServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().print("this is index.. tomcat"); } }
操作Tomcat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.lisl.tomcat; import com.lisl.servlet.IndexServlet; import org.apache.catalina.LifecycleException; import org.apache.catalina.core.StandardContext; import org.apache.catalina.startup.Tomcat; public class TestEmbedTomcat { private static int PORT = 8080; private static String CONTEXT_PATH="/lisl"; private static String SERVLET_NAME = "indexServlet"; public static void main(String[] args) throws LifecycleException { /*创建tomcat服务器*/ Tomcat tomcat = new Tomcat(); /*指定端口*/ tomcat.setPort(PORT); /*是否设置自动部署*/ tomcat.getHost().setAutoDeploy(false); /*创建上下文*/ StandardContext standardContext = new StandardContext(); standardContext.setPath(CONTEXT_PATH); /*监听上下文*/ standardContext.addLifecycleListener(new Tomcat.FixContextListener()); /*tomcat添加standardContext*/ tomcat.getHost().addChild(standardContext); /*创建servlet*/ tomcat.addServlet(CONTEXT_PATH, SERVLET_NAME, new IndexServlet()); /*servleturl映射*/ standardContext.addServletMappingDecoded("/index", SERVLET_NAME); /*启动tomcat*/ tomcat.start(); System.out.println("tomcat服务器已启动!"); /*异步进行接收请求*/ tomcat.getServer().await(); } }
运行main函数,在浏览器输入:
http://localhost:8080/lisl/index
可以看到结果,说明内嵌Tomcat成功。
SpringMVC整合
修改pom.xml,加入springMvc依赖:
1 2 3 4 5 6 7 8 9 10 11 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.5.RELEASE</version> </dependency>
新增service:
1 2 3 4 5 6 7 8 9 10 11 package com.lisl.service; import org.springframework.stereotype.Service; @Service public class UserService { public String index(){ return "springboot 2.0 我正在加载UserService"; } }
新建RestController
1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class IndexController { @Autowired private UserService userService; @RequestMapping(value = "/index", produces = "text/html;charset=UTF-8") public String index(){ return userService.index(); } }
常用业务类已经准备完毕,接下来就是真正的整合了。
DispatcherServlet配置类
虽然前面有了service,有了controller,但依然没有把这些组建交给spring,对于springmvc来说,有个DispatcherServlet,这是springmvc的前端控制器,以前是配置在web.xml中。只是现在用的是注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.lisl.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; @Configuration public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // 加载根配置信息 spring核心 protected Class<?>[] getRootConfigClasses() { return new Class[] { RootConfig.class }; } // springmvc 加载 配置信息 protected Class<?>[] getServletConfigClasses() { return new Class[] { WebConfig.class }; } // springmvc 拦截url映射 拦截所有请求 protected String[] getServletMappings() { return new String[] { "/" }; } }
新建RootConfig.java:
1 2 3 4 5 6 7 8 9 package com.lisl.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan("com.lisl") public class RootConfig { }
新建WebConfig.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.lisl.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; @Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.lisl.controller"}) public class WebConfig implements WebMvcConfigurer { }
集成tomcat,跟之前一样,启动tomcat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.lisl; import org.apache.catalina.LifecycleException; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.core.StandardContext; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.webresources.DirResourceSet; import org.apache.catalina.webresources.StandardRoot; import javax.servlet.ServletException; import java.io.File; public class TomcatApp { public static void main(String[] args) throws ServletException, LifecycleException { start(); } public static void start() throws ServletException, LifecycleException { /*创建tomcat*/ Tomcat tomcat = new Tomcat(); /*设置端口*/ tomcat.setPort(8090); /*读取项目路径 加载静态资源*/ StandardContext standardContext = (StandardContext) tomcat.addWebapp("/", new File("embedtomcat/src/main").getAbsolutePath()); /*禁止重新载入*/ standardContext.setReloadable(false); // class文件读取地址 File file = new File("target/classes"); // 创建WebRoot WebResourceRoot resourceRoot = new StandardRoot(standardContext); // tomcat内部读取Class执行 resourceRoot.addPreResources(new DirResourceSet(resourceRoot, "/WEB-INF/classes", file.getAbsolutePath(), "/")); tomcat.start(); // 异步等待请求执行 tomcat.getServer().await(); } }
启动,在地址栏输入:
http://localhost:8090/index ;
便可以看到结果。
对JSP支持
要支持JSP,回忆学习springmvc中的内容,需要用到一个试图解析器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.lisl.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; @Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.lisl.controller"}) public class WebConfig implements WebMvcConfigurer { // 创建SpringMVC视图解析器 public void configureViewResolvers(ViewResolverRegistry registry) { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp"); // 可以在JSP页面中通过${}访问beans viewResolver.setExposeContextBeansAsAttributes(true); registry.viewResolver(viewResolver); } }
新增contrller:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.lisl.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class UserController { @RequestMapping("/pageIndex") public String pageIndex() { return "pageIndex"; } }
增加jsp:
在resources里面新增WEB-INF\views\pageIndex.jsp
1 2 3 4 5 6 7 8 9 10 11 12 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>无敌</title> </head> <body> <h1>这是个jsp页面</h1> </body> </html>
重启tomcat ,访问http://localhost:8090/pageIndex;即可看到结果。
Tomcat加载流程
问题一:SpringBoot中tomcat在哪里加载,并启动的?
首先,spring-boot-starter中,我们可以找到spring.factories
其中加载了这样一个类:
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
点进去,可以看到,
ServletWebServerFactoryAutoConfiguration 这类里面有个TomcatServletWebServerFactoryCustomizer 这个类实现了WebServerFactoryCustomizer
这个里面引入了EmbeddedTomcat,然后一直点进去,发现是这样一个过程,最终在getWebServer()方法里,new了一个tomcat,并启动了tomcat。
EmbeddedTomcat ->
TomcatServletWebServerFactory ->
TomcatServletWebServerFactory.getWebServer() ->
getTomcatWebServer ->
TomcatWebServer ->启动tomcat
问题二:getWebServer()谁调用的?
那么,我们其实是可以猜测,肯定是SpringBoot中main方法进行启动的。
SpringBoot启动流程分析
ApplicationContextInitializer Context初始化后调用的类
SpringApplicationRunListener SpringBoot运行监听的类
ApplicationRunner
CommandLineRunner
上面着这个几乎可以等价,用于启动后做客户自定义的操作
新建TestApplicationContextInitializer.java:
1 2 3 4 5 6 7 8 9 10 11 package com.ccsu.listener; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; public class TestApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println("TestApplicationContextInitializer.initialize()执行了" + applicationContext); } }
新建TestSpringApplicationRunListener.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.ccsu.listener; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; public class TestSpringApplicationRunListener implements SpringApplicationRunListener { /*必须有的构造器*/ public TestSpringApplicationRunListener(SpringApplication application, String[] args) { } @Override public void starting() { System.out.println("TestSpringApplicationRunListener.starting()执行了!"); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { System.out.println("TestSpringApplicationRunListener.environmentPrepared()执行了!"); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("TestSpringApplicationRunListener.contextPrepared()执行了!"); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("TestSpringApplicationRunListener.contextLoaded()执行了!"); } @Override public void started(ConfigurableApplicationContext context) { System.out.println("TestSpringApplicationRunListener.started()执行了!"); } @Override public void running(ConfigurableApplicationContext context) { System.out.println("TestSpringApplicationRunListener.running()执行了!"); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { System.out.println("TestSpringApplicationRunListener.failed()执行了!"); } }
新建TestApplicationRunner.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.ccsu.listener; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class TestApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("TestApplicationRunner.run()执行了"); } }
新建TestCommandLineRunner.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.ccsu.listener; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.util.Arrays; @Component public class TestCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("TestCommandLineRunner.run()执行了"+ Arrays.asList(args)); } }
resources/META-INF/spring.factories增加:
1 2 3 4 5 org.springframework.context.ApplicationContextInitializer=\ com.ccsu.listener.TestApplicationContextInitializer org.springframework.boot.SpringApplicationRunListener=\ com.ccsu.listener.TestSpringApplicationRunListener
让SpringBoot能识别并加载。
运行SpringBoot项目,会发现在启动过程中,会打印我们新建的几个测试类的输出信息,说明在启动过程中加载以上的类并执行了方法。
接下来,跟着SpringBoot的启动类中的run方法,进去看看源码:
里面第一步新建了SpringApplication,源码如下:
1 2 3 4 public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); }
其实SpringBoot启动就这两个步骤,先创建ConfigurableApplicationContext ,然后再调用Run方法。
在这个SpringApplication里,会看到加载了ApplicationContextInitializer类,这是在初始化上下文。源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); //保存主类 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //判断当前是什么类型项目 this.webApplicationType = WebApplicationType.deduceFromClasspath(); //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); //从类路径下找到META-INF/spring.factories配置的所有ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }
可见这一步非常简单,只是把一些相关的类都加载了而已,并没执行。
然后我们再回到之前的,new SpringApplication(primarySources).run(args) 重要的是这个run方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); //从类路径下META‐INF/spring.factories,取得SpringApplicationRunListeners; SpringApplicationRunListeners listeners = getRunListeners(args); //回调所有的获取SpringApplicationRunListener.starting()方法 listeners.starting(); try { //封装命令行参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //准备环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); //创回调SpringApplicationRunListener.environmentPrepared(); //表示环境准备完成 //打印Banner Banner printedBanner = printBanner(environment); //根据环境创建context context = createApplicationContext(); //错误的异常报表 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //准备上下文环境; //将environment保存到ioc中; //applyInitializers()调用所有的ApplicationContextInitializer的initialize方法 //调用所有的SpringApplicationRunListener的contextPrepared(); prepareContext(context, environment, listeners, applicationArguments, printedBanner); //SpringApplicationRunListener的contextLoaded //刷新容器 //扫描,创建,加载所有组件; refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //所有的SpringApplicationRunListener回调started方法 listeners.started(context); //获取所有的ApplicationRunner和CommandLineRunner进行调用 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { //所有的SpringApplicationRunListener的running(); listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
这个run方法就可以看到,我们之前写的几个测试类,里面的方法都在这里调用了,这就是整个启动过程。
然后我们看看这里面的refreshContext()方法,点进去调用了refresh()
这个方法里源码如下:
1 2 3 4 protected void refresh(ApplicationContext applicationContext) { Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext); ((AbstractApplicationContext) applicationContext).refresh(); }
里面还调用了refresh(),在点进去,我们很熟悉的发现这是Spring的东西。
然后,我们看看里面的onRefresh();里面是空的,然后我们发现有很多子类实现了该方法,然后我们选择我们SpringBoot相关的子类实现的该方法(ps:我的版本是2.1.3,其他版本可能不同,只要看SpringBoot相关的子类实现即可)。
所以我们找到:
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
这个类下的实现;
里面有一个调用了createWebServer();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); }
我们终于发现,这里调用了getWebServer()
即:this.webServer = factory.getWebServer(getSelfInitializer());这段代码。
到此,我们就可以理顺整个SpringBoot的整个启动流程。