SpringBoot 学习笔记

SpringBoot 学习笔记

目录

1. SpringBoot简介

1.1 什么是Spring

  • Spring:中文译为春天 --> 给 Java 程序员的春天
  • 2002年,首次推出了 Spring 框架的雏形: interface 21 框架
  • Spring框架即以 interface 21 框架位基础,并不断丰富其内涵,于2004年3月24日,发布了1.0正式版
  • Rod Johnson , Spring Frameword 创始人,著名作者。很难想象 Rod Johnson 的学历,他是悉尼大学的博士,然而他的专业不是计算机,而是音乐学
  • Spring 理念:使现有的技术更加容易使用,本身就是个大杂烩

Spring 是为了解决企业应用开发的复杂性而创建的,简化开发

1.2 Spring 是如何简化Java开发的

为了降低 Java 开发的复杂性,Spring 采用一下4种关键策略:

  1. 基于 POJO(老式 Java 对象)的轻量级和最小侵入性编程,所有东西都是一个 JavaBean
  2. 通过 IOC,依赖注入(DI)和面向接口编程实现松耦合
  3. 基于切面(AOP)和惯例进行声明式编程
  4. 通过切面和模板减少样式代码,RedisTemplate,xxxTemplate等

1.3 什么是 SpringBoot

  • SpringBoot 是由Pivotal团队在2013年开始研发、2014年4月发布第一个版本的全新开源的轻量级框架。它基于Spring4.0设计,不仅集成了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。另外,SpringBoot 通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。

  • SpringBoot就是一个javaWeb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置,you can "just run",能迅速地开发web应用,几行代码开发一个http接口。

  • SpringBoot 是基于 Spring 开发的,SpringBoot 本身并不提供 Spring 框架的核心特性以及扩展功能,=只是用来快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。 SpringBoot 以 约定大于配置 的核心思想,默认帮我们进行了很多配置,多数 SpringBoot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 应用中这些第三方库可以零配置地开箱即用。

1.4 SpringBoot 的主要优点

  • 为所有的 Spring 开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

2. 第一个 SpringBoot 程序

2.1 环境准备

  • JDK 1.8
  • Maven-3.6.1
  • SpringBoot 2.X 最新版
  • IDEA

2.2 创建第一个工程项目

有三种创建 SpringBoot 工程项目的方式

  1. Spring 官方提供了非常方便的工具让我们快速构建应用

    官网快速构建网址:https://start.spring.io/

    image-20200707092419075

  2. 使用 IDEA 快速构建应用

    IDEA 开发工具已经集成了 SpringBoot 项目的创建,可以直接使用 IDEA 工具进行快速构建应用

    • File --> Project... 或者是 create New Project,然后就弹出下面的构建先项目的框

      image-20200707092941710

    • 配置项目信息

      image-20200707093819013

    • 添加外部依赖

      可以自行选择添加外部依赖

      image-20200707094451428

    • 后面的可以一直next下去

  3. 使用 Maven 创建 SpringBoot 应用

    创建 Maven 工程后,导入如下依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    

构建完SpringBoot工程后,项目结构如下:

image-20200707101733187

2.3 编写程序代码

  • 修改服务器端口

    修改 resources 目录下 application.properties 文件

    # 应用服务 WEB 访问端口
    server.port=8088
    
  • 编写Controller

    创建个 Controller 包并创建 HelloController

    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String hello(){
            return "Hello,SpringBoot!";
        }
    
    }
    
  • 启动 SpringBoot 应用程序

    image-20200707103604124

    启动后控制台日志输出如下:

    若输出如下信息,则代表启动成功

    image-20200707103810636

  • 访问 /hello 接口

    image-20200707104020424

至此,我们的第一个 SpringBoot 工程项目创建完毕

注意事项

  • 使用 IDEA 创建 SpringBoot 的时候,若网络不好或者连接不上官网,可以采用阿里的 SpringBoot 源:http://start.aliyun.com

  • SpringBoot 内置 Tomcat,若需要配置 Tomcat,则需要修改 application.properties 文件,比如上面的设置 Tomcat 服务器端口号。Tomcat 启动失败,需要注意端口号是否冲突等问题,设置 Tomcat 配置即可解决

  • 所有的代码必须放在 SpringBoot 主启动类同级目录下或同级目录的包下,因为 SpringBoot 的 @SpringBootApplication 注解中的 @ComponentScan 注解默认配置的是扫描同级包下的类以及同级包下所有包里面的类。编写代码时必须得按照这个规范来。且主启动类必须有如下代码

    public static void main(String[] args) {
        // XXXX.class 中的xxx为当前主启动类的类名,args 是 main 方法中的 args
        SpringApplication.run(Springboot01Application.class, args);
    }
    
  • 主启动类必须有 @SpringBootApplication 注解

2.4 打包

使用 maven 的package,将项目打成jar包

image-20200707111149007

打包插件代码如下:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

打包完成后,会在target目录下生成一个jar包,并会在控制台输出如下打包日志

image-20200707111333343

小彩蛋

在 resources 目录下创建一个 banner.txt 文件,可以替换 SpringBoot 的启动界面。

比如下图设置了个佛祖的文字图像

image-20200707112912850

文字如下:

////////////////////////////////////////////////////////////////////
//                          _ooOoo_                               //
//                         o8888888o                              //
//                         88" . "88                              //
//                         (| ^_^ |)                              //
//                         O\  =  /O                              //
//                      ____/`---'\____                           //
//                    .'  \\|     |//  `.                         //
//                   /  \\|||  :  |||//  \                        //
//                  /  _||||| -:- |||||-  \                       //
//                  |   | \\\  -  /// |   |                       //
//                  | \_|  ''\---/''  |   |                       //
//                  \  .-\__  `-`  ___/-. /                       //
//                ___`. .'  /--.--\  `. . ___                     //
//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
//      ========`-.____`-.___\_____/___.-`____.-'========         //
//                           `=---='                              //
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
//            佛祖保佑       永不宕机     永无BUG                    //
////////////////////////////////////////////////////////////////////

3. SpringBoot 运行原理

这里只是简单了解下 SpringBoot 的运行原理,若有错误,可以指出,勿喷。

3.1 pom.xml

我们先从 pom.xml 进行追入

从 pom.xml 我们可以发现,SpringBoot 应用中的 pom.xml 是有个父依赖的,父依赖是 spring-boot-starter-parent,主要是管理项目的资源过滤及插件

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

我们进入这个父依赖,会发现还有个父依赖,父依赖是:spring-boot-dependencies。

这个才是真正管理 SpringBoot 应用里面所有依赖版本的地方,SpringBoot 的版本控制中心

以后我们导入依赖默认是不需要写版本,但是如果导入的包没有在依赖中管理着,就需要手动配置版本

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.3.1.RELEASE</version>
</parent>

若是阿里源创建的 SpringBoot 项目,则是使用 dependencyManagement 来管理 SpringBoot 版本依赖,其引入的依赖也是 spring-boot-dependencies

<!-- 依赖版本管理 -->
<dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    </dependencies>
</dependencyManagement>

我们导入了 SpringBoot 的依赖后,会发现依赖名字都有一个共同的规律:artifactId 都是 spring-boot-starter-xxxxx。其中,这些 spring-boot-starter-xxxxx 就是 spring-boot 的场景启动器

比如:

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

我们这么写,maven就自动帮我们导入了 SpringBoot web模块所需要依赖的组件。

SpringBoot 将所有的功能场景都抽取出来,做成一个个的 starter(启动器)。我们只需要在项目中引入这些 starter ,所有相关的依赖都会导入进来。我们需要用什么功能,就导入什么样的场景启动器即可。我们未来也可以自己自定义 starter。

3.2 主启动类

pom.xml 帮我们定义好了场景启动器的版本号,接下来看看 SpringBoot 的启动类

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

可以发现 SpringBoot 启动类十分简单,只有一个注解。但真的是这样吗?

我们点进 @SpringBootApplication 这个注解,慢慢追进去,会发现里面大有乾坤。现在我们就开始一层一层点进去理解。

3.2.1 注解

1. @ComponentScan

中文直译:“组件扫描”。刚点进 @SpringBootApplication 这个注解,我们最熟悉的就是@ComponentScan这个注解了,它在这里的作用是扫描主启动类包下的类或者是主启动类包下其他包里面的类,将这些类注入到 Spring 容器中。所以,我们写的代码必须写在主启动包下或其包的子包下

这个注解参数加了 @Filter 是为了过滤不符合条件的包下的类

2. @SpringBootConfiguration

中文直译:“SpringBoot配置”。这个注解加了 @Configuration 注解,说明这个注解是配置类,并且会被 Spring 进行接管。这个注解的作用是 声明这个主启动类是 SpringBoot 的配置类,并且交由 SpringBoot 托管(或按本质来说,是 Spring 的配置类,交由 Spring 托管)

3. @EnableAutoConfiguration

中文直译:“自动导入配置”。这一个注解才是 @SpringBootApplication 这个注解的核心,或者是说 SpringBoot 自动配置的核心。这里面有两个注解:@AutoConfigurationPackage@Import

  • @AutoConfigurationPackage

    中文直译:“自动配置包”。这个注解是 SpringBoot 自动导入包的注解。它这个注解里面还有个 @Import 注解。使用 @Import 注解的源码部分如下:

    @Import(AutoConfigurationPackages.Registrar.class)
    

    通过这行代码,我们可以知道它是用来注册被扫描的包下的类到 Spring 容器中的。

    如果有兴趣,可以点进 AutoConfigurationPackages 这个类去继续追源码。它里面有两个静态修饰的类 RegistrarBasePackages, 还有一个final 定义的内部类 PackageImportsBasePackages 类是用来获取被扫描的所有的包下的类,Registrar 类是用来将这些类注册到 Spring 容器中的。PackageImports 类是导入包的包装类,用 String 数组存储待注册到 Spring 容器的包名。

  • @Import(AutoConfigurationImportSelector.class)

    这行注解是导入 AutoConfigurationImportSelector 这个类,这个类直译就是:“自动导入配置选择器” 。

    如果有兴趣,可以自己点进 AutoConfigurationImportSelector 这个类去追,由于能力有限,只能给个追的路线。

    • 方法 getCandidateConfigurations()

      获取所有的配置

    /**
     * Return the auto-configuration class names that should be considered. By default
     * this method will load candidates using {@link SpringFactoriesLoader} with
     * {@link #getSpringFactoriesLoaderFactoryClass()}.
     * @param metadata the source metadata
     * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
     * attributes}
     * @return a list of candidate configurations
     */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
             getBeanClassLoader());
       Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
             + "are using a custom packaging, make sure that file is correct.");
       return configurations;
    }
    
    • 方法 loadFactoryNames()

      方法位置如图所示,然后我们点进这个方法看代码

    image-20200708103253255

    /**
      * Load the fully qualified class names of factory implementations of the
      * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
      * class loader.
      * @param factoryType the interface or abstract class representing the factory
      * @param classLoader the ClassLoader to use for loading resources; can be
      * {@code null} to use the default
      * @throws IllegalArgumentException if an error occurs while loading factory names
      * @see #loadFactories
      */
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    }
    

    根据注释第二行 given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given

    image-20200708104018794

    上图中的 META-INF/spring.factories 在下图中 这个位 置

    image-20200708104405797

    点进 spring.factories ,可以看到很多 Spring 已经帮我们定义好的 jar 包

    image-20200708104750250

3.2.2 run方法

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

分析该方法主要分两部分,一部分是 SpringApplication 的实例化,二是 run 方法的执行;

SpringApplication 这个类主要做了以下四件事情:

  1. 推断应用的类型是普通的项目还是 Web 项目
  2. 查找并加载所有可用初始化器,设置到 initializers 属性中
  3. 找出所有的应用程序监听器,设置到 listeners 属性中
  4. 推断并设置 main 方法的定义类,找到运行的主类

查看构造器

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {    
 this.webApplicationType = WebApplicationType.deduceFromClasspath();    this.setInitializers(this.getSpringFactoriesInstances();    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));    this.mainApplicationClass = this.deduceMainApplicationClass();                        }

run方法流程分析

image-20200708111835419

(图引自狂神说公众号)

4. yaml 配置注入

SpringBoot 官方提供了两种配置文件:properties 和 yaml(推荐)

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

  • application.properties

    语法结构:key=value (中间不能有空格)

  • application.yml

    语法结构:key:空格 value (yml文件的配置必须有空格,且yml对空格十分敏感)

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

比如我们使用配置文件修改 SpringBoot 内置的 Tomcat 服务器默认启动的端口号

application.properties:

# 应用服务 WEB 访问端口
server.port=8088

application.yml:

# port:后面有一个空格
server:
  port: 8088

4.1 yaml 简介

YAML 是一个可读性高,用来表达数据库序列化的格式。YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子右键的数据格式(RFC 2822)中获得灵感。Clark Evans 在2001年首次发表了这种语言。

YAML是“YAML Ain't a Markuo Language”(YAML 不是一种标记语言)的递归缩写。在开发这种语言时,YAML的意思其实是“Yet Another Markup Language”(仍然时一种标记语言),但为了强调这种语言以数据作为中心,而不是以标记语言为重点,而用反向缩略语重命名。

4.2 yaml 基础语法

说明:yaml 语法要求严格

  1. 空格不能省略。
  2. 以缩进来控制层级关系
  3. 属性和值的大小写都是十分敏感的。
  4. 缩进长度没有限制,只要元素左边对齐就能表示同一层级
  5. 使用#表示注释
  6. 字符串可以不用引号标注
  • 字面量:普通的值 [数字,布尔值,字符串]

    直面两直接写在后面就可以,字符串默认不用加上双引号或者单引号

    # 注意中间一定要有空格
    key: val
    

    注意:

    1. "" 双引号,不会转义字符串里面的特殊字符,特殊字符会作为本身想表示的意思

      比如:name: "xp \n com" 输出 xp 换行 com

    2. '' 单引号,会转义特殊字符,特殊字符最终会变成和普通字符一样输出

      比如:name: 'xp \n com' 输出: xp \n com

  • 对象、Map(键值对)

    # 对象、Map格式
    key:
    	val1:
    	val2:
    

    在下一行来写对象的属性和值的关系,注意缩进,比如:

    student: 
    	name: xp
    	age: 18
    

    行内写法:

    student: {name: xp,age: 18}
    
  • 数组(List、Set)

    用 - 值表示数组中的一个元素,比如:

    pets:
    	- cat
    	- dog
    	- pig
    

    行内写法:

    pets: [cat,dog,pig]
    

4.3 注入配置文件

yaml 文件更强大的地方在于,它可以给我们的实体类直接注入匹配值

4.3.1 两种配置bean的方式

