Springboot核心技术学习笔记一

  • 第 1 章 SpringBoot 入门
    • 1.1 Spring Boot 简介
    • 1.2 微服务
    • 1.3 环境准备
    • 1.4 SpringBoot HelloWorld
    • 1.5 探究 HelloWorld
        1. pom 文件
        1. 导入依赖
        1. Main 主程序类
        1. 启动日志分析
    • 1.6 快速创建 SpringBoot 项目
  • 第 2 章 SpringBoot 配置
    • 2.1 配置文件
    • 2.2 YAML 语法
        1. 基本语法
        1. 值的写法
        1. 配置属性列表
    • 2.3 配置文件值的注入
        1. @ConfigurationProperties 配置文件属性的绑定
        1. 松散绑定
        1. Properties 乱码问题
        1. @ConfigurationProperties 与 @Value
        1. @PropertySource 与 @ImportResource
        1. @Configuration 和 @Bean
    • 2.3 配置文件占位符
        1. 属性配置占位符
        1. 随机数
    • 2.4 Profile 多环境配置文件切换
    • 2.5 配置文件加载位置
    • 2.6 配置文件加载顺序(重点)
    • 2.7 自动配置原理(重难点)
    • 2.8 @Conditional 自动配置报告
  • 第 3 章 SpringBoot 日志
    • 3.1 常见日志框架
    • 3.2 SLF4j 使用
    • 3.3 SpringBoot 日志关系(了解)
    • 3.4 日志使用
        1. 常见日志配置属性
        1. 指定日志框架配置文件
    • 3.5 切换日志框架
  • 第 4 章 SpringBoot 与 WEB 开发
    • 4.1 简介

    • 4.2 SpringBoot 静态资源映射规则

        1. Webjars
    • 4.3 模板引擎

        1. 引入Thymeleaf
        1. Thymeleaf 的使用
        1. Thymeleaf 语法
    • 4.4 SpringMVC 自动配置

    • 4.5 修改 SpringBoot 默认配置

    • 4.6 Restful crud 项目

        1. 国际化
          1. 国际化原理
          1. 实现语言切换功能
        1. 登录
        1. 显示员工列表
        1. 跳转到员工页面
        1. 添加员工
        1. 重定向与转发
        1. 日期格式化
        1. 跳转到员工编辑页面
        1. 编辑员工信息
        1. 员工删除
    • 4.7 定制错误页面

        1. SpringBoot 默认错误处理机制
    • 4.8 配置嵌入式 Servlet 容器

        1. 修改 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常用命令&操作
        1. 镜像操作
        1. 容器操作
        1. 安装 MySQL 示例
  • 第 6 章 SpringBoot 与数据访问
    • 6.1 数据源初始化与 JDBC
        1. 配置 MySQL
        1. 数据源自动配置原理
        1. 数据表自动初始化
        1. 使用 JdbcTemplate 查询数据
        1. 数据库自动初始化原理
    • 6.2 使用外部数据源
    • 6.3 自定义数据源原理
    • 6.4 配置 Druid 数据源
    • 6.5 整合 MyBatis
        1. 注解版
        1. Mybatis 常见配置
        1. xml 版
    • 6.6 整合 SpringData JPA
        1. Spring Data 简介
        1. 整合 SpringData JPA
  • 第 7 章 SpringBoot 启动配置原理
    • 7.1 启动流程
        1. 创建SpringApplication对象
        1. 运行run方法
    • 7.2 事件监听机制
  • 第 8 章 SpringBoot 自定义 starter
    • 8.1 starter 原理
    • 8.2 自定义 starter
    1. SpringBoot 与开发热部署
  • 进阶学习
  • 待补充
  • 推荐阅读
  • 参考文档

你无法掌握所有的知识,抓大放小,不要关注边边角角的知识,保证最重要的知识烂熟于胸即可 —— 罗翔

第 1 章 SpringBoot 入门

1.1 Spring Boot 简介

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

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

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

