Springboot核心技术学习笔记一
- 第 1 章 SpringBoot 入门
- 1.1 Spring Boot 简介
- 1.2 微服务
- 1.3 环境准备
- 1.4 SpringBoot HelloWorld
- 1.5 探究 HelloWorld
- pom 文件
- 导入依赖
- Main 主程序类
- 启动日志分析
- 1.6 快速创建 SpringBoot 项目
- 第 2 章 SpringBoot 配置
- 2.1 配置文件
- 2.2 YAML 语法
- 基本语法
- 值的写法
- 配置属性列表
- 2.3 配置文件值的注入
- @ConfigurationProperties 配置文件属性的绑定
- 松散绑定
- Properties 乱码问题
- @ConfigurationProperties 与 @Value
- @PropertySource 与 @ImportResource
- @Configuration 和 @Bean
- 2.3 配置文件占位符
- 属性配置占位符
- 随机数
- 2.4 Profile 多环境配置文件切换
- 2.5 配置文件加载位置
- 2.6 配置文件加载顺序(重点)
- 2.7 自动配置原理(重难点)
- 2.8 @Conditional 自动配置报告
- 第 3 章 SpringBoot 日志
- 3.1 常见日志框架
- 3.2 SLF4j 使用
- 3.3 SpringBoot 日志关系(了解)
- 3.4 日志使用
- 常见日志配置属性
- 指定日志框架配置文件
- 3.5 切换日志框架
- 第 4 章 SpringBoot 与 WEB 开发
-
4.1 简介
-
4.2 SpringBoot 静态资源映射规则
- Webjars
-
4.3 模板引擎
- 引入Thymeleaf
- Thymeleaf 的使用
- Thymeleaf 语法
-
4.4 SpringMVC 自动配置
-
4.5 修改 SpringBoot 默认配置
-
4.6 Restful crud 项目
-
- 国际化
- 国际化原理
- 实现语言切换功能
- 登录
- 显示员工列表
- 跳转到员工页面
- 添加员工
- 重定向与转发
- 日期格式化
- 跳转到员工编辑页面
- 编辑员工信息
- 员工删除
-
-
4.7 定制错误页面
- SpringBoot 默认错误处理机制
-
4.8 配置嵌入式 Servlet 容器
- 修改 Servlet 容器的相关配置
- 使用其他嵌入式 Servlet 容器
-
4.9 嵌入式 Servlet 容器自动配置原理
-
4.10 嵌入式 Servlet 容器启动原理
-
4.11 使用外置的 Servlet 容器
-
- 第 5 章 SpringBoot 与 Docker
- 5.1 Docker 简介
- 5.2 核心概念
- 5.3 安装Docker
- 安装linux虚拟机
- 在linux虚拟机上安装docker
- 5.4 Docker常用命令&操作
- 镜像操作
- 容器操作
- 安装 MySQL 示例
- 第 6 章 SpringBoot 与数据访问
- 6.1 数据源初始化与 JDBC
- 配置 MySQL
- 数据源自动配置原理
- 数据表自动初始化
- 使用 JdbcTemplate 查询数据
- 数据库自动初始化原理
- 6.2 使用外部数据源
- 6.3 自定义数据源原理
- 6.4 配置 Druid 数据源
- 6.5 整合 MyBatis
- 注解版
- Mybatis 常见配置
- xml 版
- 6.6 整合 SpringData JPA
- Spring Data 简介
- 整合 SpringData JPA
- 6.1 数据源初始化与 JDBC
- 第 7 章 SpringBoot 启动配置原理
- 7.1 启动流程
- 创建SpringApplication对象
- 运行run方法
- 7.2 事件监听机制
- 7.1 启动流程
- 第 8 章 SpringBoot 自定义 starter
- 8.1 starter 原理
- 8.2 自定义 starter
- SpringBoot 与开发热部署
- 进阶学习
- 待补充
- 推荐阅读
- 参考文档
你无法掌握所有的知识,抓大放小,不要关注边边角角的知识,保证最重要的知识烂熟于胸即可 —— 罗翔
第 1 章 SpringBoot 入门
1.1 Spring Boot 简介
简化Spring应用开发的一个框架;
整个Spring技术栈的一个大整合;
J2EE开发的一站式解决方案;
传统的 Spring 开发需要经历以下步骤:
- 配置 pom.xml,引入 SSM 项目各种依赖
- 配置
web.xml
,设置监听器ContextLoaderListener
,当项目启动时自动启动 Spring 容器 - 配置
web.xml
,设置 Spring 的配置文件applicationContext.xml
- 配置
web.xml
,设置 SpringMVC 前端控制器DispatcherServlet
拦截所有请求 - 配置
web.xml
,设置字符集编码过滤器CharacterEncodingFilter
,隐藏HTTP请求过滤器HiddenHttpMethodFilter
- 配置
applicationContext.xml
,加载 properties 配置文件,设置 Spring 自动扫描的包component-scan
- 配置
applicationContext.xml
,设置数据源,数据库连接信息和相关属性 - 配置
applicationContext.xml
,和 Mybatis 整合,设置SqlSessionFactory
Bean,设置 Mybatis 扫描的接口,会生成代理类加入容器 - 配置
dispatcherServlet-servlet.xml
,设置 SpringMVC 视图解析器InternalResourceViewResolver
- 配置
dispatcherServlet-servlet.xml
,设置注解驱动,设置上传文件的beanCommonsMultipartResolver
- 对数据源配置事务管理器
DataSourceTransactionmanager
,开启基于注解的事务 - 配置
pom.xml
,开发环境配置 Jetty Maven 插件,使用jetty:run
启动项目 - 项目发布需要打 war 包,配置服务器环境,包括 tomcat,MySQL等。
[图片上传失败...(image-927ff1-1606212213488)]
具体配置参考项目 spring-boot-01-ssm
,更多相关参考 SSM整合教程,
繁多的配置,低下的开发效率,复杂的部署流程,第三方技术集成难度大,SpringBoot 是为了简化 Spring 应用开发而生,具有以下优点:
- 快速创建 Spring 项目,快速与主流框架集成
- 去除了 Spring 中繁琐的 XML 配置,开箱即用,以前需要配置 web.xml、applicationContext.xml等文件才可以使用
- 使用嵌入式 Tomcat 容器,无需打成 war 包,帮助快速开发和部署
- starters 自动依赖管理与版本控制,比如要使用 WEB 工程,导入 WEB starters 即可,WEB 依赖的其他 jar 包,starters 会自动导入依赖并控制版本,避免了版本不兼容和依赖繁琐的问题
- 大量的自动配置,简化开发。以前创建一个项目,要懂得 SpringMVC Mybatis 如何配置
- 准生产环境的运行时应用监控
- 与云计算的天然集成
1.2 微服务
2014,martin fowler 在博客中提出了微服务概念
微服务:架构风格(服务微化)
一个应用应该是一组小型服务;
可以通过HTTP的方式进行互通;(RPC?)
单体应用:ALL IN ONE,所有服务都在一个应用中,不方便扩展,一处错误可能影响整个应用使用
微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元;
1.3 环境准备
- jdk1.8,Spring Boot 推荐jdk1.7及以上
- Apache Maven 3.3.9
- IntelliJIDEA2017
- SpringBoot 1.5.9.RELEASE
1.4 SpringBoot HelloWorld
实现一个功能:浏览器发送 hello 请求,服务器接收请求并处理,返回 Hello SpringBoot 字符串。
传统 Spring 开发: 进行 Spring、SpringMVC 各种 xml 配置,然后进行开发,最后打成 war 包,放入 Tomcat 后运行。
SpringBoot 开发:
- 创建一个 Maven 项目
- 引入 SpringBoot 依赖
<!-- 继承SpringBoot父项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<!-- 添加SpringBoot web启动器依赖,不需要设置版本,在父项目中已经添加 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 编写一个主程序,启动 SpringBoot 应用
/**
* @SpringBootApplication 来标注一个主程序类,说明这是一个 SpringBoot 应用
*/
@SpringBootApplication
public class HelloWorldMainApplication {
public static void main(String[] args) {
// 启动Spring应用
SpringApplication.run(HelloWorldMainApplication.class, args);
}
}
- 开发:直接编写 Controller 与 Service,不需要配置 Spring 与 SpringMVC 了
@Controller
public class HelloContorller {
@RequestMapping("/hello") // 设置访问的url
@ResponseBody // 表示数据直接返回给浏览器,如果是对象则转为json数据
public String hello() {
return "Hello SpringBoot...";
}
}
- 测试:直接启动主程序 Main 即可,不需要打包到 Tomcat 启动
访问 http://localhost:8080/hello ,显示 Hello SpringBoot...
-
部署:
- 引入
Maven SpringBoot
插件
<!-- 这个插件,将应用打包成一个可执行的jar包,用于项目部署时使用,会包含项目所有依赖的Jar包,包括Tomcat*.jar,Sprint-aop.jar等--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
maven package
打 jar 包,在/target
下生成spring-boot-01-helloworld-1.0-SNAPSHOT.jar
,可以在 jar 包\BOOT-INF\lib
目录下看到项目所依赖的 jar,包括 log4j.jar,spring-aop.jar,spring-beans.jar,spring-webmvc.jar,tomcat.jar 等;\BOOT-INF\classes
下为我们的源码。 -
java -jar spring-boot-01-helloworld-1.0-SNAPSHOT.jar
在生产环境 Cmd 命令行启动该项目,可以正常访问,不需要部署Tomcat环境。
- 引入
注意: 如果不引入Maven SpringBoot
插件,使用 Maven 打包虽然也能生成 jar,但是不包含项目依赖的jar包,测试环境部署会出现错误。
1.5 探究 HelloWorld
1. pom 文件
SpringBoot 项目都需要继承一个父项目 spring-boot-starter-parent,双击可以进入 spring-boot-starter-parent.pom 文件
<!-- SpringBoot项目的父项目,父项目一般用来做依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<!-- 上项目的父项目是spring-boot-dependencies -->
<!-- 用来管理 SpringBoot 项目中所有依赖的版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
spring-boot-dependencies.pom 中设置了各种依赖的版本 version,包括 aspect.version,log4j.version,commons-dbcp.version,servlet-api.version,所以 SpringBoot 项目依赖一般不需要声明版本version,这就是前文中所说的 SpringBoot 自动依赖管理与版本控制功能。
2. 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter:SpringBoot场景启动器;帮我们导入了 web 模块运行所依赖的组件,包括 tomcat,jackson,webmvc 等。如果没有 starter 启动器,我们就需要手动导入依赖的组件并设置版本,这个操作非常麻烦且容易出错。
SpringBoot 将所有的功能场景都抽取成 starter 启动器,比如需要 aop 功能就导入 spring-boot-starter-aop 依赖,需要 redis 功能就导入 spring-boot-starter-data-redis 依赖,相关场景的所有依赖都会导入进来。
3. Main 主程序类
/**
* @SpringBootApplication 来标注一个主程序类,说明这是一个 SpringBoot 应用
*/
@SpringBootApplication
public class HelloWorldMainApplication {
public static void main(String[] args) {
// 启动Spring应用
SpringApplication.run(HelloWorldMainApplication.class, args);
}
}
-
@SpringBootApplication
:标注SpringBoot的的主配置类,SpringBoot 就应该运行这个类的 main 方法来启动 SpringBoot 应用下面是作用相同的使用注解创建容器的 Spring 代码,@Configuration 的作用是设置配置类,在创建容器时传入被标记的配置类
Application
,相当于之前的new ClassPathXmlApplicationContext("spring-config.xml")
,@Configuration // 标注该类为配置类, 与 spring-config.xml 作用类似 @ComponentScan("com.imooc") public class Application { public static void main(String[] args) { // 1. 启动容器, 解析配置类, 扫描注解标记的 Bean 到容器 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class); // 2. 从容器中获取 Bean WelcomService welcomeService = (WelcomService) applicationContext.getBean("welcomeService"); welcomeService.sayHello("spring..."); }
@SpringBootApplication 是一个组合注解,包括 @SpringBootConfiguration,而该注解底层是 Spring @Configuration,即 Spring 的配置类注解,类似于配置文件,@Configuration 底层是 @Component,也是 Spring 的一个组件,会被 Spring 扫描加入到容器中。
@SpringBootApplication 替代了 @Configuration + @ComponentScan,在 main 方法中传入被标记的类作为配置类,该注解源码如下:
// SpringBoot配置类,自动配置注解 @SpringBootConfiguration @EnableAutoConfiguration // 设置为\扫描的包为标记类所在的包 @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), @Filter(type = FilterType.CUSTOM, classes ={AutoConfigurationExcludeFilter.class})}) public @interface SpringBootApplication {
-
@EnableAutoConfiguration
:开启自动配置功能;
以前我们需要配置的东西,SpringBoot帮我们自动配置,该注解就是告诉 SpringBoot 开启自动配置功能。是一个组合注解,源码如下:
@AutoConfigurationPackage
@Import({EnableAutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
-
@AutoConfigurationPackage
:自动配置包,将主配置类(@SpringBootApplication标注的类)所在包下及子包里的所有组件都扫描添加到 Spring 容器,这也是为什么我们配有配置扫描包 component-scan,依然能够注入所有 Component 组件的原因底层是 Spring @Import,作用是给容器中导入一个组件。类似于 @Componet
为了验证 @AutoConfigurationPackage 是扫描主配置类所在包下及子包里的所有组件到 Spring 容器,所以我们在其他包下创建一个 Controller,验证发现确实不会被扫描到。
上述注解@Import,@AutoConfigurationPackage 的底层都是 Spring 的注解,参考Spring注解驱动教程
-
EnableAutoConfigurationImportSelector
:导入哪些组件的选择器;Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作;
4. 启动日志分析
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.9.RELEASE) 说明SpringBoot版本
: Starting HelloWorldMainApplication on DESKTOP-TDM8SAD with PID 15920 # 项目启动,进程ID
: No active profile set, falling back to default profiles: default # 生效的配置文件profile
: Tomcat initialized with port(s): 8080 (http) # Tomcat的访问端口
: Starting service [Tomcat]
: Starting Servlet Engine: Apache Tomcat/8.5.23
: Initializing Spring embedded WebApplicationContext
: Root WebApplicationContext: initialization completed in 1297 ms
: Mapping servlet: 'dispatcherServlet' to [/] # dispatcherServlet 拦截所有请求
: Mapping filter: 'characterEncodingFilter' to: [/*] # 字符集编码过滤器
: Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
: Mapping filter: 'httpPutFormContentFilter' to: [/*]
: Mapping filter: 'requestContextFilter' to: [/*]
: Looking for @ControllerAdvice: org.springframework.boot.context.embedded.
: Mapped "{[/hello]}" onto public String com.atguigu.controller.HelloContorller.hello() # 映射用户编写的Controller
: Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity # 映射/error页面
: Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
: Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
: Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
: Registering beans for JMX exposure on startup
: Tomcat started on port(s): 8080 (http) # Tomcat启动完成,端口为8080
: Started HelloWorldMainApplication in 2.047 seconds (JVM running for 2.35) # HelloWorldMainApplication项目启动完成,耗时2.0秒
1.6 快速创建 SpringBoot 项目
在 IDEA 中安装 Spring Assistant 插件,然后新建项目:
- 选择 Spring Assistant,选择 Default 即从 Spring 官网(https://start.spring.io/ )联网创建 SpringBoot项目,
- 填写项目名称信息
- 选择需要导入的依赖,如 web,MySQL 等,还会自动引入 Maven SpringBoot 打包插件。
这样就很快捷的创建了一个 SpringBoot 项目,这种创建方式需要联网,创建成功后查看主程序与 POM 文件,发现与手动创建完全一致。
默认生成的Spring Boot项目;
- 主程序已经生成好了,我们只需要写自己的业务逻辑
- resources文件夹中目录结构 static:保存所有的静态资源; js css images;
- templates:保存所有的模板页面;(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页 面);可以使用模板引擎(freemarker、thymeleaf);
- application.properties:Spring Boot应用的配置文件;可以修改一些默认设置,如Tomcat端口;
补充: @ResponseBody 注解表示返回字符串,对象则转为json数据
//@ResponseBody // 表示数据直接返回给浏览器,如果是对象则转为json数据
//@Controller
@RestController // 等价于@ResponseBody + @Controller,可以通过@RestController源码查看
public class HelloController {
@RequestMapping("/hello") // 设置访问的url
public String hello() {
return "Hello SpringBoot quickly...";
}
// RESTAPI的方式,即把返回数据直接发送给浏览器,而不是页面跳转
}
第 2 章 SpringBoot 配置
2.1 配置文件
SpringBoot 使用一个全局的配置文件,文件名称默认为
application.properties
application.yml
配置文件的作用:修改 SpringBoot 自动配置的默认值,如修改访问端口,修改项目根路径 Context-path
YAML 适合用来做配置文件,替换我们以前使用的 xml 配置文件:
server:
port: 8081
传统的 XML 配置如下,不禁冗长,还容易出现标签笔误:
<server>
<prot>8081</port>
</server>
2.2 YAML 语法
1. 基本语法
key: value
表示一对键值对,大小写敏感,注意冒号后必须有空格
以空格的缩进来控制层级关系,左对齐的一列数据都是同一层级
2. 值的写法
字面量:
普通的值(数字,字符串,布尔) k: v:字面直接来写;
字符串默认不用加上单引号或者双引号;
""
:双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思
name: "zhangsan \n lisi":输出;zhangsan 换行 lisi
''
:单引号;原样输出,特殊字符最终只是一个普通的字符串数据
name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi
对象、Map:
friends:
lastName: zhangsan
age: 20
行内写法:
friends: {lastName: zhangsan, age: 18}
数组(List,Set):
用-
标志数组中的一个元素
pets:
- cat
- dog
- pig
行内写法
pets: [cat, dog, pig]
3. 配置属性列表
配置文件中所有能够配置的属性可以查看官方文档
2.3 配置文件值的注入
1. @ConfigurationProperties 配置文件属性的绑定
配置文件使用 @ConfigurationProperties 注解与Java类的绑定。
配置文件:
# application.yml 配置文件,定义了person对象的值
person:
lastName: 张三 # 使用last-name也可以,支持松散绑定
age: 18
boss: false
birth: 2002/1/2
maps: {k1: v1, k2: 12}
list:
- lisi
- wangwu
dog:
name: doudou
age: 3
JavaBean:
/**
* 将配置文件中每一个属性的值,映射到这个类中
* @ConfigurationProperties 告诉SpringBoot将本类中所有属性与配置文件中
* 相关配置进行绑定, prefix选择配置文件中的属性
*
* @Component 作用是将组件加入 Spring 容器,使用时 @Autowired 获取
*/
@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;
这是一个非常重要的注解,SpringBoot 自动配置原理也是使用该注解,根据配置前缀(prefix="spring.dataSource")来读取相关配置与配置类 DataSourceProperties 绑定。
使用@Value注解也可以实现属性的映射
/**
* 使用 @Value 注解实现配置属性的注入
*/
@Component
public class Person2 {
/**
* <bean class="com.atguigu.springboot.Person">
* <property name="lastName" value="${person.last-name}"></property>
* </bean>
* 在xml中注入bean并配置属性,Spring注解@Component替代了xml配置注入bean的方式
* 注解@Value用于设置属性值
*/
// @Value不支持松散绑定,${person.lastName}无法获取到值,必须与配置文件key完全一致
@Value("${person.last-name}") // ${} 从配置文件获取属性值
// @Email // @Value 不支持JSR303校验,该注解无作用
private String lastName;
@Value("#{10+8}") // SpEL表达式
private Integer age;
IDEA提示功能:
在 pom.xml 中导入配置文件处理器,这样在配置文件中写配置就会有提示属性功能
<!-- 导入配置文件处理器,配置文件设置属性时会有提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2. 松散绑定
在上文例子中,属性名为lastName
,配置文件中last-name: zhangsan
,因为 @ConfigurationProperties 注解支持松散绑定,SpringBoot可以准确识别并绑定。而@value注解则不支持松散绑定,属性名必须与配置文件中的 key 完全一致。
对于以下三种写法,属性lastName
均可成功匹配绑定:
– person.lastName:
– person.last-name:
– person.last_name:
– PERSON_LAST_NAME:
3. Properties 乱码问题
SprinBoot 除了 YAML 配置文件,也可以使用 application.properties 作为配置文件,
# server.port=8081
# 配置person的值,中文会出现乱码问题
person.last-name=张三
person.age=18
person.birth=2001/2/3
person.boss=false
# 设置map和数组的属性值
person.maps.k1=v1
person.maps.k2=v2
person.lists=a,b,c
# 设置对象的属性值
person.dog.name=doudou
person.dog.age=2
YAML 中不会出现中文乱码问题,IDEA中 Properties 文件默认使用GBK编码,而IDEA项目使用 UTF-8编码,导致运行时乱码。
解决办法: 在IDEA->设置-> file encoding,将编码设置为 UTF-8,勾选 native->ascii。
修改后,可以在 IDEA 右下角看到文件编码变为 UTF-8,勾选 native->ascii 的作用是为了方便在 IDEA中查看中文内容。
[图片上传失败...(image-b925ca-1606212213489)]
4. @ConfigurationProperties 与 @Value
- @ConfigurationProperties 与 @Value 两个注解均是从配置文件中获取属性值,并注入给示例相关属性。
- @ConfigurationProperties 一般用于注入属性值到整个对象,更为便捷,支持松散绑定,支持 JSR303 数据校验。
- @ConfigurationProperties 只是绑定属性,但并不会将标注的类加入到 Spring 容器中,一般与@Component,@EnableConfigurationProperties,@Bean 配合使用,将标注的类加入到 Spring 容器
@ConfigurationProperties | @Value | |
---|---|---|
功能 | 批量注入配置文件中的属性 | 需要一个个指定属性值 |
松散绑定 | 支持 | 不支持 |
JSR303数据校验 | 支持 | 不支持 (2.x版本也支持?) |
SpEL表达式 | 不支持 | 支持 |
@ConfigurationProperties 组件加入容器的三种方式:
- @EnableConfigurationProperties
// 将配置文件中属性与该类属性绑定
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
}
@Configuration(proxyBeanMethods = false)
// 将制定的配置类DataSourceProperties 加入Spring容器,使其生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
}
- @Bean
// 配置制定的 Druid 数据源,将方法返回的对象加入Spring容器
@Bean
// 绑定配置文件中的属性到指定类
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druid() {
return new DruidDataSource();
}
- @Component
@Value 注入 Map 等复杂类型参考文章,教程中说 @Value 不支持复杂类型注入是错误的。
- 专门编写的JavaBean来和配置文件映射,推荐使用 @ConfigurationProperties
- 业务逻辑某处使用配置文件中的属性时,推荐使用 @Value
5. @PropertySource 与 @ImportResource
@PropertySource: 加载指定的配置文件,默认加载application.properties
配置文件。
@Component
// 表示加载person.properties配置文件
@PropertySource({"classpath:person.properties"})
@ConfigurationProperties(prefix = "person")
public class Person {
@ImportResource: 导入 Spring 的配置文件,不推荐使用
下面的代码导入了 Spring 的配置文件 beans.xml,配置了JavaBean HelloService,类似于 HelloService 类上添加 @Service 注解,使自动注入 Spring 容器,可以被 @AutoWired 使用。
@SpringBootApplication
@ImportResource("classpath:beans.xml")
public class SpringBoot02ConfigApplication {
<!-- beans.xml 使用xml方式注入bean -->
<bean id="helloService" class="com.atguigu.springboot.service.HelloService"></bean>
@Component 与 @Bean 类似,标注这个类需要注入到 Spring 容器,与xml中配置<bean>
的作用一致。
6. @Configuration 和 @Bean
小知识: IDEA 中 Spring Beans 与 MVC 的可视化
显示Spring容器中所有的Bean,方便查看Bean是否添加到了容器,对于Spring的内置Bean,还可以直接跳转到文档查看bean的介绍
[图片上传失败...(image-9f4842-1606212213489)]
显示所有url映射,方便查看url与Controller的映射,还可以过滤各种类型的请求
[图片上传失败...(image-549186-1606212213489)]
@Configuration:
-
@Configuration 底层是 @Component 注解,会被 Spring 扫描添加到容器中
-
使用全注解的方式映射配置类与 Spring 配置,类似于@ConfigurationProperties,不过后者针对的是配置文件properties。
-
@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并将这些 Bean 加入到 Spring 容器
@Bean:
- @Bean 用于告诉方法,返回一个Bean对象,将其加入Spring容器,产生这个Bean对象的方法Spring只会调用一次(单例)
- @Bean注解把当前方法的返回值作为bean对象存入spring容器中,其name属性用于指定bean的id(若没写该属性,默认值是当前的方法名)
- @Bean 是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。
SpringBoot 推荐用全注解的方式向容器中添加组件,替代编写 Spring XML配置文件
- 标注 Spring 配置类 @Configuration
- 使用 @Bean 向容器中添加组件
/**
* 以前在配置文件beans.xml中添加<bean></bean>,然后使用@ImportResource导入
* @Configuration 标注类为一个配置类,就是来替代之前的Spring xml配置文件
*/
@Configuration
public class MyAppConfig {
/**
* 将方法的返回值添加到容器中,容器中这个组件Bean默认的id就是方法名
* @Bean 给容器中添加组件
*/
@Bean
public HelloService helloService2() {
System.out.println("配置类@Bean给容器中添加组件...");
return new HelloService();
}
}
@Configuration 和 @Bean 的详细讲解参考Spring注解驱动教程
2.3 配置文件占位符
1. 属性配置占位符
可以引用前面定义的属性,使用冒号设置默认值
app.name=WeChat
app.description=${app.name:MyApp} is a SpringBoot Application.
2. 随机数
${randowm.uuid}、 ${random.int}、 ${random.long}
${random.int(10)}、 ${random.int[1024,65536]}
2.4 Profile 多环境配置文件切换
生产环境和开发环境一般使用的配置文件并不相同,SpringBoot提供了三种多环境配置文件切换功能
- 配置文件中指定
# properties设置启动的配置文件为 application-dev.properties
spring.profiles.active=dev
# yml设置启动的配置文件为 application-dev.yml
spring:
profiles:
active: dev
注意: 是 profiles 不是 profile
- 命令行参数指定
java -jar MyApp.jar --spring.profiles.active=dev
也可以在IDEA 运行->编辑配置->程序参数 中添加--spring.profiles.active=dev
- JVM 参数指定
# 生产环境一般使用 jvm 参数激活配置文件:
C:\Users\mao> java -jar -Dspring.profiles.active=prod MyApp.jar
2.5 配置文件加载位置
SpringBoot会从以下 4 个位置加载全部配置文件,不相同的属性进行互补,优先级1级的会覆盖优先级2级的相同的属性配置,知晓即可,不常用
demo/ # 项目根路径
config/
application.properties # 优先级1
application.properties # 优先级2
src/
java/
resource/ # classpath:下面的文件打包后会在 classes下
config/
application.properties # 优先级2
application.properties # 优先级4(常用)
在项目打包完成后,需要修改配置时可以使用命令行参数spring.config.location
指定配置文件,优先级最高
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties
2.6 配置文件加载顺序(重点)
优先级由高到低,高优先级会覆盖低优先级配置:
- 命令行参数
- Java系统属性(System.getProperties())
- jar包外部的application-{profile}.properties/yml(带spring.profile)配置文件
- jar包内部的application-{profile}.properties/yml(带spring.profile)配置文件
- jar包外部的application.properties/yml(不带spring.profile)配置文件
- jar包内部的application.properties/yml(不带spring.profile)配置文件
- @Configuration注解类上的@PropertySource指定的配置文件(参考#2.3.5 中Person类指定的配置文件)
- 通过SpringApplication.setDefaultProperties指定的默认属性
命令行参数适用生产环境修改某个配置时使用,jar包外面的配置文件,适合生产环境批量修改多个配置时使用
2.7 自动配置原理(重难点)
// 补充:自动配置原理其实并没有搞懂,后面进行补充
- SpringBoot 启动时加载主程序类,@SpringBootApplication 注解包含了 @EnableAutoConfiguration
- @EnableAutoConfiguration 表示开启自动配置功能
- @EnableAutoConfiguration 作用是将 META-INF/spring.factories 里面配置的所有XXXAutoConfiguration的组件加入到了容器中
- 源码参考 AutoConfigurationImportSelector#selectImports,里面获取了所有自动配置类,如 DataSourceAutoConfiguration,WebMvcAutoConfiguration
- xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置,如
HttpEncodingAutoConfiguration
来做Http编码自动配置
@Configuration //表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件
//让指定的 HttpEncodingProperties 配置类生效;将配置文件中对应的值和HttpEncodingProperties绑定起来;并把 HttpEncodingProperties加入到ioc容器中
@EnableConfigurationProperties(HttpEncodingProperties.class)
//Spring底层@Conditional注解(Spring注解版),根据不同的条件,如果 满足指定的条件,整个配置类里面的配置就会生效; 判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication
//判断当前项目有没有这个类 CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true) //判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的 //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final HttpEncodingProperties properties;
- 所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着,配置文件能配置什么就可以参照某个功能对应的这个属性类。如
HttpEncodingProperties.class
//从配置文件中获取指定的值和bean的属 性进行绑定 @ConfigurationProperties(prefix = "spring.http.encoding") public class HttpEncodingProperties { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF‐8");
·
扩展:
向Spring容器添加组件的三种方式:
- @Componet,@Controller,@Service,@Repository,@Configuration:局限于自己写的组件
- @Bean:注解用于告诉方法,返回一个Bean对象,将其加入Spring容器。产生这个Bean对象的方法Spring只会调用一次(单例)。Bean导入第三方包的组件,是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。@Bean注解把当前方法的返回值作为bean对象存入spring容器中,其name属性用于指定bean的id(若没写该属性,默认值是当前的方法名)
- @Import:导入组件
2.8 @Conditional 自动配置报告
// 补充:没搞懂,配合SPring注解教程学习
第 3 章 SpringBoot 日志
3.1 常见日志框架
小张;开发一个大型系统;
- 刚开始使用System.out.println("");将关键数据打印在控制台;
- 为了记录记录系统的一些运行时信息,开发了一个日志框架 zhanglogging-1.0.jar;
- 迭代更新日志框架,异步模式,按日期自动归档等 zhanglogging-2.0.jar
- 为了替换日志框架,重新修改之前相关的API,特别麻烦
- 参考 JDBC--数据库驱动,门面设计模式,统一的接口层,不同数据库的代码都是一样的,需要数据库去实现驱动接口
- 实现一个统一的接口层:日志门面(日志的一个抽象层)logging-abstract.jar
- 给项目中导入具体的日志实现就行了,日志框架升级、替换也不需要修改我们的代码
日志门面(抽象层) | 日志实现 |
---|---|
SLF4j, |
Logback,Log4j,Log4j2 |
Spring 日志框架是 jakarta-commons-Logging,SpringBoot 默认日志框架是 SLF4j + Logback (推荐使用)
3.2 SLF4j 使用
参考SLF4j 官网手册,记录日志只需要使用 SLF4j 的 API,不应该直接调用日志框架 Logback 的实现类。
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 与日志框架配合使用
3.3 SpringBoot 日志关系(了解)
SpringBoot-Starter-Web 依赖 spring-boot-starter,后者又依赖 spring-boot-starter-logging,SpringBoot 使用它来做日志记录。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
SpringBoot 依赖 Spring,而 Spring 使用的是 jakarta-commons-Logging 日志框架,需要集成的Hibernate 使用 jboss-logging日志框架,SLF4j 把这些日志框架也都替换为了 SLF4j 框架。
[图片上传失败...(image-ef1761-1606212213489)]
总结:
- SpringBoot 底层也是使用 slf4j+logback 的方式进行日志记录
- SpringBoot 也把其他的日志都替换成了 slf4j 中间替换包
- slf4j 中间替换包本质是将 jakarta-commons-Logging 等日志框架使用相同类名重写了一次
- 如果要引入其他日志框架,需要将中间替换包移除
slf4j 中间替换包本质是将 JCL(jakarta-commons-Logging) Log4j 等日志框架使用相同类名重写了一次,下图是slf4j的替换包,与被替换框架的包名、类名完全一致。
slf4j 中间替换包3.4 日志使用
// 日志记录器,注意是slf4j 的 LoggerFactory
Logger logger = LoggerFactory.getLogger(getClass());
/**
* 使用slf4j,
*/
@Test
void contextLoads() {
// 日志级别由低到高
logger.trace("这是trace日志...");
logger.debug("这是debug日志...");
// 默认使用info级别日志输出
logger.info("这是info日志...");
logger.warn("这是warn日志...");
logger.error("这是error日志...");
}
1. 常见日志配置属性
在 SpringBoot 配置文件 application.properties
中修改日志配置
# 生产环境下设置项目所有日志级别为WARN
# logging.level.root=WARN
# 设置下面包 com.atguigu 的日志级别为trace
logging.level.com.atguigu=TRACE
logging.file.path=/mao/log
# 指定日志输出文件,默认在当前项目根目录,与1.x的属性不同
logging.file.name=springboot.log
# 设置在控制台输出日志的格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
# 设置在日志文件输出日志的格式
logging.pattern.file=
# 设置控制台日志颜色
spring.output.ansi.enabled=ALWAYS
# 设置启动时打印所有的请求路径映射
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=trace
测试环境可以使用命令行输出 DEBUG 级别的日志记录
$ java -jar myapp.jar --debug
2. 指定日志框架配置文件
在 SpringBoot 配置文件 application.properties
中修改日志配置,如果要修改的属性过多,推荐加入日志框架自己的配置文件,会自动生效
Logging System | 配置文件名称 |
---|---|
Logback | logback-spring.xml,logback.xml |
Log4J2 | log4j2-spring.xml,log4j2.xml |
logback-spring.xml
这种名称可以使用 SpringBoot profile 高级功能,即在不同环境下使用不同的日志配置。使用spring.profiles.active=dev
属性控制。
<springProfile name="dev">
<!-- 可以指定某段配置在开发环境下生效 -->
<!-- spring.profiles.active=dev 配置该属性设置环境为开发环境 -->
</springProfile>
logback-spring.xml 的配置在项目 spring-boot-03-logging 下,有详细注释,使用时可进行参考。
3.5 切换日志框架
切换为 slf4j + log4j:
排除 SpringBoot 默认依赖的日志框架,移除 logback 依赖,移除 slf4j-log4j 中间包,然后引入 log4j 依赖。由于 log4j 框架性能一般,不推荐使用
<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>
切换为 slf4j + log4j2:
SpringBoot 提供 log4j2 starter 启动器,包含了 log4j2 及 slf4j-log4j2 中间包,移除之前自带的 logging starter依赖,添加 log4j2 启动器依赖。
<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>
// 补充:SpringBoot 2.x 版本日志框架有了变化,没有这么麻烦了?
第 4 章 SpringBoot 与 WEB 开发
4.1 简介
SpringBoot 开发三步走:
- 创建SpringBoot 应用,选择需要使用的模块,如 web,Mybatis等
- SpringBoot 已经默认将这些场景配置好了,只需要配置文件中指定少量配置就可以运行起来
- 编写业务代码
自动配置原理:
自动配置相关的类都在spring-boot-autoconfiguration-2.3.1.jar
中
-
DataSourceAutoConfiguration
:数据源自动配置类,该类使用注解 @EnableConfigurationProperties 让指定的数据源配置映射类 DataSourceProperties 生效 -
DataSourceProperties
:数据源配置映射类,所有可以配置的属性都在该类中,该类使用注解 @ConfigurationProperties 指定映射的配置属性的前缀 prefix="spring.datasource"
小知识: 分析类的依赖
IDEA 中右键相关类,选择分析->分析依赖->整个项目,可以看到当前类的依赖。如下图所示,项目主程序依赖了String、SpringBootApplication 等类。在大型项目中不熟悉项目结构可以使用这种方式查看。
[图片上传失败...(image-12175f-1606212213489)]
4.2 SpringBoot 静态资源映射规则
1. Webjars
webjars 是以jar包的方式引入静态资源
对于日常的web开发而言,像css、js、images、font等静态资源文件管理是非常的混乱的、比如jQuery、
Bootstrap、Vue.js等,可能每个框架使用的版本都不一样、一不注意就会出现版本冲突或者重复添加的问题。所以诞生了 WebJars 技术。
原本我们在进行web开发时,一般上都是讲静态资源文件放置在webapp目录下,在SpringBoot里面,一般是将资源文件放置在src/main/resources/static目录下。而在Servlet3中,允许我们直接访问WEB-INF/lib下的jar包中的/META-INF/resources目录资源,即WEB-INF/lib/{*.jar}/META-INF/resources下的资源可以直接访问。
WebJars 正是利用了此功能,将所有前端的静态文件打包成一个jar包.
对于用户而言,和普通的jar引入是一样的,还能很好的对前端静态资源进行管理。WebJars是将这些通用的Web前端资源打包成Java的Jar包,然后借助Maven工具对其管理,保证这些Web资源版本唯一性,依赖配置参考 https://www.webjars.org/
- 所有的/webjars/** 下的静态资源,都去 classpath:/META-INF/resources/webjars/ 下寻找资源
<!-- 以webjars的方式引入 jQuery 依赖 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
引入 jQuery webjar 的依赖,结构如下图所示,jquery.js 确实在 classpath:/META-INF/resources/webjars/ 目录下:
jQuery webjar的结构- 访问静态资源jquery.js http://localhost:8080/webjars/jquery/3.3.1/jquery.js
静态资源的配置类为 ResourceProperties,源码如下:
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
//可以设置和静态资源有关的参数,缓存时间等
WebMvcAutoConfiguration:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
// 未注册的资源,都去静态资源目录寻找
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
// 添加静态资源目录
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
-
静态资源路径,classpath 指项目/src/main/resource,与java类的根路径同级,使用java/resource区分资源,本身都是在classpath下,打包后刚好在classpath下即\BOOT-INF\classes
- classpath:/META‐INF/resources/
- classpath:/resources/
- classpath:/static/
- classpath:/public/
- /:当前项目的根路径
-
欢迎页映射,访问 localhost:8080,会去静态资源文件夹下寻找index页面
WebMvcAutoConfiguration:
//配置欢迎页映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ResourceProperties resourceProperties) {
return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
}
- 网站icon映射,会去静态资源文件夹下寻找 favicon.ico
小知识
IDEA 搜索 文件: 双击 Shift 快捷键,可以搜索项目和库中的 类、符号(属性方法)、模板(xml等文件)
IDEA 搜索 文件内容: Ctrl+Shift+F 快捷键,可以搜索项目中的内容,支持限制文件类型(.xml,.java),还可以限制搜索 注释、字符串等。
4.3 模板引擎
模板引擎作用是将模板与动态数据结合生成html页面
[图片上传失败...(image-76c3ac-1606212213489)]
常见的模板引擎:JSP,Thymeleaf,Freemarker
SpringBoot 推荐使用 Thymeleaf,语法简单,功能强大。
1. 引入Thymeleaf
Thymeleaf 读作 [taim li:f]
<!-- 引入thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. Thymeleaf 的使用
根据 Thymeleaf 默认配置可知,只要讲HTML页面放在classpath:/templates/
下(就是src/main/resources/templates/),Thymeleaf 就能自动渲染。
Thymeleaf只渲染 /templates 下的页面HTML页面 ,不能放在static或resource下面。
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
- 在 src/main/resources/templates/ 下创建 success.html 文件
- 向 success.html 中导入 Thymeleaf 的名称空间,代码提示功能还需要使用IDEA-U版打开 Thymeleaf 插件
- 使用 Thymeleaf 语法编写html文件,<div th:text="${key1}"></div>
// 携带map数据,跳转到success.html页面,
@RequestMapping("/hello2")
public String hello2(Map<String, String> map) {
map.put("key1", "hello thymeleaf...");
// 跳转到 classpath:/templates/success.html
return "success";
}
success.html:
<!DOCTYPE html>
<!-- 引入Thymeleaf名称空间,配合IDEA收费版才有代码提示功能 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>访问success成功...</h2>
<!-- th:text设置div内的文本内容 -->
<!-- 如果不经过模板引擎解析,则显示"你好",否则显示后端传递的hello变量-->
<div th:text="${key1}">你好</div>
</body>
</html>
小知识 :IDEA-U 激活
- http://idea.medeming.com/jet/
- http://lookdiv.com/
- https://www.jianshu.com/p/d1b1ff342951?tdsourcetag=s_pctim_aiomsg
- 淘宝
4. Thymeleaf 语法
标签:
-
th:text
改变当前元素内的文本内容th: 可以搭配任意的 html 属性,来覆盖原生属性的值,如
th:id
th:class
<!-- 后台传来的数据会覆盖掉html中的标签数据,id,class等 --> <div id="div01" class="div02" th:id="${myId}" th:class="${myClass}" th:text="${hello}">你好</div>
-
th:each
遍历标签 -
th:if
条件标签 -
更多th标签参考官方文档
[图片上传失败...(image-da2260-1606212213489)]
表达式:
- ${} 变量,后台传过来的变量
-
{} 国际化
- @{} 项目根路径,引入css,js等
- ~{} 页面公共片段(导航栏)引入
-
[[${}]]
获取变量行内写法,等价于标签内th:text="${}"
Simple expressions:
Variable Expressions: ${...} # 获取变量值,对象属性,数组元素,内置对象
Selection Variable Expressions: *{...}
Message Expressions: #{...} # 国际化内容
Link URL Expressions: @{...} # 定义URL,表示项目根路径,引用webjar中的css资源
Fragment Expressions: ~{...} # 页面公共片段引入
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,…
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:
语法比较乱,和Freemarker一样,用的时候多查多模仿多总结即可,不要死记硬背。
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax
遍历数组的两种方式:
<!-- 遍历数组方式1,后台传递过来的数组 users -->
<h4 th:each="user:${users}" th:text="${user}"> </h4>
<!-- 遍历数组方式2,行内写法,与上面等价 -->
<h4>
<span th:each="user:${users}">[[${user}]]</span>
</h4>
引入webjars的静态资源
<link th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}" rel="stylesheet">
4.4 SpringMVC 自动配置
SpringBoot 自动配置好了 SpringMVC,包含以下默认配置:
-
配置了ViewResolver视图解析器,根据方法的返回值得到视图对象,渲染页面
ContentNegotiatingViewResolver
BeanNameViewResolver
-
静态资源路径、webjars
-
静态首页访问 index.html
-
网站图标访问 Favicon.ico
-
自动注册了 Converter,GenericConverter,Formatter bean
- Converter:转换器,类型转换
- Formatter:格式化器,时间格式转换
-
HttpMessageConverters SpringMVC用来转换http请求和响应,比如将user对象转为json返回给浏览器
// 补充: p31 p32源码这里不太懂建议复习SpringMVC,9小时
4.5 修改 SpringBoot 默认配置
- SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如 果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默 认的组合起来;
- 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置
- 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置
4.6 Restful crud 项目
1. 国际化
-
创建和编写国际化配置文件
[图片上传失败...(image-ea5caa-1606212213489)] -
SpringBoot 自动配置好了 ResourceBundleMessageSource 管理国际化资源文件的组件,默认国际化文件路径为
classpath:message.properties
,需要修改如下
# application.properties 设置国际化资源文件目录,默认为classpath:messages
spring.messages.basename=i18n.login
通过源码可知,国际化资源文件默认路径为classpath:message.properties
ResourceBundleMessageSource:
private String basename = "messages";
public String getBasename() {
return this.basename;
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
// 设置国际化资源文件的基础名为message.properties,放在类路径下
// 这里去除了国际化文件的语言国家后缀
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
- 在页面使用
#{}
获取国际化内容
<label class="sr-only" th:text="#{login.username}">Username</label>
-
修改浏览器语言,访问页面,查看国际化效果
修改浏览器语言为
English(United States)
,此时的请求体中Accept-Language: en-US,zh-CN;
页面显示语言为英文,相关资源在login.en_US.properties
。注意:如果将浏览器语言修改为
English
,那么国际化将不生效,此时的请求体中Accept-Language: en,zh-CN;
,需要配置文件login_en.properties
才能生效。
1. 国际化原理
国际化Locale(区域信息对象) LocaleResolver(获取区域信息对象)
WebMvcAutoConfiguration:
@Bean
@ConditionalOnMissingBean // 如果容器中不存在Id相同的bean,才使用该bean,即用户自定义bean可以替代
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
// 优先使用默认的locale信息
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 根据浏览器请求体中的AcceptHeader设置LocalResolver信息
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
2. 实现语言切换功能
- 修改前面页面,添加语言切换按钮,发送请求携带参数
l='zh_CN'
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
- 自定义区域解析器MyLocaleResolver,根据请求参数修改语言
/**
* 实现网站语言切换功能
* 自定义LocaleResolver实现LocaleResolver
*/
public class MyLocaleResolver implements LocaleResolver {
// 解析区域信息,优先返回 用户选择的语言>浏览器语言>服务器默认语言
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 获取切换语言请求中的参数
String l = request.getParameter("l");
if (!StringUtils.isEmpty(l)) {
String language = l.split("_")[0];
String country = l.split("_")[1];
// 根据语言与国家信息en_US创建 locale
Locale locale = new Locale(language, country);
return locale;
}
// 获取浏览器的区域信息 accept-language
String acceptLanguage = request.getHeader("Accept-Language");
if (!StringUtils.isEmpty(acceptLanguage)) {
Locale requestLocale = request.getLocale();
return requestLocale;
}
// 返回项目系统区域信息
return Locale.getDefault();
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
- 将区域解析器MyLocaleResolver加入到容器,使其生效
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
// 向容器中加入我们自定义的bean MyLocaleResolver,bean的id为方法名,
// 用于替换默认的localeResolver,当然也可以在@中设置id
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
2. 登录
小知识: Thymeleaf 页面修改实时生效,不用重启项目
开发期间,模板引擎页面修改以后,要实时生效
- 禁用模板引擎缓存
spring.thymeleaf.cache=false
- 页面修改完成后 ctrl+f9,重新编译
-
登录表单
<form class="form-signin" th:action="@{/user/login}" method="post"> <input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus=""> <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required=""> </form>
-
处理登录请求,登录成功后,一定要重定向到主页,如果是转发,那么刷新页面就会有表单重复提交问题
@RequestMapping("/user/login") public String login(@RequestParam("username") String username, @RequestParam String password, Map<String, Object> map, HttpSession session) { if ("admin".equals(username) && "admin".equals(password)) { // 在session中保存一个属性,用于访问时识别是否登录,拦截器中会使用 session.setAttribute("loginUser", username); // 防止表单重复提交,重定向到主页 return "redirect:/dashboard"; } // 为啥能将返回map? SpringMvc入参中的map,方法返回时,会将Map数据添加到模型中 map.put("msg", "用户名或密码错误"); return "index"; }
-
显示登录失败信息
<p th:if="${not #strings.isEmpty(msg)}" style="color: red" th:text="${msg}"></p>
-
拦截器,
http://localhost:8080/crud/dashboard
,需要登录才能访问,所以设置拦截器检查用户是否登录-
继承HandlerInterceptor实现登录拦截器,如果请求 session 中没有 loginUser,则在请求域设置权限信息,再转发到首页。
-
Session对应的类为javax.servlet.http.HttpSession类。每个来访者对应一个Session对象,所有该客户的状态信息都保存在这个Session对象里。Session对象是在客户端第一次请求服务器的时候创建的,保存在服务端。Servlet里通过request.getSession()方法,根据请求中携带的 JsessionID 在服务端查询对应的客户Session,然后获得该客户的所有 Session 属性,如果能查到,说明服务端保存了该会话,所以不需要登录,如果查不到,则说明服务端不认识该会话,需要登录。
-
拦截器在 DispatchServlet.doDisatch 后生效,在具体请求的 Controller 前生效
-
拦截器是通过 AOP 实现
例如:
-
```java
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 根据request中携带的jsessionId,查询对应的Session,找到对应属性的值,如果服务端保存了该会话,则不需要重新登录
Object user = request.getSession().getAttribute("loginUser");
//判断用户身份在session中是否存在,其他属性都可以,不一定要是loginUser,也可以在登录时保存登录时间到session
if(user == null) {
request.setAttribute("msg", "没有权限,请先登录");
// 用户未登录,则转发到首页,只有转发才能携带请求信息
request.getRequestDispatcher("/").forward(request, response);
return false;
}
return true;
}
```
- 注册登录拦截器,设置拦截的url
```java
@Configuration
public class MyMvcConfig extends WebMvcConfigurer {
// 添加url与视图的映射
@Override
public void addViewControllers(ViewControllerRegistry registry) {
/*
* 浏览器发送 /atguigu 请求来到 success
* 因为success.html保存在/templates中,只有模板引擎能够访问,所以需要映射url与页面success
* 为什么 index.html没有在这里绑定url也能访问? 因为在 HelloController中绑定了
*/
registry.addViewController("/atguigu").setViewName("success");
// 映射url与视图,前者是url,后者是页面,与LoginController.dashboard()作用一致
registry.addViewController("/dashboard").setViewName("dashboard");
}
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Springboot已经做好了静态资源映射,所以我们不需要设置静态资源
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/", "/index.html", "/user/login", "/asserts/**", "/webjars/**");
}
/**
* 向容器中加入我们自定义的bean MyLocaleResolver,bean的id为方法名,
* 用于替换默认的localeResolver,当然也可以在@中设置id
*/
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
}
```
3. Restful CRUD
普通请求与 RestfulCRUD 的区别如下:
功能 | 普通CRUD(uri区分操作) | RestfulCRUD |
---|---|---|
查询 | getEmp | emp-GET |
添加 | addEmp?xxx | emp-POST |
修改 | updateEmp?id=123&xxx=xx | emp/{id}-PUT |
删除 | deleteEmp?id=123 | emp/{id}-DELETE |
下面来实现员工的 RestfulCRUD:
功能 | 请求URI | 请求方式 |
---|---|---|
查询所有员工 | emps | GET |
查询某个员工(访问修改页面) | emp/{id} | GET |
访问添加员工页面 | emp | GET |
添加员工 | emp | POST |
修改员工 | emp | PUT |
删除员工 | emp/{id} | DELETE |
4. 页面公共元素抽取
- 抽取页面公共元素,如导航栏等
1. 使用 ~{templatename::fragmentname}:模板名::片段名
抽取公共片段:
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
引入公共片段:footer是公共片段的文件名
<div th:insert="~{footer :: copy}">
</div>
2. 使用 ~{templatename::#selector}:模板名::选择器
<div id="copy1">
© 2011 The Good Thymes Virtual Grocery
</div>
引入公共片段
<div th:insert="~{footer :: #copy1}">
</div>
3. 默认效果: th:insert 是将公共片段放在下面的 div 标签中
<div th:insert="~{footer :: #copy1}">
三种引入公共片段的th属性:
- th:insert:将公共片段整个插入到声明引入的元素中
- th:replace:将声明引入的元素替换为公共片段 (推荐使用)
- th:include:将被引入的片段的内容包含进这个标签中
详细参考 Thymeleaf 8.2 Template Layout
5. 显示员工列表
显示员工列表:GET请求,url 为 /emps,查询到的员工集合保存到model中返回前台。
@GetMapping("/emps")
public String emps(Model model) {
Collection<Employee> employees = employeeDao.getAll();
// 添加数据到请求域中
model.addAttribute("emps", employees);
// 跳转到Thymeleaf渲染的 classpath:/templates/emp/list.html
return "/emp/list";
}
使用 th:each 遍历员工集合 emps,使用三元表达式显示性别,使用内置 date 对象格式化日期
<tbody>
<tr th:each="emp : ${emps}">
<td th:text="${emp.id}"></td>
<td th:text="${emp.lastName}"></td>
<td th:text="${emp.gender}==1 ? '男' : '女' "></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${emp.email}"></td>
<!-- 修改日期格式 -->
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd')}"></td>
<td >
<button class="btn btn-sm btn-primary">编辑</button>
<button class="btn btn-sm btn-danger" >删除</button>
</td>
</tr>
</tbody>
6. 跳转到员工页面
员工列表显示页面,加入【添加员工】按钮,请求 /emp
<a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a>
返回添加员工页面:响应 GET请求 /emp,添加员工页面需要显示部门名称,所以将部门集合返回,跳转到 /emp/add.html
@GetMapping("/emp")
public String toAddPage(Model model) {
// 查询部门,返回到添加页面
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments", departments);
return "/emp/add";
}
添加员工页面:add.html,遍历显示部门名称,设置部门信息为id
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" >
</div>
<div class="form-group">
<label>department</label>
<!-- 遍历返回的部门集合,显示部门名称 -->
<!--提交的是部门的id,发送的数据名称name是 Employee.department.id,值value是dept.id 级联属性-->
<select class="form-control" name="department.id">
<option th:each="dept:${departments}"
th:text="${dept.departmentName}"
th:value="${dept.id}">
</option>
</select>
</div>
</form>
7. 添加员工
添加员工:POST请求/emp,保存员工信息后重定向请求员工列表/emps,返回员工列表
- SpringMVC自动将请求参数与入参对象的属性进行一一绑定,Employee属性与请求参数名称一致
- 其中部门信息传过来的是
department.id=123
,会自动与javabean Employee 的属性 department 对象的 id 绑定起来。级联属性的绑定 - 添加操作完成后,需要返回员工列表list.html页面,但是不能返回,因为添加请求没有查询所有员工数据并返回,所以我们选择重定向到显示员工请求 /emps,该请求会查询所有员工并返回到list.html页面,参考第5小节 显示员工列表
// 添加员工
// SpringMVC自动将请求参数与入参对象的属性进行一一绑定
@PostMapping("/emp")
public String addEmp(Employee employee) {
System.out.println("保存的员工信息" + employee);
employeeDao.save(employee);
// 添加完成后不应该返回/emp/list.html,因为这个请求不会携带list页面需要的员工数据
// 应该转发或重定向到/emps请求显示全部员工数据
// redirect:表示重定向到一个新地址
// forward:表示转发到一个新地址
return "redirect:/emps";
}
8. 重定向与转发
重定向:发送一个新的请求,并且不携带本次请求的数据
转发:
p40 重定向与转发源码讲解
ThymeleafViewResolver:
protected View createView(String viewName, Locale locale) throws Exception {
if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
} else {
String forwardUrl;
// 视图名称以redirect:开头,则进行重定向
if (viewName.startsWith("redirect:")) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
forwardUrl = viewName.substring("redirect:".length(), viewName.length());
// 最终在renderMergedOutputModel方法 中调用servlet原生重定向response.sendRedirect(encodedURL);
RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
} else if (viewName.startsWith("forward:")) {
// 视图名称以forward:,则进行转发
vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
forwardUrl = viewName.substring("forward:".length(), viewName.length());
// 最终在renderMergedOutputModel方法 中调用servlet原生转发requestDispatcher.forward
return new InternalResourceView(forwardUrl);
} else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
} else {
vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
return this.loadView(viewName, locale);
}
}
}
9. 日期格式化
SpringBoot 默认日期格式是yyyy/MM/dd
,如果需要修改可以在 application.properties 中配置
spring.mvc.format.date=yyyy-MM-dd
10. 跳转到员工编辑页面
GET请求 /emp/{id},将该员工信息返回到前台,由于还要显示所有部门信息,所以查询所有部门,返回信息无论添加到Map,还是ModelAndView都是一样的。
返回页面为 /emp/add.html,这是将添加与编辑功能混合起来的页面
@GetMapping("/emp/{id}")
public String toeditPage(@PathVariable("id") Integer id, Map<String, Object> map) {
Employee employee = employeeDao.get(id);
map.put("employee", employee);
// 查询部门,返回到编辑页面
Collection<Department> departments = departmentDao.getDepartments();
map.put("departments", departments);
return "/emp/add";
}
11. 编辑员工信息
PUT请求,/emp,返回编辑员工页面
- 由于编辑员工与添加员工共用一个页面add.html,需要在表单中加入 _method 属性,当编辑时生效
- 注意发送的仍然为 POST 请求,只是会携带参数
_method=PUT
,但是 SpringBoot 中的 HiddenHttpMethodFilter 过滤器会对隐藏的请求方式进行修改,将请求修改为 PUT 请求。 - form 表单本身不支持 PUT DELETE 请求
- 注意url中没有 /{id}
<input type="hidden" name="_method" value="PUT" th:if="${employee != null}">
开启SpringBoot 中的 HiddenHttpMethodFilter 过滤器,这样才能将 POST 请求转为 PUT
# 将POST请求转换为PUT请求,springboot2.x默认关闭
spring.mvc.hiddenmethod.filter.enabled=true
保存员工信息,
// 编辑员工信息
@PutMapping("/emp")
public String updateEmp(Employee employee) {
System.out.println("保存的员工信息" + employee);
// save 方法是employee没有id,则新增;有id则更新
employeeDao.save(employee);
return "redirect:/emps";
}
隐藏的请求方式转换过滤器 HiddenHttpMethodFilter 源码如下
HiddenHttpMethodFilter:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
// 查看是否为POST请求
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 获取请求中携带的_method属性
String paramValue = request.getParameter("_method");
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
// 如果method为 PUT 或 DELETE,则创建新的请求,设置请求方式为method
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
12. 员工删除
DELETE请求 /emp/{id},重定向到员工列表请求 /emps,返回员工列表页面 /list.html
- 发送的还是 POST请求,SpringBoot 会转换为 DELETE 请求
// 删除员工信息
@DeleteMapping("/emp/{id}")
public String deleteEmp(@PathVariable("id") Integer id) {
employeeDao.delete(id);
// 重定向到员工列表页面,返回所有员工信息
return "redirect:/emps";
}
小知识: 不要从 pdf 复制代码
查看下方两个 form-control,上面是从pdf复制的代码,下面是手动敲的代码,看起来完全一致,但是在代码中替换会报错。
查看二者的十六进制编码,发现上方-
的编码是 E28090,下方-
编码是 2D,对应 ASCII 码表中的-
form‐control
form-control
00000000: 66 6F 72 6D E2 80 90 63 6F 6E 74 72 6F 6C 0D 0A formb..control..
00000010: 66 6F 72 6D 2D 63 6F 6E 74 72 6F 6C form-control
注:使用 VSCode 插件 Hexdump 查看的十六进制编码
4.7 定制错误页面
1. SpringBoot 默认错误处理机制
-
访问一个不存在的url,会发生错误,返回错误页面。SpringBoot 默认错误页面如下
SpringBoot错误页面 -
如果使用其他客户端(Postman),则返回一个json数据
{ "timestamp": "2020-07-07T09:20:38.492+00:00", "status": 404, "error": "Not Found", "message": "", "path": "/crud/dsahw" }
原因:之所以二者返回结果不同,是因为浏览器请求错误页面,请求头中包含参数
Accept: text/html
,表示优先接受 html 数据,所以响应返回html页面。而postman 请求错误页面请求头参数为Accept: */*
,所以响应返回json数据。
步骤:
- 当系统发生 4xx 或 5xx 错误,ErrorPageCustomizer错误页面响应规则就会生效,就会转发到 /error 请求
- /error 请求由 BasicErrorController 处理,返回 html 页面或 json 数据
- 使用 DefaultErrorAttributes 获取错误信息
- 将上一步获取的错误信息添加到 ModelAndView中返回。如果返回 html 页面,则 DefaultErrorViewResolver 根据状态码去 /error 目录下查找对应的 4xx.html 页面并返回;
原理:
ErrorMvcAutoConfiguration,错误处理的自动配置,这个类给容器中添加了以下组件:
-
ErrorPageCustomizer:错误页面配置信息
public class ErrorProperties { // 从配置文件读取错误页面路径,默认为/error // 类似与在web.xml中注册的错误页面 @Value("${error.path:/error}") private String path = "/error";
扩展:在web.xml中配置错误响应或异常对应的页面
https://blog.csdn.net/qq_41642093/article/details/79100579 -
BasicErrorController:处理/error请求
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { // 响应html类型的数据,浏览器请求来这个方法处理 @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); // 使用 DefaultErrorAttributes 获取错误信息,并返回到页面 Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 哪个页面作为错误页面?ModelAndView包含页面地址和页面数据 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } // 响应json类型的数据,postman来这个方法处理 @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); }
-
DefaultErrorAttributes:获取错误页面属性集合
@Override public Map<String, Object> getErrorAttributes( RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); // 返回错误页面展示的信息,包括时间,状态码,错误信息,请求路径等 errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; }
- DefaultErrorViewResolver:根据错误码去 /error 下查找对应的 4xx.html 并返回
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // 根据状态码去查找错误页面 modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { // SpringBoot默认去找错误页面,路径为 error/45xx.html String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { // 如果模板引擎可用,则返回并渲染错误页面 return new ModelAndView(errorViewName, model); } // 模板引擎不可用,则去返回 static/45xx.html 静态页面 return resolveResource(errorViewName, model); }
2. 定制错误响应页面
- 定制错误页面
- 创建错误页面 /templates/error/404.xml,若发生 404 错误,模板引擎则渲染返回该页面
- 也可以创建 4xx.html 来匹配 4xx 错误,精确优先
- 页面可以获取的信息:
- timestamp 时间戳,
- status 错误状态码,
- error 错误提示,
- exception 异常对象,
- messgae 异常信息
- errors JSR303数据校验错误
- 如果模板引擎找不到匹配的错误页面,则去静态资源文件夹下查找并返回对应的静态页面
- 如果以上都没有,则使用SpringBoot默认的错误页面
在错误页面显示异常信息需要手动开启:
# 返回异常信息到错误页面
server.error.include-exception=true
server.error.include-message=always
在错误页面显示错误信息:
<!-- 行内写法与普通写法,前者更简便 -->
<h2>status: [[${status}]]</h2>
<h2 th:text="'timestamp: ' + ${timestamp}"></h2>
<h2>error: [[${error}]]</h2>
<h2>exception: [[${exception}]]</h2>
<h2>message: [[${message}]]</h2>
<h2>errors: [[${errors}]]</h2>
- 定制错误json数据
-
使用注解@ControllerAdvice 自定义异常处理器,发生异常后返回 json 数据
// 自定义异常处理器,需要注解 @ControllerAdvice @ControllerAdvice public class MyExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class); // 只处理UserNotExistException异常,返回json数据,包含错误码code,异常信息message @ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String, Object> handlerUserNotException(Exception e) { logger.error("用户不存在异常:{}", e); Map<String, Object> map = new HashMap<>(); map.put("code", "user.notexist"); map.put("message", e.getMessage()); return map; } }
-
转发到/error进行自适应响应效果处理,浏览器返回页面,postman返回json
@ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //传入我们自己的错误状态码 4xx 5xx, // 不传入的话会访问/error成功,状态码为200,就不返回错误页面 5xx.html了 /** * BasicErrorController中获取状态码如下: * 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","用户出错啦"); request.setAttribute("ext",map); //转发到/error return "forward:/error"; }
4.8 配置嵌入式 Servlet 容器
SpringBoot 默认使用的嵌入式 Servlet 容器为 Tomcat,查看 pom 依赖,可以看到 web-starter 依赖 tomcat-starter,使用的是 9.0 版本的 Tomcat。
[图片上传失败...(image-4ead43-1606212213489)]
1. 修改 Servlet 容器的相关配置
-
在
application.properties
中配置 Tomcat,属性参考 ServerPropertiesserver.port=8090 server.servlet.context-path=/crud server.tomcat.max-connections=8 server.tomcat.uri-encoding=UTF-8
下面是 ServerProperties 源码,配置的 server 属性都绑定到该类属性
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { // Tomcat端口,server.port private Integer port; // Servlet有属性contextPath,表示项目路径 server.servlet.context-path private final Servlet servlet = new Servlet(); // Tomcat有属性uriEncoding,表示uri编码 server.tomcat.uri-encoding private final Tomcat tomcat = new Tomcat();
-
自定义 WebServerFactoryCustomizer,配置 Tomcat 属性,优先级高于配置文件
参考 SpringBoot 服务器配置文档(与SpringBoot 1.x 配置方式不同),自定义 WebServerFactoryCustomizer,设置Tomcat 端口,uri编码等规则。
@Configuration public class MyMvcConfig extends WebMvcConfigurerAdapter { // 配置 Tomcat,将自定义配置加入容器 @Bean public WebServerFactoryCustomizer webServerFactoryCustomizer() { return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() { // 定制嵌入式Servlet容器相关的规则,对其他容器也生效 @Override public void customize(ConfigurableWebServerFactory factory) { factory.setPort(8090); } }; }
2. 注册 Servlet 三大组件
请求处理过程: 一个请求进入Tomcat,需要经过 Filter -> Servlet -> Interceptor -> Controller 四个步骤,详细如下:
20200708043128Servlet、Filter、Listener 三大组件之前是配置在 web.xml 中,SpringBoot默认以 jar 包方式启动嵌入式 Tomcat 来运行 SpringBoot 的 web应用,没有 web.xml,所以使用以下方式注册 Servlet 三大组件:
- 注册自定义 Servlet,
- 使用 ServletRegistrationBean 注册自定义 Servlet 组件到容器,需要@Bean注解
- 这个Servlet不会被前面的拦截器拦截,因为自定义的 MyServlet 与 DispatchServlet 平级,拦截器是在 DispatchServlet 后,对应 Controller 处理前生效的,所以不会拦截自定义的 MyServlet
- Spring的@Bean注解用于告诉方法,返回一个Bean对象,将其加入Spring容器。产生这个Bean对象的方法Spring只会调用一次(单例)。
// 自定义Servlet
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 返回 hello serlvet 给页面
resp.getWriter().write(" hello serlvet...");
}
}
// @Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法
// 这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并将这些 Bean 加入到 Spring 容器
// 注册自定义 Servlet 组件到容器
@Configuration
public class MyServerConfig {
// 注册Servlet组件
@Bean
public ServletRegistrationBean myServlet() {
// 创建注册器,参数为MyServlet与映射路径/mySerlvet
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/mySerlvet");
return registrationBean;
}
}
实际案例: SpringBoot 不需要在 web.xml 中配置 SpringMVC 的前端控制器 DispatcherServlet ,其自动注册 DispatcherServlet,注册方式就是使用 ServletRegistrationBean 将 DispatcherServlet 添加到容器
@Bean(name = {"dispatcherServletRegistration"})
@ConditionalOnBean(value = {DispatcherServlet.class}, name = {"dispatcherServlet"})
public ServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
// 创建注册器,参数为DispatcherServlet与映射路径 /
// 可以通过 server.servlet-path 修改dispatcherServlet 的映射路径
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet, new String[]{this.serverProperties.getServletMapping()});
registration.setName("dispatcherServlet");
// 设置启动顺序为 -1
registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.m ultipartConfig);
}
return registration;
}
- 注册自定义过滤器 Filter
- 使用 FilterRegistrationBean 注册自定义过滤器到容器,需要@Bean注解
- 过滤器会在指定url请求到 servlet 之前生效
- 过滤器由Servlet容器管理,而拦截器则可以通过IoC容器来管理
- 常见过滤器:编码过滤器,敏感词过滤器,压缩资源过滤器
// 自定义过滤器
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 过滤处理
System.out.println("MyFilter doFilter...");
// 调用过滤器链中的下一个过滤器
chain.doFilter(request, response);
}
@Override
public void destroy() {}
}
@Configuration
public class MyServerConfig {
// 注册自定义过滤器到容器
@Bean
public FilterRegistrationBean myFilter() {
FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MyFilter());
// 设置过滤的url
registrationBean.setUrlPatterns(Arrays.asList("/myServlet"));
return registrationBean;
}
除了注册过滤器的方式,还可以使用注解注册过滤器 https://www.cnblogs.com/paddix/p/8365558.html
常见的过滤器实现参考,包括编码过滤器,敏感词过滤器,压缩资源过滤器 https://mp.weixin.qq.com/s/psRMhj4IlcjyVPE0a64vBA
- 注册自定义监听器
- 使用 ServletListenerRegistrationBean 注册自定义监听器到容器,需要@Bean注解
- 常见监听器:网站访问人数统计
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized...web应用启动");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextInitialized...web应用销毁");
}
}
// 注册Listener到容器
@Bean
public ServletListenerRegistrationBean myListener() {
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}
过滤器与监听器都可以参考 https://github.com/ZhongFuCheng3y/3y
使用其他嵌入式 Servlet 容器
SpringBoot 支持 3 种嵌入式 Servlet 容器:
- Tomcat 默认
- Jetty 长链接友好
- Undertow 非阻塞式,并发性能好
切换为 Jetty: 修改 pom.xml 配置文件,移除 tomcat-starter,引入 jetty-starter
<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>
<!-- 前面排除默认依赖的tomcat,现在引入jetty-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
重新启动项目,Jetty started on port(s) 8080 (http/1.1) with context path '/crud'
切换为 Undertow:
<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>
<!-- 前面排除默认依赖的tomcat,现在引入undertow-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
小技巧: IDEA 排除 pom.xml 中的依赖包
右击 pom.xml,选择 Diagram,找到要排除的包,右击选择 Exclude
4.9 嵌入式 Servlet 容器自动配置原理
// 补充:p49,SpringBoot 2.x 这块变动较大
4.10 嵌入式 Servlet 容器启动原理
// 补充:p50,SpringBoot 2.x 这块变动较大。这两个章节是面试重点,但是并没有搞懂
4.11 使用外置的 Servlet 容器
-
嵌入式Servlet容器:应用打成可执行的jar
- 优点:简单、便携;
- 缺点:默认不支持JSP、优化定制比较复杂
-
外置的Servlet容器:外面安装Tomcat,将应用打为war包并部署;
// 补充:流程与原理