以前配置bean的方式配置:

  1. 创建一个实体类 Cat,并使用 @value 注入属性值

    @Component
    public class Cat {
    
        @Value("miao")
        private String name;
        @Value("18")
        private Integer age;
    
        public Cat() {
        }
    
        public Cat(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Cat{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
  2. 在 SpringBoot 的测试类中编写测试代码

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        private Cat cat;
    
        @Test
        void contextLoads() {
            System.out.println(cat);
        }
    
    }
    
  3. 点击运行测试

使用yaml配置bean

  1. 创建实体类,在实体类上加上 @ConfigurationProperties 注解,并配置prefix。其中prefix的值就是yaml文件中配置的bean的名

    @Component
    @ConfigurationProperties(prefix = "cat")
    public class Cat {
    
    //    @Value("miao")
        private String name;
    //    @Value("18")
        private Integer age;
    
        public Cat() {
        }
    
        public Cat(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Cat{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
  2. 在 resource 目录下创建 application.yml 文件,并编写 bean

    cat:
      name: miao1
      age: 17
    
  3. 点击运行测试

如果idea 报了如下错误

Spring Boot Configuration Annotation Processor not found in classpath

只需在 pom.xml 中引入如下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

看到这里可能发现好像yaml来注入bean好像并没有什么优势,那么我们再接下来看看 @value 和 @ConfigurationProperties 两个的区别

4.3.2 @ConfigurationProperties 和 @value

回顾properties配置

我们上面采用的yaml方法都是最简单的方式,开发中最常用的,也是SpringBoot所推荐的!

配置文件中,除了yaml文件,还有properties文件。

先创建配置文件 cat.properties

cat.name=miao
cat.age=3

在原来的实体类中修改注解

@Component
@PropertySource(value = "classpath:cat.properties")
public class Cat {
    @Value("${cat.name}")
    private String name;
    @Value("${cat.age}")
    private Integer age;
}

使用@ConfigurationProperties

松散绑定

我们在yml中写 cat-name 时,效果和catName是一样的

封装对象

像之前在yml中定义的cat,就是一个对象,而 @value无法封装对象

对比小结

@ConfigurationProperties @Value
功能 批量注入配置文件中的属性 一个个指定
松散绑定(松散语法) 支持 不支持
SpEL 不支持 支持
JSR303数据校验 支持 不支持
复杂类型封装 支持 不支持
  1. @ConfigurationProperties 只需要写一次即可,@Value 则需要每个字段都添加
  2. 松散绑定,在yml中写 cat-name 时,效果和catName是一样的。- 后面跟着的字母默认是大写的。这就是松散绑定。
  3. JSR303数据校验,这个就是我们在字段里增加一层过滤器,可以保证数据的合法性。
  4. 复杂类型封装,yml可以封装对象,使用@Value不支持

结论

配置yml和配置properties都可以获取到值,推荐yml。

如果我们在某个业务中,只需要获取配置文件中某个值,可以使用以下@Value

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

5. JSR303 数据校验及多环境切换

5.1 JSR303 简介

JSR 是 Java Specification Requests 的缩写,意思是 Java 规范提案。是指向 JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交 JSR,以向 Java 平台增添新的API和服务。JSR 已成为 Java 界的一个重要的标准。

JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现, Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

5.2 常见参数

空检查

注解 作用
@Null 验证对象是否为null。
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串。
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.

Boolean 检查

注解 作用
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false

长度检查

注解 作用
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内。
@Length(min=,max=) 验证 String 的长度是否在 min 和 max 之间

日期检查

注解 作用
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则

数值检查

建议使用在 String,Integer 类型,不建议使用在 int 类型上,因为表单值为 "" 时无法转换为 int,但可以转换为 String 为 "", Integer 为 null

注解 作用
@Min 验证 Number 和 String 对象是否大于等于指定的值。
@Max 验证 Number 和 String 对象是否小于等于指定的值。
@DecimalMax 被标注的值必须不大于约束中指定的最大值,这个约束的参数时一个通过BigDecimal 定义的最大值的字符串表示,小输存在精度。
@DecimalMin 被标注的值必须不大于约束中指定的最大值,这个约束的参数时一个通过BigDecimal 定义的最小值的字符串表示,小输存在精度。
@Digits 验证 Number 和 String 的狗成是否合法。
@Digits(integer=,fraction=) 验证字符串是否符合指定格式的数字,interger指定整数,fraction 指定小输精度。
@Range(min=,max=) 检查数字是否介于 min 和 max 之间

其他检查

注解 作用
@Valid 递归的对关联对象进行校验。如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果时一个map,则对其中的值部分进行校验。(是否进行递归验证)
@CreditCardNumber 信用卡验证
@Email 验证是否是邮件地址,如果为 null,不进行验证,算通过验证。
@ScriptAssert(lang=,script=,alias=)
@URL(protocol=,host=,port=,regexp=,flags=) 验证URL 是否正确

5.3 简单使用

  1. 导入 SpringBoot validation 启动器

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 在原来 Cat 的实体类上加上校验注解

    注:这里必须加上 @Validated 注解才能让 jsr303 校验的注解生效

    @Component
    @ConfigurationProperties(prefix = "cat")
    @Validated  // 校验注解,只有加上这个注解才能进行 jsr303 校验
    public class Cat {
    	// @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
        @NotBlank(message = "名字不能为空")
        private String name;
        // @Range(min=,max=) 检查数值是否在min和max之间
        @Range(min = 0,max = 150,message = "年龄必须在0-150之间")
        private Integer age;
    }
    
  3. 修改 yml 中配置的bean

    cat:
      name:
      age: -1
    
  4. 运行测试类

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Cat cat;
    
        @Test
        void contextLoads() {
            System.out.println(cat);
        }
    }
    
  5. 运行结果如下

    image-20200709104717844

5.4 注意和小结

  • 使用 JSR303 校验,必须加上 @Validated 注解,不然将不会进行校验
  • 需要导入 SpringBoot validation 启动器 的依赖,不然将不能使用这些注解进行校验,且会爆红
  • 合理使用 JSR303 校验,可以简化开发,减少 if else 的使用,更加轻便,使代码更加简洁。

6. 多环境切换

6.1 多配置文件

profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境半年本,实现快速切换环境。

我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml ,用来指定多个环境版本。

例如:

application-test.properties 代表测试环境配置

application-dev.properties 代表开发环境配置

但是 SpringBoot 并不回直接启动这些配置文件,它默认使用 application.properties/yml 主配置文件

激活环境

我们需要通过一个配置来选择需要激活的环境:

这里我们测试 Tomcat 服务器端口号的配置

application.yml

spring:
  profiles:
    active: dev # 配置环境的名称 
server:
  port: 8082

然后我们创建一个 application-dev.yml

application-dev.yml

server:
  port: 8083

再然后我们启动 SpringBoot 应用程序查看 SpringBoot 内置Tomcat启动的端口号

image-20200709113459462

发现 Tomcat 是从8083端口启动,说明我们切换环境成功

注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!

6.2 配置文件加载位置

6.2.1 配置文件加载位置和加载顺序

外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置

在官方外部配置文件说明参考文档 https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#boot-features-external-config-typesafe-configuration-properties 24.3 Application Property Files 中有明确说明配置文件的加载位置和加载顺序,具体如下图:

image-20200709141716946

配置位置以相反的顺序进行搜索,配置位置是:classpath:/,classpath:/config/,file:./,file:./config/ 。搜索的顺序如下:

  1. file:./config/
  2. file:./
  3. classpath:/config/
  4. classpath:/

SpringBoot 启动会扫描以下位置的 application.properties 或者是 application.yml 文件作为 SpringBoot 的默认配置文件:

优先级1:项目路径下的 config 包下的配置文件
优先级2:项目路径下的配置文件
优先级3:资源路径下的 config 包下的配置文件
优先级4:资源路径下的配置文件

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

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

6.2.2 测试

我们分别在项目路径下创建config包并在该目录下创建 application.yml,项目根目录下创建 application.yml,resource 目录下创建 application.yml,reource 目录下创建 config 包并该目录下创建 application.yml

创建后目录及其配置文件位置如下:

image-20200709154505437

在这4个 application.yml 中,都配置 Tomcat 启动端口,且每个端口号都不同,测试SpringBoot 的 Tomcat 启动哪个端口来确定这些包下的配置文件 application.yml 的优先级

server:
  port: 8081
server:
  port: 8082
server:
  port: 8083
server:
  port: 8084

7. 自动配置原理

SpringBoot 官方文档中由大量的配置,我们无法全部记住。官方文档配置:https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/html/appendix-application-properties.html#common-application-properties

image-20200709150019728

7.1 分析自动配置原理

我们就以 HttpEncodingAutoConfiguration 这个类来分析自动配置原理

// @Configuration 表明这是一个配置类
@Configuration(proxyBeanMethods = false)
// 启动指定类 ServerProperties 的功能
// 点进这个类,我们会发现里面的成员变量对应的是我们配置文件中的server下的配置,比如 server.port=8080,
// 里面有个注解 @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true),它的作用是将配置文件中"server"前缀对应的值和 ServerProperties 的成员变量绑定起来,并加入到 iOC 容器中
@EnableConfigurationProperties(ServerProperties.class)
// 判断当前项目是不是web应用程序,如果是,则当前配置类生效,否则不生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 判断当前项目是否有 CharacterEncodingFilter(SpringMVC进行乱码 解读的过滤器) 这个类,如果有,则当前配置类生效,否则不生效
@ConditionalOnClass(CharacterEncodingFilter.class)
// 判断是否手动配置了server.servlet.encoding (设置编码),若手动配置了,则使用手动配置的值,否则使用默认值
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
	// 它已经在 SpringBoot 的配置文件映射了
   private final Encoding properties;
	// 只有一个有参构造器的情况下,参数的值就会从容器中拿
   public HttpEncodingAutoConfiguration(ServerProperties properties) {
      this.properties = properties.getServlet().getEncoding();
   }

    // 给容器中添加一个组件,这个组件的某些值需要从上面定义的 final Encoding properties中获取
   @Bean
    // 如果当前IOC容器中已经有这个 Bean 了,则不注入,否则将这个Bean注入到IOC容器中
   @ConditionalOnMissingBean
   public CharacterEncodingFilter characterEncodingFilter() {
      CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
      filter.setEncoding(this.properties.getCharset().name());
      filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
      filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
      return filter;
   }
}
  • @Configuration(proxyBeanMethods = false)

    标明这个类是配置类

  • @EnableConfigurationProperties(ServerProperties.class)

    启动指定类 ServerProperties 的功能

    如果我们点进 ServerProperties 这个类,我们会发现里面的成员变量对应的是我们配置文件中的server下的配置,比如 server.port=8080,或

    server:
      port: 8083
    

    ServerProperties 类如下

    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties {
       /**
        * Server HTTP port.
        */
       private Integer port;
    }
    

    它有一个注解 @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) ,它的作用是将配置文件中"server"前缀对应的值和 ServerProperties 的成员变量绑定起来,并加入到 IOC 容器中。

    这里的 port 就是我们配置文件中可以配置的属性上面已经有示例和解释了。

    server:
      port: 8083
    
  • @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)

    判断当前项目是不是web应用程序,如果是,则当前配置类生效,否则不生效

  • @ConditionalOnClass(CharacterEncodingFilter.class)

    判断当前项目是否有 CharacterEncodingFilter (SpringMVC进行乱码解读的过滤器) 这个类,如果有,则当前配置类生效,否则不生效

  • @ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)

    判断是否手动配置了server.servlet.encoding (设置编码),若手动配置了,则使用手动配置的值,否则使用默认值

  • @ConditionalOnMissingBean

    如果当前IOC容器中已经有这个 Bean 了,则不注入,否则将这个Bean注入到 IOC 容器中

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

  • 一旦这个配置类生效,这个配置类就会给容器中添加各种组件。
  • 这些组件的属性是从对应的 properties 类中获取的,这些类里面的每一个属性又是和配置文件绑定的。
  • 所有的配置文件中能配置的属性都是 xxxxProperties 类中封装着。
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类

这就是自动装配的原理

7.2 使用自动装配的注意事项

  1. SpringBoot 启动会加载大量的自动装配类。

  2. 我们看我们需要的功能有没有在 SpringBoot 默认写好的自动装配类当中。

  3. 我们再来看这个自动配置类中到底配置了哪些组件。(只要我们要用的组件存在其中,我们就不需要再手动配置了)

  4. 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们只需要再配置文件中指定这些属性的值即可。

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

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

7.3 @Conditional

我们刚刚在分析 HttpEncodingAutoConfiguration 这个类的时候,我们会发发现,很多注解的底层都有 @Conditional 及以这个开头的注解

@Conditional 是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。

我们看其源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

分析其源码,发现这个注解可以标注在类和方法上,并且有个 value 属性,可以传入 Class 对象数组,这些 Class 对象,必须继承 Condition 接口。

我们来看看 Condition 的源码

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

实现 Condition 接口,就必须实现 matches 方法。这个方法返回 true,则将 bean 注入到 IOC 容器中,返回 false,则不注入

到这里,我们就更加能够理解它自动装配的原理了:**@Conditional 的派生注解,实现了Condition 接口,重写了 matches 方法,重写这个方法的时候,规定了添加这个注解后类或方法需要达到的条件。SpringBoot 的自动装配类,都添加了 @Conditional 注解,SpringBoot 自动装配时,根据该自动装配类的注解里 matches 方法里的条件判断,决定这个配置类是否生效 **。

用我们比较好听懂的话来说,就是 只有达到自动装配类中注解里的 matches 方法的条件,该自动装配类才会自动装配

现在我们再来看看 @Conditional 的派生注解

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

那么多的配置类,必须在一定的条件下才能生效。也就是说,我们加载了这么多的配置类,但不是所有的都生效了

那我们怎么知道哪些自动配置类生效呢?

我们只需要在配置文件中启用 debug=true 属性,来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效

debug:
  true

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

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

Unconditional classes: (没有条件的类)

7.4 小结

SpringBoot 所有自动装配都是在启动的时候扫描并加载的: spring.factories 所有的自动装配类都在这里,但是不一定生效,要判断条件是否成立,只要导入对应的 starter ,就有对应的启动器了,有了启动器,我们自动装配就会生效。

  1. SpringBoot 在启动的时候,从类路径下 /META-INF/spring.factories 获取指定的值
  2. 将这些自动配置的类导入容器,自动配置就会生效,帮我们进行自动装配
  3. 以前我们需要自动配置的东西,限制 SpringBoot 帮我们做了
  4. 整合 JavaEE,解决方法和自动配置的东西都在 spring-boot-autoconfigure-2,2,0,RELEASE.jar 这个包下
  5. 它会把所有需要导入的组件,以类命的方式返回,这些组件就会被添加到容器
  6. 容器中也会存在非常多的 xxxAutoConfiguration 的文件,就是这些类给容器中导入了这个场景需要的所有组件

8. 自定义 Starter

我们知道了 SpringBoot 的自动装配原理,那么我们也可以尝试自定义一个启动器来玩玩!

8.1 说明

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

命名规约

官方命名:

  • 前缀:spring-boot-starter-xxx
  • 比如:spring-boot-starter-web

自定义命名:

  • 后缀:xxx-spring-boot-starter-xxx
  • 比如:mybatis-spring-boot-starter

8.2 编写启动器

  1. 首先我们先创建两个工程项目,一个是我们编写自己diy的启动类的项目和一个测试的项目,项目目录如下

image-20200710100331081

  1. 创建 HelloProperties

    根据我们的源码,可以知道 xxxProperties 是存放配置信息的,所以我们先创建一个 HelloProperties

    package com.xp;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "hello.msg")
    public class HelloProperties {
    
        private String prefix;
    
        private String suffix;
    
        public String getPrefix() {
            return prefix;
        }
    
        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }
    
        public String getSuffix() {
            return suffix;
        }
    
        public void setSuffix(String suffix) {
            this.suffix = suffix;
        }
    }
    
  2. 创建一个 HelloService

    创建一个 HelloService,用来存储我们启动器类的方法

    public class HelloService {
    
        HelloProperties helloProperties;
    
        public HelloService(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public HelloProperties getHelloProperties() {
            return helloProperties;
        }
    
        public void setHelloProperties(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public String sayHello(String msg){
            return helloProperties.getPrefix()+msg+helloProperties.getSuffix();
        }
    
    }
    
  3. 编写自动配置类 HelloAutoConfiguration

    SpringBoot 启动器的核心就是自动配置类,它将判断这个启动类是否生效以及帮我们自动配置

    package com.xp;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ConditionalOnWebApplication
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloAutoConfiguration {
    
        @Autowired
        private HelloProperties helloProperties;
    
        @Bean
        public HelloService helloService(){
            return new HelloService(helloProperties);
        }
    
    }
    
  4. 配置 spring.factories

    之前我们分析 SpringBoot 运行原理的时候发现,SpringBoot 应用程序启动的时候会自动扫描 **META-INF\ ** 目录下的 spring.factories 。所以我们需要在 resource 目录下创建个 META-INF 目录,并在该目录下创建 spring.factories 并配置我们这个启动类

    # 开启我们自己diy的自动配置类
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.xp.HelloAutoConfiguration
    
  5. 将我们自己 diy 的启动器打成jar包,安装到 Maven 中

    image-20200710101841887

  6. 在我们的测试类中引入刚刚我们自定义的启动器的依赖

    <!-- 引入我们自己diy的启动器包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    
  7. 编写 Controller

    启动器已经配置好了,编写 Controller 接口来测试我们的启动器是否编写成功

    @RestController
    public class HelloController {
    
        @Autowired
        private HelloService helloService;
    
        @RequestMapping("/hello")
        public String hello() {
            return  helloService.sayHello("hello");
        }
    
    }
    
  8. 在 application.yml/properties 中配置我们刚刚的启动类

    # 服务器启动端口
    server:
      port: 8080
    # 自定义启动器的设置
    hello:
      msg:
        prefix: 这是前缀
        suffix: 这是后缀
    
  9. 测试

    启动 SpringBoot 项目,在浏览器输入 URL 进行访问刚刚写好的接口

    image-20200710102411271

到这里,我们自定义的 Starter 就创建好了

9. 整合 JDBC

9.1 SpringData 简介

对于数据访问层,无论是 SQL(关系型数据库)还是NOSQL(非关系型数据库),SpringBoot 底层都是采用 Spring Data 的方式进行统一处理。

SpringBoot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与SpringBoot、SpringCloud 等齐名的知名项目。

SpringData 官网:https://spring.io/projects/spring-data#learn

数据库相关的启动器,可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/htmlsingle/#using-boot-starter

9.2 整合 JDBC

9.2.1 JDBC 简单测试

  1. 创建 SpringBoot 项目,引入 JDBC API 模块

    若没有在创建项目时引入,则需要自己手动引入 spring-boot-starter-jdbc (SpringBoot JDBC启动器)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
  2. 在 application.yml/properties 中配置数据源

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
    
  3. 编写测试类

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                // 获取数据库连接
                Connection connection = dataSource.getConnection();
                // 编译SQL语句
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                // 获得结果集并循环输出
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    
  4. 分析 JDBC 自动配置原理

    在控制台中,connection 对象输出的信息是 HikariProxyConnection@215638041 wrapping com.mysql.cj.jdbc.ConnectionImpl@797c3c3b 。但我们并没有手动配置这个类。

    那我们就去全局搜索 DataSourceAutoConfiguration 这个类去查看数据源是如何自动配置的。下面是关于数据库连接池的配置

    @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
          DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
          DataSourceJmxConfiguration.class })
    protected static class PooledDataSourceConfiguration {
    
    }
    

    这里导入的类都在 DataSourceConfiguration配置类下,可以看出 SpringBoot 2.3.0默认使用 HikanDataSource 数据源,而以前的版本,如:SpringBoot 1.5默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源。

    HikariDataSource 号称 JavaWeb 当前速度最快的数据源,相比于传统的 C3P0、DBCP、Tomcat jdbc 等连接池更加优秀

    可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。

