目录
  1. 1. 解决传统SSM的问题
  2. 2. SpringBoot特点
  3. 3. SpringBoot的功能
  4. 4. SpringBoot项目搭建
  5. 5. 编译打包
  6. 6. 集成redis
  7. 7. 集成RabbitMQ
  8. 8. Actuator监控管理
  9. 9. 自定义starter
  10. 10. SpringBoot CLI
  11. 11. SpringBoot性能优化
  12. 12. JVM参数调优
  13. 13. Undertow容器
  14. 14. 多数据源与jta+atomikos分布式事务
  15. 15. SpringBoot源码分析
SpringBoot框架

解决传统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

实现简单注册登录功能

  1. 使用数据库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:

  1. 首先,需要一个属性配置类;即:xxxxProperties.java

该配置类即定义application.properties需要的属性

  1. 其次,我们需要一个自动配置类,让前面定义的属性能够生效,即xxxxAutoConfiguration.java

  2. 最后,我们需要在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 CLI
https://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足够,尤其是微服务

另外还发现最大值和最小值两个设置的并不一样,来看下会有什么问题

  1. 设置JVM参数

-XX:+PrintGCDetails -Xmx32M -Xms1M

这样设置后发现有大量的GC,更可怕的还有大量的FULL GC存在

频繁的GC对性能影响是很大的。

频繁调用http://localhost:8080/hello,发现垃圾回收特别频繁

  1. 设置JVM参数,把最大的内存数设置成1024

-XX:+PrintGCDetails -Xmx1024M -Xms1M

GC次数明显减少,但既然还会出现几个full GC

频繁调用http://localhost:8080/hello,发现垃圾回收依然特别频繁,这不断的申请内存,释放内存对性能是有不小的影响

  1. 置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`)
);
  1. 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>
  1. 生成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分布式事务:

  1. 修改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>
  1. 新增配置类,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;
}
}
  1. 修改数据源配置类,使用全局数据源,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);
}
}
  1. 修改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的整个启动流程。

文章作者: Shawn Lee
文章链接: https://longshilee.github.io/2019/10/18/SpringBoot%E6%A1%86%E6%9E%B6/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Shawn·Lee
打赏
  • 微信
  • 支付宝

评论