传统的 Spring 开发需要经历以下步骤:

  1. 配置 pom.xml,引入 SSM 项目各种依赖
  2. 配置web.xml,设置监听器 ContextLoaderListener,当项目启动时自动启动 Spring 容器
  3. 配置web.xml,设置 Spring 的配置文件applicationContext.xml
  4. 配置web.xml,设置 SpringMVC 前端控制器DispatcherServlet拦截所有请求
  5. 配置web.xml,设置字符集编码过滤器 CharacterEncodingFilter,隐藏HTTP请求过滤器HiddenHttpMethodFilter
  6. 配置applicationContext.xml,加载 properties 配置文件,设置 Spring 自动扫描的包 component-scan
  7. 配置applicationContext.xml,设置数据源,数据库连接信息和相关属性
  8. 配置applicationContext.xml,和 Mybatis 整合,设置SqlSessionFactory Bean,设置 Mybatis 扫描的接口,会生成代理类加入容器
  9. 配置dispatcherServlet-servlet.xml,设置 SpringMVC 视图解析器InternalResourceViewResolver
  10. 配置dispatcherServlet-servlet.xml,设置注解驱动,设置上传文件的bean CommonsMultipartResolver
  11. 对数据源配置事务管理器DataSourceTransactionmanager,开启基于注解的事务
  12. 配置pom.xml,开发环境配置 Jetty Maven 插件,使用 jetty:run启动项目
  13. 项目发布需要打 war 包,配置服务器环境,包括 tomcat,MySQL等。

[图片上传失败...(image-927ff1-1606212213488)]

具体配置参考项目 spring-boot-01-ssm,更多相关参考 SSM整合教程
繁多的配置,低下的开发效率,复杂的部署流程,第三方技术集成难度大,SpringBoot 是为了简化 Spring 应用开发而生,具有以下优点:

  1. 快速创建 Spring 项目,快速与主流框架集成
  2. 去除了 Spring 中繁琐的 XML 配置,开箱即用,以前需要配置 web.xml、applicationContext.xml等文件才可以使用
  3. 使用嵌入式 Tomcat 容器,无需打成 war 包,帮助快速开发和部署
  4. starters 自动依赖管理与版本控制,比如要使用 WEB 工程,导入 WEB starters 即可,WEB 依赖的其他 jar 包,starters 会自动导入依赖并控制版本,避免了版本不兼容和依赖繁琐的问题
  5. 大量的自动配置,简化开发。以前创建一个项目,要懂得 SpringMVC Mybatis 如何配置
  6. 准生产环境的运行时应用监控
  7. 与云计算的天然集成

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 开发:

  1. 创建一个 Maven 项目
  2. 引入 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>
  1. 编写一个主程序,启动 SpringBoot 应用
