0%

SpringBoot

本部分涉及SpringBoot的核心部分或者说基础部分.

Spring Boot入门

1.SpringBoot简介

简化Spring应用开发的一个框架;

整个Spring技术栈的一个大整合;

J2EE开发的一站式解决方案;

2.微服务

2014,martin fowler

微服务:架构风格(服务微化)

一个应用应该是一组小型服务;可以通过HTTP的方式进行互通;

单体应用:ALL IN ONE

微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元;

详细参照微服务文档

3.Spring Boot HelloWorld

一个功能:

浏览器发送hello请求,服务器接受请求并处理,响应Hello World字符串;

1.创建一个maven工程;(jar)

2.导入spring boot相关的依赖

1
2
3
4
5
6
7
8
9
10
11
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

3.编写一个主程序启动Spring Boot应用

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用
*/
@SpringBootApplication
public class HelloWorldMainApplication {

public static void main(String[] args) {

// Spring应用启动起来
SpringApplication.run(HelloWorldMainApplication.class,args);
}
}

4.编写相关的Controller,Service

1
2
3
4
5
6
7
8
9
@Controller
public class HelloController {

@ResponseBody
@RequestMapping("/hello")
public String hello(){
return "Hello World!";
}
}

5.运行主程序测试

6.简化部署

1
2
3
4
5
6
7
8
9
<!-- 这个插件,可以将应用打包成一个可执行的jar包;-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

将这个应用打成jar包,直接使用java -jar的命令进行执行.

4.Hello World探究

1.POM文件

1.父项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>

他的父项目是
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
他来真正管理Spring Boot应用里面的所有依赖版本;

spring-boot-dependencies中列出来非常多的开发要用的jar包版本.因此在自己的pom文件中不需要写版本,除了里面没有列出来的.

2.启动器

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-==web==:

Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器.现在是写Web项目,自然用spring-boot-starter-web这个启动器.看一下里面的依赖:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>

可以看到里面导入了webmvc,tomcat,json等必要的包或启动器.此外还包含了spring-boot-starter这个基本的启动器.

2.主程序类,主入口类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用
*/
@SpringBootApplication
public class HelloWorldMainApplication {

public static void main(String[] args) {

// Spring应用启动起来
SpringApplication.run(HelloWorldMainApplication.class,args);
}
}

@SpringBootApplication: Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用.查看这个注解的代码:

image-20200720212401438

可以看到这是一个复合注解.

@SpringBootConfiguration:Spring Boot的配置类.在查看这个注解的代码:

image-20200720212549125

看到了熟悉的Configuration注解,这是Spring提供的.Configuration注解也是一个被Component注解的,因此最终SpringBootApplication也是一个组件.

EnableAutoConfiguration:开启自动配置功能,部分代码:

image-20200720213216735

该注解又被AutoConfigurationPackage注解.这个注解导入了一个组件:@Import(AutoConfigurationPackages.Registrar.class).Debug一下,发现进入了里面的Register类的方法中:

image-20200720214302494

里面有个new PackageImports.看名字知道是导包的意思.查看getPackageNames值:

image-20200720214432504

即==将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器==

如何知道包名?通过加在主配置类上的@SpringBootApplication的metadata获得:

image-20200720214814367

回到EnableAutoConfiguration注解,它也导入了一个组件:@Import(AutoConfigurationImportSelector.class),查看代码:

image-20200720230445962

这是其中一部分.其中的configurations是导入的配置类.将所有需要导入的组件以全类名的方式返回.这些组件就会被添加到容器中.会给容器中导入非常多的自动配置类(xxxAutoConfiguration).就是给容器中导入这个场景需要的所有组件,并配置好这些组件.如图所示,里面全都是配置类.

image-20200720230756029

问题是这些配置哪来的?其实在上面图里这个方法:

image-20200720231355716

点进去看看:,里面用到了一个方法loadFactoryNames,查看这个方法:

image-20200720231646355

来到了一个getResources方法,里面有个常量image-20200720231736683

嗨呀,这下明白了,去文件夹里找找:

image-20200720232014909

可以看到SpringBoot相关的包里有这个文件,里面有非常多的配置.

找到autoconfig那个包,看里面的配置文件:

image-20200720232217177

这个配置文件就和那个长度为124的配置列表对上了.

==Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作.==

所以,一开始的spring-boot-dependencies这个pom文件就落实到org.springframework.boot.autoconfigure这个包里.

5.使用Spring Initializer快速创建Spring Boot项目

具体创建很简单,不演示.

  • resources文件夹中目录结构
    • static:保存所有的静态资源, js css images,
    • templates:保存所有的模板页面,(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面),可以使用模板引擎(freemarker.thymeleaf),
    • application.properties:Spring Boot应用的配置文件,可以修改一些默认设置.

配置文件

1.配置文件

SpringBoot使用一个全局的配置文件,配置文件名是固定的:

•application.properties

•application.yml

该配置文件用来修改SpringBoot的默认值.

2.YAML语法:

1.基本语法

k:(空格)v:表示一对键值对(空格必须有).

空格的缩进来控制层级关系,只要是左对齐的一列数据,都是同一个层级的

1
2
3
server:
port: 8081
path: /hello

属性和值也是大小写敏感.

  • 对于字面量(数字,字符串,布尔):k: v.其中字符串默认不用加单引号,或双引号.如果加上了单引号,字符串里的特殊字符会转义,而双引号不会.

  • 对于对象,Map:可以看作属性和值的键值对

1
2
3
person:
name: 张三
age: 20

还可以用大括号变成一行内:

1
person: {name: 张三,age: 20}
  • 对于数组(List,Set)

-值表示数组中的一个元素

1
2
3
4
pets:
- cat
- dog
- pig

行内写法

1
pets: [cat,dog,pig]