9.2.2 JdbcTemplate

  • 有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库。
  • 即使不使用第三方数据库操作框架,如 MyBatis 等, Spring 本身也对原生的 JDBC 做了轻量级的封装,即 JdbcTemplate。
  • 数据库操作的所有 CRUD 方法都在 JdbcTemplate中。
  • SpringBoot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。
  • JdbcTemplate 的自动配置依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

JdbcTemplate 主要提供了以下几类方法:

  • execute 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句。
  • update 方法及 batchUpdate 方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句。
  • query方法及queryForxxx方法:用于执行查询相关语句。
  • call 方法:用于执行存储过程、函数相关语句。

测试

@RestController
public class JdbcTemplateController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @RequestMapping("/query")
    public List<Map<String, Object>> query(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    @RequestMapping("/update")
    public String update(){
        String sql = "update user set name=?,password=? where id = ?";
        jdbcTemplate.update(sql,"123","123","6");
        return "update ok!";
    }


    @RequestMapping("/insert")
    public String insert(){
        String sql = "insert into user (name,password,hobby) values(?,?,?)";
        jdbcTemplate.update(sql,"xp","xp","敲代码");
        return "insert ok!";
    }

    @RequestMapping("/delete")
    public String delete(){
        String sql = "delete from user where id = ?";
        jdbcTemplate.update(sql,6);
        return "delete ok!";
    }
}

到这里 JdbcTemplate 就实现了。免去了我们自己写工具类的麻烦。

10. 集成 Druid

10.1 Druid 简介

Java 程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。

Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

Druid 已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考研。

SpringBoot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 和 Druid 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 SpringBoot 如何集成 Druid 数据源,如何实现数据库监控。

Druid GitHub官网: https://github.com/alibaba/druid/

配置 缺省值 说明
name 配置这个属性的意义在于,如果存在多歌数据源,监控的时候可以通过名字来取分开来。如果没有配置,将会生成一个名字,格式是:“DataSource-” +System.identityHashCode(this)。
url 连接数据库的url,不同数据库不一样,例如:mysql:jdbc:mysql://10.20.153.104:3306/druid2 oracle:jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码/如果你不希望密码直接写在配置文件中,可以使用 ConfigFilter
driverClassName 根据 url自动识别 这一项可配可不配,如果不配置 druid 会根据 url 自动识别 dbType ,然后选择响应的 driverClassName
initialSize 0 初始化时简历物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxldle 8 已经不再使用,配置了也没效果
minldle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 userUnfairLock 属性为 true 使用非公平锁。
poolPreparedStatements false 是否缓存 preparedStatement ,就就是说 PSCache。PSCache 对支持游标的数据库性能提升巨大,比如说 oracle。在 mysql 下建议关闭。
maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为 true。在Druid 中不回存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一些,比如说100
vvalidationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果 validationQuery 为 null,testOnBorrow、testOnReturn、testWhileldle 都不回起做用
validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement 对象的 void setQueryTimeout(int seconds) 方法
testOnBorrow true 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnRetrun false 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能
testWhileldle false 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
timeBetweenEvictionRunsMillis 一分钟(1.0.14) 有两个含义:1、Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableldleTimeMillis 则关闭物理连接。2、testWhileldle 的判断依据,详细看 testWhileldle 属性的说明
numTestsPerEvictionRun 不再使用,一个 DruidDataSource 只支持一个EvictionRun
minEvictableldleTimeMillis 30分钟(1.0.14) 连接保持空闲而不被驱逐的最长时间
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat 日志用的 filter:log4j 防御 sql 注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了 filters 和 proxyFilters,是组合关系,并非替换关系

10.2 配置数据源

  1. 引入依赖

    <dependency>    
        <groupId>com.alibaba</groupId>    
        <artifactId>druid</artifactId>    
        <version>1.1.21</version>
    </dependency>
    
  2. 切换数据源

    SpringBoot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切换成 Druid 数据源
        type: com.alibaba.druid.pool.DruidDataSource
    
  3. 测试数据源是否已经成功切换

    使用之前的测试类进行测试

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                Connection connection = dataSource.getConnection();
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    在控制台中查看 connection 中,是否已经切换成 Druid 数据源

    image-20200711135712276

  4. 配置 Druid 数据源参数

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切换成 Druid 数据源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  5. 导入 Log4j 依赖

    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  6. 创建 Druid 自动配置类

    @Configuration
    public class DruidConfig {
        
        /*
           将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
           绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
           @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
           前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
         */
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    
    }
    
  7. 测试

    在刚刚测试类的基础上,加上输出我们刚刚配置后 druid 信息的代码

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        private Cat cat;
    
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                Connection connection = dataSource.getConnection();
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
                DruidDataSource druidDataSource = (DruidDataSource) dataSource;
                System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
                System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
    
                //关闭连接
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    至此,我们就配置好了 Druid 数据源

10.3 配置 Druid 数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装路由器时,人家也提供了一个默认的 web 页面。

  1. 设置 Druid 的后台管理页面,比如登录账号、密码等。配置后台管理

    在我们刚刚的 Druid 配置类中,配置如下的bean

    /**
     * 配置druid后台监控的servlet
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
    
        // 后台需要有人登录,账号密码配置
        HashMap<String, String> initParams  = new HashMap<>();
    
        initParams .put("loginUsername","admin");
        initParams .put("loginPassword","12345");
    
        // 允许谁可以访问
        initParams .put("allow","");
    
        bean.setInitParameters(initParams);
        return bean;
    }
    
  2. 访问 http://localhost:8084/druid/login.html ,进入 druid 后台监控

    image-20200711191120176

  3. 然后我们访问我们之前的写好的接口,然后在 druid 监控后台查看具体监控信息

    image-20200711191258428

    查看 druid 后台监控信息

    image-20200711191354097

  4. 配置 Druid web 监控 filter 过滤器

    /**
     * Druid 后台监控过滤器
     */
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>(new WebStatFilter());
    
        // exclusions : 设置哪些请求进行过滤排除掉,从而不进行统计
        HashMap<String, String> initParams = new HashMap<>();
        initParams.put("exclusions","*.js,*.css./druid/*,/jdbc/*");
        bean.setInitParameters(initParams);
    
        // /* 代表过滤所有请求
        bean.setUrlPatterns(Collections.singletonList("/*"));
        return bean;
    }
    

到这里,我们就成功在 SpringBoot 中集成了 Druid

11. 整合MyBatis

11.1 MyBatis 简介

  • MyBatis 是一款优秀的持久层框架,它支持自定义 SQL 存储过程以及高级映射, MyBatis 面出了几乎所有的 JDBC 代码以及设置和获取结果集的工作。MyBatis 可以通过简单XML注解来配置和映射原始类型接口和 Java POJO (Plain Old Java Objects,普通老师 Java 对象)为数据库中的记录。
  • Mybatis 本是 apache 的一个开源项目 iBatis,2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis
  • 2013年11月迁移到 GitHub
  • 官网:https://mybatis.org/mybatis-3/zh/index.html
  • GitHub:https://github.com/mybatis/mybatis-3

11.2 整合 MyBtis

  1. 导入 MyBatis 所需要的依赖

    <!-- SpringBoot 整合 mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    
    <build>
        <!-- 插件 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    	<!-- 静态资源解析 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  2. 配置数据库连接信息(不变)

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切换成 Druid 数据源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  3. 在测试类中测试数据库是否连接成功

  4. 创建实体类

    @Component
    public class User {
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        private String hobby;
    
        public User() {
        }
    
        public User(Integer id, String name, String password, String hobby) {
            this.id = id;
            this.name = name;
            this.password = password;
            this.hobby = hobby;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getHobby() {
            return hobby;
        }
    
        public void setHobby(String hobby) {
            this.hobby = hobby;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", password='" + password + '\'' +
                    ", hobby='" + hobby + '\'' +
                    '}';
        }
    }
    
  5. 配置 Mybaits

    在 properties.yml 文件中配置 MyBatis

    mybatis:
      # 给实体类起别名
      type-aliases-package: com.xp.model
      # 将 resource 目录下的所有 mapper.xml 注册到 MyBatis 中
      mapper-locations: classpath:mapper/*.xml
    
  6. 编写 Mapper 接口 以及对应的 mapper.xml

    UserMapper

    @Mapper // 这个注解表示这个接口是MyBatis的mapper接口
    @Component  // 将mapper注入到Spring容器中
    public interface UserMapper {
    
        List<User> queryAllUser();
    
        int updateUser(@Param("user") User user);
    
        int deleteUser(Integer id);
    
        int addUser(@Param("user") User user);
    
    }
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.xp.mapper.UserMapper">
        <select id="queryAllUser" resultType="User">
            select * from user;
        </select>
    
        <update id="updateUser" parameterType="User">
            update user
            set name = #{user.name},password=#{user.password},hobby=#{user.hobby}
            where id=#{user.id};
        </update>
    
        <insert id="addUser" parameterType="User">
            insert into user (name,password,hobby) values (#{user.name},#{user.password},#{user.hobby})
        </insert>
    
        <delete id="deleteUser">
            delete from user where id = #{id}
        </delete>
    
    </mapper>
    
  7. 编写 Controller 接口测试

    @RestController
    @RequestMapping("/mybatis")
    public class MyBtisController {
    
        @Autowired
        UserMapper userMapper;
    
        @RequestMapping("/query")
        public List<User> query() {
            return userMapper.queryAllUser();
        }
    
        @RequestMapping("/update")
        public String update() {
            int row = userMapper.updateUser(new User(5, "zhangsan", "zs", "敲代码"));
            return row > 0 ? "update OK!" : "update Fail!";
        }
    
        @RequestMapping("/delete")
        public String delete() {
            int row = userMapper.deleteUser(2);
            return row > 0 ? "delete OK!" : "delete Fail";
        }
    
        @RequestMapping("/add")
        public String add() {
            int row = userMapper.addUser(new User(null, "lisi", "ls", "打篮球"));
            return row > 0 ? "add OK!" : "add Fail!";
        }
    }
    
    
  8. 启动 SpringBoot 项目,通过 URL 访问接口测试

    image-20200712114806592

到这里,我们就成功在 SpringBoot 中整合了 MyBatis

12. Web 开发静态资源处理

12.1 静态资源映射规则

首先,我们搭建一个普通SpringBoot项目,回顾一下Helloword程序
写请求非常简单。那我们要引入我们前端资源,比如 css、js等文件,这个 SpringBoot 怎么处理呢?
如果我们是一个web应用,我们的main下会有一个webapp,哦我们以前都是将所有的页面导在这里面的。但是我们现在的 pom 呢,打包方式是 jar 的方式,那么这种方式 SpringBoot 能布恩那个来给我们写页面呢?当然也是可以的,但是 SpringBoot 对于静态资源放置的位置,是有规定的

我们先来聊聊这个静态资源映射规则
在 SpringBoot 中, SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面。我们可以去看看 WebMvcAutoConfigurationAdapter 中的配置方法。

SpringBoot 中有一个 addResourceHandlers 方法,它的作用是添加资源处理

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   // 判断用户是否已经手动配置了静态资源处理,isAddMappings 中的 addMappings 默认是true
    if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
    // 缓存控制
   Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
   CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
   // webjars 配置,所有的 /webjars/** 都需要去 /META-INF/resources/webjars/ 找对应的资源
   if (!registry.hasMappingForPattern("/webjars/**")) {
      customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
   // 静态资源配置,第二种加载静态资源的方式
   String staticPathPattern = this.mvcProperties.getStaticPathPattern();
   if (!registry.hasMappingForPattern(staticPathPattern)) {
      customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
            .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
}

读源码,我们可以知道,如果我们用户没有手动配置姿态资源处理,它会走默认的静态资源处理。默认的静态资源处理,是所有的 /webjars/** 都需要去 /META-INF/resources/webjars/ 找对应的资源

12.2 什么是 webjars

webjars 的本质是以 jar 包的方式引入我们的静态资源,我们以前要导入一个静态资源文件,直接导入即可。

webjars 官网:https://www.webjars.org

要使用 jQuery,我们只需要引入 jQuery 对应版本的 pom 依赖即可

<!-- webjars 引入 jQuery -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

成功导入后我们去查看 webjars 目录结构,会发现如下: 它的包路径是 /META-INF/resources/webkars/jquery/3.4.1/jquery.js ,WebMvcAutoConfigurationAdapter 这个类中静态资源默认加载的路径和这个符合。

访问:只要是静态资源,SpringBoot 就会去对应的路径寻找资源。我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

image-20200712131159611

12.3 第二种静态资源映射规则

那我们项目中要是使用自己的静态资源该怎么导入呢?

在刚刚的源码中,我们发现第二种静态资源的方式是staticPathPattern中配置的,我们点进 ResourceProperties 这个类,会发现 CLASSPATH_RESOURCE_LOCATIONS 规定了第二种映射规则:

// 默认静态资源路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
	"classpath:/META-INF/resources/",
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};

/**
  * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
  * /resources/, /static/, /public/].
  */
// 存储所有静态资源路径的String数组
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 获取静态资源路径
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 设置静态资源路径
public void setStaticLocations(String[] staticLocations) {
    this.staticLocations = appendSlashIfNecessary(staticLocations);
}
// 遍历 /META-INF/resources/, /resources/, /static/, /public/ 下的静态资源,并添加斜杠
private String[] appendSlashIfNecessary(String[] staticLocations) {
    String[] normalized = new String[staticLocations.length];
    for (int i = 0; i < staticLocations.length; i++) {
        String location = staticLocations[i];
        normalized[i] = location.endsWith("/") ? location : location + "/";
    }
    return normalized;
}

我们会发现,当我们将静态资源放在 /META-INF/resources/, /resources/, /static/, /public/ 这些静态资源目录下时,ResourceProperties 这个类会自动帮我们扫描并加载。

测试:在 resources 目录下创建三个文件夹,分别是 resources,static,public,并在这些目录下创建我们自己定义的 js ,my.js。目录和文件结构如下:

image-20200712133842884

然后,这三个 js 文件,都自定义写入不同的内容,访问 http://localhost:8080/my.js 测试:

image-20200712134041969

经测试,这三个路径的优先级为:resources > static > public 。

12.4 自定义静态资源路径

我们也可以自己通过配置文件来指定防止静态资源文件的目录。

在 application.yml 文件中配置 resources的static-locations:

spring:
  resources:
    static-locations: classpath:/my/

然后在 resources 包下创建 my 目录,并在该目录下创建 diy.js

image-20200712134816860

启动我们的 SpringBoot 应用,访问 http://localhost:8080/diy.js 测试:

image-20200712134728219

注意一旦自己配置了静态文件存放的目录,原来的自动配置就都会失效

12.5 首页处理

静态资源文件夹说完后,我们继续想下看源码!可以看到一个欢迎页的映射,就是我们首页!

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
      FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
   WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
         new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
         this.mvcProperties.getStaticPathPattern());
   welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
   welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
   return welcomePageHandlerMapping;
}

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

private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