/**
 * @SpringBootApplication 来标注一个主程序类,说明这是一个 SpringBoot 应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {
    public static void main(String[] args) {

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

  1. 开发:直接编写 Controller 与 Service,不需要配置 Spring 与 SpringMVC 了
@Controller
public class HelloContorller {

    @RequestMapping("/hello")     // 设置访问的url
    @ResponseBody                 // 表示数据直接返回给浏览器,如果是对象则转为json数据
    public String hello() {
        return "Hello SpringBoot...";
    }
}
  1. 测试:直接启动主程序 Main 即可,不需要打包到 Tomcat 启动

访问 http://localhost:8080/hello ,显示 Hello SpringBoot...

  1. 部署:

    1. 引入 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>
    
    1. 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 下为我们的源码。
    2. 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 插件,然后新建项目:

  1. 选择 Spring Assistant,选择 Default 即从 Spring 官网(https://start.spring.io/ )联网创建 SpringBoot项目,
  2. 填写项目名称信息
  3. 选择需要导入的依赖,如 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 组件加入容器的三种方式:

  1. @EnableConfigurationProperties
// 将配置文件中属性与该类属性绑定
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {

}

@Configuration(proxyBeanMethods = false)
// 将制定的配置类DataSourceProperties 加入Spring容器,使其生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
}
  1. @Bean
    // 配置制定的 Druid 数据源,将方法返回的对象加入Spring容器
    @Bean
    // 绑定配置文件中的属性到指定类
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druid() {
        return new DruidDataSource();
    }
  1. @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配置文件

  1. 标注 Spring 配置类 @Configuration
  2. 使用 @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提供了三种多环境配置文件切换功能

  1. 配置文件中指定
# properties设置启动的配置文件为 application-dev.properties
spring.profiles.active=dev
# yml设置启动的配置文件为 application-dev.yml
spring:
  profiles:
    active: dev

注意: 是 profiles 不是 profile

  1. 命令行参数指定
java -jar MyApp.jar --spring.profiles.active=dev

也可以在IDEA 运行->编辑配置->程序参数 中添加--spring.profiles.active=dev

  1. 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 配置文件加载顺序(重点)

优先级由高到低,高优先级会覆盖低优先级配置:

  1. 命令行参数
  2. Java系统属性(System.getProperties())
  3. jar包外部的application-{profile}.properties/yml(带spring.profile)配置文件
  4. jar包内部的application-{profile}.properties/yml(带spring.profile)配置文件
  5. jar包外部的application.properties/yml(不带spring.profile)配置文件
  6. jar包内部的application.properties/yml(不带spring.profile)配置文件
  7. @Configuration注解类上的@PropertySource指定的配置文件(参考#2.3.5 中Person类指定的配置文件)
  8. 通过SpringApplication.setDefaultProperties指定的默认属性

命令行参数适用生产环境修改某个配置时使用,jar包外面的配置文件,适合生产环境批量修改多个配置时使用

2.7 自动配置原理(重难点)

// 补充:自动配置原理其实并没有搞懂,后面进行补充

  1. SpringBoot 启动时加载主程序类,@SpringBootApplication 注解包含了 @EnableAutoConfiguration
  2. @EnableAutoConfiguration 表示开启自动配置功能
  3. @EnableAutoConfiguration 作用是将 META-INF/spring.factories 里面配置的所有XXXAutoConfiguration的组件加入到了容器中
    • 源码参考 AutoConfigurationImportSelector#selectImports,里面获取了所有自动配置类,如 DataSourceAutoConfiguration,WebMvcAutoConfiguration
  4. 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; 
  1. 所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着,配置文件能配置什么就可以参照某个功能对应的这个属性类。如HttpEncodingProperties.class
    //从配置文件中获取指定的值和bean的属 性进行绑定 
    @ConfigurationProperties(prefix = "spring.http.encoding") 
    public class HttpEncodingProperties { 
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF‐8");
    

·

扩展:
向Spring容器添加组件的三种方式:

  1. @Componet,@Controller,@Service,@Repository,@Configuration:局限于自己写的组件
  2. @Bean:注解用于告诉方法,返回一个Bean对象,将其加入Spring容器。产生这个Bean对象的方法Spring只会调用一次(单例)。Bean导入第三方包的组件,是一个方法级别上的注解,主要用在@Configuration注解的类里,也可以用在@Component注解的类里。@Bean注解把当前方法的返回值作为bean对象存入spring容器中,其name属性用于指定bean的id(若没写该属性,默认值是当前的方法名)
  3. @Import:导入组件

2.8 @Conditional 自动配置报告

// 补充:没搞懂,配合SPring注解教程学习

第 3 章 SpringBoot 日志

3.1 常见日志框架

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

  1. 刚开始使用System.out.println("");将关键数据打印在控制台;
  2. 为了记录记录系统的一些运行时信息,开发了一个日志框架 zhanglogging-1.0.jar;
  3. 迭代更新日志框架,异步模式,按日期自动归档等 zhanglogging-2.0.jar
  4. 为了替换日志框架,重新修改之前相关的API,特别麻烦
  5. 参考 JDBC--数据库驱动,门面设计模式,统一的接口层,不同数据库的代码都是一样的,需要数据库去实现驱动接口
  6. 实现一个统一的接口层:日志门面(日志的一个抽象层)logging-abstract.jar
  7. 给项目中导入具体的日志实现就行了,日志框架升级、替换也不需要修改我们的代码
日志门面(抽象层) 日志实现
SLF4jjakarta-commons-Logging 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)]

总结:

  1. SpringBoot 底层也是使用 slf4j+logback 的方式进行日志记录
  2. SpringBoot 也把其他的日志都替换成了 slf4j 中间替换包
  3. slf4j 中间替换包本质是将 jakarta-commons-Logging 等日志框架使用相同类名重写了一次
  4. 如果要引入其他日志框架,需要将中间替换包移除

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 开发三步走:

  1. 创建SpringBoot 应用,选择需要使用的模块,如 web,Mybatis等
  2. SpringBoot 已经默认将这些场景配置好了,只需要配置文件中指定少量配置就可以运行起来
  3. 编写业务代码

自动配置原理:

自动配置相关的类都在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/

  1. 所有的/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的结构
  1. 访问静态资源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));
        }
    }
  1. 静态资源路径,classpath 指项目/src/main/resource,与java类的根路径同级,使用java/resource区分资源,本身都是在classpath下,打包后刚好在classpath下即\BOOT-INF\classes

    • classpath:/META‐INF/resources/
    • classpath:/resources/
    • classpath:/static/
    • classpath:/public/
    • /:当前项目的根路径
  2. 欢迎页映射,访问 localhost:8080,会去静态资源文件夹下寻找index页面

WebMvcAutoConfiguration:
//配置欢迎页映射 
    @Bean
    public WelcomePageHandlerMapping welcomePageHandlerMapping(ResourceProperties resourceProperties) {
        return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
    }
  1. 网站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";
  1. 在 src/main/resources/templates/ 下创建 success.html 文件
  2. 向 success.html 中导入 Thymeleaf 的名称空间,代码提示功能还需要使用IDEA-U版打开 Thymeleaf 插件
  3. 使用 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 激活

4. Thymeleaf 语法

标签:

  1. 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>
    
  2. th:each 遍历标签

  3. th:if 条件标签

  4. 更多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 默认配置

  1. SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如 果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默 认的组合起来;
  2. 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置
  3. 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置

4.6 Restful crud 项目

1. 国际化

  1. 创建和编写国际化配置文件
    [图片上传失败...(image-ea5caa-1606212213489)]

  2. 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;
    }
  1. 在页面使用#{}获取国际化内容
    <label class="sr-only" th:text="#{login.username}">Username</label>
  1. 修改浏览器语言,访问页面,查看国际化效果

    修改浏览器语言为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. 实现语言切换功能

  1. 修改前面页面,添加语言切换按钮,发送请求携带参数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>
  1. 自定义区域解析器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) {

    }
}
  1. 将区域解析器MyLocaleResolver加入到容器,使其生效
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    // 向容器中加入我们自定义的bean MyLocaleResolver,bean的id为方法名,
    // 用于替换默认的localeResolver,当然也可以在@中设置id
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

2. 登录

小知识: Thymeleaf 页面修改实时生效,不用重启项目

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

  1. 禁用模板引擎缓存
spring.thymeleaf.cache=false
  1. 页面修改完成后 ctrl+f9,重新编译

  1. 登录表单

         <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> 
    
  2. 处理登录请求,登录成功后,一定要重定向到主页,如果是转发,那么刷新页面就会有表单重复提交问题

     @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";
     }
    
  3. 显示登录失败信息

         <p th:if="${not #strings.isEmpty(msg)}" style="color: red" th:text="${msg}"></p>
    
  4. 拦截器,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. 抽取页面公共元素,如导航栏等
1. 使用 ~{templatename::fragmentname}:模板名::片段名 
    抽取公共片段:
    <div th:fragment="copy"> 
        &copy; 2011 The Good Thymes Virtual Grocery 
    </div> 

    引入公共片段:footer是公共片段的文件名
    <div th:insert="~{footer :: copy}">
    </div> 
2. 使用 ~{templatename::#selector}:模板名::选择器 
    <div id="copy1"> 
        &copy; 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 默认错误处理机制

  1. 访问一个不存在的url,会发生错误,返回错误页面。SpringBoot 默认错误页面如下

    SpringBoot错误页面
  2. 如果使用其他客户端(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数据。

步骤:

  1. 当系统发生 4xx 或 5xx 错误,ErrorPageCustomizer错误页面响应规则就会生效,就会转发到 /error 请求
  2. /error 请求由 BasicErrorController 处理,返回 html 页面或 json 数据
  3. 使用 DefaultErrorAttributes 获取错误信息
  4. 将上一步获取的错误信息添加到 ModelAndView中返回。如果返回 html 页面,则 DefaultErrorViewResolver 根据状态码去 /error 目录下查找对应的 4xx.html 页面并返回;

原理:

ErrorMvcAutoConfiguration,错误处理的自动配置,这个类给容器中添加了以下组件:

  1. 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

    
    
  2. 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);
     }
    
  3. 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; 
     }
    
  1. 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. 定制错误响应页面

  1. 定制错误页面
    • 创建错误页面 /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>
  1. 定制错误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,属性参考 ServerProperties

    server.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 四个步骤,详细如下:

20200708043128

Servlet、Filter、Listener 三大组件之前是配置在 web.xml 中,SpringBoot默认以 jar 包方式启动嵌入式 Tomcat 来运行 SpringBoot 的 web应用,没有 web.xml,所以使用以下方式注册 Servlet 三大组件:

  1. 注册自定义 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;
    }
  1. 注册自定义过滤器 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

  1. 注册自定义监听器
    • 使用 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包并部署;

// 补充:流程与原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260