3.配置文件值注入

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
person:
lastName: hello
age: 18
boss: false
birth: 2017/12/12
maps: {k1: v1,k2: 12}
lists:
- lisi
- zhaoliu
dog:
name: 小狗
age: 12

javaBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 将配置文件中配置的每一个属性的值,映射到这个组件中
* @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
* prefix = "person":配置文件中哪个下面的所有属性进行一一映射
*
* 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能;
*
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {

private String lastName;
private Integer age;
private Boolean boss;
private Date birth;

private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;

另外还需导入一个依赖:

1
2
3
4
5
6
<!--导入配置文件处理器,配置文件进行绑定就会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

除了在yml中写,在properties中写也是可以的.这里不演示.

ConfigurationProperties这个注解可以一次性给属性从配置文件中赋值.还有另一个注解,Value,加在属性上方,单个赋值.二者区别:

@ConfigurationProperties @Value
功能 批量注入配置文件中的属性 一个个指定
松散绑定(松散语法) 支持 不支持
SpEL 不支持 支持
JSR303数据校验 支持 不支持
复杂类型封装 支持 不支持

如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value;

如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties.

@PropertySource&@ImportResource&@Bean

对于Person这样的配置文件写在application.xml这个全局配置文件中并不好.因为如果其它Bean也加进来,会使配置文件越来越长.

所以可以把Person的配置单独写一个配置文件.然后在Person类上使用PropertySource这个注解,指定配置文件位置.

上面的一些操作是给Spring中的Bean组件用配置文件赋值,也就是说Bean已经在IOC容器中了.那么让Bean进入IOC容器的方式是?(上面Person演示了一种,用Conponent注解)

方式一:在Spring中添加组件可以使用xml文件,里面可以包扫描,写bean标签等.Spring Boot也可以通过它来添加组件

@ImportResource:导入Spring的配置文件,让配置文件里面的内容生效.Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别,想让Spring的配置文件生效,加载进来,则用@ImportResource标注在一个配置类上.

这种方式并不推荐.Spring Boot推荐使用注解方式.就像原来在Spring中那样,完全使用注解即可.

现在需要一个配置类,这个配置类是Spring中的配置类,和Spring Boot中加了@SpringBootApplication注解的配置类没有关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件
*
* 在配置文件中用<bean><bean/>标签添加组件
*
*/
@Configuration
public class MyAppConfig {

//将方法的返回值添加到容器中;容器中这个组件默认的id就是方法名
@Bean
public HelloService helloService02(){
System.out.println("配置类@Bean给容器中添加组件了...");
return new HelloService();
}
}

4.配置文件占位符

1.随机数

1
2
${random.value}.${random.int}.${random.long}
${random.int(10)}.${random.int[1024,65536]}

2.占位符获取之前配置的值,如果没有可以是用:指定默认值

1
2
3
4
5
6
7
8
9
person.last-name=张三${random.uuid}
person.age=${random.int}
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=14
person.lists=a,b,c
person.dog.name=${person.hello:hello}_dog
person.dog.age=15

第八行想获取前面配置过的值,但实际没配置,如果没有冒号后的默认值就会把person.hello原样显示.

5.Profile

用来做多环境切换.

1.多Profile文件

我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml

默认情况下使用的是application.properties这个配置文件.如果需要切换则有几种方式:

​ 1.在配置文件中指定 spring.profiles.active=dev(yml文件中要转换写法)

​ 2.命令行:

​ java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar –spring.profiles.active=dev;

​ 可以直接在测试的时候,配置传入命令行参数

​ 3.虚拟机参数;

​ -Dspring.profiles.active=dev

除了写几个配置文件的方式,还可以用yml文件把多个Profile写在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8081
spring:
profiles:
active: prod

---
server:
port: 8083
spring:
profiles: dev


---

server:
port: 8084
spring:
profiles: prod #指定属于哪个环境

所以配置文件有多种多样,启动配置文件的方式也不一样,那么优先级是什么?

2.配置加载

先看看配置文件加载顺序:

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件

  • –file:./config/

  • –file:./

  • –classpath:/config/

  • –classpath:/

优先级由高到底,高优先级的配置会覆盖低优先级的配置.SpringBoot会加载这四个位置的配置文件,互补配置.

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;

java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar –spring.config.location=G:/application.properties

除了这几个地方加载配置文件,还有其他许多地方可以影响配置.这里简要列出:

  1. 命令行参数

所有的配置都可以在命令行上进行指定

java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar –server.port=8087 –server.context-path=/abc

多个配置用空格分开: –配置项=值

  1. 来自java:comp/env的JNDI属性

  2. Java系统属性(System.getProperties())

  3. 操作系统环境变量

  4. RandomValuePropertySource配置的random.*属性值

==由jar包外向jar包内进行寻找==

==优先加载带profile==

  1. jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件

  2. jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件

==再来加载不带profile==

  1. jar包外部的application.properties或application.yml(不带spring.profile)配置文件

  2. jar包内部的application.properties或application.yml(不带spring.profile)配置文件

  3. @Configuration注解类上的@PropertySource

  4. 通过SpringApplication.setDefaultProperties指定的默认属性

以上优先级也是从高到低,可以配置互补.

需要注意的是打包时默认是只将类路径下的文件打包,因此在写在工程目录下的配置文件是不包含在内的.

所有配置来源:

参考官方文档

6.自动配置原理

1.自动配置原理

第一节已经讲过,这里再复述一下:

1)、SpringBoot启动的时候加载主配置类,开启了自动配置功能 ==@EnableAutoConfiguration==

2)、@EnableAutoConfiguration 作用:

  • 利用AutoConfigurationImportSelector给容器中导入一些组件.
    • 可以查看selectImports()方法的内容.(Spring Boot 1.x中),现在是直接到getCandidateConfigurations方法
    • List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置image-20200721173941169

