绿色健康小清新

耐得住寂寞,守得住繁华

SpringBoot

一.Spring Boot 入门

1.SpringBoot简介

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

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

J2EE开发的一站式解决方案


优点

  1. 快速创建独立运行的Spring项目以及与主流框架集成
  2. 使用嵌入式的Servlet容器,应用无需打成WAR包
    starters自动依赖与版本控制
  3. 大量的自动配置,简化开发,也可修改默认值
  4. 无需配置XML,无代码生成,开箱即用
  5. 准生产环境的运行时应用监控
  6. 与云计算的天然集成

2.微服务

2014,martin fowler

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

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

单体应用:ALL IN ONE

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

详细参照微服务文档

集群、分布式、微服务概念和区别

3.环境约束

–jdk1.8:Spring Boot 推荐jdk1.7及以上;

–maven3.x:maven 3.3以上版本;


4.Maven设置

给maven 的settings.xml配置文件的profiles标签添加:(设置使用的jdk版本)

开发工具中的maven设置为自己配置的maven

1
2
3
4
5
6
7
8
9
10
11
12
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>

创建一个maven工程

导入spring boot相关的依赖

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.clboy.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* springBootApplication:标注一个主程序类,表示这个是一个Springboot应用
*/

@SpringBootApplication
public class HelloWorldMainApplication {

public static void main(String[] args) {
//启动
SpringApplication.run(HelloWorldMainApplication.class, args);
}
}

编写一个Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.clboy.springboot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* RestController:是spring4里的新注解,是@ResponseBody@Controller的缩写。
*/

@Controller
public class HelloController {

@ResponseBody
@RequestMapping("hello")
public String hello(){
return "hello world";
}

}

行主程序Main方法测试

访问 http://localhost:8080/hello

简化部署

添加maven插件

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>

使用mvn package进行打包

进入打包好的jar包所在目录

使用 java -jar jar包名称 运行

5.Hello World探究

依赖

1
2
3
4
5
6
7
<!--Hello World项目的父工程是org.springframework.boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/>
</parent>
1
2
3
4
5
6
7
8
9
10
11
12
  <!--
org.springframework.boot他的父项目是spring-boot-dependencies
他来真正管理Spring Boot应用里面的所有依赖版本;
Spring Boot的版本仲裁中心;
以后我们导入依赖默认是不需要写版本;(没有在dependencies里面管理的依赖自然需要声明版本号)
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>

启动器

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

spring-boot-starter-web:

spring-boot-starter:spring-boot场景启动器;帮我们导入了web模块正常运行所依赖的组件;

Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器

主程序类,主入口类

1
2
3
4
5
6
7
8
@SpringBootApplication
public class HelloWorldMainApplication {

public static void main(String[] args) {
//启动
SpringApplication.run(HelloWorldMainApplication.class, args);
}
}

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

@SpringBootApplication注解等于@EnableAutoConfiguration注解加上@ComponentScan注解。

看一下@SpringBootApplication这个注解类的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Target({ElementType.TYPE})    //可以给一个类型进行注解,比如类、接口、枚举
@Retention(RetentionPolicy.RUNTIME) //可以保留到程序运行的时候,它会被加载进入到 JVM 中
@Documented //将注解中的元素包含到 Javadoc 中去。
@Inherited //继承,比如A类上有该注解,B类继承A类,B类就也拥有该注解

@SpringBootConfiguration

@EnableAutoConfiguration


/*
*创建一个配置类,在配置类上添加 @ComponentScan 注解。
*该注解默认会扫描该类所在的包下所有的配置类,相当于之前的 <context:component-scan>。
*/
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication

@SpringBootConfiguration:Spring Boot的配置类;标注在某个类上,表示这是一个Spring Boot的配置类;

1
2
3
4
5
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration

@Configuration:配置类上来标注这个注解;

配置类 ----- 配置文件;配置类也是容器中的一个组件;@Component

1
2
3
4
5
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration

@EnableAutoConfiguration:开启自动配置功能;

以前我们需要配置的东西,SpringBoot帮我们自动配置;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能;这样自动配置才能生效;

1
2
3
4
5
6
7
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration

@AutoConfigurationPackage:自动配置包

1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage

@Import:Spring的底层注解@Import,给容器中导入一个类。有时没有把某个类注入到IOC容器中,但在运用的时候需要获取该类对应的bean,此时就需要用到@Import注解。可以直接拿到该类的对象。

导入的组件由org.springframework.boot.autoconfigure.AutoConfigurationPackages.Registrar将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器;





这里controller包是在主程序所在的包下,所以会被扫描到,我们在springboot包下创建一个test包,把主程序放在test包下,这样启动就只会去扫描test包下的内容而controller包就不会被扫描到,再访问开始的hello就是404

@Import({AutoConfigurationImportSelector.class})

AutoConfigurationImportSelector.class将所有需要导入的组件以全类名的方式返回;这些组件就会被添加到容器中;会给容器中导入非常多的自动配置类(xxxAutoConfiguration);就是给容器中导入这个场景需要的所有组件,并配置好这些组件;

有了自动配置类,免去了我们手动编写配置注入功能组件等的工作;

可见selectImports()是AutoConfigurationImportSelector的核心函数,其核心功能就是获取spring.factories中EnableAutoConfiguration所对应的Configuration类列表,由@EnableAutoConfiguration注解中的exclude/excludeName参数筛选一遍,再由AutoConfigurationImportFilter类所有实例筛选一遍,得到最终的用于Import的configuration和exclusion。

Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfigura
tion指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作;以前我们需要自己配置的东西,自动配置类都帮我们完成了;

源码

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
 public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
//......
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 如果AutoConfiguration没开,返回{}
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 将spring-autoconfigure-metadata.properties的键值对配置载入到PropertiesAutoConfigurationMetadata对象中并返回
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 基于各种配置计算需要import的configuration和exclusion
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