通过源码,我们可以得知,静态资源文件夹下的所有 index.html 页面;被/** 映射。

也就是说,当我们访问 http://localhost:8080/ 时,会自动赵大鹏静态资源文件夹下的index.html 文件。

13. Thymeleaf 模板引擎

13.1 模板引擎

前端交给我们的页面,是 html 页面。如果我们以前开发,我们需要把它们转成 jsp 页面,jsp 好处就是当我们查出一些数据转发到 JSP 页面以后,我们可以用 jsp 轻松实现页面数据的显示及交互等。

将商品支持非常强大的功能,包括能写 Java 代码,但是呢,我们现在的这种情况,SpringBoot 这个项目首先是以 jar 的方式,不是 war 。我们的还是嵌入的 T欧美cat,所以呢, SpringBoot 默认是不支持 JSP 的

那不支持 jsp ,如果我们直接用纯静态页面的方式,那给我们的开发会带来非常大的麻烦。那怎么办呢?

SpringBoot 支持使用模板引擎,默认是 Thymeleaf

那么什么是模板引擎呢?

其实我们以前用的 jsp ,它就是一个模板引擎,还有用得比较多的 FreeMaker,包括 SpringBoot 默认的 Thymeleaf 。模板引擎,它们的思想都是一样的。具体的思想如下图:

image-20200712154051491

模板引擎的作用就是我们来写一个页面模板,比如有些值,是动态的,我们写一些表达式。而这些值,就是从我们后台封装的一些数据。然后把这个模板和这个数据交给我们的模板引擎。模板引擎会根据我们后台封装的数据根据模板中写的表达式解析,填充到我们指定的位置。然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是模板引擎。不管是 jsp 还是其他模板引擎,都是这个思想,只不过,就是不同模板引擎之间,它们的语法可能有点不同。

13.2 Thymeleaf 简介

Thymeleaf 是一款用于渲染 XCML/XHTML/HTML5 内容的模板引擎,类似 JSP,Velocity,FreeMaker。它可以轻易余 SpringMVC 等 Web 框架进行集成作为 Web 应用的模板引擎,是 SpringBoot 官方使用的模板引擎。

官网:https://www.thymeleaf.org/

13.3 特点

  • 动静结合: Thymeleaf 在有网络和无网络的环境下皆可运行。
  • 开箱即用:它提供标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、OGNL 表达式效果
  • 多方言支持: Thymeleaf 提供 Spring 标准方言和一个与 SpringMVC 完美集成的可选模板,可以快速地实现表单绑定、属性编辑器、国际化等功能。
  • 与 SpringBoot 完美整合,SpringBoot 提供了 Thymeleaf 的默认配置,并且为 Thymelaef 配置了视图解析器,可以像以前操作 jsp 一样操作 Thymeleaf ,代码几乎没有任何区别,就是在模板语法上有区别。

13.4 常用语法

Thymeleaf 的主要作用是把 model 中的数据渲染到html 中,因此其语法主要是如何解析 model 中的数据。从以下方面来学习:

  • 变量、方法、条件判断、循环、运算【逻辑运算、布尔运算、比较运算、条件运算】
  • 其它
  1. Thymeleaf 通过 ${...} 来获取 model 中的变量,语法和 el 表达式差不多,但它是 ognl 表达式

    <!--/*@thymesVar id="thymeleaf" type="java.lang.String"*/-->
    <div th:text="${thymeleaf}"></div>
    
  2. Themeleaf 通过 th:object 自定义变量,可以通过 *{...} 取出对应的属性

    <!--/*@thymesVar id="user" type="com.xp.entity.User"*/-->
    <div th:object="${user}">
        <h2 th:text="*{name}"></h2>
        <h2 th:text="*{age}"></h2>
        <!--/*@thymesVar id="friend" type="com.xp.entity.Friend"*/-->
        <h2 th:text="*{friend.name}"></h2>
    </div>
    
  3. ognl 表达式本身就支持方法调用,但需要注意的是必须使用注释指明该变量是哪个类的

    <!--/*@thymesVar id="user" type="com.xp.entity.User"*/-->
    <!--/*@thymesVar id="name" type="java.lang.String"*/-->
    <!--/*@thymesVar id="age" type="java.lang.Integer"*/-->
    <div th:object="${user}">
        <h2 th:text="*{name.hashCode()}"></h2>
        <h2 th:text="*{age.hashCode()}"></h2>
        <!--/*@thymesVar id="friend" type="com.xp.entity.Friend"*/-->
        <h2 th:text="*{friend.name.hashCode()}"></h2>
    </div>
    

    Thymeleaf 中提供了一些内置对象,并且这些对象中提供了一些方法,方便我们调用、获取这些对象,需要使用 #对象名 来调用

    • 一些环境相关的对象

      对象 作用
      #ctx 获取 Thymeleaf 自己的 Context 对象
      #request 如果是 web 程序,可以获取 HttpServletRequest 对象
      #respone 如果是 web 程序,可以获取 HttpServletResponse 对象
      #session 如果是 web 程序,可以获取 HttpSession 对象
      #servletContext 如果是web 程序,可以获取 HttpServletContext 对象
    • Thymeleaf 提供的全局对象

      对象 作用
      #datas 处理 java.util.date 的工具对象
      #calendars 处理 java.util.calendar 的工具对象
      #numbers 用来对数字格式的方法
      #strings 用来处理字符串的方法
      #bools 用来判断布尔值的方法
      #arrays 用来护理数组的方法
      #lists 用来处理 List 集合的方法
      #sets 用来处理 Set 集合的方法
      #maps 用来处理 Map 集合的方法

      例如:

      <div th:text="${#dates.format(data,'yyyy-MM-dd HH:mm:ss')}"></div>
      
      <div th:Object="${#session.getAttribute('user')}">
          <h1 th:text="*{name}"></h1>
          <h1 th:text="*{age}"></h1>
          <h1 th:text="*{friend.name}"></h1>
      </div>
      
  4. 字面值

    • 字符串字面值:使用一对 '' (单引号)引用的内容就是字符串的字面值了

      <div th:text="'字符串字面值'"></div>
      
    • 数字字面值:不需要任何特殊语法,写的是是什么就是什么,可以进行算术运算

      <div th:text="2020"></div>
      <div th:text="2018+2"></div>
      
    • 布尔字面值:只有 true 或 false

      <div th:if="true">布尔值:true</div>
      
  5. 字符串拼接

    • 我们经常使用得普通字符串拼接方法

      <div th:text="'欢迎 '+${user.name}+‘ !’"></div>
      
    • Thymeleaf 使用一对 | 拼接

      <div th:text="|欢迎 +${user.name} !|"></div>
      
  6. 运算

    • 算术运算

      支持的运算符: + - * / %

      <div th:text="${user.age}%2"></div>
      
    • 比较运算运算

      支持的比较运算: >,<,>=,<=,但是 >,< 不能直接使用,因为 html 会解析为标签,要使用别名

      注意 == 和 != 不仅可以比较数值,类似于 equals 的功能

      可以使用的别名:gt(>), lt(<), ge(>=) , le(<=), not(!), eq(==), neq/ne(!=)

    • 条件运算

      • 三元运算

        <div th:text="${user.isAdmin}?'管理员':'普通会员'"></div>
        
      • 默认值

        有的时候,我们取一个值可能为空,这个时候需要做非空判断,可以使用表达式 ?: 默认值简写

        <span th:text="${user.name} ?: '二狗'"></span>
        
  7. Thymeleaf 通过 th:each 实现循环

    <div th:each="list:${lists}">
        <h1 th:text="${list}"></h1>
    </div>
    

    遍历的结合可以是以下类型

    • Iterable,实现了Iterable接口的类
    • Enumeration,枚举
    • Interator,迭代器
    • Map,遍历得到的是Map.Entry
    • Array,数组及其它一切符合数组结果的对象
  8. Thymeleaf 使用 th:if 或者 if:unless 来进行逻辑判断

    <div th:if="${user.age} >= 18">
        <h1>成年人</h1>
    </div>
    

    如果表达式的值为 true,则标签会渲染到页面,否则不进行渲染。

    以下情况会被认为 true

    • 表达式值为 true
    • 表达式值为非0数值
    • 表达式值为非0字符
    • 表达式值为字符串,但不是“false”、“no”,“off”
    • 表达式不是布尔、字符串、数字、字符中的任何一种

    其它情况包括 null 都被认定为 false

  9. Thymeleaf 使用 th:switchth:case 来进行分支控制

    <div th:switch="${user.role}">
      <p th:case="'admin'">用户是管理员</p>
      <p th:case="'manager'">用户是经理</p>
      <p th:case="*">用户是别的玩意</p>
    </div>
    

    需要注意的是,一旦有一个 th:case 成立,其它的则不再判断。与 java 中的 switch 是一样的

    另外 th:case="*" 表示默认,放在最后

  10. Thymeleaf 使用 th:inline="javascript" 来声明该 script 标签的脚本是需要特殊处理的 js 脚本

    <script th:inline="javascript">
        var user = /*[[${user}]]*/ {};
        var age = /*[[${user.age}]]*/ 20;
        console.log(user);
        console.log(age)
    </script>
    
    var user = /*[[Thymeleaf表达式]]*/
    

    因为 Thymeleaf 被注释起来,因此即便是静态环境下,js 代码也不会报错,而是采用表达式后面跟着的默认值。且 User 对象会直接处理为 json 格式

13.5 简单使用

  1. 引入 Thymeleaf

    <!-- thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  2. Thymeleaf 分析

    从 SpringBoot 的配置原理进行分析我们这个 Thymeleaf 的自动配置规则。

    我们先去找到 ThymeleafProperties 这个类

    @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";
    
       /**
        * Whether to check that the template exists before rendering it.
        */
       private boolean checkTemplate = true;
    
       /**
        * Whether to check that the templates location exists.
        */
       private boolean checkTemplateLocation = true;
    
       /**
        * Prefix that gets prepended to view names when building a URL.
        */
       private String prefix = DEFAULT_PREFIX;
    
       /**
        * Suffix that gets appended to view names when building a URL.
        */
       private String suffix = DEFAULT_SUFFIX;
    
       /**
        * Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
        */
       private String mode = "HTML";
    
       /**
        * Template files encoding.
        */
       private Charset encoding = DEFAULT_ENCODING;
    
       /**
        * Whether to enable template caching.
        */
       private boolean cache = true;
    }
    

    在这个配置类里面,我们看到很多熟悉的东西,比如 prefix 和 suffix 。

    我们只需要把我们的 html 页面放在类路径下的 templates 下,thymeleaf 就可以帮我们自动选软了。

  3. 在 resources 的 templates 目录下创建一个 text.html 页面

    使用前,我们需要引入 xmlns:th="http://www.thymeleaf.org",来让idea给我们增加提示

    ${test}:这个和我们之前 jsp 使用的 EL 表达式差不多,后端通过 model 设置值,然后在前端页面使用 ${XXX} 来获取 model 中设置的 XXX 值

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Thymeleaf 测试</title>
    </head>
    <body>
    
        <h1>Thymeleaf 测试</h1>
        <div th:text="${test}"></div>
    
    </body>
    </html>
    
  4. 编写 Controller

    ThymeleafController

    @Controller
    public class ThymeleafController {
    
        @RequestMapping("/test")
        public String test(Model model){
            // 通过 model 向前端传值
            model.addAttribute("test","通过Thymeleaf模板引擎传值");
            return "test";
        }
    
    }
    
  5. 测试

    输入 URL 访问我们的 Controller 接口

    image-20200712162204864

    到这里,我们 SpringBoot 中 Thymeleaf 的简单使用就完成了

14. SpringMVC 自动装配原理

在进行项目编写前吗,我们还需知道一个东西,就是 SpringBoot 对我们的SpringMVC 还做了哪些配置,包括如何扩展,如何定制。

只有把这些都搞清楚了,我们在之后使用才会更加得心应手。

途径一:源码分析。途径二:官方文档

官方文档地址:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

官方文档关于 SpringMVC 的自动配置描述如下:

Spring MVC Auto-configuration
// Spring Boot 为 Spring MVC 提供了自动配置,它可以很好地与大多数应用程序一起工作
Spring Boot provides auto-configuration for Spring MVC that works well with most applications .
// 自动配置在 Spring 默认设置的基础上添加了一下功能:
The auto -configuration adds the following features on top of Spring's defaults:
// 包含内容协商视图解析器和Bean名字视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支持静态资源文件夹的路径,以及Webjars
Support for serving static resources ,including support for WebJars
// 自动注册了 Converter(转换器,这就是我们网页提交数据到后台自动封装成对象的东西,比如把“1”字符串自动转换为int类型) 
// Formatter(格式化器,比如一个网页给我们了一个 2020-7-12,它会给我们自动格式化为Date对象)
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters (SpringMVC 用来转换 Http 请求和响应的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释)
Support for HttpMessageConverters (covered later in this document)
// 定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首页定制
Static index.html support.
// 图标定制
Custon support (covered later in this document).
// 初始化数据绑定器(帮我们把请求数据绑定到JavaBean中)
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
/*
如果你希望保留Spring Boot MVC 功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能)
则可以添加自己的 @Configuration 类,类型为 WebMvcConfigurer ,但不添加 @EnableWebMvc。
如果希望提供 RequestMappingHandlerMapping RequestMappingHandlerAdaptor 或者 ExceptionHandlerExceptionResolver 的自定义示例,
可以声明 WebMvcRegistrationsAdapter 示例来提供此类组件
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration(interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of the type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdaptor, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
// 如果你向完全控制 Spring MVC ,可以添加自己的 @Configuration,并用 @EnableWebMvc 进行注释
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

上面已经告诉了我们 Spring MVC 自动配置了什么,那我们开始追下源码来深入了解把

14.1 ContentNegotiatingViewResovler 内容协商视图解析器

ContentNegotiatingViewResovler 自动配置了了 ViewResolver ,就是我们之前学习的 SpringMVC 的视图解析器。

即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。

SpringBoot 中关于 SpringMVC 的自动配置都在 WebMvcConfiguration,然后搜索 ContentNegotiatingViewResovler 找到如下方法

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
   ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
   resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
   // ContentNegotiatingViewResolver uses all the other view resolvers to locate
   // a view so it should have a high precedence
   resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
   return resolver;
}

点进 ContentNegotiatingViewResovler 这个类,我们会发现它实现了 ViewResolver 接口,然后我们再点进 ViewResolver,会发现它只定义了一个 resolveViewName() 方法

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
      implements ViewResolver, Ordered, InitializingBean {
}

public interface ViewResolver {
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

我们回到 ContentNegotiatingViewResovler 这个类去查看从 ViewResolver 接口实现的 resolveViewName() 方法

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
   RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    // 断言attrs 是 ServletRequestAttributes 类型的,也就是说断言非空
   Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
   List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
   if (requestedMediaTypes != null) {
       // 获取所有候选视图
      List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
       // 从所有的候选视图中获取最优的视图
      View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
      if (bestView != null) {
         return bestView;
      }
   }
   // 下面就是打印日志的方法,与视图解析无关
   String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
         " given " + requestedMediaTypes.toString() : "";

   if (this.useNotAcceptableStatusCode) {
      if (logger.isDebugEnabled()) {
         logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
      }
      return NOT_ACCEPTABLE_VIEW;
   }
   else {
      logger.debug("View remains unresolved" + mediaTypeInfo);
      return null;
   }
}

看完源码后,我们可以得出结论:ContentNegotiatingViewResovler 这个视图解析器就是用来组合所有的视图解析器的,它会筛选出最优的视图解析器

我们再去研究下他的组合逻辑,看到有个属性 ViewResolvers,看看它是在哪里进行赋值的!

protected void initServletContext(ServletContext servletContext) {
    // 这里它是从beanFactory工具中获取容器中的所有视图解析器
    // ViewRescolver.class 把所有的视图解析器来组合的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
    }
    // ...............
}

既然它是在容器中去找视图解析器,我们是否可以猜想,我们自己也可以去实现一个视图解析器呢?

我们可以自己容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来。

  1. 先自定义我们自己的视图解析器 ViewResolver

    MyViewResolver

    public class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
    
  2. 配置 SpringMVC

    创建一个配置类叫 WebMvcConfig,并将我们自己定义的视图解析器 MyViewResolver 注册到 Spring 容器中

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
    
    }
    
  3. 在 DispatcherServlet 这个类的 doDispatch() 这个方法上打上断点

    因为 SpringMVC 的所有请求,都会进入 DispatcherServlet 的 doDispatch() 这个方法进行处理,

    这里的图是已经启动调试进入断点了

    image-20200713112942149

  4. DEBUG 启动 SpringBoot 应用程序

    随便访问一个我们以前写好的 Controller 接口,查看 参数信息

    image-20200713113040794

    我们可以发现,我们自己写的视图解析器 MyViewResolver 已经注册到 Spring 容器中了

到这里,我们自定义的解析器就成功生效了。如果我们想要使用自己定制化的东西,我们只需要给容器中加入这个组件就好了。剩下的事情 SpringBoot 会自动帮我们做。

14.2 转换器和格式化器

搜索 formattingConversionService 这个方法:

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 从配置文件中获取格式化规则
    Format format = this.mvcProperties.getFormat();
    // 将取出的配置信息放进 DateTimeFormatters 这个类中封装
   WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
         .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
    // 将日期格式的格式化器注册进 Spring 容器中
   addFormatters(conversionService);
   return conversionService;
}

我们点进配置文件 MvcProperties 这个类,发现这个格式化的规则是 new 了一个 Format 对象

private final Format format = new Format();

我们继续点进 Format 这个类

public static class Format {

   /**
    * Date format to use, for example `dd/MM/yyyy`.
    */
   private String date;

   /**
    * Time format to use, for example `HH:mm:ss`.
    */
   private String time;

   /**
    * Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
    */
   private String dateTime;
}