前面说过,读取到的”META-INF/spring.factories”里面每一个都是xxxAutoConfiguration类.它们被加入容器用来做自动配置.以HttpEncodingAutoConfiguration为例解释自动配置原理:

下面是该类的注解:

1
2
3
4
5
@Configuration(proxyBeanMethods = false)//表示这是一个配置类
@EnableConfigurationProperties(ServerProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
  • @EnableConfigurationProperties注解的作用是:使使用 @ConfigurationProperties 注解的类生效

具体参见连接

要启用的配置类的注解如下:

image-20200721195733843

所有在配置文件中能配置的属性都是在xxxxProperties类中封装着.

  • @ConditionalOnWebApplication注解是依赖的Spring的Conditional注解.意为满足条件时加载Bean.这里的条件是

判断当前应用是web应用,如果是,配置类生效.

  • @ConditionalOnClass(CharacterEncodingFilter.class),判断当前项目有没有这个类,这个类是SpringMVC中进行乱码解决的过滤器

  • @ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true) ,判断配置文件中是否存在某个配置 spring.http.encoding.enabled,如果不存在,判断也是成立的.

如果满足上述条件,则HttpEncodingAutoConfiguration配置类生效.

该配置类中使用了Bean注解,添加了两个Bean组件:

image-20200721202003267

第一个是字符过滤器,其代码是:

image-20200721204407802

可以看到它从HttpEncodingAutoConfiguration的属性properties中取得了字符集名字.具体如何取得看下图;

image-20200721204248240

精髓:

1)、SpringBoot启动会加载大量的自动配置类

2)、我们看我们需要的功能有没有SpringBoot默认写好的自动配置类

3)、我们再来看这个自动配置类中到底配置了哪些组件(只要我们要用的组件有,我们就不需要再来配置了,例如上面的HttpEncodingAutoConfiguration就加入了两个组件)

4)、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值

xxxxAutoConfigurartion:自动配置类,给容器中添加组件.

xxxxProperties:封装配置文件中相关属性.

在上面的代码展示中,常常出现了@Conditionalxxx这类注解,这里列举一些:

@Conditional扩展注解 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean;
@ConditionalOnMissingBean 容器中不存在指定Bean;
@ConditionalOnExpression 满足SpEL表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

日志

1、日志框架

市面上的日志框架;

JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j….

日志门面 (日志的抽象层) 日志实现
JCL(Jakarta Commons Logging) SLF4j(Simple Logging Facade for Java) jboss-logging Log4j JUL(java.util.logging) Log4j2 Logback

左边选一个门面(抽象层)、右边来选一个实现:

日志门面: SLF4J,

日志实现:Logback,

SpringBoot:底层是Spring框架,Spring框架默认是用JCL,

==SpringBoot选用 SLF4j和logback;==

2、SLF4j使用

1、如何在系统中使用SLF4j https://www.slf4j.org

以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法.

给系统里面导入slf4j的jar和 logback的实现jar

1
2
3
4
5
6
7
8
9
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}

图示:

concrete-bindings

每一个日志的实现框架都有自己的配置文件.使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件.

2.遗留问题

对于不同的框架组件,用的日志实现不统一.现在可以用SLF4J提供的包进行替换:

图中的深灰色xxx-over-slf4j.jar就是中间包,用来替换原有的日志框架.

image-20200722102024845

图里并没有看到xxx-over-slf4j这类包.这是因为Spring Boot2有了一些改动,那两个xxx-to-slf4j就是转换包.另外,Spring5原来用的jcl被改写为slf4j(内部实际还是jcl).因此pom文件中也没有排除spring日志框架这一部分.

3.日志使用

1.默认配置

SpringBoot默认已经配置类日志,直接使用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//记录器
Logger logger = LoggerFactory.getLogger(getClass());
@Test
public void contextLoads() {
//System.out.println();

//日志的级别;
//由低到高 trace<debug<info<warn<error
//可以调整输出的日志级别;日志就只会在这个级别以以后的高级别生效
logger.trace("这是trace日志...");
logger.debug("这是debug日志...");
//SpringBoot默认给我们使用的是info级别的,没有指定级别的就用SpringBoot默认规定的级别;root级别
logger.info("这是info日志...");
logger.warn("这是warn日志...");
logger.error("这是error日志...");


}
1
2
3
4
5
6
7
8
9
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
1
2
3
4
5
6
7
8
logging:
level:
com.company: trace
file:
name: 'G:\log\sb.log'
pattern:
console: "%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n"

注意单引号和双引号的使用,特别是日志格式那里,一旦出错,程序无法运行.

2.指定配置

给类路径下放上每个日志框架自己的配置文件即可

Logging System Customization
Logback logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

logback.xml:这类文件名直接就被日志框架识别了,而如果使用logback-spring.xml这种文件名,日志框架是不识别的,但是SpringBoot可以识别.识别后由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能.

1
2
3
4
<springProfile name="staging">
<!-- configuration to be enabled when the "staging" profile is active -->
可以指定某段配置只在某个环境下生效
</springProfile>

例如指定日志配置文件在开发环境中生效.

如果说非要切换回log4j这样的框架,则需要先把替换包给排除,然后引入log4j的依赖.但是不推荐这样做.

slf4j+log4j的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

切换为log4j2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

Web开发

1.SpringBoot对静态资源的映射规则

以前的Web项目会有一个webapp目录,其中可以放静态资源等文件.而在Spring Boot中没有这个目录.所以需要使用资源映射的方式.具体看图:

image-20200722142918697

在WebMvcAutoConfiguration类中有一个静态类,该静态类有个方法addResourceHandlers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}