// 判断AudoConfiguration是否开启
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
// 如果配置文件中有"spring.boot.enableautoconfiguration",返回该字段的值;否则返回true
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取注解的属性值
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 从META-INF/spring.factories文件中获取EnableAutoConfiguration所对应的configurations
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重,List转Set再转List
configurations = removeDuplicates(configurations);
// 从注解的exclude/excludeName属性中获取排除项
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 对于不属于AutoConfiguration的exclude报错
checkExcludedClasses(configurations, exclusions);
// 从configurations去除exclusions
configurations.removeAll(exclusions);
// 由所有AutoConfigurationImportFilter类的实例再进行一次筛选,去
configurations = filter(configurations, autoConfigurationMetadata);
// 把AutoConfigurationImportEvent绑定在所有AutoConfigurationImportListener子类实例上
fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回(configurations, exclusions)组
return new AutoConfigurationEntry(configurations, exclusions);
}
// ......
}```

二、配置文件

1、配置文件

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

application.properties

application.yml

配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好;

YAML(YAML Ain’t Markup Language)

YAML A Markup Language:是一个标记语言

YAML isn’t Markup Language:不是一个标记语言;

标记语言:

       以前的配置文件;大多都使用的是 xxxx.xml文件;

       YAML:以数据为中心,比json、xml等更适合做配置文件;

       YAML:配置例子

1
2
server:
port: 8081

       XML:

1
2
3
<server>
<port>8081</port>
</server>

2、YAML语法:

1、基本语法

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

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

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

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

2、值的写法

字面量:普通的值(数字,字符串,布尔)

     k: v:字面直接来写;

     字符串默认不用加上单引号或者双引号;

     “”:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思

      name: “zhangsan \n lisi”:输出;zhangsan 换行 lisi

     ‘’:单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据

      name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi

对象、Map(属性和值)(键值对):

k: v:在下一行来写对象的属性和值的关系;注意缩进

对象还是k: v的方式

1
2
3
friends:
lastName: zhangsan
age: 20

行内写法:

1
friends: {lastName: zhangsan,age: 18}
数组(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:

@ConfiguratoionProperties(prefix = "配置文件的前缀")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 将配置文件中配置的每一个属性的值,映射到这个组件中
* @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>
1、properties配置文件在idea中默认utf-8可能会乱码
1
2
3
4
5
6
7
8
9
10
#配置person的值
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.last-name:hello}_dog
person.dog.age=15

调整

在设置中找到File Encodings,将配置文件字符集改为UTF-8,并勾选:

  • [x] Transparent native-to-ascii conversion

idea配置乱码

2、@Value获取值和@ConfigurationProperties获取值比较
1
2
3
4
5
6
@Value("${person.last-name}")
private String lastName;
@Value("#{11*2}")
private Integer age;
@Value("true")
private Boolean boss;
@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SpEL不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持

配置文件yml还是properties他们都能获取到值;

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

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

注意:

  • @ConfigurationProperties里面的数据可以直接注入属性,并且优先级最高
  • @PropertySource里面的数据需要通过@Value(${})

3、配置文件注入值数据校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@ConfigurationProperties(prefix = "person")
@Validated
public class Person {

/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property>
* <bean/>
*/

//lastName必须是邮箱格式
@Email
//@Value("${person.last-name}")
private String lastName;
//@Value("#{11*2}")
private Integer age;
//@Value("true")
private Boolean boss;

private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
4、@PropertySource&@ImportResource&@Bean

@PropertySource:加载指定的配置文件;

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
/**
* 将配置文件中配置的每一个属性的值,映射到这个组件中
* @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;
* prefix = "person":配置文件中哪个下面的所有属性进行一一映射
*
* 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能;
* @ConfigurationProperties(prefix = "person")默认从全局配置文件中获取值;
*
*/
@PropertySource(value = {"classpath:person.properties"})
@Component
@ConfigurationProperties(prefix = "person")
//@Validated
public class Person {

/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}从环境变量、配置文件中获取值/#{SpEL}"></property>
* <bean/>
*/

//lastName必须是邮箱格式
// @Email
//@Value("${person.last-name}")
private String lastName;
//@Value("#{11*2}")
private Integer age;
//@Value("true")
private Boolean boss;

@ImportResource:导入Spring的配置文件,让配置文件里面的内容生效;

Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别;

想让Spring的配置文件生效,加载进来;@ImportResource标注在一个配置类上

1
2
@ImportResource(locations = {"classpath:beans.xml"})
导入Spring的配置文件让其生效
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


<bean id="helloService" class="com.atguigu.springboot.service.HelloService"></bean>
</beans>

不来编写Spring的配置文件

SpringBoot推荐给容器中添加组件的方式;推荐使用全注解的方式

1、配置类 @Configuration ------>Spring配置文件

2、使用 @Bean给容器中添加组件

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
3
${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.last-name:hello}_dog
person.dog.age=15

5、Profile

1、多Profile文件

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

默认使用application.properties的配置,可以在application.properties中写spring.profiles.active=dev来指定使用哪一个配置文件;

2、yml支持多文档块方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

server:
port: 8081
spring:
profiles:
active: prod

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


---

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

3、激活指定profile

     1、在配置文件中指定 spring.profiles.active=dev

     2、命令行(最终执行,优先级最高):

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

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

     3、虚拟机参数;

     -Dspring.profiles.active=dev


6、配置文件加载位置和优先级

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件(并且properties比yml的优先级高,会限制性yml,后面的properties进行覆盖)

–file:./config/

–file:./

–classpath:/config/

–classpath:/

优先级由高到底,高优先级的配置会覆盖低优先级的配置;

SpringBoot会从这四个位置全部加载主配置文件;互补配置

我们还可以通过spring.config.location来改变默认的配置文件位置(重要)

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

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

7、外部配置加载顺序

SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置

1.命令行参数

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

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

多个配置用空格分开; --配置项=值

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

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

4.操作系统环境变量

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

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

优先加载带profile

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

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

再来加载不带profile

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

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

10.@Configuration注解类上的@PropertySource

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

所有支持的配置加载来源;

参考官方文档

8、自动配置原理

配置文件到底能写什么?怎么写?自动配置原理;

配置文件能配置的属性参照

1、自动配置原理:

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

2)、@EnableAutoConfiguration 作用:

  • 利用EnableAutoConfigurationImportSelector给容器中导入一些组件?

  • 可以查看selectImports()方法的内容;

  • Listconfigurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置

    • SpringFactoriesLoader.loadFactoryNames()
    • 扫描所有jar包类路径下 META-INF/spring.factories
    • 把扫描到的这些文件的内容包装成properties对象
    • 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中

将 类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中;

所有需要导入的AutoConfiguration

#Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,
org.springframework.boot.autoconfigure.mobile.DeviceResolverAutoConfiguration,
org.springframework.boot.autoconfigure.mobile.DeviceDelegatingViewResolverAutoConfiguration,
org.springframework.boot.autoconfigure.mobile.SitePreferenceAutoConfiguration,
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,
org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,
org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,
org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,
org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,
org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,
org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,
org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,
org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration,
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,
org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,
org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration


每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;

3)、每一个自动配置类进行自动配置功能;

4)、以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;

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
package org.springframework.boot.autoconfigure.web.servlet;

......

//表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件
@Configuration(
proxyBeanMethods = false
)

/**
* 启动指定类的ConfigurationProperties功能;
* 将配置文件中对应的值和HttpProperties绑定起来;
* 并把HttpProperties加入到ioc容器中
*/
@EnableConfigurationProperties({HttpProperties.class})

/**
* Spring底层@Conditional注解
* 根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效;
* 判断当前应用是否是web应用,如果是,当前配置类生效
*/
@ConditionalOnWebApplication(
type = Type.SERVLET
)

//判断当前项目有没有这个类
@ConditionalOnClass({CharacterEncodingFilter.class})

/**
* 判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的
* 即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
*/
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {

//它已经和SpringBoot的配置文件映射了
private final Encoding properties;

//只有一个有参构造器的情况下,参数的值就会从容器中拿(刚好是导入的配置类)
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}

@Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取
@ConditionalOnMissingBean //判断容器有没有这个组件?(容器中没有才会添加这个组件)
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}

......

根据当前不同的条件判断,决定这个配置类是否生效?

一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;

5)、所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;配置文件能配置什么就可以参照某个功能对应的这个属性类

1
2
3
4
@ConfigurationProperties(prefix = "spring.http.encoding")  //从配置文件中获取指定的值和bean的属性进行绑定
public class HttpEncodingProperties {

public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

精髓:

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

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

     3)、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了)

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

xxxxAutoConfigurartion:自动配置类;

给容器中添加组件

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

2、细节

1、@Conditional派生注解(Spring注解版原生的@Conditional作用)

作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;

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

自动配置类必须在一定的条件下才能生效;

我们怎么知道哪些自动配置类生效;

我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

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
Console:

=========================
CONDITIONS EVALUATION REPORT (环境评估报告)
=========================


Positive matches:(自动配置类启用的)
-----------------

DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition)


Negative matches:(没有启动,没有匹配成功的自动配置类)
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)

AopAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice' (OnClassCondition)


三、日志

1、日志框架

小张;开发一个大型系统;

  1、System.out.println("");将关键数据打印在控制台;去掉?写在一个文件?

  2、框架来记录系统的一些运行时信息;日志框架 ; zhanglogging.jar;

  3、高大上的几个功能?异步模式?自动归档?xxxx? zhanglogging-good.jar?

  4、将以前框架卸下来?换上新的框架,重新修改之前相关的API;zhanglogging-prefect.jar;

  5、JDBC—数据库驱动;

    写了一个统一的接口层;日志门面(日志的一个抽象层);logging-abstract.jar;

    给项目中导入具体的日志实现就行了;我们之前的日志框架都是实现的抽象层;

市面上的日志框架;

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

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

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

日志门面: SLF4J

日志实现:Logback

Slf4J和Logback以及log4j是同一个人写的,但是log4j版本太老

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");
}
}

图示

slf4j

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

2、遗留问题

a(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis、xxxx

统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出?

统一实现

如何让系统中所有的日志都统一到slf4j;

1、将系统中其他日志框架先排除出去;

2、用中间包来替换原有的日志框架;

3、我们导入slf4j其他的实现

3、SpringBoot日志关系

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

SpringBoot使用它来做日志功能;

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

底层依赖关系

底层依赖

总结

1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录

2)、SpringBoot也把其他的日志都替换成了slf4j

3)、中间替换包?

1
2
3
4
5
6
@SuppressWarnings("rawtypes")
public abstract class LogFactory {

static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = "http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j";

static LogFactory logFactory = new SLF4JLogFactory();

4)、如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉?

         Spring框架用的是commons-logging;

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;

4、日志使用;

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日志...");


}

日志输出格式:

%d表示日期时间,

%thread表示线程名,

%-5level:级别从左显示5个字符宽度

%logger{50} 表示logger名字最长50个字符,否则按照句点分割。

%msg:日志消息,

%n是换行符

–>

%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n

SpringBoot修改日志的默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
logging.level.com.atguigu=trace

#logging.file.path=
#不指定路径在,当前项目下生成的springboot.log日志
#可以指定完整的路径
#logging.file.name=G:/springbppt.log
#logging.file.name=springbppt.log
# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件
logging.file.path=E:\\知识学习\\01尚硅谷SpringBoot核心技术篇\\代码\\spring-boot-03-logging\\src\\main
#logging.file.name=logging-temp.log

# 在控制台输出的日志的格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
# 指定文件中日志输出的格式
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n
logging.filelogging.pathExampleDescription
(none)(none)ds只在控制台输出
指定文件名(none)my.log输出日志到my.log文件
(none)指定目录/var/log输出到指定目录的 spring.log 文件中
2、指定配置

给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了

Logging SystemCustomization
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy
Log4j2log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging)logging.properties

logback.xml:直接就被日志框架识别了;

logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能

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

如(具体配置看文件夹):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>

点击查看logback-spring.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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<!-- 定义日志的根目录 -->
<property name="LOG_HOME" value="F:\\日志存放\\manager" />
<!-- 定义日志文件名称 -->
<property name="debug" value="manager-codekiller-debug"></property>
<property name="info" value="manager-codekiller-info"></property>
<property name="error" value="manager-codekiller-error"></property>
<property name="warn" value="manager-codekiller-warn"></property>
<property name="job" value="manager-codekiller-job"></property>
<property name="fileName" value="logback"></property>



<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
%-15.15():如果记录的线程字符长度小于15(第一个)则用空格在右侧补齐,如果字符长度大于15(第二个),则从开头开始截断多余的字符
%-50.50():如果记录的logger字符长度小于50(第一个)则用空格在右侧补齐,如果字符长度大于50(第二个),则从开头开始截断多余的字符
%highlight():颜色,info为蓝色,warn为浅红,error为加粗红,debug为黑色
%boldMagenta:粗红
%magenta:洋红
$cyan:青色
%white:白色
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n</pattern>
</springProfile>
</layout>
</appender>

<!--控制台打印警告信息的单独处理-->
<appender name="consoleWarn" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n)</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n)</pattern>
</springProfile>
</layout>
</appender>

<!--控制台打印错误信息的单独处理-->
<appender name="consoleError" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%boldMagenta(%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n)</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%boldMagenta(%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%-15.15(%thread)]) %cyan( %-50.50(%logger{50} )) : %msg%n)</pattern>
</springProfile>
</layout>
</appender>



<!--debug日志存储-->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${debug}/${fileName}.log</file>

<!--过滤,只存储DEBUG级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${LOG_HOME}/${debug}/${fileName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>


<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
那些为了归档而创建的目录也会被删除。
-->
<MaxHistory>365</MaxHistory>

<!--
当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy
是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15(%thread)] %-50.50(%logger{50}) : %msg%n</pattern>
</layout>
</appender>

<!--info日志存储-->
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${info}/${fileName}.log</file>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_HOME}/${info}/${fileName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>

<MaxHistory>365</MaxHistory>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15(%thread)] %-50.50(%logger{50}) : %msg%n</pattern>
</layout>
</appender>

<!--warn日志存储-->
<appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${warn}/${fileName}.log</file>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_HOME}/${warn}/${fileName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>

<MaxHistory>365</MaxHistory>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15(%thread)] %-50.50(%logger{50}) : %msg%n</pattern>
</layout>
</appender>

<!--error日志存储-->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${error}/${fileName}.log</file>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_HOME}/${error}/${fileName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>

<MaxHistory>365</MaxHistory>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15(%thread)] %-50.50(%logger{50}) : %msg%n</pattern>
</layout>
</appender>



<!--job日志存储-->
<appender name="job" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${job}/${fileName}.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${job}/${fileName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>

<MaxHistory>365</MaxHistory>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-15.15(%thread)] %-50.50(%logger{50}) : %msg%n</pattern>
</layout>
</appender>

<!--
logger主要用于存放日志对象,也可以定义日志类型、级别,并且会先执行。
name:表示匹配的logger类型前缀,也就是包的前半部分
level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,即是否将信息提交给root
false:表示只用当前logger的appender-ref,
true:表示当前logger的appender-ref和rootLogger的appender-ref都有效
-->

<!--job包的单独输出-->
<logger name="top.codekiller.manager.examination.job" level="debug" additivity="false">
<appender-ref ref="job"></appender-ref>
<appender-ref ref="console"></appender-ref>
</logger>

<!--debug的单独输出-->
<logger name="top.codekiller.manager.examination" level="debug" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="debug" />
</logger>

<!--warn的单独输出-->
<logger name="top.codekiller.manager.examination" level="warn" additivity="false">
<appender-ref ref="consoleWarn"/>
<appender-ref ref="warn" />
</logger>


<!--error的单独输出-->
<logger name="top.codekiller.manager.examination" level="error" additivity="false">
<appender-ref ref="consoleError"/>
<appender-ref ref="error"/>
</logger>

<!--
root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="debug" />
<appender-ref ref="info" />
<appender-ref ref="warn" />
<appender-ref ref="error" />
</root>

</configuration>


如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误

1
no applicable action for [springProfile]

5、切换日志框架

可以按照slf4j的日志适配图,进行相关的切换;

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;

1)、创建SpringBoot应用,选中我们需要的模块;

2)、SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来

3)、自己编写业务代码;

自动配置原理?

这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?xxx

xxxxAutoConfiguration:帮我们给容器中自动配置组件;

xxxxProperties:配置类来封装配置文件的内容;

web自动配置规则

  1. WebMvcAutoConfiguration
  2. WebMvcProperties
  3. ViewResolver自动配置
  4. 静态资源自动映射
  5. Formatter与Converter自动配置
  6. HttpMessageConverter自动配置
  7. 静态首页
  8. favicon
  9. 错误处理

2、SpringBoot对静态资源的映射规则;

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
WebMvcAuotConfiguration.java:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
//追踪resourceProperties
Integer cachePeriod = this.resourceProperties.getCachePeriod();
//将classpath:/META-INF/resources/webjars/的jar包进入静态资源
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(
registry.addResourceHandler("/webjars/**")
.addResourceLocations(
"classpath:/META-INF/resources/webjars/")
.setCachePeriod(cachePeriod));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
//静态资源文件夹映射,非webjars的静态资源
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(
this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
1
2
3
4
5
6
7
8
9
10
一个静态内部类
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) //有一个配置类
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {

private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
//属性
private final ResourceProperties resourceProperties;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)//可以设置和静态资源有关的参数,缓存时间等
public class ResourceProperties {
//非webjars的访问路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
private String[] staticLocations;
private boolean addMappings;
private final ResourceProperties.Chain chain;
private final ResourceProperties.Cache cache;

public ResourceProperties() {
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
this.addMappings = true;
this.chain = new ResourceProperties.Chain();
this.cache = new ResourceProperties.Cache();
}

1)、所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;

      webjars:以jar包的方式引入静态资源;

http://www.webjars.org/

webjars

localhost:8080/webjars/jquery/3.3.1/jquery.js

1
2
3
4
5
6
7

<!--引入jquery-webjar-->在访问的时候只需要写webjars下面资源的名称即可
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>

2)、"/**" 访问当前项目的任何资源,都去(静态资源的文件夹)找映射

  • “classpath:/META-INF/resources/”,
  • “classpath:/resources/”,
  • “classpath:/static/”,
  • “classpath:/public/”
  • “/”:当前项目的根路径
    localhost:8080/abc === 去静态资源文件夹里面找abc

3)、欢迎页; 静态资源文件夹下的所有index.html页面;被"/**"映射;

       localhost:8080/ 找index页面

location就是静态资源路径,所以欢迎页的页面就是上面静态资源下的index.html,被/**映射,因此直接访问项目就是访问欢迎页

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
还是在WebMvcAutoConfiguration.java中
//配置欢迎页映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}

//获得欢迎页面
private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

//location就是静态资源路径,所以欢迎页的页面就是上面静态资源下的index.html,被/**映射,因此直接访问项目就是访问欢迎页
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}


WelcomePageHandlerMapping.java中
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}

4)、所有的 **/favicon.ico 都是在静态资源文件下找;

3、模板引擎

JSP、Velocity、Freemarker、Thymeleaf

模板引擎

SpringBoot推荐的Thymeleaf;

语法更简单,功能更强大;

1、引入thymeleaf;

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--切换thymeleaf版本,一般不需要-->
<properties>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本 -->
<!-- thymeleaf2 layout1-->
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
</properties>

2、Thymeleaf使用

1
2
3
4
5
6
7
8
9
10
11
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");

public static final String DEFAULT_PREFIX = "classpath:/templates/";

public static final String DEFAULT_SUFFIX = ".html";
//

只要我们把HTML页面放在classpath:/templates/,thymeleaf就能自动渲染;

使用:

1、导入thymeleaf的名称空间
1
<html lang="en" xmlns:th="http://www.thymeleaf.org">
2、使用thymeleaf语法;
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>成功!</h1>
<!--th:text 将div里面的文本内容设置为 -->
<div th:text="${hello}">这是显示欢迎信息</div>
</body>
</html>
3、语法规则

1)、th:text;改变当前元素里面的文本内容;

         th:任意html属性;来替换原生属性的值

语法

2)、表达式?

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
70
71
72
73
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>


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: _

4、SpringMVC自动配置

https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications

1. Spring MVC auto-configuration

Spring Boot 自动配置好了SpringMVC

以下是SpringBoot对SpringMVC的默认配置:(WebMvcAutoConfiguration)

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

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

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

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

  • 自动注册了 of 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 (see below).

    • HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json;
    • HttpMessageConverters 是从容器中确定;获取所有的HttpMessageConverter;
      自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Component)
  • Automatic registration of MessageCodesResolver (see below).定义错误代码生成规则

  • Automatic use of a ConfigurableWebBindingInitializer bean (see below).
    我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器)

       初始化WebDataBinder;

       请求数据=====JavaBean;

org.springframework.boot.autoconfigure.web:web的所有自动场景;

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

2、扩展SpringMVC

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>

编写一个配置类(@Configuration),WebMvcConfigurer;不能标注@EnableWebMvc;

既保留了所有的自动配置,也能用我们扩展的配置;

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
    //使用WebMvcConfigurer可以来扩展SpringMVC的功能     
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送直接请求到静态资源文件
registry.addRedirectViewController("/viewhello","index.html");
}

//所有的WebMvcConfigurer组件都会一起起作用
@Bean
public WebMvcConfigurer webMvcConfigurerAdapter(){
WebMvcConfigurer webMvcConfigurer=new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("index.html").setViewName("index");
//Controller中重定向无法访问私有文件,需要用viewcontroller进行转发
registry.addViewController("/main.html").setViewName("dashboard");
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
//springboot设定了不会拦截*.css,*.js,*.png等静态资源,如果拦截了还请自己排除
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html","/","/user/login",
"/asserts/**","/webjars/**");
}

};

return webMvcConfigurer;
}
}

原理:

1)、WebMvcAutoConfiguration是SpringMVC的自动配置类

2)、在做其他自动配置时会导入;@Import(EnableWebMvcConfiguration.class)

3)、容器中所有的WebMvcConfigurer都会一起起作用;

4)、我们的配置类也会被调用;

      效果: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
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
WebMvcAutoConfiguration.java中的一个内部类:
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //将这个类导入ioc容器
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
---------------------------------------------------------------------------
EnableWebMvcConfiguration.java
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
---------------------------------------------------------------------------
DelegatingWebMvcConfiguration.java
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
//从容器中获取所有的WebMvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
//调用成员变量的方法,把包含所有WebMvcConfigurer的list集合传入
this.configurers.addWebMvcConfigurers(configurers);
}
}
}
----------------------------------------------------------------------------
class WebMvcConfigurerComposite implements WebMvcConfigurer {
private final List<WebMvcConfigurer> delegates = new ArrayList();

public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
//进行一个遍历,将所有WebMvcConfigurer放入自身的list集合中
while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}

.......
public void addResourceHandlers(ResourceHandlerRegistry registry) {.....}

public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
//将需要使用该配置时,进行一个遍历,调用所有WebMvcConfigurer接口实现类的对应方法
while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}
..........

3、全面接管SpringMVC;

SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了

我们需要在配置类中添加@EnableWebMvc即可;

1
2
3
4
5
6
7
8
9
10
11
12
//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//浏览器发送 /atguigu 请求来到 success
registry.addViewController("/atguigu").setViewName("success");
}
}

原理:

为什么@EnableWebMvc自动配置就失效了;

1)@EnableWebMvc的核心

1
2
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {

2)、

1
2
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

3)、

1
2
3
4
5
6
7
8
9
10
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
//容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

4)、@EnableWebMvc将WebMvcConfigurationSupport组件导入进来;

5)、导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能;

5、如何修改SpringBoot的默认配置

模式

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

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

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

6、RestfulCRUD

注意:

1.在html页面中,若想访问私有的静态文件,要么需要通过一个controller再进行转发,要么配置>view-controller

2.注意注册或者删除等带参数的操作url会带参数,在controller中进行跳转的时候尽量使用重定向
3.在html中可以抽取出相同部分,通过th:fragment=""或者id,通过replace,insert或者include进行调用
4.在使用each生成一些使用删除或者更新的按钮时,需要使用form表单进行rest风格操作,为了防止产生过多的form表单,可以使用单击事件监听,通过th:attr(x=xxx)获得属性传给放在其他地方的form表单并且实现submmit。

1)、默认访问首页

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
方式一:
//会优先访问pubilc中的资源,这样设置就会访问template中的了(两种方式)
@RequestMapping({"/","index.html"})
// public String index(){
// return "index";
// }



//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
//@EnableWebMvc 不要接管SpringMVC
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//浏览器发送 /atguigu 请求来到 success
registry.addViewController("/atguigu").setViewName("success");
}

//所有的WebMvcConfigurerAdapter组件都会一起起作用
@Bean //将组件注册在容器
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
}
};
return adapter;
}
}



2)、国际化

1)、编写国际化配置文件;

2)、使用ResourceBundleMessageSource管理国际化资源文件

3)、在页面使用fmt:message取出国际化内容

步骤

1)、编写国际化配置文件,抽取页面需要显示的国际化消息

国际化

2)、SpringBoot自动配置好了管理国际化资源文件的组件:MessageSourceAutoConfiguration

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
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {

/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages";
//我们的配置文件可以直接放在类路径下叫messages.properties;

@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(this.basename)) {
//设置国际化资源文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
StringUtils.trimAllWhitespace(this.basename)));
}
if (this.encoding != null) {
messageSource.setDefaultEncoding(this.encoding.name());
}
messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
messageSource.setCacheSeconds(this.cacheSeconds);
messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat);
return messageSource;
}

3)、去页面获取国际化的值;

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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
</head>

<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"/> [[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>

</body>

</html>

效果:根据浏览器语言设置的信息切换了国际化;

原理:

国际化Locale(区域信息对象);LocaleResolver(获取区域信息对象);

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties
.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}

默认的就是根据请求头带来的区域信息获取Locale进行国际化

4)、点击链接切换国际化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 可以在连接上携带区域信息
*/
public class MyLocaleResolver 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) {

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

3)、登陆

开发期间模板引擎页面修改以后,要实时生效

1)、禁用模板引擎的缓存

1
2
# 禁用缓存
spring.thymeleaf.cache=false

2)、页面修改完成以后ctrl+f9:重新编译;

登陆错误消息的显示

1
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

4)、拦截器进行登陆检查

拦截器

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
/**
* 登陆检查,
*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if(user == null){
//未登陆,返回登陆页面
request.setAttribute("msg","没有权限请先登陆");
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
}else{
//已登陆,放行请求
return true;
}

}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
}

注册拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  //所有的WebMvcConfigurer组件都会一起起作用
@Bean //将组件注册在容器
public WebMvcConfigurer webMvcConfigurerAdapter(){
WebMvcConfigurer webMvcConfigurer=new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("index.html").setViewName("index");
registry.addViewController("/main.html").setViewName("dashboard");
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html","/","/user/login",
"/asserts/**","/webjars/**");
}

};
return webMvcConfigurer;
}

5)、CRUD-员工列表

实验要求:

1)、RestfulCRUD:CRUD满足Rest风格;

URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作

操作普通CRUD(uri来区分操作)RestfulCRUD
查询getEmpemp—GET
添加addEmp?xxxemp—POST
修改updateEmp?id=xxx&xxx=xxemp/{id}—PUT
删除deleteEmp?id=1emp/{id}—DELETE

2)、实验的请求架构;

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

3)、员工列表:

thymeleaf公共页面元素抽取

1、抽取公共片段

1
2
3
<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

2、引入公共片段

1
2
3
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector}:模板名::选择器(或者通过公共部分的id:#id)
~{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>

引入片段的时候传入参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<nav class="col-md-2 d-none d-md-block bg-light sidebar" id="sidebar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active"
th:class="${activeUri=='main.html'?'nav-link active':'nav-link'}"
href="#" th:href="@{/main.html}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
1
2
<!--引入侧边栏;传入参数-->
<div th:replace="commons/bar::#sidebar(activeUri='emps')"></div>

6)、CRUD-员工添加

添加页面

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
<form>
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" placeholder="zhangsan@atguigu.com">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>

提交的数据格式不对:生日:日期;

2017-12-12;2017/12/12;2017.12.12;

日期的格式化;SpringMVC将页面提交的值需要转换为指定的类型;

2017-12-12—Date; 类型转换,格式化;

默认日期是按照/的方式;

7)、CRUD-员工修改

修改添加二合一表单

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
<!--需要区分是员工修改还是添加;-->
<form th:action="@{/emp}" method="post">
<!--发送put请求修改员工数据-->
<!--
1、SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的)
2、页面创建一个post表单
3、创建一个input项,name="_method";值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp!=null}?${emp.email}">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp!=null}?${emp.gender==1}">
<label class="form-check-label"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp!=null}?${emp.gender==0}">
<label class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--提交的是部门的id-->
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id == emp.department.id}" th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>

8)、CRUD-员工删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==0?'女':'男'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">编辑</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>
</td>
</tr>


<script>
$(".deleteBtn").click(function(){
//删除当前员工的
$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
return false;
});
</script>

7、错误处理机制

1)、SpringBoot默认的错误处理机制

默认效果:

1)、浏览器,返回一个默认的错误页面,注意看发送请求的请求头:

默认的servererror显示

2)、如果是其他客户端,默认响应一个json数据

默认的clienterror显示

原理

查看org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration源码,

这里是springboot错误处理的自动配置信息

主要给日容器中注册了以下组件:

  • ErrorPageCustomizer 系统出现错误以后来到error请求进行处理;相当于(web.xml注册的错误页面规则)
  • BasicErrorController 处理/error请求
  • DefaultErrorViewResolver 默认的错误视图解析器
  • DefaultErrorAttributes 错误信息
  • defaultErrorView 默认错误视图

ErrorPageCustomizer

1
2
3
4
@Bean
public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;

protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}

//注册错误页面
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//getPath()获取到的是"/error",见下图
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
}

public int getOrder() {
return 0;
}
}

path
当请求出现错误后就会转发到/error

然后这个error请求就会被BasicErrorController处理;

BasicErrorController:处理默认/error请求

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(
value = {ErrorController.class},
search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

处理/error请求

1
2
3
4
5
6
7
8
9
Controller
/**
* 使用配置文件中server.error.path配置
* 如果server.error.path没有配置使用error.path
* 如果error.path也没有配置就使用/error
*/
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController

server
client

这两个方法一个用于浏览器请求响应html页面,一个用于其他客户端请求响应json数据

处理浏览器请求的方法 中,modelAndView存储到哪个页面的页面地址和页面内容数据

看一下调用的resolveErrorView方法

1
2
3
4
5
6
7
8
9
10
11
AbstractErrorController.java:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}

ErrorViewResolver从哪里来的呢?

已经在容器中注册了一个DefaultErrorViewResolver

DefaultErrorViewResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {

private final ApplicationContext applicationContext;

private final ResourceProperties resourceProperties;

DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}

//产生一个DefaultErrorViewResolver
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}

}

然后调用ErrorViewResolver的resolveErrorView()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//把状态码和model传过去获取视图
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);

//上面没有获取到视图就使用把状态吗替换再再找,以4开头的替换为4xx,5开头替换为5xx,见下文(如果定制错误响应)
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}

return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
//viewName传过来的是状态码,例:/error/404
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
//模板引擎可以解析这个页面地址就用模板引擎解析
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}

如果模板引擎不可用,就调用resolveResource方法获取视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//获取的是静态资源文件夹
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//例:static/error.html
resource = resource.createRelative(viewName + ".html");
//存在则返回视图
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}

return null;
}

DefaultErrorAttributes

页面能获取的信息;

timestamp:时间戳

status:状态码

error:错误提示

exception:异常对象

message:异常消息

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

步骤:

一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理;

1)响应页面;去哪个页面是由DefaultErrorViewResolver解析得到的;
1
2
3
4
5
6
7
8
9
10
11
12

protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
//所有的ErrorViewResolver得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
2)、如果定制错误响应:
如何定制错误的页面

       1)、看模板引擎下有没有error/状态码文件; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】,发生此状态码的错误就会来到 对应的页面;

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

页面能获取的信息;

timestamp:时间戳

status:状态码

error:错误提示

exception:异常对象

message:异常消息

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

       2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;

       3).还没有,就去找4xx和5xx的页面

       4)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;

如何定制错误的json数据

1)、自定义异常处理&返回定制json数据;(无自适应效果,不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}
}
//没有自适应效果...

2)、转发到/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";
}

3)、将我们的定制数据携带出去;

出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);

1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;

2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;

容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;

自定义ErrorAttributes

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

@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String,Object> map=super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company","atguigu");
//我们的异常处理此携带的数据
Map<String,Object> ext=(Map<String,Object>)webRequest.getAttribute("ext",0);
map.put("ext",ext);
return map;
}
}

最终的效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容,

8、配置嵌入式Servlet容器

SpringBoot默认使用Tomcat作为嵌入式的Servlet容器;

问题?

1、如何定制和修改Servlet容器的相关配置;

1、修改和server有关的配置(ServerProperties【也是EmbeddedServletContainerCustomizer】);

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

2、WebServerFactoryCustomizer:嵌入式的Servlet容器的定制器;来修改Servlet容器的配置

1
2
3
4
5
6
7
8
9
10
11
@Bean  //一定要将这个定制器加入到容器中
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
return new EmbeddedServletContainerCustomizer() {

//定制嵌入式的Servlet容器相关的规则
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(8083);
}
};
}

2、注册Servlet三大组件【Servlet、Filter、Listener】

由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用,没有web.xml文件。

注册三大组件用以下方式

ServletRegistrationBean

1
2
3
4
5
6
//注册三大组件
@Bean
public ServletRegistrationBean myServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
return registrationBean;
}

FilterRegistrationBean

1
2
3
4
5
6
7
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MyFilter());
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}

ServletListenerRegistrationBean

1
2
3
4
5
@Bean
public ServletListenerRegistrationBean myListener(){
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}

SpringBoot帮我们自动SpringMVC的时候,自动的注册SpringMVC的前端控制器;DIspatcherServlet;

DispatcherServletAutoConfiguration中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public ServletRegistrationBean dispatcherServletRegistration(
DispatcherServlet dispatcherServlet) {
ServletRegistrationBean registration = new ServletRegistrationBean(
dispatcherServlet, this.serverProperties.getServletMapping());
//默认拦截: / 所有请求;包静态资源,但是不拦截jsp请求; /*会拦截jsp
//可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径

registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(
this.webMvcProperties.getServlet().getLoadOnStartup());
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
return registration;
}

SpringBoot能不能支持其他的Servlet容器;

3、替换为其他嵌入式Servlet容器

默认支持:

Tomcat(默认使用)

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

Jetty

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>

Undertow

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-undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>

4、嵌入式Servlet容器自动配置原理;

查看web容器自动配置类

2.0以下是:EmbeddedServletContainerAutoConfiguration

ServletWebServerFactoryAutoConfiguration:嵌入式的web服务器自动配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration(
proxyBeanMethods = false
)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableConfigurationProperties({ServerProperties.class})

//---看这里---
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
//导入BeanPostProcessorsRegistrar:Spring注解版;给容器中导入一些组件
//导入了EmbeddedServletContainerCustomizerBeanPostProcessor:(具体看下方)
//后置处理器:bean初始化前后(创建完对象,还没赋值赋值)执行初始化工作
EmbeddedTomcat.class,
EmbeddedJetty.class,
EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {

EmbeddedTomcat.class

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
   @Configuration(
proxyBeanMethods = false
)
//判断当前是否引入了Tomcat依赖;
@ConditionalOnClass({Servlet.class, Tomcat.class, UpgradeProtocol.class})
/**
*判断当前容器没有用户自己定义ServletWebServerFactory:嵌入式的web服务器工厂;
*作用:创建嵌入式的web服务器
*/
@ConditionalOnMissingBean(
value = {ServletWebServerFactory.class},
search = SearchStrategy.CURRENT
)
static class EmbeddedTomcat {
TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}

ServletWebServerFactory:嵌入式的web服务器工厂

1
2
3
4
5
@FunctionalInterface
public interface ServletWebServerFactory {
//获取嵌入式的Servlet容器
WebServer getWebServer(ServletContextInitializer... initializers);
}

工厂实现类

ServletWebServerFactory的实现

WebServer:嵌入式的web服务器实现

WebServer的实现

TomcatServletWebServerFactory为例,下面是TomcatServletWebServerFactory类
通过把一些配置信息写在tomcat.class中,创建TomcatWebServer服务

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
   public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}

//创建一个Tomcat
Tomcat tomcat = new Tomcat();

//配置Tomcat的基本环境,(tomcat的配置都是从本类获取的,tomcat.setXXX)
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();

while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}

this.prepareContext(tomcat.getHost(), initializers);

//将配置好的Tomcat传入进去,返回一个WebServer;并且启动Tomcat服务器
return this.getTomcatWebServer(tomcat);
}

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
//当端口大于零时,TomcatWebServer中的autoStart当此刻端口大于零时生效
return new TomcatWebServer(tomcat, getPort() >= 0);
}

5、我们对嵌入式容器的配置修改是怎么生效?

1
2
3
4
5
6
7
8
  //首先导入一个静态内部类
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
.......

BeanPostProcessorsRegistrar:后置处理器注册器(也是给容器注入一些组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;

public BeanPostProcessorsRegistrar() {...}

public void setBeanFactory(BeanFactory beanFactory) throws BeansException {...}

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (this.beanFactory != null) {
//注册了下面两个组件
this.registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", WebServerFactoryCustomizerBeanPostProcessor.class);
this.registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class);
}
}
private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {...}
}

webServerFactoryCustomizerBeanPostProcessor

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
public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {

......

//在Bean初始化之前
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//判断添加的Bean是不是WebServerFactory类型的
if (bean instanceof WebServerFactory) {
this.postProcessBeforeInitialization((WebServerFactory)bean);
}

return bean;
}

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
//获取所有的定制器,调用每一个定制器的customize方法来给Servlet容器进行属性赋值;
((Callbacks)LambdaSafe.callbacks(WebServerFactoryCustomizer.class, this.getCustomizers(), webServerFactory, new Object[0]).withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)).invoke((customizer) -> {
customizer.customize(webServerFactory);
});
}

步骤:

1)、SpringBoot根据导入的依赖情况,给容器中添加相应的AbstractServletWebServerFactory【比如TomcatServletWebServerFactory】

2)、容器中某个组件要创建对象就会惊动后置处理器;WebServerFactoryCustomizerBeanPostProcessor;

     只要是嵌入式的Servlet容器工厂,后置处理器就工作;

3)、后置处理器,从容器中获取所有的WebServerFactoryCustomizer,调用定制器的定制方法

6、嵌入式Servlet容器启动原理;

什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat;

获取嵌入式的Servlet容器工厂:

1)、SpringBoot应用启动运行run方法
2)、在SpringApplication类中refreshContext(context);

SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每一个组件】;如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext,否则:AnnotationConfigApplicationContext

SpringApplication.run

SpringApplication.createApplicationContext

3)、在ServletWebServerApplicationContext中,refresh(context);刷新刚才创建好的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
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
stopAndReleaseWebServer();
throw ex;
}
}

在父类的refresh方法中
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();
}
4)、在父类的refresh()方法中调用了onRefresh()方法; 而web的ioc容器重写了onRefresh方法
1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
5)、webioc容器会创建嵌入式的Servlet容器;createWebServer ();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
//首先获取一个ServletWebServerFactory
//一旦创建了一个工厂,如TomcatServletWebServerFactory,后置处理器一看是这个对象,就会获取所有的定制器customizer来给当前的servlet容器进行赋值
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();
}
6)、获取嵌入式的Servlet容器工厂:
1
2
3
4
5
6
7
8
9
10
11
12
13
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

从ioc容器中获取ServletWebServerFactory 组件;TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;

7)、使用容器工厂获取嵌入式的Servlet容器:

this.webServer = factory.getWebServer(getSelfInitializer());

8)、嵌入式的Servlet容器创建对象并启动Servlet容器;

先启动嵌入式的Servlet容器,在根据refresh()中的流程将剩下的组件创建出来

IOC容器启动创建嵌入式的Servlet容器

7、使用外置的Servlet容器

嵌入式Servlet容器:应用打成可执行的jar包

优点:简单、便携;

缺点:默认不支持JSP、优化定制比较复杂(使用定制器【ServerProperties、自定义EmbeddedServletContainerCustomizer】,自己编写嵌入式Servlet容器的创建工厂【EmbeddedServletContainerFactory】);

外置的Servlet容器:外面安装Tomcat—应用war包的方式打包;

步骤

1)、必须创建一个war项目;(利用idea创建好目录结构)

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);
}

}

4)、启动服务器就可以使用;

原理

jar包:执行SpringBoot主类的main方法,启动ioc容器,创建嵌入式的Servlet容器;

war包:启动服务器,服务器启动SpringBoot应用【SpringBootServletInitializer】,启动ioc容器;

servlet3.0(Spring注解版):

8.2.4 Shared libraries / runtimes pluggability:

规则

1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例:

2)、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名

3)、还可以使用**@HandlesTypes**,在应用启动的时候加载我们感兴趣的类;

流程

1)、启动Tomcat

2)、org\springframework\spring-web\4.3.14.RELEASE\spring-web-4.3.14.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer:

Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer
在这里插入图片描述

3)、SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set<Class<?>>;为这些WebApplicationInitializer类型的类创建实例;

4)、每一个WebApplicationInitializer都调用自己的onStartup;

5)、相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法
(项目中的的ServletInitializer继承自SpringBootServletInitializer,而SpringBootServletInitializer实现了WebApplicationInitializer接口,相当于当项目启动时,将我们项目中的ServletInitializer类实例化了)

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
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}

public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList();
Iterator var4;
if (webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();

while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}

if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();

while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}

}
}
}

6)、SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext;创建容器

1
2
3
4
5
6
7
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
。。。。。。}
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
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
//1、创建SpringApplicationBuilder
SpringApplicationBuilder builder = createSpringApplicationBuilder();
StandardServletEnvironment environment = new StandardServletEnvironment();
environment.initPropertySources(servletContext, null);
builder.environment(environment);
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);

//调用configure方法,子类重写了这个方法,将SpringBoot的主程序类传入了进来
builder = configure(builder);
// 子类重写的configure方法
// @Override
// protected SpringApplicationBuilder configure(SpringApplicationBuilder //application) {//springboot应用启动的类
// return application.sources(SpringBoot04WebJspApplication.class);
// }
//)

//使用builder创建一个Spring应用
SpringApplication application = builder.build();
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
//启动Spring应用
return run(application);
}

7)、Spring的应用就启动并且创建IOC容器(SpringApplication)(和前面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
31
32
33
34
35
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);

//刷新IOC容器
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}

启动Servlet容器,再启动SpringBoot应用

五、Docker

详细请看

1.简介

Docker是一个开源的应用容器引擎;是一个轻量级容器技术;

Docker支持将软件编译成一个镜像;然后在镜像中各种软件做好配置,将镜像发布出去,其他使用者可以直接使用这个镜像;

运行中的这个镜像称为容器,容器启动是非常快速的。

原理

2.核心概念

docker主机(Host):安装了Docker程序的机器(Docker直接安装在操作系统之上);

docker客户端(Client):连接docker主机进行操作;

docker仓库(Registry):用来保存各种打包好的软件镜像;

docker镜像(Images):软件打包好的镜像;放在docker仓库中;

docker容器(Container):镜像启动后的实例称为一个容器;容器是独立运行的一个或一组应用




3.安装docker

1.检查内核版本,必须是3.10及以上

1
uname -r    

2安装docker.

1
yum install docker

3.启动docker

1
2
systemctl start docker
docker -v 查看版本,检查有没有成功安装

4、开机启动docker

1
systemctl enable docker


3.docker常用命令&操作

1)、镜像操作

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

2)修改镜像源(一定要修改,不然慢死)

修改 /etc/docker/daemon.json ,写入如下内容(如果文件不存在请新建该文件)

# 内容:

1
2
3
4
5
6
7
8
9
vim /etc/docker/daemon.json
---------
{
"registry-mirrors":["http://hub-mirror.c.163.com"]
}
-----------
sudo systemctl daemon-reload//重新加载
-----------
sudo systemctl restart docker//重启
国内镜像源地址
Docker官方中国区https://registry.docker-cn.com
网易http://hub-mirror.c.163.com
中国科技大学https://docker.mirrors.ustc.edu.cn
阿里云https://pee6w651.mirror.aliyuncs.com

(推荐:可以在阿里云官网搜索容器镜像服务,创建一个镜像服务,从镜像加速器获取地址)

3)、容器操作

步骤(以tomcat为例:):

1.下载tomcat镜像

1
docker pull tomcat

如需选择具体版本,可以在https://hub.docker.com/搜索tomcat

1
docker pull tomcat:7.0.96-jdk8-adoptopenjdk-hotspot

2.根据镜像启动容器,不加TAG默认latest,如果没有下载latest会先去下载再启动

1
2
#错误的创建,没有端口映射,正确的在下方
docker run --name mytomcat -d tomcat:latest

–name:给容器起个名字

-d:后台启动,不加就是前端启动,然后你就只能开一个新的窗口连接,不然就望着黑乎乎的窗口,啥也干不了,Ctrl+C即可退出,当然,容器也会关闭
完整命令:
docker run -it -p 8888:8888 --name test (-v id:文件 文件) jiang/centostest (/bin/bash,dockerfile后面已====有该CMD,直接进入该容器)
docker run -p 8888:8888 --name test (-v id:文件 文件) -d jaing/centostest (在后台运行,但如果前台没有一个进程会自动结束,因为-d命令会覆盖CMD)
docker run -it --name dc02 --volumes-from dc01 tulong/centos通过数据卷容器创建容器实现共享

3.查看运行中的容器

1
docker ps

4.停止运行中的容器

1
2
3
4
5
docker stop  容器的id

# 或者

docker stop 容器的名称,就是--name给起的哪个名字

5.查看所有的容器

1
docker ps -a

6.启动容器

1
docker start 容器id/名字

7.删除一个容器

1
docker rm 容器id/名字

8.启动一个做了端口映射的tomcat

1
2
3
docker run -d -p 8888:8080 tomcat
#或者
docker run -p 8888:8080--name tomcatt -d tomcat

-d:后台运行

-p: 将主机的端口映射到容器的一个端口 主机端口(8888):容器内部的端口(8080)

外界通过主机的8888端口就可以访问到tomcat,前提是8888端口开放

注意阿里云环境下需要配置(搞了我好久):

1
2
3
docker exec -it tomcat 容器id(通过docker ps查看)  /bin/bash
rmdir webapps
mv webapps.dist webapps

因为所有的文件都在webapps.dist下,而访问的是webapps下的内容

9.关闭防火墙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看防火墙状态
service firewalld status

# 关闭防火墙
service firewalld stop

#设置防火墙对该端口不设拦截(推荐)
iptables -I INPUT -p tcp --dport 8889 -j ACCEPT
#或者
firewall-cmd --zone=public --add-port=22/tcp --permanent
firewall-cmd --reload #重新加载
firewall-cmd --zone=public --query-port=22/tcp #查询是否生效
firewall-cmd --zone=public --remove-port=22/tcp --permanent #限制端口
firewall-cmd --zone=public --list-ports #查看端口

阿里云环境下还要增加一个安全组

10.查看容器的日志

1
2
docker logs 容器id/名字
docker logs -ft --tail 倒数几条 容器ID

11.可以用一个镜像启动多个容器

1
docker run -d -p 8889:8080 tomcat

12. 查看容器的内部细节

1
docker inspect 容器ID

13. 进入正在运行的容器并以命令行交互:

1
2
3
4
5
6
7
8
直接进入容器启动命令的终端,不会启动新的进程

docker attach 容器ID

是在容器中打开新的终端,并且可以启动新的进程

docker exec -it 容器ID /bin/bash

14. 退出容器

1
2
3
exit 容器停止并退出

Ctrl + P + Q 容器不停止退出

15.从容器内拷贝文件到主机上(主机到容器类似):

1
docker cp 容器ID:容器内路径 目标主机路径

16.镜像Commit

1
docker commit -m="提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[标签名]

17.build镜像

1
docker build -f /mydocker/dockerfile(文件路径) -t tulong/centos (镜像名) .  (注意最后面有一个点)

18.数据卷容器

1
docker run -it --name dc02 --volumes-from dc01 tulong/centos

配置文件

1
每次修改配置文件记得重启一下容器

命名的容器挂载数据卷,其它容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器,称之为数据卷容器.
容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用它为止

更多命令参看
https://docs.docker.com/engine/reference/commandline/docker/
可以参考每一个镜像的文档



### 4.安装myqsl(5.5版本,5.7版本有问题) ##### 获取镜像
1
docker pull mysql:5.5
##### 创建容器
1
docker  run -p 3306:3306 --name mysqldb -e MYSQL_ROOT_PASSWORD=123456-d mysql:5.5
##### 出现错误,删除该容器,并`重启服务`
1
systemctl restart docker
##### 配置防火墙
1
2
iptables -I INPUT -p tcp --dport 8889 -j ACCEPT

##### 配置阿里云安全组
其他的几个高级操作
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的一些配置参数

5.安装Redis

1
docker run -p 6379:6379 --name redis -v /root/redis/data:/data -v /root/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf -d redis redis-server /usr/local/etc/redis/redis.conf --appendonly yes --requirepass "jcl5412415845"

6.安装nginx

记得挂载conf.d和nginx.conf,其中conf.d里有一个default.conf,并且还需要挂载一个存放静态资源的文件夹,不然很麻烦。

1
[root@iZ2ze71edtq2kxflzq2m01Z conf.d]# docker run  --name nginxtest -d -p 80:80  -v /root/nginx/conf.d:/etc/nginx/conf.d  -v /root/nginx/nginx.conf:/etc/nginx/nginx.conf  -v /root/nginx/html:/usr/share/nginx/html   nginx 

六、SpringBoot与数据访问

1、JDBC

1). 测试

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
1
2
3
4
5
6
spring:
datasource:
username: xxx
password: xxx
url: jdbc:mysql://localhost:3306/learn?characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver

springboot默认是使用com.zaxxer.hikari.HikariDataSource作为数据源,2.0以下是用org.apache.tomcat.jdbc.pool.DataSource作为数据源;

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

2). 自动配置原理:

dbc的相关配置都在org.springframework.boot.autoconfigure.jdbc包下

参考DataSourceConfiguration,根据配置创建数据源,默认使用Hikari连接池;可以使用spring.datasource.type指定自定义的数据源类型;

springboot默认支持的连池:

  • org.apache.commons.dbcp2.BasicDataSource
  • com.zaxxer.hikari.HikariDataSource
  • org.apache.tomcat.jdbc.pool.DataSource

自定义数据源类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Generic DataSource configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {

@Bean
public DataSource dataSource(DataSourceProperties properties) {
//使用DataSourceBuilder创建数据源,利用反射创建响应type的数据源,并且绑定相关属性
return properties.initializeDataSourceBuilder().build();
}

}

3). 启动引用执行sql

SpringBoot在创建连接池后还会运行预定义的SQL脚本文件,具体参考org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationConfiguration配置类,

在该类中注册了dataSourceInitializerPostProcessor(老版本使用的是ApplicationListener)

下面是获取schema脚本文件的方法

1
List<Resource> scripts = this.getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");

DataSourceInitializer
DataSourcePeoperties

可以看出,如果我们没有在配置文件中配置脚本的具体位置,就会在classpath下找schema-all.sqlschema.sql ,platform获取的是all,platform可以在配置文件中修改

具体查看createSchema()方法和initSchema()方法

initSchema()方法获取的是data-all.sqldata.sql

我们也可以在配置文件中配置sql文件的位置(getScripts方法中的resources就是配置文件DataSourcePeoperties中的一个list集合,为data和schema)

1
2
3
4
5
spring:
datasource:
schema:
- classpath:department.sql
- 指定位置

测试:

在类路径下创建schema.sql,运行程序查看数据库是否存在该表

1
2
3
4
5
6
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

程序启动后发现表并没有被创建,DEBUG查看以下,发现在运行之前会有一个判断
DataSourceInitializer

1
2
3
4
void initSchema() {
List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
......
}

DataSourceInitializer
上面方法也不知道在干什么,反正就是只要是NEVER和EMBEDDED就为true,而DataSourceInitializationMode枚举类中除了这两个就剩下ALWAYS了,可以在配置文件中配置为ALWAYS

DataSourceInitializer

1
2
3
4
5
6
7
spring:
datasource:
username: root
password: root
url: jdbc:mysql://172.16.145.137:3306/springboot
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always

schema.sql,schema-all.sql:建表语句

data.sql,data-all.sql:插入数据

当然混合使用也可以,愿意咋来咋来

注意:项目每次启动都会执行一次sql

4)、操作数据库:自动配置了JdbcTemplate操作数据库

2、整合Druid数据源

选择哪个数据库连接池

  • DBCP2 是 Appache 基金会下的项目,是最早出现的数据库连接池 DBCP 的第二个版本。
  • C3P0 最早出现时是作为 Hibernate 框架的默认数据库连接池而进入市场。
  • Druid 是阿里巴巴公司开源的一款数据库连接池,其特点在于有丰富的附加功能。
  • HikariCP 相较而言比较新,它最近两年才出现,据称是速度最快的数据库连接池。最近更是被 Spring 设置为默认数据库连接池。

不选择 C3P0 的原因:

  • C3P0 的 Connection 是异步释放。这个特性会导致释放的在某些情况下 Connection 实际上 still in use ,并未真正释放掉,从而导致连接池中的 Connection 耗完,等待状况。
  • Hibernate 现在对所有数据库连接池一视同仁,官方不再指定『默认』数据库连接池。因此 C3P0 就失去了『官方』光环。


不选择 DBCP2 的原因:

  • 相较于 Druid 和 HikariCP,DBCP2 没有什么特色功能/卖点。基本上属于 能用,没毛病 的情况,地位显得略有尴尬。

在 Spring Boot 项目中加入druid-spring-boot-starter依赖 (点击查询最新版本)

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>

配置信息

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
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.1.2:3306/learn?characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always
type: com.alibaba.druid.pool.DruidDataSource
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
initialSize: 5 #初始化创建的连接数
minIdle: 5 #最小空闲连接数
maxActive: 20 #最大活跃连接数,不宜设置过多
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
#用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。
validationQuery: select 'x';
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,
testWhileIdle: true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
#是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
poolPreparedStatements: true
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
#在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,stat用于监控统计,'wall'用于防火墙,防御sql注入,slf4j用于日志
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
#创建一个druidconfig类,在里面注册一个StatViewServlet和WebStatFilter,也可以在类中配置
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: root
allow: 127.0.0.1

类中配置

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
@Configuration
public class DruidConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource durid(){
return new DruidDataSource();
}

//配置Durid的监控
//1.配置一个管理后台的servlet
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean=new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
//可以配置一些初始化参数
Map<String,String> initParams=new HashMap<>();
initParams.put("loginUsername","admin");
initParams.put("loginPassword","123456");
initParams.put("allow",""); //“”默认为允许所有
initParams.put("deny","192.168.15.22"); //拒绝某个ip访问
bean.setInitParameters(initParams);
return bean;
}


//2.配置一个监控的filter
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean=new FilterRegistrationBean(new WebStatFilter());
bean.setUrlPatterns(Arrays.asList("/*"));

Map<String,String> initParams=new HashMap<>();
initParams.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
return bean;
}

}

3、整合MyBatis

1). 引入依赖

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
    <modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</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-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

依赖关系图

依赖关系图

步骤:

      1)、配置数据源相关属性(见上一节Druid)

      2)、给数据库建表

      3)、创建JavaBean

2).注解版

开启驼峰命名法

我们的实体类和表中的列名一致,一点问题也没有

我们把department表的departmentName列名改为department_name看看会发生什么

访问:http://localhost:8080/dep/1获取数据

1
[{"id":1,"departmentName":null}]

由于列表和属性名不一致,所以就没有封装进去,我们表中的列名和实体类属性名都是遵循驼峰命名规则的,可以开启mybatis的开启驼峰命名配置

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true

然后重启项目,重新插入数据,再查询就发现可以封装进去了

也可以通过向spring容器中注入org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer的方法设置mybatis参数

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MybatisConfig {

@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(org.apache.ibatis.session.Configuration configuration) {
configuration.setMapUnderscoreToCamelCase(true);
}
};
}
}

MybatisAutoConfiguration
MybatisAutoConfiguration

Mapper扫描

使用@mapper注解的类可以被扫描到容器中,但是每个Mapper都要加上这个注解就是一个繁琐的工作,能不能直接扫描某个包下的所有Mapper接口呢,当然可以,在springboot启动类上加上@MapperScan

1
2
3
4
5
6
7
8
9
@MapperScan("cn.clboy.springbootmybatis.mapper")
@SpringBootApplication
public class SpringbootMybatisApplication {

public static void main(String[] args) {
SpringApplication.run(SpringbootMybatisApplication.class, args);
}

}

3).配置文件版

创建mybatis全局配置文件(建议别写,我的好像跟yml文件中有些配置冲突了,不知道的为什么,直接在yml配置一些就可以了)

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="cn.clboy.springbootmybatis.model"/>
</typeAliases>
<!--开启驼峰-->
</configuration>
1
2
3
mybatis:
config-location: classpath:mybatis/mybatis-config.xml 指定全局配置文件的位置
mapper-locations: classpath:mybatis/mapper/*.xml 指定sql映射文件的位置

配置文件(application.yaml)中指定配置文件和映射文件的位置

1
2
3
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml

4).完整使用

创建一个接口

1
2
3
4
5
6
@Mapper //可以使用MapperScan注解
public interface EmployeeMapper {
public Employee getEmpById(@Param("id") int id);

public void insertEmp(Employee emp);
}

创建一个mapper的xml文件(直接在resources下创建一个mybatis包进行创建)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.EmployeeMapper">
<select id="getEmpById" resultType="Employee" parameterType="int">
select * from employee where id=#{id}
</select>

<insert id="insertEmp" parameterType="Employee" useGeneratedKeys="true" keyProperty="id">
insert into employee values(#{id},#{lastName},#{eami},#{gender},#{d_id})
</insert>
</mapper>

yml文件进行配置mybatis的configuration(或者用ConfigurationCustomizer进行配置)

1
2
3
4
5
6
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml #配置configuration.xml的位置
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.example.demo.pojo

附:

使用mybatis的xml文件进行配置

包图

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- <settings>-->
<!-- <setting name="mapUnderscoreToCamelCase " value="true"/>-->
<!-- </settings>-->
<typeAliases>
<package name="com.example.demo.pojo"/>
</typeAliases>
</configuration>

千万别写上上面yml中的其他配置,可能会报错,并且我前面在yml或者ConfiguratinoCustomizer中加入的驼峰命名法配置也失效了,要在xml文件中配置才生效

1
2
3
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml

更多使用参照

http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

4、整合SpringData JPA

1)、SpringData简介

2)、整合SpringData JPA

JPA:ORM(Object Relational Mapping);

1)、编写一个实体类(bean)和数据表进行映射,并且配置好映射关系;

1
2
3
4
5
6
7
8
9
10
11
12
13
//使用JPA注解配置映射关系
@Entity //告诉JPA这是一个实体类(和数据表映射的类)
@Table(name = "tbl_user") //@Table来指定和哪个数据表对应;如果省略默认表名就是user;
public class User {

@Id //这是一个主键
@GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键
private Integer id;

@Column(name = "last_name",length = 50) //这是和数据表对应的一个列
private String lastName;
@Column //省略默认列名就是属性名
private String email;

2)、编写一个Dao接口来操作实体类对应的数据表(Repository)

1
2
3
4
//继承JpaRepository来完成对数据库的操作
public interface UserRepository extends JpaRepository<User,Integer> {
}

3)、基本的配置JpaProperties

1
2
3
4
5
6
7
spring:  
jpa:
hibernate:
# 更新或者创建数据表结构
ddl-auto: update
# 控制台显示SQL
show-sql: true

七、启动配置原理

几个重要的事件回调机制

配置在META-INF/spring.factories

ApplicationContextInitializer

SpringApplicationRunListener

只需要放在ioc容器中

ApplicationRunner

CommandLineRunner

启动流程:
启动流程

1、创建SpringApplication对象

1
2
3
4
public static void main(String[] args) {
//xxx.class:主配置类,(可以传多个)
SpringApplication.run(xxx.class, args);
}

从run方法开始,创建SpringApplication,然后再调用run方法

1
2
3
4
5
6
7
8
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//创建SpringApplication对象并执行run方法
return new SpringApplication(primarySources).run(args);
}
1
2
3
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
//保存主配置类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//获取当前应用的类型,是不是web应用
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//从类路径下找到META‐INF/spring.factories配置的所有ApplicationContextInitializer;然后保存起来,见2.2
setInitializers((Collection)
getSpringFactoriesInstances(ApplicationContextInitializer.class));
//从类路径下找到META‐INF/spring.ApplicationListener;然后保存起来,原理同上
setListeners((Collection)
getSpringFactoriesInstances(ApplicationListener.class));
//从多个配置类中找到有main方法的主配置类,见下图(在调run方法的时候是可以传递多个配置类的)
this.mainApplicationClass = deduceMainApplicationClass();
}

SpringApplication.java

1
2
3
4
5
6
7
8
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

SpringFactoriesLoader.java

1
2
3
4
5
6
7
8
9
10
11
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}


private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
······
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
······
}

2、运行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
  public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//声明IOC容器
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
//从类路径下META‐INF/spring.factories获取SpringApplicationRunListeners,原理同上方获取ApplicationContextInitializer和ApplicationListener
SpringApplicationRunListeners listeners = this.getRunListeners(args);
//遍历上一步获取的所有SpringApplicationRunListener,调用其starting方法
listeners.starting();
try {
//封装命令行
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//准备环境,把上面获取到的listeners传过去,见2.1
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
//打印Banner,就是控制台那个Spring字符画
Banner printedBanner = this.printBanner(environment);
//根据当前环境利用反射创建IOC容器
context = this.createApplicationContext();
//从类路径下META‐INF/spring.factories获取SpringBootExceptionReporter,原理同2中获取ApplicationContextInitializer和ApplicationListener
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//准备IOC容器,见2.2
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新IOC容器,可查看配置嵌入式Servlet容器原理
this.refreshContext(context);
//这是一个空方法
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
//调用所有SpringApplicationRunListener的started方法
listeners.started(context);
//见2.4 ,从ioc容器中获取所有的ApplicationRunner和CommandLineRunner进行回调,ApplicationRunner先回调,CommandLineRunner再回调
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}

try {
//调用所有SpringApplicationRunListener的running方法
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}

2.1

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
        //获取或者创建环境,有则获取,无则创建
        ConfigurableEnvironment environment = this.getOrCreateEnvironment();
        //配置环境
        this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach((Environment)environment);
        //创建环境完成后,调用前面获取的所有SpringApplicationRunListener的environmentPrepared方法
        listeners.environmentPrepared((ConfigurableEnvironment)environment);
        this.bindToSpringApplication((ConfigurableEnvironment)environment);
        if (!this.isCustomEnvironment) {
            environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
        }
    ConfigurationPropertySources.attach((Environment)environment);
    return (ConfigurableEnvironment)environment;
}

2.2

    protected ConfigurableApplicationContext createApplicationContext() {
        Class contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch(this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                    break;
                case REACTIVE:
                    contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                    break;
                default:
                    contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                }
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }
    return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}

2.3

    private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
        //将创建好的环境放到IOC容器中
        context.setEnvironment(environment);
        //注册一些组件
        this.postProcessApplicationContext(context);
        //获取所有的ApplicationContextInitializer调用其initialize方法,这些ApplicationContextInitializer就是在2步骤中获取的,见2.3.1
        this.applyInitializers(context);
        //回调所有的SpringApplicationRunListener的contextPrepared方法,这些SpringApplicationRunListeners是在步骤3中获取的
        listeners.contextPrepared(context);
    //打印日志
    if (this.logStartupInfo) {
        this.logStartupInfo(context.getParent() == null);
        this.logStartupProfileInfo(context);
    }

    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }

    if (beanFactory instanceof DefaultListableBeanFactory) {
        ((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }

    if (this.lazyInitialization) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }

    Set<Object> sources = this.getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    this.load(context, sources.toArray(new Object[0]));
    //回调所有的SpringApplicationRunListener的contextLoaded方法
    listeners.contextLoaded(context);
}

2.3.1

 protected void applyInitializers(ConfigurableApplicationContext context) {
        Iterator var2 = this.getInitializers().iterator();
    while(var2.hasNext()) {
        ApplicationContextInitializer initializer = (ApplicationContextInitializer)var2.next();
        Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
        Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
        initializer.initialize(context);
    }

}

2.4

 private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List runners = new ArrayList();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator()
     while(var4.hasNext()) {
    Object runner = var4.next();
    if (runner instanceof ApplicationRunner) {
        this.callRunner((ApplicationRunner)runner, args);
    }

    if (runner instanceof CommandLineRunner) {
        this.callRunner((CommandLineRunner)runner, args);
    }
}

}

3、事件监听机制

1).创建ApplicationContextInitializer和SpringApplicationRunListener的实现类,并在META-INF/spring.factories文件中配置

1
2
3
4
5
6
public class HelloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("ApplicationContextInitializer...inittialize..."+applicationContext);
}
}

HelloSpringApplicationRunListener,记得配置一个带两个参数的构造器,可以去SpringApplicationRunListener的其他实现类中查看

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
public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {

public HelloSpringApplicationRunListener(SpringApplication application,String[] args) {

}

@Override
public void starting() {
System.out.println("SpringApplicationRunListener...");
}

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
Object o=environment.getSystemProperties().get("os.name");
System.out.println("SpringApplicationRunListener...environmentPrepared"+o);
}

@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("SpringApplicationRunListener...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");
}

因为这两个类是通过加载spring.factories获得的,所以需要创建在spring.facories中配置
创建spring.factories

1
2
3
4
5
org.springframework.context.ApplicationContextInitializer=\
com.example.demo.listener.HelloApplicationContextInitializer

org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.HelloSpringApplicationRunListener

启动报错:说是没有找到带org.springframework.boot.SpringApplication和String数组类型参数的构造器,需要添加一个构造器,看上方代码
没写构造器的报错

2).创建ApplicationRunner实现类和CommandLineRunner实现类,注入到容器中

ApplicationRunner

1
2
3
4
5
6
7
@Component
public class HelloApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner...run\t--->"+ Arrays.asList(args));
}
}

CommandLineRunner

1
2
3
4
5
6
7
@Component
public class HelloCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("TestCommandLineRunn.runt\t--->"+ Arrays.toString(args));
}
}

4.修改Banner

1
2
3
4
5
6
7
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";

static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";

static final String DEFAULT_BANNER_LOCATION = "banner.txt";

static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };

默认是找类路径下的banner.txt,可以在配置文件中修改

1
spring.banner.location=xxx.txt

生成banner的网站:http://patorjk.com/software/taag

也可以使用图片(将其像素解析转换成assii编码之后打印),默认是在类路径下找名为banner后缀为"gif", "jpg", "png"的图片

1
static final String[] IMAGE_EXTENSION = new String[]{"gif", "jpg", "png"};

也可以在配置文件中指定

1
spring.banner.image.location=classpath:abc.png

八.自定义Starter

启动器只用来做依赖导入

专门来写一个自动配置模块;

启动器依赖自动配置模块,项目中引入相应的starter就会引入启动器的所有传递依赖
依赖关系

启动器

启动器模块是一个空 JAR 文件,仅提供辅助性依赖管理,这些依赖可能用于自动 装配或者其他类库

1.命名规约

  • 官方命名

spring-boot-starter-模块名

eg:spring-boot-starter-web、spring-boot-starter-jdbc、spring-boot-starter-thymeleaf

  • 自定义命名

模块名-spring-boot-starter

eg:mybatis-spring-boot-start

2.如何编写自动配置

1
2
3
4
5
6
7
@Configuration //指定这个类是一个配置类
@ConditionalOnXXX //在指定条件成立的情况下自动配置类生效
@AutoConfigureAfter //指定自动配置类的顺序
@Bean //给容器中添加组件
@ConfigurationPropertie结合相关xxxProperties类来绑定相关的配置
@EnableConfigurationProperties //让xxxProperties生效加入到容器中
public class XxxxAutoConfiguration {

自动配置类要能加载,需要将启动就加载的自动配置类配置在META-INF/spring.factories

eg:

1
2
3
4
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

3.案例

1.创建一个自动配置模块,和创建普通springboot项目一样,不需要引入其他starter

2.删除掉多余的文件和依赖

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
<?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 https://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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>atguigu-spring-boot-starter-autoconfigurer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>atguigu-spring-boot-starter-autoconfigurer</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>


<dependencies>
<!--引入spring‐boot‐starter;所有starter的基本配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--可以生成配置类提示文件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>


</project>

删掉了启动类
包图

3.创建配置类和自动配置类

1
2
3
4
5
6
7
ConfigurationProperties(prefix = "atguigu")
public class HelloProperties {
private String prefix;
private String suffix;

......省略set,get方法和构造器
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloService {


HelloProperties helloProperties;



public String sayHelloAtguigu(String name){
return helloProperties.getPrefix()+name+helloProperties.getSuffix();
}

.....省略set,get方法
}
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
@Configuration
@ConditionalOnWebApplication //web应用中生效
@EnableConfigurationProperties(HelloProperties.class) //让配置类生效
public class HelloServiceAutoConfiguration {

HelloProperties helloProperties;

/**
* 通过构造器注入
* @param helloProperties
*/
public HelloServiceAutoConfiguration(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}

@Bean
public HelloService helloService(){
HelloService service=new HelloService();
service.setHelloProperties(helloProperties);
return service;
}


}

4.在resources文件夹下创建META-INF/spring.factories
创建spring.factories

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.HelloServiceAutoConfiguration

5.安装到本地仓库

6.创建starter,选择maven工程即可,只是用于管理依赖,添加对AutoConfiguration模块的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigustarter</groupId>
<artifactId>atguigu-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<!--导入依赖-->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>atguigu-spring-boot-starter-autoconfigurer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

7.安装到本地仓库

8.创建项目测试,选择添加web场景,因为设置是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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?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 https://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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>spring-boot-08-starter-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-08-starter-test</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>com.atguigustarter</groupId>
<artifactId>atguigu-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

9.创建Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class HelloController {

@Autowired
HelloService helloService;

@GetMapping("hello")
public String hello(){
String str=helloService.sayHelloAtguigu("haha");
return str;
}

}

10.在配置文件中配置

1
2
atguigu.prefix=ATGUIGU
atguigu.suffix=DADAAD

九.缓存

1.JSR107

Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。

  1. CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。

  2. CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。

  3. Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。

  4. Entry是一个存储在Cache中的key-value对。

  5. Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置

2.基础环境搭建

1).引入cache模块和mybatis

1
2
3
4
5
6
7
8
9
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>

2) 导入数据库文件,创建出表

3)创建javaBean封装数据

4)整合MyBatis操作数据库

  1. 配置数据源信息

    1
    2
    3
    4
    5
    spring.datasource.url=jdbc:mysql://localhost:3306/springboot?characterEncoding=UTF-8&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    mybatis.configuration.map-underscore-to-camel-case=true
  2. 创建mapper,service和controller

3. 体验缓存

1) EnableCaching(开启基于注解的缓存)

1
2
3
4
@MapperScan("com.atguigu.cache.mapper")
@EnableCaching
@SpringBootApplication
public class SpringBoot01CacheApplication {

2)Cacheable(开启缓存)

1
@Cacheable(cacheNames = "emp",key = "#id")

4.相关概念

Cache缓存接口,定义缓存操作。实现有: RedisCache EhCacheCache ConcurrentMapCache
CacheManager**缓存管理器,管理各种缓存( ** Cache )组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存。
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时 key 生成策略
serialize**缓存数据时 ** value 序列化策略

5. @Cacheable的参数

cacheNames/value

      指定缓存的名字;将方法的返回结果放在哪个缓存(Cache)中,是数组的方式,可以指定多个。

key

      缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 1- 方法的返回值。也可以用SpEL编写

      key可以使用的一些值

methodNameroot object当前被调用的方法名#root.methodName
methodroot object当前被调用的方法#root.method.name
targetroot object当前被调用的目标对象#root.target
targetClassroot object当前被调用的目标对象类#root.targetClass
argsroot object当前被调用的方法的参数列表#root.args[0]
cachesroot object当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache#root.caches[0].name
argument nameevaluation context方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引;#iban 、 #a0 、 #p0
resultevaluation context方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false)#result
1
2
3
@Cacheable(cacheNames = "emp",key = "#id")
@Cacheable(cacheNames="emp",key="#root.args[0]")
@Cacheable(cacheNames = "emp",key="#root.methodName+'['+#id+']'")

keyGenerator

      key的生成器,可以自己指定key的生成器的组件id

         注意:key和keyGenerator二选一,不然会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyKeyGenerator {

@Bean
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString()+"]";
}
};
}
}
1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/emp/{id}")
public Employee getEmployee(@PathVariable("id") Integer id){
return employeeService.getEmp(id,"testGenerator", Arrays.asList("1","2","3"));
}

@Cacheable(cacheNames = "emp",condition = "#id>0",keyGenerator = "keyGenerator")
public Employee getEmp(Integer id, String name, List<String> list){
System.out.println("查询"+id+"员工");
Employee employee=employeeMapper.getEmpById(id);
return employee;
}

产生的key为:getEmp[[1, testGenerator, [1, 2, 3]]]

condition

      指定符合条件的情况下才缓存(在方法调用前或调用后都能判断)

1
@Cacheable(cacheNames = "emp",key = "#id",condition = "#id>0 and #root.methodName eq 'aaa'")

unless

       否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存

1
2
//@Cacheable(cacheNames = "emp",key = "#id",condition = "#id>0",unless = "#result==null")
@Cacheable(cacheNames = "emp",key = "#id",condition = "#id>1",unless = "#id==2")

       结果:当id=1或者id=2时不缓存

CacheManager

      管理多个Cache组件的,对缓存的真正CRUD操作在Cache中,每一个缓存组件有自己唯一一个名字。

cacheResolver

      指定获取解析器.

         注意:cacheManager和cacheResolver二选一

sync

       是否使用异步属性,当使用了异步模式,unless就不支持了

6. @CachePut

    @CachePut:既调用方法,又更新缓存数据。修改了数据库的某个数据,同时更新缓存

   调用时机(和Cacheable相反):

      1.先调用了目标方法

      2.将目标方法的结果放入缓存中

   测试步骤

        1.查询一号员工;查到的结果会放到缓存中

                   key:Id:1 ------value:lastName:张三

        2.以后查询还是之前的结果

        3.更新一号员工[lastName:zhangsan]

                  将方法的返回值也放进缓存了;

                  key:传入的employee对象 -------value:返回的employee对象

        4.查询一号员工

                  应该是更新后的员工(如果没有指定key,那么查询的结果没变。因为查询的key和更新产生数据的key不一样)

                        key="#employee.id":使用传入的参数的员工id

                        key=“#result.id”:使用返回的id       

1
2
3
4
5
6
@CachePut(cacheNames = "emp",key = "#employee.id")
public Employee updateEmp(Employee employee){
System.out.println("updateEmp"+employee);
employeeMapper.updateEmp(employee);
return employee;
}

注意:如果还是不一样,请检查更新的key和查询的key是否一样。还有可能一个用的是key,一个是keyGeneratoer

7. @CacheEvict

      key="#id":清除指定key的缓存

      allEntries=false/true:是否清除全部key的缓存

      beforeInvocation=false/true:是否在方法调用前删除,默认是在方法执行之后清除。

         当为false(默认)时,如果出现异常,缓存则不会被清除

         当为true时,不敢有没有出现异常,都会被清除

1
2
3
4
@CacheEvict(cacheNames = "emp",/*key = "#id"*/allEntries = true,beforeInvocation=true)
public void deleteEmp(Integer id){
System.out.println("deleteEmp:"+id);
}

8.@Caching

       用于构建复杂的缓存条件(cacheable,put,evict)

1
2
3
4
5
6
@Caching(
cacheable = {@Cacheable(value = "emp",key = "#lastName")},
put = {@CachePut(value = "emp",key = "#result.id"),
@CachePut(value = "emp",key = "#result.email")}
//evict={}
)

9.@CacheConfig

       用于配置公共的参数(cacheNames,keyGenerator,cacheManager,cacheResolver)

10.缓存工作原理

CacheAutoConfiguration

1
2
3
4
5
6
7
8
9
//没有自定义CacheManager或者CacheResolver才自动配置
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
//将CacheProperties注入到容器中
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
//导入CacheConfigurationImportSelector到容器中
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {

CacheConfigurationImportSelector

1
2
3
4
5
6
7
8
9
10
11
12
13
static class CacheConfigurationImportSelector implements ImportSelector {

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}

}

导入的配置类:

哪个配置默认生效(通过debug=true查看)?

SimpleCacheConfiguration

给容器中注册一个一个CacheManager:ConcurrentMapCacheManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
//当前容器中有CacheManager就不创建CacheManager
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {

//给容器中注册一个CacheManager
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}

}

ConcurrentMapCacheManager

可以获取ConcurrentMapCache类型的缓存组件,并且保存到cacheMap中;(CacheNames-ConcurrentMapCache)

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

...
//创建一个ConcurrentMapCache类型的缓存组件
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256),
isAllowNullValues(), actualSerialization);
}
}

ConcurrentMapCache

ConcurretnMapCache的作用是将数据保存在ConcurrentMap当中(key-value)

1
2
3
4
5
public class ConcurrentMapCache extends AbstractValueAdaptingCache {

private final String name;

private final ConcurrentMap<Object, Object> store;

运行流程

  1. 方法运行之前,先去ConcurrentMapCacheManager中查询Cache(缓存组件),按照cacheNames指定的名字来获取。第一次获取缓存如果没有Cache组件会自动创建出来放在cacheMap中。

ConcurrentMapCacheManager

  1. 在Cache中查找缓存的内容,使用一个key,默认就是方法的参数

    ConcurrentMapCache

哪么这个key是怎么生成的呢?

CacheAspectSupport中,通过keyGenerator中获取。

CacheAspectSupport

1
2
3
4
5
6
7
8
9
@Nullable
protected Object generateKey(@Nullable Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
}
//通过keyGenerator获取
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}

keyGenerator(SimpleKeyGenerator)是通过CacheOperationMetadata中获取

1
2
3
4
5
protected static class CacheOperationMetadata {
...
private final KeyGenerator keyGenerator;
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
//public static final SimpleKey EMPTY = new SimpleKey();
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
  • 如果没有参数,会自己new一个SimpleKey对象
  • 如果就一个参数,key=参数的值
  • 如果有多个参数,key=new SimpleKey(params);
  1. 没有查到缓存就调用目标方法
  2. 将目标方法放到map当中(ConcurrentMap),以后再要调用,就可以直接使用缓存中的数据

核心

  • 使用CacheManager(ConcurrentMapCacheManager)按照名字得到Cache(ConcurrentMapCache)组件
  • key使用keyGenerator生成的,默认是SimpleKeyGenerator

11.搭建redis环境

1).docker运行redis

1
docker run -p 6379:6379 --name redis -v /root/redis/data:/data -v /root/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf -d redis redis-server /usr/local/etc/redis/redis.conf --appendonly yes --requirepass "xxx"

2).安装软件(不需要)

3)导入依赖

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

4)修改配置文件,连接redis

1
2
spring.redis.host=192.168.10.1
spring.redis.password=xxx

12.RedisAutoConfiguration

创建了两个用于操作redis数据库的组件:RedisTemplateStringRedisTemplate(用于操作字符串的)

(类似于操作sql的JdbcTemplate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

13.测试

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
@Autowired
RedisTemplate redisTemplate;

@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
RedisTemplate empRedisTemplate;

@Test
void testRedis(){
//在Redis中某个key追加数据
stringRedisTemplate.opsForValue().append("msg","hello");
//从Redis中获取数据
String msg=stringRedisTemplate.opsForValue().get("msg");
System.out.println(msg);

//对list进行lpush
stringRedisTemplate.opsForList().leftPush("list1","10");
stringRedisTemplate.opsForList().leftPush("list1","11");
}

//测试存储一个对象
@Test
void testRedis2(){
Employee employee=employeeMapper.getEmpById(2);
//默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中
stringRedisTemplate.opsForValue().set("emp-02", String.valueOf(employee));
//将数据以json的方式保存
//(1)自己将对象转为json
//(2)修改redisTemplate默认的序列化规则
empRedisTemplate.opsForValue().set("emp-01",employee);
}

结果

1
2
3
4
5
6
7
8
9
10
key:emp-02 value:Employee [id=2, lastName=lisi, email=null, gender=0, dId=null]
key:"emp-01"
value:
{
"id": 2,
"lastName": "lisi",
"email": null,
"gender": 0,
"dId": null
}

当存储对象时,默认是使用序列化的方式

RedisTemplate默认使用的序列化器JdkSerializationRedisSerializer

1
2
3
4
5
if (defaultSerializer == null) {

defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}

可以自己修改默认的序列化规则

Redis序列化方式

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyRedisConfig {

@Bean
public RedisTemplate<Object, Object> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
template.setDefaultSerializer(jackson2JsonRedisSerializer);
return template;
}
}

14.RedisTemplate配置

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
/**
* redis配置类
*
*/
@Configuration
public class RedisConfig {

@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

15.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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
package top.codekiller.manager.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* @author codekiller
* @date 2020/5/26 23:58
*
* redis的工具类
*/

@Slf4j
public class RedisUtils {

@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("指定缓存失效时间失败",e);
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("判断key是否存在错误",e);
return false;
}
}

/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}

/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("String类型放入缓存错误",e);
return false;
}
}

/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error("String类型放入限时缓存错误",e);
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}

/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}

// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}

/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error("hash类型在指定的key中放入多个键值缓存错误",e);
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("hash类型在指定的key中放入多个限时键值缓存错误",e);
return false;
}
}

/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error("hash类型在指定的key中放入一对键值错误",e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error("hash类型在指定的key(限时)中放入一对键值错误" ,e);
return false;
}
}

/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/

public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}

/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}

/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error("set类型通过指定的key获取值错误",e);
return null;
}
}

/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
log.error("set类型中在指定的key中查询值是否存在错误",e);
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error("set类型缓存增加错误",e);
return 0;
}
}

/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
log.error("set类型限时缓存增加错误");
return 0;
}
}

/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
log.error("set类型缓存获取长度错误",e);
return 0;
}
}

/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
log.error("set类型缓存移除值错误",e);
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error("list缓存获取内容错误",e);
return null;
}
}

/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error("list缓存获取内容长度失败",e);
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error("list缓存通过索引获取值失败",e);
return null;
}
}

/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error("list放入缓存错误",e);
return false;
}
}

/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
log.error("list限时放入缓存错误",e);
return false;
}
}

/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error("list类型缓存中增加多个值错误",e);
return false;
}
}

/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
log.error("缓存中放入list(限时)类型多个值错误",e);
return false;
}
}

/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {

redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error("list类型中通过索引修改数据错误",e);
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
log.error("list类型中移除多个指定值错误",e);
return 0;
}
}

}

-------------本文结束感谢您的阅读-------------
六经蕴籍胸中久,一剑十年磨在手

欢迎关注我的其它发布渠道