发现这些日期格式在注释中已经说明了默认值。

如果配置了我们自己的格式化方式,就会注册到 Bean 中生效,我们可以在配置文件中配置日期格式化的规则。

@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.mvc.format.date")
public String getDateFormat() {
   return this.format.getDate();
}
spring:
    mvc:
      format:
        date: yyyy-MM-dd

14.3 修改 SpringBoot 的默认配置

这么多的自动配置,原理都是一样的,通过这个 WebMVC 的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论。这个结论一定是属于自己的,而且一通百通。

SpringBoot 的底层,大量用到了这些设计思想细节,所以,没事需要多阅读源码!得出结论。

SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置 @Bean ),如果有就用用户配置的,如果没有就用自动配置的。

如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

拓展使用 Spring MVC 官方文档如下:

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors,formatters,view controllers,and other features),you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc .If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter,or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

我们要做的就是编写一个 @Configuration 注解类,并且类型要为 WebMvcConfigurer,而且还不能表主 @EnableWebMvc 注解

那我们现在再自己写一个配置

在我们刚刚写的 WebMvcConfig 中重写 addViewControllers() 方法

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/test1").setViewName("test");
    }
}

然后启动 SpringBoot 应用程序,进入 /test1 这个路径测试是否进入我们之前写的 test 页面

image-20200713142235926

的确是可以跳转过来,同时,我们发现 Thymeleaf 没有接收到参数时,会隐藏内容。

我们要扩展 SpringMVC ,官方就推荐我们这么去使用,即保留 SpringBoot 所有的自动配置,也能用我们扩展的配置

我们可以去分析一下原理:

  1. WebMvcAutoConfiguration 是 SpringMVC 的自动配置类,里面有一个类 WebMvcAutoConfigurationAdapter

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
          ValidationAutoConfiguration.class })
    // WebMvcAutoConfiguration webmvc 自动配置类
    public class WebMvcAutoConfiguration {
    	
        @Configuration(proxyBeanMethods = false)
    	@Import(EnableWebMvcConfiguration.class)
        // @Import(EnableWebMvcConfiguration.class) 引入 EnableWebMvcConfiguration 类
    	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    	@Order(0)
        // WebMvcAutoConfigurationAdapter类
    	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
        }
    
    }
    
  2. 这个类上有一个注解,在做其他自动配置时会导入: @Import(EnableWebMvcConfiguration.class)

  3. 我们点进 EnableWebMvcConfiguration 这个类看一下,它继承了一个父类: DelegatingWebMvcConfiguration

    @Configuration(proxyBeanMethods = false)
    // 继承 DelegatingWebMvcConfiguration
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
        	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    	// 从容器中获取所有的 webmvcConfigurer
    	@Autowired(required = false)
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
    			this.configurers.addWebMvcConfigurers(configurers);
    		}
    	}
    
    }
    
  4. 我们可以在这个类中寻找一个我们刚才设置的 viewController 当作参考,发现它调用了一个

    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
       this.configurers.addViewControllers(registry);
    }
    
  5. 我们点进去看一下

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // 将所有的 webMvcConfigurer 相关配置一起调用!包括我们自己配置的 和 Spring 给我们配置的
        for (WebMvcConfigurer delegate : this.delegates) {
          delegate.addViewControllers(registry);
       }
    }
    

所以得出结论:所有的 WebMbcConfiguration 都会被作用,不止 Spring 自己的配置类,我们自己的配置类当然也会被调用

14.4 @EnableWebMvc 注解让 SpringBoot MVC 自动配置失效的原因

在官网中写道,如果我们需要 SpringBoot 进行自动配置和扩展,则在 SpringMVC 的配置类上不能加上 @EnableWebMvc 注解,这是为什么呢?

我们都知道,SpingBoot MVC自动配置类是 WebMvcAutoConfiguration ,我们点进这个类,会发现上面有个注解是 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),这个注解的意思是:
// 如果 Spring 容器中没有 WebMvcConfigurationSupport 这个类,那么 SpringBoot 自动配置类 WebMvcAutoConfiguration 才会生效。
// 也就是说,如果存在 WebMvcConfigurationSupport 这个类,SpringBoot MVC 的自动配置全部失效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
      ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
}
  1. @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

    这个注解的意思是:如果 Spring 容器中没有 WebMvcConfigurationSupport 这个类,那么 SpringBoot 自动配置类 WebMvcAutoConfiguration 才会生效。

    也就是说:如果存在 WebMvcConfigurationSupport 这个类, SpringBoot MVC 的自动配置全部失效

    @ConditionalOnMissingBean(XXX.class):前面提到过这个注解,当没有XXX这个类的时候,被注解的类生效

  2. @EnableWebMvc

    我们点进 WebMvcConfigurationSupport 这个类,发现类上的注释中有个 @see EnableWebMvc

    / * @author Rossen Stoyanchev
     * @author Brian Clozel
     * @author Sebastien Deleuze
     * @since 3.1
     * 这个 @see EnableWebMvc ctrl+右键点进去   
     * @see EnableWebMvc
     * @see WebMvcConfigurer
     */
    public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
    }
    

    点进去后,源码如下:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    // @Import(DelegatingWebMvcConfiguration.class) 导入 DelegatingWebMvcConfiguration 类
    @Import(DelegatingWebMvcConfiguration.class)
    public @interface EnableWebMvc {
    }
    

    我们可以看到,这个注解的作用,就是引入了 DelegatingWebMvcConfiguration 这个类。

    那为什么引入了这个类就会让 SpringBoot MVC 的自动配置全部失效呢?

    我们再点进 DelegatingWebMvcConfiguration 这个类

    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    }
    

    会发现,它继承了 WebMvcConfigurationSupport 这个类。

    我们刚刚在 SpringBoot MVC 自动配置类 WebMvcAutoConfiguration 中看到 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 这个注解,而 @EnableWebMvc 这个注解就是导入了 WebMvcConfigurationSupport 的子类 DelegatingWebMvcConfiguration

到这里,我们就知道了为什么加了 @EnableWebMvc 这个注解,SpringBoot MVC 的自动配置会全部失效了。

14.5 全面接管 SpringMVC

官方文档:

If you want to take complete control of Spring MVC
you can add your own @Configuration annotated with @EnableWebMvc.

翻译过来就是,如果你想完全接管 Spring MVC,你可以在你拥有 @Configuration 注解的类上添加 @EnableWebMvc 注解

全面接管即: SpringBoot 对 SpringMVC 的自动配置不需要了,所有的配置都是由我们自己去配置!

如果我们要全面接管 Spring MVC,SpringBoot 给我们配置的静态资源映射就一定会无效。我们现在可以去测试一下:

  1. 在 templates 包下创建 index.html

    SpringBoot 中的 MVC 自动配置会将静态资源路径下的 index.html 作为首页

    <!DOCTYPE html>
    <html lang="en" >
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>首页</h1>
    </body>
    </html>
    
  2. 启动 SpringBoot 应用程序,访问URL根目录

    image-20200713162730945

    我们可以发现首页可以正常访问

  3. 在我们之前的 SpringBoot MVC 配置类中增加 @EnableWebMvc注解

    @Configuration
    @EnableWebMvc
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/test1").setViewName("test");
        }
    }
    
  4. 重启 SpringBoot 应用程序,访问URL根目录

    image-20200713163140233

    我们会发现这个首页已经进不去了,这说明了我们在 @Configuration 的类上添加 @EnableWebMvc 注解后,SpringBoot MVC 的自动配置全部失效,包括静态资源映射

当然,在我们开发中,不推荐使用全面接管 SpringMVC

15. 页面国际化

有时候,我们的网站会涉及中英文甚至多语言的切换,这时我们就需要学习国际化了!

15.1 准备工作

在 IDEA 中统一设置 properties 的编码问题!

image-20200713184548019

编写国际化配置文件,抽取页面需要显示的国际化页面消息。我们可以去登录页面查看一下,哪些内容我们需要编写国际化的配置

登录页面代码:

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

<body class="text-center">
<form class="form-signin" th:action="@{/login}">
    <img class="mb-4" src="img/bootstrap-solid.png" alt="" width="72" height="72">
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
    <span style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></span>
    <label class="sr-only" th:text="#{login.username}">Username</label>
    <input type="text" class="form-control" th:placeholder="#{login.username}" name="username" required="" autofocus="">
    <label class="sr-only" th:text="#{login.password}">Password</label>
    <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me">[[#{login.remember}]]
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit">[[#{login.btn}]]</button>
    <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
    <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>
</form>

</body>

</html>

15.2 配置文件编写

  1. 在 resources 目录下创建一个 i18n 目录,用来存放国际化配置文件

  2. 在 i18n 目录下建立 login.properties、login_en_US.properties、login_zh_CN.properties 国际化配置文件。创建时,我们会发现 IDEA 自动帮我们做了整合

    image-20200713203958535

  3. 配置国际化配置文件

    在我们点击配置文件后,我们会发现下面多了个东西

    image-20200713204233579

    点击 Resource Bundle,然后就能同时编辑三个国际化配置文件了

    image-20200713204650651

    然后将我们需要编写的内容编写完成

15.3 创建 LocaleResolver

MessageSourceAutoConfiguration 中有一个核心方法 messageSource()

@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
   ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
   if (StringUtils.hasText(properties.getBasename())) {
       // 设置国际化文件的基础名(去掉语言国家代码的
       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;
}

根据这个源码,我们需要配置 basename:

spring:
  messages:
    basename: i18n.login

配置完后在我们的前端页面使用 thymeleaf 取值。:取值格式是 #{xxx} ,而不是${xxx}

查看我们刚刚配置的这些是否生效。

image-20200713223312138

如果我们的配置生效,则会显示如下的页面,若配置出错,则可能显示 ??login.username??

这时候,我们就得从头开始检查是否有写错

  1. 创建 properties 文件,这里它会自动生成,不要手动去修改

    image-20200713223652231

  2. 前端获取的单词拼写是否和 properties 文件中设置的一致

  3. 检查 application.yml/properties 的配置,注意 yml/properties 文件的语法书写规范

  4. 注意细节,不要 ‘.’ 打成了 ‘,’ 或其他符号

按照上述步骤排错, ??login.username?? 这种类型的错误就能解决了

15.4 配置国际化解析

上面是成功的将我们自己定义的国际化信息展现出来了,但我们真正想要的,是点击按钮切换国际语言,这个得怎么做呢?

在 Spring 中有一个国际化得 Locale (区域信息对象);里面有一个叫做 LocaleResolver (获取区域信息对象)得解析器。

我们进入 WebMvcAutoConfiguration 这个类,寻找 localeResolver 这个方法

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
    // 容器中没有就自己配,有的话就用用户配置的
   if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
      return new FixedLocaleResolver(this.mvcProperties.getLocale());
   }
    // 接收头国际化分解
   AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
   localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
   return localeResolver;
}

其中 AcceptHeaderLocaleResolver 实现了 LocaleResolver 接口,并且里面重写了接口里的 resolveLocale() 方法。

@Override
public Locale resolveLocale(HttpServletRequest request) {
   // 获取默认的 Locale
    Locale defaultLocale = getDefaultLocale();
   // 默认的就是根据请求头带来的区域信息获取 Locale 进行国际化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
      return defaultLocale;
   }
   Locale requestLocale = request.getLocale();
   List<Locale> supportedLocales = getSupportedLocales();
   if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
      return requestLocale;
   }
   Locale supportedLocale = findSupportedLocale(request, supportedLocales);
   if (supportedLocale != null) {
      return supportedLocale;
   }
   return (defaultLocale != null ? defaultLocale : requestLocale);
}

也就是说,我们寻找向点击连接让我们的国际化资源生效,就需要让我们自己的 Locale 生效!

我们去自己写一个自己的 LocaleResolver ,可以在链接上携带区域信息!

根据我们前端定义好的参数,写我们自己的 LocaleResolver

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

自定义我们自己的 LocaleResolver :LocaleResolver 接口是 org.springframework.web.servlet 包下的

package com.xp.config;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //获取请求的语言参数
        String language = request.getParameter("l");
        Locale locale = Locale.getDefault();    //如果没有就使用默认的
        //如果请求携带了国际化的参数
        if(!StringUtils.isEmpty(language)){
            //zh_CN
            String [] spilt= language.split("_");
            //国家,地区
            locale = new Locale(spilt[0],spilt[1]);
        }
        return locale;
    }

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

    }
}

然后将我们写好的区域化信息注入到 Spring 容器中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 进行重定向
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }


    //使自定义的国际化组件生效
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

}

如果直接注入 Spring 容器,可能会不生效。我网上查的是需要进行重定向。配置个视图控制器进行重定向就可以了。

16. 集成 Swagger

相信无论是前端还是后端开发,都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。其实无论是前端调用后端,还是 后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样,经常会抱怨别人写得代码没有写注释。然而自己写起代码来,最讨厌的,也是写注释。所以仅仅通过强制来规范大家是不够的。随着时间推移,版本迭代,接口文档往往很容易跟不上代码了。

16.1 Swagger 简介

前后端分离

  • 前端 -> 前端控制层、视图层
  • 后端 -> 后端控制层、服务层、数据访问层
  • 前后端通过 API 进行交互
  • 前后端相对独立且松耦合

产生的问题

  • 前后端集成,前端或者后端无法做到 “及时协商,尽早解决” ,最终导致问题集中爆发

解决方案

  • 首先定义 schema [计划的提纲],并实时跟踪最新的 API,降低集成风险

Swagger

  • 号称世界上最流行的 API 框架

  • Restful API 文档在线自动生成器 => API 文档与 API 定义同步更新

  • 直接运行,在线测试 API

  • 支持多种语言(如:Java、PHP等)

  • 官网:https://swagger.io/

16.2 SpringBoot 集成 Swagger

使用 Swagger ,JDK 必须 1.8 以上,否则 swagger2 无法运行

  1. 添加 Maven 依赖

    <!-- swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  2. 创建一个 Swagger 配置类

    SwaggerConfig

    @Configuration
    @EnableSwagger2 // 开启 swagger
    public class SwaggerConfig {
    }
    
  3. 访问测试:http://localhost:8080/swagger-ui.html

    访问该 URL 后看到的 swagger 的界面:

    image-20200714125907920

16.3 配置 Swagger

  1. Swagger 实例 Bean 是 Docket,所以通过配置 Docket 实例来配置 Swagger。

    在我们刚刚的 Swagger 配置类中注册 Docket 的 Bean

    // 配置 Docket 以配置 Swagger 具体参数
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2);
    }
    
  2. 可以通过 apliInfo() 属性配置文档信息

    private ApiInfo apiInfo(){
        Contact contact = new Contact("XP","https://www.cnblogs.com/windowsxpxp/","997612568@qq.com");
        return new ApiInfo(
                "Swagger学习",// 标题
                "学习演示如何配置Swagger", // 描述
                "v1.0", // 版本
                "http://terms.service.url/组织链接", // 组织链接
                contact, // 联系人信息
                "Apach 2.0 许可", // 许可
                "许可链接", // 许可连接
                new ArrayList<>()// 扩展
        );
    }
    
  3. Docket 实例关联上 apiInfo()

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
    }
    
  4. 重启 SpringBoot 应用,并重新访问 Swagger 后台页面

    发现部分信息已经改变了

    image-20200714133600197