所有 /webjars/** (这里是指访问请求),都去 classpath:/META-INF/resources/webjars/ 找资源

webjars:以jar包的方式引入静态资源.以jQuery为例:

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

请求路径:http://localhost:8080/webjars/jquery/3.5.1/jquery.js

image-20200722144602179

对于静态资源,也存在一个xxxProperties作为配置类,这里就是ResourceProperties这个类:

image-20200722145217620

除了webjars可以获取到jar包里的资源,但是对于其它资源就不一样了.addResourceHandlers这个方法里的第二部分就是另一种方式:

image-20200722145648961

这个getStaticPathPattern方法最终得到private String staticPathPattern = "/**";

这个路径表示访问当前项目的任何路径如果没有处理,默认都去(静态资源的文件夹)找映射,具体的文件夹如下:

1
2
3
4
5
"classpath:/META-INF/resources/", 
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径

(这里的类路径是指resources文件夹)

如果找static文件夹里的asserts文件夹里的js文件夹下的jquery.js,则是这样写:localhost:8080/asserts/js/jquery.js

特比的对于index页面,则被/**映射,直接写:localhost:8080/ .index页面放在上面几个位置都可以.

另外还有一个图标的访问: **/favicon.ico这样的访问路径也是在上面几个文件夹找资源.

2.模板引擎

1.引入thymeleaf

首先引入这个starter:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

然后修改一下版本:

1
2
3
4
5
<properties>
<java.version>14</java.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.4.1</thymeleaf-layout-dialect.version>
</properties>

2.使用thymeleaf

image-20200722161949638

根据其属性类,只要我们把HTML页面放在classpath:/templates/,thymeleaf就能自动渲染.

1
2
3
4
5
@RequestMapping("/success")
public String success(Map<String, String> map) {
map.put("hello", "你好");
return "success";
}

在jsp中,map被放入请求域中,页面上直接使用$便可以取值.而使用thymeleaf后要这样写:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>success</title>
</head>
<body>
<h1>成功</h1>
<div th:text="${hello}"></div>
</body>
</html>

3.语法规则

th:表示任意html属性.它用来替换原生属性的值.例如上面的th:text就是替换div的text属性.

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
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL,
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.

${session.foo}
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样
补充:配合 th:object="${session.user},
#<div th:object="${session.user}">
#<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
#<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
#<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
#</div>
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL
@{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}:片段引用表达式
<div th:insert="~{commons :: main}">...</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _

简单使用下:

1
2
3
4
5
6
7
8
9
10
<body>
<h1>成功</h1>
<div th:text="${hello}"></div>
<hr/>
<h4 th:text="${user}" th:each="user:${users}"></h4>
<hr/>
<h4>
<span th:each="user:${users}">[[${user}]]</span>
</h4>
</body>

3.SpringMVC配置

1.自动配置举例

同样的SpringBoot也为SpringMVC做了自动配置.

以下是WebMvcAutoConfiguration的一些配置:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

    • 自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染
    • ContentNegotiatingViewResolver:组合所有的视图解析器
    • ==如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来==,自定义的解析器也被组合到ContentNegotiatingViewResolver中
  • Support for serving static resources, including support for WebJars,静态资源文件夹路径,webjars

  • Static index.html support. 静态首页访问

  • Custom Favicon support (see below). favicon.ico

    这些上面都提过

  • 自动注册了 Converter, GenericConverter, Formatter beans.

    • Converter:转换器 ,public String hello(User user):类型转换使用Converter
    • Formatter 格式化器,2017.12.17==>Date
1
2
3
4
5
@Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")//在文件中配置日期格式化的规则
public Formatter<Date> dateFormatter() {
return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件
}

​ ==自己添加的格式化器转换器,我们只需要放在容器中即可==

  • Support for HttpMessageConverters

    • HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json.

    • HttpMessageConverters 是从容器中确定,获取所有的HttpMessageConverter.

      ==自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Component)==

  • Automatic registration of MessageCodesResolver定义错误代码生成规则

  • Automatic use of a ConfigurableWebBindingInitializer bean 用来初始化WebDataBinder,例如请求数据绑定到JavaBean上,涉及到数据转换就要调用前面的转换器

    ==也可以配置一个ConfigurableWebBindingInitializer来替换默认的(添加到容器)==

2.扩展SpringMVC

上述一些配置是远远不够的.在原来的SpringMVC中,可以使用xml文件进行配置:

1
2
3
4
5
6
7
<mvc:view-controller path="/hello" view-name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>

用配置方法来写:编写一个配置类,并且继承WebMvcConfigurerAdapter类型(Spring5之前的方法),不能标注@EnableWebMvc.在Spring5,SpringBoot2中,这种方法过时.两种新方法:1.直接实现WebMvcConfigurerAdapter的父接口WebMvcConfigurer,该接口内的所有方法都是默认方法,需要配置什么就重写什么.

image-20200722221948117

重写其中一个方法:

1
2
3
4
5
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//返送/company请求也到success页面
registry.addViewController("/company").setViewName("success");
}

事实上上面的自动配置类WebMvcAutoConfiguration中有一些方法也是通过这个接口实现的:

image-20200722223316785

2.另一种方式是继承WebMvcConfigurationSupport这个类.仍然是重写其中方法即可.关于这两种方式的详细信息,参见

参考链接

3.全面接管SpringMVC

如果不需要自动配置,只需要添加@EnableWebMvc.为什么加了这个自动配置就失效,看图:

image-20200722231002039

而@EnableWebMvc中有@Import(DelegatingWebMvcConfiguration.class),而这个DelegatingWebMvcConfiguration的父类正是WebMvcConfigurationSupport.

4.如何修改SpringBoot的默认配置

模式:

1. SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来.

2. 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置

3. 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置

5.RestfulCRUD

1.访问默认首页

除了使用下面这种方式:

1
2
3
4
@RequestMapping({"/","/login"})
public String index(){
return "login";
}

还可以使用这种方式:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
}
}