16.4 配置扫描接口

  1. 构建 Docket 时通过 select() 方法配置扫描接口以及如何扫描

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .select() // 通过 .select() 方法,去配置扫描接口 .RequestHandlerSelectors 配置扫描接口以及如何扫描
            .apis(RequestHandlerSelectors.basePackage("com.xp.controller")).build()
                .groupName("xp");
    }
    
  2. 除了通过包路径扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:

    any() // 扫描所有,项目中的所有接口都会被扫描到
    none() // 不扫描接口
    // 通过方法上的注解扫描,如 withMethodAnnotation(GetMapping.class) 只扫描 get 请求
    withMethodAnnotation(final Class<? extends Annotation> annotation)
    // 通过类上的注解扫描,如 withClassAnnotation(Controller.class) 只扫描有 @Controller 注解的类中的接口
    withClassAnnotation(final Class<? extends Annotation> annotation)
    basePackage(final String basePackage) // 根据包路径扫描接口
    
  3. 除此之外,我们还可以配置接口扫描过滤

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xp.controller"))
            .path(PathSelectors.ant("/xp/**")) // 配置如何通过 path 过滤,即这里只扫描请求以 /xp 开头的接口
            .build();
    }
    
  4. 这里可选值还有

    any() // 任何请求都扫描
    none() // 任何请求都不扫描
    regex(final String pathRegex) // 通过正则表达式控制
    ant(final String antPatten) // 通过 ant() 控制
    

16.5 配置 Swagger 开关

  1. 通过 enable() 方法配置是否启用 swagger,如果是 false,swagger 将不能在浏览器中访问了

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xp.controller")).build()
                .enable("xp");
    }
    
  2. 如何动态配置当项目处于 test、dev 环境时显示 swagger,处于 prod 时不显示?

    // 配置 Docket 以配置 Swagger 具体参数
    @Bean
    public Docket docket(Environment environment) {
        // 设置要显示swagger的环境
        Profiles of = Profiles.of("dev", "test");
        // 判断当前是否处于该环境
        // 通过 enable() 接收此参数判断是否要显示
        boolean b = environment.acceptsProfiles(of);
    
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问
                .select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
                .apis(RequestHandlerSelectors.basePackage("com.xp.controller"))
                // 配置如何通过path过滤,即这里只扫描请求以/xp开头的接口
                .paths(PathSelectors.ant("/xp/**"))
                .build();
    }
    
  3. 测试

16.6 配置 API 分组

  1. 如果没有配置分组,默认是 default。通过 groupName() 方法即可配置分组:

    @Bean
    public Docket docket(Environment environment) {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
          .groupName("hello") // 配置分组
           // 省略配置....
    }
    
  2. 重启项目查看分组

  3. 设置多个分组

    我们要配置多个分组,就只需配置多个 docket 即可:

    @Bean
    public Docket docket1(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
    }
    @Bean
    public Docket docket2(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
    }
    @Bean
    public Docket docket3(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
    }
    
  4. 重启项目查看

16.7 实体配置

  1. 新建一个实体类

    @ApiModel("用户实体类")
    public class User {
        @ApiModelProperty("用户id")
        private Integer id;
        @ApiModelProperty("用户姓名")
        private String name;
        @ApiModelProperty("用户密码")
        private String password;
        @ApiModelProperty("用户爱好")
        private String hobby;
    }
    
  2. 只要这个实体在请求接口的返回值上(即使是泛型),否能映射到实体项中:

    @RequestMapping("/getUser")
    public User getUser(){
       return new User();
    }
    
  3. 重启查看测试

    image-20200714145510379

    :并不是因为 @ApiModel 这个注解让实体显示在这里,而是只要出现在接口方法的返回值上的实体都会显示在这里,而 @ApiModel 和 @ApiModelProperties 这两个注解只是为实体添加注释的

    @ApiModel 为类添加注释

    @ApiModelProperties 为类属性添加注释

16.8 常用注解

Swagger 的所有注解定义在 io.swagger.annotations 包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

Swagger 注解 简单使用
@Api(tags = "xxx模块说明") 作用在模块类上
@ApiOperation("xxx接口说明") 作用在接口方法上
@ApiModel("xxxPOJO说明") 做那个用在模型类上,如VO、DTO
@ApiModelProperties(value = "xxx属性说明",hidden = true) 作用在类方法和属性上,hidden 设置为 true 可以隐藏该属性
@ApiParam("xxx参数说明") 作用在参数、方法和字段上,类似 @ApiModelProperties
@ApiOperation("xp的接口")
@PostMapping("/xp")
@ResponseBody
public String xp(@ApiParam("这个名字会被返回")String username){
   return username;
}

这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!

相较于传统的 Postman 或 Curl 方式测试接口,使用 swagger 简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击 Execute。如果再配合自动化框架,可以说基本就不需要人为操作了。

Swagger 是个优秀的工具,现在国内已经有很多中小心互联网公司都在使用它。相较于传统的要先出 Word 接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家再正式环境要记得关闭 Swagger ,一来处于安全考虑,而来也可以节省运行时内存。

17. 异步、定时、邮件任务

在我们的工作中,常常会用到异步处理任务,比如我们在网站上发送邮件,后台会去发送右键,此时前台会照成响应不动。知道邮件发送完毕,响应才会成功。所以我们一般会采用多线程的方式去处理这些任务。还有一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。还有就是邮件的发送,微信的前生也是邮件服务呢,这些东西是怎么实现的呢?其实 SpringBoot 都给我们提供了对应的支持,我们上手使用十分的简单,只需要开启一些注解支持,配置一些配置文件即可!那我们来看看吧。 ——引自狂神说

17.1 异步任务

  1. 创建一个 AsyncService

    先创建一个 Service 包,然后在这个包下创建 AsynService 类。

    异步处理还是非常常用的,比如我们在网站上发送邮件,后台回去发送邮件。此时前台会造成响应不动,直到邮件发送完毕,响应才会成功。所以我们一般会采用多线程的方式去处理这些任务。

    编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况。

    @Service
    public class AsyncService {
    
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("业务进行中。。。。");
        }
    
    }
    
  2. 创建 AsyncController

    在 controller 包下创建 AsyncController 类,编写接口,模拟发邮件的延迟

    @RestController
    public class AsyncController {
    
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping("/hello")
        public String hello() {
            asyncService.hello();
            return "success";
        }
    
    }
    
  3. 启动 SpringBoot 应用程序,访问接口

    我们可以发现,我们请求 /hello 接口后,页面并没有马上跳转,也没有马上刷新,浏览器页面阻塞。得等到后台响应后才会进行跳转和刷新。这样用户体验就非常不好了!

    image-20200714190220161

  4. 给 hello 方法添加 @Async 注解

    我们如果想让用户直接得到消息,就在后台使用多线程得方式进行处理即可,但是每次都需要自己手动去编写多线程得实现的话,太麻烦了。我们只需要用一个简单的办法,在我们的方法上加上一个简单的注解即可,如下:

    @Async // 高数 Spring 这是一个异步方法
    @RequestMapping("/hello")
    public String hello() {
        asyncService.hello();
        return "success";
    }
    

    SpringBoot 就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要再主程序类上添加一个注解 @EnableAsync ,开启异步注解功能

    @EnableAsync // 开启异步注解功能
    @SpringBootApplication
    public class Springboot01Application {
        public static void main(String[] args) {
            SpringApplication.run(Springboot01Application.class, args);
        }
    }
    
  5. 重启 SpringBoot 应用程序,发现网页是瞬间响应,后台代码一就执行!

17.2 定时任务

项目开发中经常需要执行一些定时任务,比如需要再每天凌晨的时候,分析一次前一天的日志信息,Spring 为我们提供了异步执行任务调度的方式,提供了两个接口。

  • TaskExecutor 接口
  • TaskScheduler 接口

两个注解

  • @EnableScheduling
  • @Scheduled

cron 表达式:

字段 允许值 允许的特殊字符
0-59 ,-*/
0-59 ,-*/
小时 0-23 ,-*/
日期 1-31 ,-*?/L W C
月份 1-12 ,-*/
星期 0-7或SUN-SAT 0,7是SUN ,-*?/L W C
特殊字符 代表含义
, 枚举
- 区间
* 任意
/ 步长
? 日/星期冲突匹配
L 最后
W 工作日
C 和 calendar 联系后计算过的值
# 星期,4#2,第2个星期三

测试步骤:

  1. 创建 ScheduledService

    在 service 包下,创建一个 ScheduledService 类,类里面有一个 hello() 方法,并设置什么时候定时执行

    @Service
    public class ScheduledService {
    
        // 秒 分 时 日 月 周几
        // 0  *  * *  *  0-7
        // 注意 cron 表达式的用法
        @Scheduled(cron = "0 50 19 * * 0-7")
        public void hello() {
            // 定义格式化规则
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            // 格式化现在的时间并输出
            System.out.println(sdf.format(System.currentTimeMillis()) + ",hello!");
        }
    
    }
    
  2. 开启定时任务功能

    我们写完定时任务后,需要在主程序类上增加 @EnableScheduling 注解开启定时任务功能

    @EnableScheduling // 开启定时任务功能
    @EnableAsync // 开启异步注解功能
    @SpringBootApplication
    public class Springboot01Application {
        public static void main(String[] args) {
            SpringApplication.run(Springboot01Application.class, args);
        }
    }
    
  3. 启动 SpringBoot 主程序类,测试

    image-20200714195028595

  4. 了解 cron 表达式

    http://www.bejson.com/othertools/cron/

  5. 常用的 cron 表达式

    (1)0/2 * * * * ?   表示每2秒 执行任务
    (1)0 0/2 * * * ?   表示每2分钟 执行任务
    (1)0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
    (2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
    (3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
    (4)0 0 10,14,16 * * ?   每天上午10点,下午2点,4点
    (5)0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
    (6)0 0 12 ? * WED   表示每个星期三中午12点
    (7)0 0 12 * * ?   每天中午12点触发
    (8)0 15 10 ? * *   每天上午10:15触发
    (9)0 15 10 * * ?     每天上午10:15触发
    (10)0 15 10 * * ?   每天上午10:15触发
    (11)0 15 10 * * ? 2005   2005年的每天上午10:15触发
    (12)0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发
    (13)0 0/5 14 * * ?   在每天下午2点到下午2:55期间的每5分钟触发
    (14)0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
    (15)0 0-5 14 * * ?   在每天下午2点到下午2:05期间的每1分钟触发
    (16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44触发
    (17)0 15 10 ? * MON-FRI   周一至周五的上午10:15触发
    (18)0 15 10 15 * ?   每月15日上午10:15触发
    (19)0 15 10 L * ?   每月最后一日的上午10:15触发
    (20)0 15 10 ? * 6L   每月的最后一个星期五上午10:15触发
    (21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
    (22)0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发
    

17.3 邮件任务

邮件发送,在我们的日常开发中,也非常的多,SpringBoot 也帮我们做了支持

  • 邮件发送需要引入 spring-boot-start-email
  • SpringBoot 自动配置 MailSenderAutoConfiguration
  • 自定义 MailProperties 内容,配置在 application.yml 中
  • 自动配置 JavaMailSender
  • 测试邮件发送

测试

  1. 引入 pom 依赖

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

    看它引入的依赖,可以看到 jakarta.mail 包

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>jakarta.mail</artifactId>
      <scope>compile</scope>
    </dependency>
    
  2. 查看自动配置类:MailSenderJndiConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
    @ConditionalOnMissingBean(MailSender.class)
    @Conditional(MailSenderCondition.class)
    @EnableConfigurationProperties(MailProperties.class)
    // 引入 MailSenderJndiConfiguration 类,这个类并没有注册bean,看一下它导入 的其他类
    @Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
    public class MailSenderAutoConfiguration {
    }
    

    这个类中存在bean,JavaMailSenderImpl

    @Bean
    JavaMailSenderImpl mailSender(Session session) {
       JavaMailSenderImpl sender = new JavaMailSenderImpl();
       sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
       sender.setSession(session);
       return sender;
    }
    

    然后我们去看下配置文件

    @ConfigurationProperties(prefix = "spring.mail")
    public class MailProperties {
    
       private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    
       /**
        * SMTP server host. For instance, `smtp.example.com`.
        */
       private String host;
    
       /**
        * SMTP server port.
        */
       private Integer port;
    
       /**
        * Login user of the SMTP server.
        */
       private String username;
    
       /**
        * Login password of the SMTP server.
        */
       private String password;
    
       /**
        * Protocol used by the SMTP server.
        */
       private String protocol = "smtp";
    
       /**
        * Default MimeMessage encoding.
        */
       private Charset defaultEncoding = DEFAULT_CHARSET;
    
       /**
        * Additional JavaMail Session properties.
        */
       private Map<String, String> properties = new HashMap<>();
    
       /**
        * Session JNDI name. When set, takes precedence over other Session settings.
        */
       private String jndiName;
    
  3. 配置文件

    mail:
      username: 997612568@qq.com
      password: 你的qq授权码
      host: smtp.qq.com
      # qq需要配置ssl
      properties:
        mail:
          smtp:
            ssl:
              enable: true
    

    获取授权码:在 QQ 邮箱中的设置 -> 账户 -> 开启 pop3和smtp 服务

    image-20200714203632908

  4. 测试

    在 SpringBoot 测试类中编写如下测试代码

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        JavaMailSenderImpl javaMailSender;
    
        // 邮件设置1:一个简单的邮件
        @Test
        void test1(){
            // 设置发送内容
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("标题:这里是发送邮件的标题");
            message.setText("内容:这是发送邮件的内容");
            // 设置发送邮箱和接收邮箱
            message.setTo("997612568@qq.com");
            message.setFrom("997612568@qq.com");
            javaMailSender.send(message);
        }
    
        //邮件设置2:一个复杂的邮件
        @Test
        void test2() throws MessagingException {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    
            helper.setSubject("标题:这里是发送邮件的标题");
            helper.setText("<p style='color:red'>内容:这是发送邮件的内容</p>",true);
    
            // 发送附件
            helper.addAttachment("1.jpg",new File("1.jpg"));
            helper.addAttachment("2.jpg",new File("1.jpg"));
    
            helper.setTo("997612568@qq.com");
            helper.setFrom("997612568@qq.com");
    
            javaMailSender.send(mimeMessage);
        }
    
    }
    

    点击运行,然后在邮箱中查看是否发送成功

    image-20200714203858732

18. Dubbo 和 ZooKeeper 集成

18.1 ZooKeeper 下载与安装

1. 进入官网下载 ZooKeeper

ZooKeeper 官网地址: https://zookeeper.apache.org/

点击上面官网链接进入官网

image-20200715091703738跳转页面后,点击下载

image-20200715093033338

image-20200715093147742

2. Zookeeper 安装

  1. 下载完后直接解压,解压后目录如下:

    image-20200715093811917

  2. 创建一个 data 文件夹和 log 文件夹

    image-20200715093508621

  3. 进入 config 文件夹,将 zoo_sample.cfg 复制一份,并重命名 zoo.cfg

    image-20200715094115465

  4. 修改 zoo.cfg 内的配置信息

    dataDir 和 dataLogDir 根据自己的真实路径填写。就是我们刚刚创建的 data 和 log 文件夹的位置

    image-20200715094354754

    # The number of milliseconds of each tick
    tickTime=2000
    # The number of ticks that the initial 
    # synchronization phase can take
    initLimit=10
    # The number of ticks that can pass between 
    # sending a request and getting an acknowledgement
    syncLimit=5
    # the directory where the snapshot is stored.
    # do not use /tmp for storage, /tmp here is just 
    # example sakes.
    dataDir=D:\\Code\\tool\\java\\zookeeper-3.4.14\\zookeeper-3.4.14\\data
    dataLogDir=D:\\Code\\tool\\java\\zookeeper-3.4.14\\zookeeper-3.4.14\\log
    # the port at which the clients will connect
    clientPort=2181
    # the maximum number of client connections.
    # increase this if you need to handle more clients
    #maxClientCnxns=60
    #
    # Be sure to read the maintenance section of the 
    # administrator guide before turning on autopurge.
    #
    # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
    #
    # The number of snapshots to retain in dataDir
    #autopurge.snapRetainCount=3
    # Purge task interval in hours
    # Set to "0" to disable auto purge feature
    #autopurge.purgeInterval=1
    

3. 运行 ZooKeeper

  1. 进入 bin 目录,启动 zkServer.cmd

    image-20200715094708467

    如果闪退的话,就代表启动失败,可能是我们刚刚配置错误了。

    编辑 zkServer.cmd 文件,增加 @pause 让它报错时停下来,然后根据报错百度查询解决方案。

    java.io.IOException: Unable to create data directory XXX 代表是文件路径错误了,检查 dataDir 和 dataLogDir 的路径

    image-20200715095005608

    @echo off
    REM Licensed to the Apache Software Foundation (ASF) under one or more
    REM contributor license agreements.  See the NOTICE file distributed with
    REM this work for additional information regarding copyright ownership.
    REM The ASF licenses this file to You under the Apache License, Version 2.0
    REM (the "License"); you may not use this file except in compliance with
    REM the License.  You may obtain a copy of the License at
    REM
    REM     http://www.apache.org/licenses/LICENSE-2.0
    REM
    REM Unless required by applicable law or agreed to in writing, software
    REM distributed under the License is distributed on an "AS IS" BASIS,
    REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    REM See the License for the specific language governing permissions and
    REM limitations under the License.
    
    setlocal
    call "%~dp0zkEnv.cmd"
    
    set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
    echo on
    call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
    @pause
    endlocal
    
  2. 运行成功

    image-20200715095250155

4. 测试

  1. 双击运行 ZooKeeper 客户端 zkCli.cmd

    image-20200715132832007

  2. 查看所有节点

    注意 ls 后面有个空格

    ls /
    

    image-20200715133713119

  3. 创建一个节点

    创建一个我们自己的节点,然后放入存入的值

    create -e /xp test
    

    image-20200715133421222

    然后我们再用 ls / 命令查看所有的节点

    可以发现这里多了我们刚刚自己注册的节点

    image-20200715133855404

  4. 获取节点的值

    get /xp
    

    可以发现,我们在创建节点时给的值获取出来了

    image-20200715134027422

18.2 dubbo-admin

dubbo 本身并不是一个服务软件。它其实就是一个 jar 包,能够帮你的 java 程序连接到 ZooKeeper,并利用 ZooKeeper 进行消费、提供服务。

但是为了让用户更好地管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin,不过这个监控即使不装也不影响使用。

我们这里来安装一下:

1. 下载 dubbo-admin 并解压

官网下载地址:https://github.com/apache/dubbo-admin/tree/master

2. 指定 ZooKeeper 地址

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址。

一般默认已经指向 ZooKepper 地址了。

这里可以自定义设置一些内容,比如 dubbo-admin 登录密码等。

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 指向 ZooKeeper 地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3. 使用 maven 打成 jar 包

使用 cmd 进入刚刚解压的根目录,并使用 maven 打成 jar 包

mvn clean package -Dmaven.test.skip=true

第一次打包时间比较长,耐心等待即可

也可将 Maven 切换成阿里源,或者使用 IDEA 进行打包。

4. 运行 jar 包

打完 jar 包后,先开启 ZooKeeper ,然后使用 cmd 进入 D:\Code\tool\java\dubbo-admin-master\dubbo-admin-master\dubbo-admin\target (根据自己的实际目录进入,dubbo-admin的 target包下)

输入 java -jar dubbo-admin-0.0.1-SNAPSHOT.jar 命令,启动 jar 包

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

然后我们就可以看到 dubbo-admin SpringBoot 项目启动了

image-20200715144351825

5. 打开 ZooKeeper 客户端,查看节点信息

我们双击打开 ZooKeeper 客户端 zkCli.cmd ,使用 ls / 命令查看 ZooKeeper 中的节点信息

如果没有显示,可能时延迟问题,就在这三个 dos 窗口中敲回车

image-20200715144631716

6. 访问 dubbo-admin

我们在浏览器中输入 http://localhost:7001/ 来访问 dubbo-admin

这里的账号密码,是我们刚刚 application.properties 中设置的。默认账号是 root,密码是 root

image-20200715144945866

输入账号和密码后,成功进入到 dubbo-admin 页面

image-20200715145133534

18.3 SpringBoot + Dubbo + Zookeeper

18.3.1 环境搭建

  1. 创建两个 SpringBoot 工程,分别是 consumer-server(服务消费者)、 provider-server(服务提供者),创建时都添加 web 依赖

  2. 引入依赖

    两个工程项目引入以下依赖

    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.6</version>
    </dependency>
    <!-- zkclient ZooKeeper -->
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.10</version>
    </dependency>
    <!-- 引入zookeeper -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>2.12.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>2.12.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.14</version>
        <!--排除这个slf4j-log4j12-->
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    

18.3.2 服务提供者

1. 提供服务接口

在 provider-server (服务提供者)这个工程项目中创建 service 包,并在包下创建 TicketService 接口以及其实现类 TicketServiceImpl

TicketService

public interface TicketService {
	// 获取票的方法
    String getTicket();

}

TicketServiceImpl

:这里的 @Service 是 org.apache.dubbo.config.annotation 包下的注解,不要导错了!

由于 @Service 注解同名了,所以我们使用 @Component 将这个服务注册到 Spring 容器中

逻辑理解:应用启动起来,dubbo就会扫描指定包下带有 @Component 注解的服务,将它发布在指定的注册中心!

package com.xp.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

@Service    // 将服务发布出去
@Component  // 注册到 Spring 容器中
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "买到票了";
    }
}
2. 修改 SpringBoot 配置文件

application.properties

将服务注册到 ZooKeeper 中

:如果显示无法连接 dubbo 的,可能是连接超时,试试将超时时间延长。如果没有设置连接超时时间,默认是 3 秒钟。

# 配置 Tomcat 服务器启动端口
server.port=8080
# 当前应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 哪些服务需要被注册
dubbo.scan.base-packages=com.xp.service
# 设置连接超时时间
dubbo.config-center.timeout=10000

18.3.3 服务消费者

1. 本来正常步骤是将服务提供者的接口打包,然后用 pom 文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同

TicketService 接口必须和服务提供者的包路径一致!

image-20200715185453035

2. 创建消费服务类

创建消费服务类 UserService 来消费服务提供者提供的服务

UserService

:这里的 @Service 是 Spring 的注解

@Service    // 注册到 Spring 容器钟
public class UserService {

    @Reference  // 远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名
    TicketService ticketService;

    public void buyTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到" + ticket);
    }

}
3. 修改 SpringBoot 配置文件

application.properties

将服务注册到 ZooKeeper 中

:如果显示无法连接 dubbo 的,可能是连接超时,试试将超时时间延长。如果没有设置连接超时时间,默认是 3 秒钟。

# 配置 Tomcat 服务器启动端口
server.port=8081
# 当前应用名字
dubbo.application.name=consumer-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 设置超时时间
dubbo.config-center.timeout=10000
4. 编写测试类
@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        userService.buyTicket();
    }

}

16.3.4 测试

1. 启动 ZooKeeper 注册中心

双击运行 zkServer.cmd ,启动 ZooKeeper 注册中心服务

2. 启动 dubbo(也可以不启动)

使用 cmd 运行 dubbo-admin 并登录到 dubbo-admin 可视化监控界面

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
3. 启动 服务提供者 SpringBoot 应用程序

image-20200715190822410

4. 点击 dubbo-admin 服务 查看服务是否注册

image-20200715191343831

5. 运行 服务消费者 的测试类

成功消费了 服务提供者 的服务

image-20200715191523977

这就是 SpringBoot + dubbo + ZooKeeper 实现分布式开发的应用,其实就是一个服务拆分的思想

19. 集成 SpringSecurity

19.1 安全简介

在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修负安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

市面上存在比较有名的安全框架:ShiroSpring Security

这里需要阐述以下的是,每一个框架的出现都是为了解决某一问题而产生的,那么 Spring Security 框架出现是为了解决什么问题呢?

首先,我们看下它的官网介绍:

Spring Security 官网地址: https://spring.io/projects/spring-security

Spring Security is a powerful and highly customizable authentication and access-control framework.It is the de-facto standard for securing Spring-based application.

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它实际是保护基于 Spring 的应用程序的标准。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring 安全性真正强大之处在于它可以轻松地扩展以满足定制需求。

从官网的我介绍中可以知道这是一个权限框架。像我们之前做项目是没有使用框架是怎么控制权限的?对于权限,一般会细分为功能权限,访问权限和菜单权限。代码会写得非常的繁琐,冗余。

怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生。Spring Security 就是其中的一种。

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证一般要求用户提供用户名权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面, Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面, Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List ACL),可以对应用中的领域对象进行细粒度的控制。

19.2 认识 SpringSecurity

Spring Security 是针对 Spring 项目的安全框架,也是 SpringBoot 底层安全模块默认的技术选型,他可以实现强大的 Web 安全检测,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义 Security 策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启 WebSecurity 模式

Spring Security 的两个主要目的是 “认证” 和 “授权” (访问控制)

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份

“授权”(Authorization)

授权发生在系统成功验证您的身份猴,最终会授权您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限

这个概念是通用的,而不是只在 Spring Security 中存在。

19.3 实战测试

19.3.1 环境搭建

  1. 新建一个 SpringBoot 项目,选择 Web 模块和 Thymeleaf 模块。

  2. 编写静态页面

    已经放在百度网盘中了,需要的可以自行下载(来源:b站 狂神说以及其 SpringBoot 视频底下的评论区)

    链接:https://pan.baidu.com/s/1XlISo7vQpiO_DisMkrcbpw
    提取码:q7bd

  3. 编写 Controller

    创建一个 Controller JumpController 进行页面跳转

    @Controller
    public class JumpController {
    
        @RequestMapping({"/","/index"})
        public String index(){
            return "index";
        }
    
        @RequestMapping("/level1/{id}")
        public String level1(@PathVariable("id") Integer id){
            return "views/level1/"+id;
        }
    
        @RequestMapping("/level2/{id}")
        public String level2(@PathVariable("id") Integer id){
            return "views/level2/"+id;
        }
    
        @RequestMapping("/level3/{id}")
        public String level3(@PathVariable("id") Integer id){
            return "views/level3/"+id;
        }
    
    }
    
  4. 测试

启动 SpringBoot 应用程序,测试是否能够成功跳转

19.3.2 认证和授权

  1. 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 编写 Spring Security 配置类

    官方参考文档:https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#servlet-applications

    点进 WebSecurityConfigurerAdapter 这个类,可以看到有关于配置的方法 configure(),他有几个重载的方法,都是用来配置 Spring Security 的

    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
       logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    
       http
          .authorizeRequests()
             .anyRequest().authenticated()
             .and()
          .formLogin().and()
          .httpBasic();
    }
    // @formatter:on
    

    根据源码中的配置,我们自定义我们自己的访问规则

    创建我们自定义的 Spring Security 的配置类 SecurityConfig ,让这个类继承 WebSecurityConfigurerAdapter

    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 自定义请求的授权规则
            // 首页所有人都可以访问
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
            // 设置登录页和登录请求的页面
            http.formLogin().loginProcessingUrl("/user/login").loginPage("/toLogin");
            // 开启注销功能,并设置注销后跳转的路径
            http.logout().logoutSuccessUrl("/");
        }
    }
    
  2. 启动测试:发现除了首页和登录页,其他的都跳去了登录页。因为我们目前没有登录的角色。其他页面的访问请求需要登录的角色拥有对应的权限才可以

    此时我们是无法登录的,因为我们并没有设置登录的账号密码和对应的角色全新啊

  3. 模拟数据库登录认证规则

    根据配置类的注释信息,我们模拟数据库的数据

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 模拟数据库中的数据
        // 现在这些数据是在内存中的,实际开发中我们页可以设置在jdbc中拿
        auth.inMemoryAuthentication().withUser("admin").password"admin").roles("vip1","vip2","vip3");
        auth.inMemoryAuthentication().withUser("xp").password("xp").roles("vip1","vip2");
        auth.inMemoryAuthentication().withUser("guest").password("12345").roles("vip1");
    }
    
  4. 重启 SpringBoot 应用程序

    此时我们会发现我们还是不能登录认证,并且页面是 404

    image-20200720160646480

    这个的原因是因为我们要将前端传来的密码进行某种方式加密,否则就无法登录。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 模拟数据库中的数据
        // 现在这些数据是在内存中的
        // Spring Security 5.0 中新增了多种加密方式,官方推荐使用 bcrypt 加密方式
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("vip1","vip2","vip3");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("xp").password(new BCryptPasswordEncoder().encode("xp")).roles("vip1","vip2");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("guest").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1");
    }
    
  5. 增加密码加密后,重启 SpringBoot 应用程序。登录测试是否能够根据角色访问对应的页面资源

19.3.3 权限控制和注销

  1. 开启自动配置的注销的功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
  //....
  //开启自动配置的注销的功能
  // /logout 注销请求
  http.logout();
}
  1. 我们在前端,index.html 增加一个注销的按钮
<a class="item" th:href="@{/logout}">
	<i class="address card icon"></i> 注销
</a>
  1. 启动 SpringBoot 应用程序测试

    在前端页面点击注销按钮测试是否成功跳转了,默认是请求 `` 跳转到登录页面

    如果没有成功跳转而是跳到了 404 页面,则可能是 csrf 阻止了我们使用 get 方式提交(a标签的 href 属性的跳转是 get 请求的跳转)

    因为 SpringSecurity 默认防止 csrf 跨站请求伪造,因为会产生安全问题。我们可以将请求改为 post 表单提交,或者在 SpringSecurity 中关闭 csrf 功能。关闭 csrf 的代码如下:

    // 关闭 SpringSecurity 的csrf 功能
    http.csrf().disable();
    
  2. 我们现在又来一个需求:用户没有登录的时候,导航栏只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如 某个用户,只有部分权限,那么登录时则只显示这几个功能,其他没有权限的功能不显示。这个就是真实的网站情况了。那我们该如何做呢?

    我们需要结合 thymeleaf 中的一些功能

    sec:authorize = "isAuthenticated()"是否认证登录,来显示不同页面

    需引入 thymeleaf-extras-springsecurity4 依赖

    <!-- thymeleaf-extras-springsecurity4 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    降级 SpringBoot 的版本,thymeleaf-extras-springsecurity4 SpringBoot 2.1.X 以上的版本不支持。

    或者使用 thymeleaf-extras-springsecurity5

    <!-- thymeleaf-extras-springsecurity5 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    :使用 thymeleaf-extras-springsecurity5 时,导入的命名空间是 xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 。使用 thymeleaf-extras-springsecurity4 时为 xmlns:th="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"

  3. 修改前端页面,测试是否达到需求的效果

    登录区域的权限认证

     <!--登录注销-->
    <div class="right menu" sec:authorize="!isAuthenticated()">
        <!--未登录-->
        <a class="item" th:href="@{/toLogin}" >
            <i class="address card icon"></i> 登录
        </a>
        <a class="item" th:href="@{/register}">
            <i class="address card icon"></i> 注册
        </a>
    </div>
    <div class="right menu" sec:authorize="isAuthenticated()">
        <!--未登录-->
        <a class="item" >
            <i class="address card icon"></i>
            用户名:<span sec:authentication="principal.username"></span>
            角色:<span sec:authentication="principal.authorities"></span>
        </a>
        <a class="item" th:href="@{/logout}">
            <i class="address card icon"></i> 注销
        </a>
    </div>
    

    页面的权限认证

       <div class="ui three column stackable grid" >
             <div class="column" sec:authorize="hasRole('vip1')">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 1</h5>
                                <hr>
                                <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                                <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                                <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div class="column" sec:authorize="hasRole('vip2')">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 2</h5>
                                <hr>
                                <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                                <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                                <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div class="column" sec:authorize="hasRole('vip3')">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 3</h5>
                                <hr>
                                <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                                <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                                <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
            </div>
    

:若是没有效果,则降低 SpringBoot 的版本,sec:authorize="isAuthenticated()" 在 SpringBoot 2.1.X 以上版本不再支持了

注: 使用 SpringSecurity 和 Thymeleaf 的包容易出现版本不兼容的问题,所以如果出现不能达到预期效果的时候,先检查自己引入的整合包的网址是否出错了(虽然这个一般不影响),其次是检查自己写的标签是否有问题,比如是否单词拼写错误,是否少了个单词,这些都是我们自以为是正确的,往往可能就是这个出问题了。最后再检查版本的问题,如果使用的整合包是 4 的,则需要降 SpringBoot 的版本,如果是 5 的版本,我之前测试有时候也有问题,但也不知道怎么的就又好了。

20. Shiro

20.1 简介

1. 什么是权限管理

权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户身份认证和授权两部分。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

2. 常见的安全框架

  • Apache Shiro

    官网:http://shiro.apache.org/

    Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证、授权、加密和绘画管理。较轻量级,入门简单,不依赖于 Spring 框架,传统的 SSM 项目使用较多。可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。

  • Spring Security

    官网:https://spring.io/projects/spring-security

    Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。较复杂,入门较难,功能较强,属于 Spring 技术模块,多用于 SpringBoot + SpringCloud 微服务分布式开发。

Shiro 和 Spring Security 比较

  • Shiro 比 Spring Security 更容易使用,实现。
  • Shiro 在 Spring Security 处理密码学方面有一个额外的模块
  • Shiro 不跟任何的框架或者容器绑定,可以独立运行。
  • Spring Security 是 Spring 的明星框架之一,与 Spring 结合较好。
  • Spring Security 有更好的社区支持。

20.2 Shiro 特性

Apache Shiro 是一个具有很多特性的综合性安全框架:

image-20200726125939785