此外,对于静态资源的访问也要做出处理:

1
2
3
4
5
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/webjars/**").addResourceLocations("/webjars/");
}

2.国际化

  1. 编写国际化配置文件

image-20200723142610188

  1. Spring Boot自动配置了管理国际化资源文件的组件

image-20200723143052726

  1. 配置应用的basename:spring.messages.basename=i18n.login

  2. 页面获取国际化的值,login就是basename

image-20200723150150403

原理:在SpringMVC中国际化与区域信息对象Locale相关,要获得区域信息对象则通过LocaleResolver.在Spring Boot中,WebMvcAutoConfiguration中配置了这个对象,将其作为Bean加入了容器.

image-20200723150613065

可以看到先试着从配置文件中获取,接着是从请求头中获取.

另外要实现连接按钮更改语言,则在这个按钮上加上不同的语言参数:

image-20200723155630298

然后需要自定义LocaleResolver,因为上面默认的LocaleResolver处理的是请求头的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyLocalResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("l");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(l)) {
String[] split = l.split("_");
locale = new Locale(split[0], split[1]);
}
return locale;
}

@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

}
}

写好自定义的LocaleResolver要将它加入容器,替换掉原来的.在自己的WebMvcConfiger类(MyMvcConfig implements WebMvcConfigurer)中中添加这个Bean:

1
2
3
4
@Bean
public LocaleResolver localeResolver() {
return new MyLocalResolver();
}

3.登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class LoginController {
@PostMapping("/user/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password,
Map<String, Object> map, HttpSession session) {
if (!StringUtils.isEmpty(username)&&"123456".equals(password)) {
session.setAttribute("loginUser", username);
//登录成功后,防止表单重复提交,要做重定向
return "redirect:/dashboard.html";
}else {
map.put("msg", "用户名密码错误");
return "login";
}
}
}

判断用户名不为空且密码为123456后重定向到dashboard页面.

为了防止不登录直接进入dashboard页面,要配置一个拦截器.具体做法是实现HandlerInterceptor接口并实现prehandle方法.因为是要在处理请求前拦截的.

1
2
3
4
5
6
7
8
9
10
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) {
request.setAttribute("msg","没有权限");
request.getRequestDispatcher("/login.html").forward(request, response);
return false;
}
return true;
}

然后还要在MyMvcConfig中添加这个Bean.

1
2
3
4
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/dashboard.html");
}

4.CRUD员工列表

实验功能 请求URI 请求方式
查询所有员工 emps GET
查询某个员工(来到修改页面) emp/1 GET
来到添加页面 emp GET
添加员工 emp POST
来到修改页面(查出员工进行信息回显) emp/1 GET
修改员工 emp PUT
删除员工 emp/1 DELETE

先写查询所有员工,这个最简单:

1
2
3
4
5
6
@GetMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps", employees);
return "emp/list";
}

thymeleaf公共元素抽取

dashboard页面和list页面中有许多页面元素是相同的,如顶部条,侧栏.考虑代码复用要把公共的部分抽取出来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、抽取公共片段
<div th:fragment="copy">
2011 The Good Thymes Virtual Grocery
</div>

2、引入公共片段
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector}:模板名::选择器
~{templatename::fragmentname}:模板名::片段名

3、默认效果:
insert的公共片段在div标签中
如果使用th:insert等属性进行引入,可以不用写~{}:
行内写法可以加上:[[~{}]];[(~{})];

三种引入公共片段的th属性:

th:insert:将公共片段整个插入到声明引入的元素中

th:replace:将声明引入的元素替换为公共片段

th:include:将被引入的片段的内容包含进这个标签中(外层标签不要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

引入方式
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>

效果
<div>
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
</div>

<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div>

5.员工添加

在员工列表页面加一个添加按钮,该按钮添加一个跳转到添加页面的超链接.

艹艹艹艹!这部分太零碎了,不写了,直接跳到下个部分.

6.错误处理机制

Spring Boot默认的错误页面已经见过了好多次了.浏览器返回一个Whitelabel Error Page页面,客户端返回一个json.

默认配置是在ErrorMvcAutoConfiguration这个类中.

先看这个类装配了哪些Bean:

image-20200725111458563

  • DefaultErrorAttributes:在页面共享信息

  • BasicErrorController:处理默认/error请求

  • ErrorPageCustomizer,是一个静态内部类,有一个方法:

    image-20200725132328064

    里面的getPath方法就是获得出错后去的路径:

1
2
@Value("${error.path:/error}")
private String path = "/error";
  • DefaultErrorViewResolver

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
    Map<String, Object> model) {
    ModelAndView modelAndView = resolve(String.valueOf(status), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
    //默认SpringBoot可以去找到一个页面? error/404
    String errorViewName = "error/" + viewName;

    //模板引擎可以解析这个页面地址就用模板引擎解析
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
    .getProvider(errorViewName, this.applicationContext);
    if (provider != null) {
    //模板引擎可用的情况下返回到errorViewName指定的视图地址
    return new ModelAndView(errorViewName, model);
    }
    //模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
    return resolveResource(errorViewName, model);
    }

一旦系统出现4xx或5xx错误,ErrorPageCustomizer就会生效(定制错误的响应规则),来到/error请求.接着由BasicErrorController这个Controller来处理/error请求:

image-20200725132832861

这个Controller处理的请求路径一样,但是产生不同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面做错误页面,包括页面地址和内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}

一个是HTML页面,另一个是json数据.

resolveErrorView方法返回了modelAndView,决定了错误页面的数据和视图.model来自于DefaultErrorAttributes.

1.定制错误响应

①定制错误页面

在有模板引擎的情况下,将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下,发生此状态码的错误就会来到 对应的页面.

可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html)

在错误页面能获取的信息:

​ timestamp:时间戳

​ status:状态码

​ error:错误提示

​ exception:异常对象

​ message:异常消息

​ errors:JSR303数据校验的错误都在这里

如果没有模板引擎,就在静态资源文件夹下找.

如果都没有,默认来到SpringBoot默认的错误提示页面.

②定制错误数据

定制json数据内容.

需要一个自定义的异常处理器:

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class MyExceptionHandler {

@ResponseBody
@ExceptionHandler(UserNotExistException.class)//处理哪种异常
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}

但是这种写法让前面的自动区分浏览器和客户端的效果没了.因为不是/error请求了.所以可以转发到这个请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 @ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
/**
* Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
*/
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message",e.getMessage());
//转发到/error
return "forward:/error";
}

但是这样做拿到的数据不是map里的,而是默认的/error请求的信息.上面说过,页面的信息放在model中,这个model

是通过DefaultErrorAttributes获取的(getErrorAttributes方法),因此自定义一个ErrorAttributes重写方法即可.

1
2
3
4
5
6
7
8
9
10
11
//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("company","atguigu");
return map;
}
}

这样错误信息除了自定义的还有map里放的.利用map还可以把异常处理器里的数据也放进来.做法是将异常处理器里的map放入request域中,随着转发使得MyErrorAttributes可以获得这个map,然后将这个map再放入MyErrorAttributes的map,即model中.

7.配置嵌入式Servlet

1.修改配置

1
2
3
4
5
6
7
8
9
server.port=8081
server.context-path=/crud

server.tomcat.uri-encoding=UTF-8

//通用的Servlet容器设置
server.xxx
//Tomcat的设置
server.tomcat.xxx

或者编写一个EmbeddedServletContainerCustomizer,嵌入式的Servlet容器的定制器来修改servlet容器的配置.在Spring Boot2中,改类被废弃.要使用WebServerFactoryCustomizer来代替.在MyConfig类中添加这个Bean:

1
2
3
4
5
6
7
8
9
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setPort(8099);
}
};
}

或者使用更简短的方式:

1
2
3
4
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
return factory -> factory.setPort(8099);
}

2.注册三大组件

用ServletRegistrationBean注册Servlet,首先先写一个Servlet:MyServlet,然后用ServletRegistrationBean注册:

1
2
3
4
@Bean
public ServletRegistrationBean<HttpServlet> myServlet() {
return new ServletRegistrationBean<HttpServlet>(new MyServlet(), "/myServlet");
}

而Filter和Listener与此类似,都是添加一个xxxRegistrationBean到容器中.

SpringBoot自动配置SpringMVC的时候也会配置DispatcherServlet,前端控制器,也有一个配置类:DispatcherServletAutoConfiguration.

3.替换其它嵌入式容器

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器;
</dependency>

默认是引入tomcat,如果要换,则要排除掉然后引入新的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 引入web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<!--引入其他的Servlet容器-->
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

4.使用外置Servlet容器

  1. 创建一个war项目

  2. 将将嵌入式的Tomcat指定为provided

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
    </dependency>
  3. 必须编写一个SpringBootServletInitializer的子类,并调用configure方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    //传入SpringBoot应用的主程序
    return application.sources(SpringBoot04WebJspApplication.class);
    }

    }

Docker

1.Docker常用命令

操作 命令 说明
检索 docker search 关键字 eg:docker search redis 我们经常去docker hub上检索镜像的详细信息,如镜像的TAG。
拉取 docker pull 镜像名:tag :tag是可选的,tag表示标签,多为软件的版本,默认是latest
列表 docker images 查看所有本地镜像
删除 docker rmi image-id 删除指定的本地镜像

以上是关于镜像的,根据镜像启动后就是容器,启动方法:

1
docker run --name mytomcat -d tomcat:latest

mytomcat是自定义名称.-d表示是后台运行,后面的是镜像名,必要时需指定tag.

启动镜像其实类似于安装某个程序,一旦”安装”完成,该程序的启动就不是通过镜像这个”安装程序”,而是通过容器本身.

接着可以使用docker ps查看运行中的容器.加上参数-a可以查看所有的容器,包括停止的.

使用docker stop id停止容器.docker start id启动容器.

如果直接在浏览器里访问tomcat的8080端口是不行的,因为这个8080端口是容器的而不是host的.所以需要端口映射:

1
docker run -d -p 8888:8080 tomcat

将host的8888端口映射到8080端口.

查看容器的日志docker logs 容器名/容器id.删除容器docker rm id

用一个镜像启动多个容器:注意名字和host端口不冲突即可.

2.安装MySQL

安装和上面一样,只是在启动时要

注意端口映射和用参数-e指定用户密码:

1
docker run -p 4406:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql

其它高级操作:

1
2
3
4
5
6
docker run --name mysql03 -v /conf/mysql:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
把主机的/conf/mysql文件夹挂载到 mysqldocker容器的/etc/mysql/conf.d文件夹里面
改mysql的配置文件就只需要把mysql配置文件放在自定义的文件夹下(/conf/mysql)

docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
指定mysql的一些配置参数

SpringBoot与数据访问

1.JDBC

SpringBoot现在默认的数据源是Hikari.

数据源的相关配置都在DataSourceProperties里面.

支持的数据源:

image-20200726144014757

Generic表示自定义数据源.创建数据源步骤:

1
2
3
4
@Bean
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}

2.整合Druid

需要Druid和log4j的依赖.

然后配置一个Druid的管理平台:

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
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid() {
return new DruidDataSource();
}

//配置一个管理后台的Servlet,并设置一些初始化参数
@Bean
public ServletRegistrationBean<StatViewServlet> statViewServlet() {
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
bean.setInitParameters(new HashMap<String, String>() {
{
put("loginUsername", "admin");
put("loginPassword", "123456");
put("allow", "");//默认允许所有访问
}
});
return bean;
}