Shiro 的目表是 Shiro 开发团队所称的 “应用程序安全的四个基石” ——身份验证、授权、会话管理和加密。

  • Authentication:身份验证、登录,验证用户是不是拥有响应的身份
  • Authorization:授权,即权限验证。验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作。如:验证某个用户是否拥有某个角色,后者细粒度的验证某个用户对某个资源是否具有某个权限。
  • SessionManager:会话管理,即用户登录猴就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的 JavaSE化境,也可以时 Web 环境。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储

还有一些额外的特性可以在不同的应用程序环境中支持和加强这些关注点,特别是:

  • Web Support:Shiro 的web 支持 API 可以非常容易地继承到 Web 环境
  • Caching:缓存,比如用户登录猴,其用户信息,拥有地角色,权限不必每次去查,这样可以提高效率
  • concurrency:Shrio 支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动地传播过去
  • Testing:提供测试支持
  • Run As:允许一个用户假装为另一个用户(如果他们允许)地身份进行访问
  • Remenber Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话就不用登录了

20.3 Shiro 框架的结构

Authentication(认证),Authorization(授权),Session Management(会话管理),Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。

  • Subject 认证的主体

    Subject:即 “当前操作用户”。但是,在 Shiro 中, Subject 这一概念并不仅仅指人,也可以是第三方进程,后台账户(Daemon Account)或其他类似事务,它仅仅意味着 “当前跟软件交互的东西”。但考虑到大多数目和用途,你可以把它认为是 Shiro 的 “用户” 概念。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作

  • Security Manager 安全管理器

    Security Manager:它是 Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 Security Manager 来管理的内部组件实例,并通过它来提供安全管理的各种服务(相当于我们 SpringMVC 中的 DispatcherServlet)

  • Authenticator 认证器

    Authentication:用户身份识别,通常被称为用户 “登录”。

  • Authorizer 授权管理器

    Authorizer:访问控制。比如某个用户是否具有某个操作的使用权限。

  • SessionManager 会话管理器

    SessionManager:托管 web 容器的绘画,对C/S桌面应用的绘画管理

  • Session DAO 操作会话数据

  • CacheManager 缓存管理器

    CacheManager:用于管理菜单的数据

  • Cryptography 密码加密技术 MD5 MD5盐值

    Cryptography:在对数据源使用加密算法加密的同时,保证易于使用。

  • Realm 领域

    Realm :“领域”的意思。用于实现用户认证和授权的主要组件。可以自定义 Realm

从内部来看:

image-20200724201149059

从外部看:

image-20200726134036538

20.4 环境搭建

  1. 导入依赖

    <dependencies>
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- web 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
    
        <!-- SpringBoot 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- mysql 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    
        <!-- SpringBoot 整合 Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.5.3</version>
        </dependency>
        <!-- 日志门面 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>
        <!-- log4j日志门面 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>
        <!-- log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!-- druid 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.23</version>
        </dependency>
    
        <!-- SpringBoot 测试类 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    
  2. 静态资源过滤

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!-- 静态资源解析 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  3. 编写前端页面和Controller,测试 Thymeleaf 是否成功生效

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
    <h1>首页</h1>
    <div th:text="${msg}"></div>
    <a th:href="@{/user/add}">add</a>|
    <a th:href="@{/user/update}">update</a>
    </body>
    </html>
    

    ShiroController

    @Controller
    public class ShiroController {
    
        @RequestMapping({"/", "/index"})
        public String toIndex(Model model) {
            model.addAttribute("msg", "hello,Shiro");
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String add() {
            return "user/add";
        }
    
        @RequestMapping("/user/update")
        public String update() {
            return "user/update";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin() {
            return "login";
        }
    
        @RequestMapping("/login")
        public String login(String username, String password, Model model) {
            return "login";
        }
    
        @ResponseBody
        @RequestMapping("/noauth")
        public String noAuth(){
            return "未经授权,不能访问";
        }
    
    }
    

    访问 /toLogin ,测试 Thymeleaf 是否成功导入

    image-20200727165739273

  4. 配置数据源和 MyBatis

    这里使用 Druid数据源

    # Spring 配置
    spring:
      # 数据源
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切换成 Druid 数据源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    # mybatis 配置
    mybatis:
      # 设置mapper的位置
      mapper-locations: classpath:mapper/*.xml
      # 取别名
      type-aliases-package: com.xp.model
    
  5. 创建 User 表和 User 实体类

    创建 User 表的 SQL语句

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE if NOT EXISTS `user`(
    	`id` INT(5) PRIMARY KEY NOT NULL auto_increment COMMENT '用户id',
    	`name` VARCHAR(30) NOT NULL COMMENT '用户名',
    	`password` VARCHAR(30) NOT NULL COMMENT '密码',
    	`perms` VARCHAR(20) COMMENT '权限'
    );
    
    INSERT INTO `user` (`name`,`password`,`perms`) VALUES
    ('root','root','user:add'),
    ('xp','xp','user:add'),
    ('test','test','user:update'),
    ('123','123',null)
    

    User

    这里使用了 Lombok 插件

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    public class User {
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        private String perms;
    
    }
    
  6. 编写 UserMapper 和 UserMapper.xml

    UserMapper

    // @Repository 将 mapper 注册到 Spring 容器中
    @Repository
    // @Mapper 将 Mapper 注册到 MyBatis 中
    @Mapper
    public interface UserMapper {
    
        User queryUserByUserName(@Param("userName") String userName);
    
    }
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.xp.mapper.UserMapper">
        
        <select id="queryUserByUserName" resultType="User">
            select * from user where name=#{userName};
        </select>
        
    </mapper>
    

    写完 UserMapper 后,我们先测试是否能成功获取数据

    @SpringBootTest
    class SpringbootShiroApplicationTests {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void contextLoads() {
            User user = UserMapper.queryUserByUserName("test");
            System.out.println(user);
        }
    
    }
    

    如果测试没有问题,则编写 service 层

    UserService

    public interface UserService {
    
        User queryUserByUserName(String userName);
    
    }
    

    UserServiceImpl

    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public User queryUserByUserName(String userName) {
            return userMapper.queryUserByUserName(userName);
        }
    }
    
  7. 自定义 Realm

    在 config 包下创建我们自定义的 Realm ——MyRealm,并让其继承 AuthorizingRealm,实现继承的方法

    /**
     * 自定义 realm 继承 AuthorizingRealm
     *
     * @author xp
     */
    public class MyRealm extends AuthorizingRealm {
    
        // 自动注入我们刚刚的写好的 service
        @Autowired
        private UserService userService;
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行了=>授权");
            return null;
        }
    
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("执行了=>认证");
            return null;
        }
    }
    
  8. 配置 ShiroConfig

    我们先创建一个类,名字叫做 ShiroConfig ,并添加 @AutoConfiguration 注解标明这个是一个 Spring 的配置类

    @Configuration
    public class ShiroConfig {
    
        // ShiroFilterFactoryBean
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
            ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
            // 设置安全管理器
            filterFactoryBean.setSecurityManager(securityManager);
    
            // 添加 shiro 的内置过滤器
            /*
                anno:无许认证就可以访问
                authc:必须认证了才能访问
                user:必须拥有记住我功能才能访问
                perms:拥有对某个资源的权限才能访问
                role:拥有某个角色权限才能访问
             */
            Map<String, String> filterChainMap = new LinkedHashMap<>();
            filterChainMap.put("/user/add","authc");
            filterChainMap.put("/user/update","authc");
            filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    
            // 未授权跳转到指定页面
            filterFactoryBean.setUnauthorizedUrl("/noauth");
    
            // 设置拦截后跳转到登录页面
            filterFactoryBean.setLoginUrl("/toLogin");
    
            return filterFactoryBean;
        }
    
        // DefaultWebSecurityManager
        @Bean(name = "securityManager")
        public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("myRealm") MyRealm myRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 关联 Realm
            securityManager.setRealm(myRealm);
            return securityManager;
        }
    
        // 创建 realm 对象,需要自定义类
        @Bean
        public MyRealm myRealm(){
            return new MyRealm();
        }
    
    }
    
  9. 启动 SpringBoot 程序进行测试

    我们会发现,我们访问首页时跳转到了登录页面,那就说明 Shiro 的安全拦截已经生效了。

    而我们如何才能够进入首页呢?

20.5 认证

我们配置 Shiro 后,想要进入首页,那么就必须通过 Shiro 的认证。

  1. 在 Controller 中获取我们登录时的账号和密码

    修改我们的 /login 请求的方法

    在 Shiro 中,Subject 对象代表当前操作用户(Subject 不仅仅指人,也可以是第三方进程,后台账户(Daemon Account)或其他类似事务,我们这里是需要拿到当前操作的用户信息)。

    我们要拿到 Subject 对象,则需要从 SecurityUtils 这个工具类中获取(因为 Subject 的构造方法是私有的)。

    token 的意思是令牌,我们这里将用户输入的账号密码封装进令牌里,然后进行认证。

    @RequestMapping("/login")
    public String login(String username, String password, Model model) {
        // 获取当前用户
        Subject subject = SecurityUtils.getSubject();
        // 封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行登录操作
        try {
            subject.login(token);
            return "index";
            // UnknownAccountException 用户名错误的异常
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用户名错误");
            e.printStackTrace();
            return "login";
            // IncorrectCredentialsException 密码错误的异常
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误");
            e.printStackTrace();
            return "login";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "login";
        }
    }
    
  2. 认证

    我们自己定义的 Realm 是实现了 AuthorizingRealm 的方法。其中 doGetAuthenticationInfo() 方法就是用来认证的(:授权和认证的方法不要认错了,因为授权和认证的英语单词非常像,认证是 Authentication)。

        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("执行了=》认证");
    
            // 获取我们前面封装好的令牌
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            // 数据库中查询用户信息
            User user = userService.queryUserByUserName(token.getUsername());
            // 若用户名不存在于数据库中
            if (user == null) {
                // return null 抛出 UnknownAccountException 异常
                return null;
            }
    
            // 密码认证交给 Shiro 做, shiro 会对密码进行加密
            return new SimpleAuthenticationInfo("", user.getPassword(), "");
        }
    
  3. 启动 SpringBoot 应用程序进行测试

    我们可以发现,登录完后,可以访问自己想访问的页面了。

    那么又有一个问题了,我们真的可以让用户想访问什么页面就访问什么页面吗?

20.6 授权

答案当然是否定的,对于一些特殊的页面,我们是不希望用户能够访问的。

那么我们该如何给用户增加限制呢?

在以前,我们可以自己使用 Filter 进行用户权限资源的拦截,但是我们写得并不是太好,而且也很复杂。

Shiro 就提供了比较方便的用户权限资源拦截的方法。

  1. 给页面资源增加权限限制

    修改我们之前写的 Shrio 配置类 ShrioConfig 中的 shiroFilterFactoryBean() 方法

    // ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        filterFactoryBean.setSecurityManager(securityManager);
    
        // 添加 shiro 的内置过滤器
        /*
            anno:无须认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有记住我功能才能访问
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
         */
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        filterChainMap.put("/user/add","perms[user:add]");
        filterChainMap.put("/user/update","perms[user:update]");
        filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    
        // 未授权跳转到指定页面
        filterFactoryBean.setUnauthorizedUrl("/noauth");
    
        // 设置拦截后跳转到登录页面
        filterFactoryBean.setLoginUrl("/toLogin");
    
    
        return filterFactoryBean;
    }
    

    ShrioFilterFactoryBean 中提供让我们自定义资源拦截的方式。

    anno:无须认证就可以可以访问

    authc:必须认证了才能访问

    user:必须拥有记住我功能才能访问

    perms:拥有某个资源的权限才能访问

    role:拥有某个角色权限才能访问

    在这里,我们定义这些资源的访问方式为 perms,即必须拥有特定字符串才允许访问我们的资源

  2. 授权

    用户登录后,用户对象是在认证方法中的,所以我们需要将认证方法中的登录对象进行授权。

    Shiro 也提供给了我们传递这个用户对象的方法。

    在刚刚的认证中,我们把密码交给 Shrio 进行验证,此时也可以传递用户对象过去。

    // 密码认证交给 Shiro 做, shiro 进行密码加密
    // 这里的 user 就是刚刚查询数据库后封装的 user
    return new SimpleAuthenticationInfo(user, user.getPassword(), "");
    

    然后,我们将我们自定义的 Realm 实现的 doGetAuthorizationInfo() 方法进行修改。

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了=》授权");
    
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 获取当前用户,也就是刚刚我们认证后通过 SimpleAuthenticationInfo 传过来的user
        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
    
        // 给用户添加访问资源的标志字符串
        info.addStringPermission(user.getPerms());
    
        return info;
    }
    
  3. 启动 SpringBoot 应用程序,测试

    发现我们拥有 user:update 的用户可以访问 update 页面,拥有 user:add 的用户可以访问 add 页面

20.7 整合 Thymeleaf

  1. 引入依赖

    跟 SpringSecurity 一样,由于 SpringBoot 对JSP的支持不太好,而没有了 JSP 作为模板引擎,我们需要使用新的模板引擎代替。这里我们采用 Thymeleaf 模板引擎代替。

    <!-- thymeleaf-extras-shiro -->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    
  2. 注入 ShiroDialect

    在 Shiro 配置类 ShiroConfig 中注入 ShiroDialect,使 Thymeleaf 和我们的 Shiro 进行整合。

    // 整合 ThymeleafDialect Thymeleaf 方言
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
    
  3. 修改前端页面

    在我们的实际场景中,我们还应该让没有该资源访问权限的用户不能查看到对应资源的跳转链接,所以我们使用 Thymeleaf 作为模板引擎,根据用户的资源权限进行显示页面。

    修改我们之前写得前端页面

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
    <h1>首页</h1>
    <div th:text="${msg}"></div>
    <shiro:guest>
        <a th:href="@{/toLogin}">登录</a>
    </shiro:guest>
    <shiro:authenticated>
        <!-- 这里的 principal 标签,poerperty="name" 相当于 ((User)Subject.getPrincipals()).getName,如果时 useName则相当于 后面是 .getUserName() -->
        用户名:<shiro:principal property="name"/>
    </shiro:authenticated>
    
    <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">add</a>
    </div>
    <div shiro:hasPermission="user:update">
        <a th:href="@{/user/update}">update</a>
    </div>
    </body>
    </html>
    

    到这里,我们的 SpringBoot 集成 Shiro 就完成了,接下来再补充一点 Shiro 和 Thymeleaf 标签

Thymeleaf Shiro 标签

  • guest 标签

    <!-- 用户没有身份验证时显示对应信息,即游客访问信息 -->
    <shiro:guset></shiro:guest>
    
  • user 标签

    <!-- 用户已经身份验证/记住我登录后显示响应的信息 -->
    <shiro:user></shiro:user>
    
  • authenticated 标签

    <!-- 用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的 -->
    <shiro:authenticated></shiro:authenticated>
    
  • notAuthenticated 标签

    <!-- 用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证,也就是说不在登录状态时 -->
    <shiro:notAuthenticated></shiro:notAuthenticated>
    
  • principal 标签

    <!-- 显示用户登录信息 这里的property="name" 相当于 ((User)Subject.getPrincipals()).getName,如果porperty="userName" 则相当于刚刚后面的变成 .getUserName() -->
    <shiro:principal property="name"></shiro:principal>
    
  • hasPermission 标签

    <!-- 如果当前 Subject 有权限将显示 body 体内容,这里是如果有 user:add 这个权限,将不显示这个标签里的内容 -->
    <shiro:hasPermission name="user:add"></shiro:hasPermission>
    
  • lacksPermission 标签

    <!-- 如果当前 Subject 没有权限将显示 body 体内容,这里是如果没有 user:add 这个权限,将不显示这个标签里的内容 -->
    <shiro:lacksPermission name="user:add"></shiro:lacksPermission>
    
  • hasRole 标签

    <!-- 如果当前 Subjecct 有角色将显示 body 体内容,这里是如果有admin角色,则显示标签里的内容 -->
    <shiro:hasRole name="admin"></shiro:hasRole>
    
  • hasAnyRole 标签

    <!-- 如果当前 Subject 有任意一个角色,将显示 body 体内容,这里是只要有admin或user角色中的其中一个角色,则显示 body 体的内容 -->
    <shiro:hasAnyRole name="admin,user"></shiro:hasAnyRole>
    
  • lacksRole 标签

    <!-- 如果当前 Subject 没有角色将显示 body 体内容,这里表示只要没有admin这个角色,则显示标签体里面的内容 -->
    <shiro:lacksRole name="admin"></shiro:lacksRole>
    
posted @ 2020-07-28 13:30  Windows_XP  阅读(791)  评论(0编辑  收藏  举报