//配置一个web监控的filter
public FilterRegistrationBean<WebStatFilter> webStatFilter() {
FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
bean.setInitParameters(new HashMap<>() {{
put("exclusions", "*.js,*.css,/druid/*");
}});
bean.setUrlPatterns(Collections.singletonList("/*"));
return bean;
}
}

1.注解版

在mapper包中写SQL语句,简单快捷,但是灵活性不够:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper
public interface DepartmentMapper {
@Select("select * from department where id=#{id} ")
public Department getDeptById(Integer id);

@Select("delete from department where id=#{id} ")
public Integer deleteDeptById(Integer id);

@Options(useGeneratedKeys = true, keyProperty = "id")//表示是否有自增,哪个是自增
@Select("insert into department(departmentName) values(#{departmentName} )")
public Integer insertDept(Department department);

@Select("update department set departmentName=#{departmentName} where id=#{id} ")
public Integer updateDept(Department department);
}

接着在Controller中注入这个Mapper,然后调用即可.

自定义Mybatis配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@org.springframework.context.annotation.Configuration
public class MyBatisConfig {

@Bean
public ConfigurationCustomizer configurationCustomizer(){
return new ConfigurationCustomizer(){

@Override
public void customize(Configuration configuration) {
configuration.setMapUnderscoreToCamelCase(true);
}
};
}
}

在接口上加的@Mapper注解可以去掉.需要在SpringBoot的启动类上加@mapperScan,并指定扫描的包.

2.配置文件版

1
2
3
mybatis:
config-location: classpath:mybatis/mybatis-config.xml 指定全局配置文件的位置
mapper-locations: classpath:mybatis/mapper/*.xml 指定sql映射文件的位置

SpringBoot启动原理

1.创建SpringApplication对象

image-20200726232702324

可以看到run方法后面创建了一个SpringApplication,让后在这个对象上调用run方法.

来看这个创建对象的过程,即其构造器.进入该来查看:

image-20200726235109418

本质上调用了两个参数的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//resourceLoader加载配置资源但是没在这初始化,后面再说
this.resourceLoader = resourceLoader;
//默认是启动类,不过有可能你自定义了一个配置类放到main class运行就需要配置一下
Assert.notNull(primarySources, "PrimarySources must not be null");
//springApplication配置类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//设置servlet环境
this.webApplicationType = deduceWebApplicationType();
////获取ApplicationContextInitializer,也是在这里开始首次加载spring.factories文件
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
//获取监听器,这里是第二次加载spring.factories文件
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

这里面最重要的就是setInitializers,setListeners.

其中setInitializers中的getSpringFactoriesInstances代表着去META-INF/spring.factories找配置的所有ApplicationContextInitializer并保存起来.

具体看这个方法:

image-20200727001738457

注意注释写了用名字保证唯一.接着用SpringFactoriesLoader的loadFactoryNames获得所有唯一的ApplicationContextInitializer(参数type指的就是他).查看这个loadFactoryNames方法:

image-20200727002625214

框住的常量就是META-INF/spring.factories.

当获取了所有ApplicationContextInitializer构成的Set集合后,用createSpringFactoriesInstances创建实例.创建完成后还排了下序,然后返回这个List.

而setInitializers方法本身就是一个setter方法,这意味着返回的list被保存在SpringApplication中.

setListeners这个方法与之类似,只不过保存的是ApplicationListener.

构造器的最后一步是deduceMainApplicationClass(),即推断哪个是main方法.做法是:

image-20200727004122412

为什么要判断?这是因为传进来的是数组,可以有多个配置类.

image-20200727004937998

现在创建SpringApplication对象的过程已结束,来到执行run方法

image-20200727005102595

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
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

前面都是些初始化操作stopwatch是计时器:

image-20200727122947429挑出后面重要的来看.第七行,获取了一个非常重要的Listener:

1
SpringApplicationRunListeners listeners = getRunListeners(args);

这个Listener如何获取,进入getRunListeners一看:

image-20200727123245183

又是前面getSpringFactoriesInstances的操作.只不过这里获取的是SpringApplicationRunListeners,和前面的ApplicationListener不是一回事.通过debug发现getSpringFactoriesInstances返回的instances实际是SpringApplicationRunListener的实现类EventPublishingRunListener.具体这个实例怎么来的呢?

在进一次createSpringFactoriesInstances.注意此时上一步获得名字时names里已经是EventPublishingRunListener.进入后里面使用了createSpringFactoriesInstances方法,该方法里使用BeanUtils.instantiateClass(constructor, args)来创建实例对象(根据构造器和参数,具体也就是通过反射得到对应的构造器方法.)这样就来到了EventPublishingRunListener的构造器:

1
2
3
4
5
6
7
8
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<?> listener : application.getListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}

构造器中有个循环.大意是将刚才存在SpringApplication中的Listeners中的Listener全部添加到initialMulticaster中.而initialMulticaster的类型是SimpleApplicationEventMulticaster,它继承了抽象类AbstractApplicationEventMulticaster.这个类中有添加方法.

image-20200727153615597

所以getRunListeners(args)这一句的的结果是下图,获取了一个事件监听器,该监听器内部还包含了多个监听器:

image-20200727123839514

接着是listeners.starting();这句其实简单,就是把Listeners里面的每个Listener的start方法调用一下.

这是每个Listener(SpringApplicationRunListener)的方法:

image-20200727141240880

而现在Listeners中的EventPublishingRunListener重写了这些方法:

image-20200727141735742

所以调用start方法时调用的也是这里重写的.debug进这个starting方法看看:

1
2
3
4
@Override
public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

这里new了一个ApplicationStartingEvent(应用启动事件).这里涉及到事件机制,可以看下这个链接事件机制

EventPublishingRunListener就是事件监听器,而事件的触发器是SpringApplication.

现在构建了一个ApplicationStartingEvent事件(先不关注事件对象怎么构建的).multicast表示多播,这里指将事件发布出去.看看这个multicastEvent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}

大意就是遍历getApplicationListeners(event, type),然后对每个listener进行invokeListener(listener, event).

先看的结果:

image-20200727155023738

而在SpringApplication中存的不止这么多,所以中间肯定有筛选.筛选条件那就是参数(event, type),event是ApplicationStartingEvent,type是:

image-20200727155451718

进入getApplicationListeners方法,根据注释可知:该方法作用:返回与给定事件类型匹配的ApplicationListeners集合,非匹配的侦听器会被提前排除,允许根据缓存的匹配结果来返回。

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
protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType) {

Object source = event.getSource();
Class<?> sourceType = (source != null ? source.getClass() : null);
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

// Quick check for existing entry on ConcurrentHashMap...
ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
return retriever.getApplicationListeners();
}

if (this.beanClassLoader == null ||
(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
// Fully synchronized building and caching of a ListenerRetriever
synchronized (this.retrievalMutex) {
retriever = this.retrieverCache.get(cacheKey);
if (retriever != null) {
return retriever.getApplicationListeners();
}
retriever = new ListenerRetriever(true);
Collection<ApplicationListener<?>> listeners =
retrieveApplicationListeners(eventType, sourceType, retriever);
this.retrieverCache.put(cacheKey, retriever);
return listeners;
}
}
else {
// No ListenerRetriever caching -> no synchronization necessary
return retrieveApplicationListeners(eventType, sourceType, null);
}
}

主要涉及到3个点:缓存retrieverCache、retrieveApplicationListeners已经retrieveApplicationListeners中调用的supportsEvent方法。流程是这样的:

1、缓存中是否有匹配的结果,有则返回

2、若缓存中没有匹配的结果,则从this.defaultRetriever.applicationListeners中过滤,这个this表示的EventPublishingRunListener对象的属性initialMulticaster(也就是SimpleApplicationEventMulticaster对象,而defaultRetriever.applicationListeners的值也是在EventPublishingRunListener构造方法中初始化的)

3、过滤过程,遍历defaultRetriever.applicationListeners集合,从中找出ApplicationStartingEvent匹配的listener,具体的匹配规则需要看各个listener的supportsEventType方法(有两个重载的方法)

4、将过滤的结果缓存到retrieverCache

5、将过滤出的结果返回回去

现在得到了一个ApplicationListeners集合,然后来到invokeListener方法.该方法注释是使用给定的事件调用给定的监听器.深入该方法发现里面实际执行了doInvokeListener(listener, event)方法.该方法实际执行了listener.onApplicationEvent方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
else if (event instanceof ContextClosedEvent
&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}

可以看到这里根据事件类型调用相应的方法.那么现在就是调用onApplicationStartingEvent方法.此时的listener是LoggingApplicationListener

image-20200727161227480

可以看到这里做了一些和日志相关的工作,具体不深究.

其它几个ApplicationListener也是差不多如此:

LoggingApplicationListener:检测正在使用的日志系统,默认是logback,支持3种,优先级从高到低:logback > log4j > javalog。此时日志系统还没有初始化

BackgroundPreinitializer:另起一个线程实例化Initializer并调用其run方法,包括验证器、消息转换器等等

DelegatingApplicationListener:此时什么也没做

LiquibaseServiceLocatorApplicationListener:此时什么也没做

回顾总结起来就是listerns的start这个事件触发了上述的事件响应.

接着来到try内部:

image-20200727162117716

这里先是封装了下参数,接着准备环境.查看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
//这里又调用listener的方法
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

listeners.environmentPrepared(environment);和刚才的starting类似,只不过事件变了:

image-20200727162655705

根据刚才的分析,事件会触发事件响应,而这个事件触发的内容就不展开了参考链接

环境准备完成后是打印banner.

接着是非常重要的createApplicationContext();方法里面判断了一下,然后利用BeanUtils.instantiateClass(contextClass)创建出来.

然后是一个异常报告,略过.接着是准备上下文:

1
prepareContext(context, environment, listeners, applicationArguments, printedBanner);

这个方法中也做了一些重要的事:

image-20200727164624937

首先保存了环境,然后是后处理注册了一些组件.

接着的applyInitializers方法做了初始化工作:

image-20200727164958362

这里使用了前面存在SpringApplication中的ApplicationContextInitializer.

接着又是listeners.contextPrepared(context);这样的方法,还是老套路,这回的事件又换了一个:

image-20200727165405272

上上张图的四个方法完成后又做了一些杂七杂八的处理,最后又是一个listener:

image-20200727165906844

这会还是大体不变,换成另一个事件:

image-20200727170040742

prepareContext准备上下文完成后,是两个方法:

image-20200727170407820

刷新容器,ioc容器初始化:

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
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

接着的afterRefresh方法是个空方法,在老版本中该方法回调用callRunners方法,现在callRunners直接挪到了run方法中.

image-20200727172307917

第一个方法发布了一个新事件:

image-20200727173150101

而callRunners是两种运行方式:

image-20200727173243795

ApplicationRunner的优先级高于CommandLineRunner.

最后run方法返回了context,即ioc容器.

SpringBoot自定义Starter

SpringBoot最强大的功能就是各种预置的starter免去了大量的配置工作.但是免不了需要自定义starter.

  1. 这个starter需要的依赖是什么?
  2. 如何编写自动配置
    • @Configuration 指定这个类是配置类
    • @ConditionalonXXX 指定在什么条件下生效的配置
    • @AutoConfigureOrder指定顺序
    • @Bean给容器添加组件
    • @ConfigurationProperties结合xxxProperties类来绑定相关配置

将需要启动就加载的自定配置类配置在META-INF/spring.properties文件中.