狂神说学习笔记:SpringBoot

SpringBoot

1、SpringBoot简介

1.1、回顾什么是Spring

Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。

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

1.2、Spring是如何简化Java开发的

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

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

1.3、什么是SpringBoot

学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;

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

所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。

是的这就是Java企业级应用 -> J2EE -> spring -> springboot的过程。

随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;

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

简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。

Spring Boot的主要优点:

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

2、第一个SpringBoor程序

准备工作

我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

官方提供了一个快速生成的网站,IDEA集成了这样网站

2.1、项目创建方式

2.1.1、项目创建方式一:使用 Spring Initializr 的 Web 页面创建项目

Spring Initializr:https://start.spring.io/

1、打开 https://start.spring.io/

2、填写项目信息,添加依赖 Spring Web

3、点击”Generate Project“按钮生成项目;下载此项目

4、解压项目包,并用 IDEA 以 Maven 项目导入,一路下一步即可,直到项目导入完毕。

5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。

image-20220501125849125

2.1.2、项目创建方式二:使用 IDEA 直接创建项目

1、创建一个新项目

2、选择 spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现

3、填写项目信息

4、选择初始化的组件(初学勾选 Web 即可)

5、填写项目路径

6、等待项目构建成功

2.1.3、项目结构分析

通过上面步骤完成了基础项目的创建。就会自动生成以下文件。

1、程序的主启动类

2、一个 application.properties 配置文件

3、一个 测试类

4、一个 pom.xml

2.2、项目创建详情(IDEA快速创建)

2.2.1、项目创建过程

  1. 选择Spring Initializr,输入项目详细信息

    image-20220501165202137

  2. 选择web支持(先不选)

    image-20220501164851913

  3. 删除多余文件

    image-20220501165412682

  4. 在pom.xml中配置

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

    用来支持web

2.2.2、更改项目的端口号

  • 在application.properties

    # 更改项目端口号
    server.port=8081
    

2.3、项目编写

2.3.1、编写流程

  1. 在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到

  2. 在包中新建一个HelloController类

    @RestController
    @RequestMapping("/hello")
    public class HelloController {
    
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return "Hello";
        }
    
    }
    
  3. 编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!

    image-20220501172637036

2.3.2、打包发布

将项目打成jar包,点击 maven的 package·

image-20220501201320898

如果打包成功,则会在target目录下生成一个 jar 包

  • 成功提示

    image-20220501201604345

  • 打包好的路径

    image-20220501201708376

打成了jar包后,在存放路径处 shift+鼠标右键 用 Powershell 窗口 运行以下命令

# 输入包名前几个字母,按Tab键能补全包名
java -jar 包名.jar

image-20220501202209735

2.3.3、彩蛋——修改控制台Banner样式

如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案

SpringBoot Banner在线生成工具:https://www.bootschool.net/ascii

  1. 在 resources 下创建 banner.txt,在其中添加想要的样式,例如:

                               .===;========.__,                 
                               (\__)___________|                 
         L__________________,--,--/ /-,-,-\ \-,   ________       
    =====)o o o o ======== )) ____,===,___""" "7_/_,_,_,_,'---,-,
         `--._,_,_,-,--,--'' (____| _ \___\oo ; ; ; ; ; ;_____ T|
                  `-'--'-/_,-------| ) ___--,__,------._ \__  |I|
                           \==----/   \\ )\--\_         `-._`-'I|
                           /=[JW]/     `"==.- -\            `-.L|
                          /==---/           \- -\                
                          '-.__/             \__7                
    
  2. 当文件图标下面出现角标(如图所示),表示已经被应用

    image-20220501182336088

  3. 应用效果

    image-20220501182323622

2.4、运行原理初探

2.4.1、pom.xml

父依赖

其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!

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

点进去,发现还有一个父依赖

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

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

<artifactId>spring-boot-dependencies</artifactId>
  • spring-boot-dependencies:核心依赖在父工程中
    • spring-boot-dependencies 中管理依赖的版本,所以在引入 SpringBoot 依赖的时候不需要指定版本
  • spring-boot-starter-parent
    • spring-boot-starter-parent 中配置资源过滤器
spring-boot-starter 启动器
<!--启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
  • 启动器:就是 SpringBoot 的启动场景
    • springboot-boot-starter-xxx 就是SpringBoot的场景启动器
  • 比如 spring-boot-starter-web,会自动导入web环境所有的依赖
  • springboot会将所有的功能场景,变成一个一个的启动器
  • 需要使用什么功能,就需要找到对应的启动器

官网starter参考:https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

2.4.2、主启动类

默认的主启动类
//@SpringBootApplication : 标注这个类是一个springboot的应用,启动类下的所有资源被导入
@SpringBootApplication
public class Springboot01HelloworldApplication {

    public static void main(String[] args) {
        //将springboot应用启动
        SpringApplication.run(Springboot01HelloworldApplication.class, args);
    }

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

    • @SpringBootConfiguration:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类

      • @Configuration:说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件
        • @Component :说明启动类本身也是Spring中的一个组件而已,负责启动应用
    • @ComponentScan:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中

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

      • @AutoConfigurationPackage:自动配置包

        • @Import({Registrar.class}):给容器导入注册组件
          • Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器
      • @Import({AutoConfigurationImportSelector.class}):给容器导入选择器组件

        • AutoConfigurationImportSelector :自动配置导入选择器

          1. 此类中有个获取所有的配置的方法

            //获取候选配置
            protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
                //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
                List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
            }
            
            //上面的 getSpringFactoriesLoaderFactoryClass 就是下面这个
            protected Class<?> getSpringFactoriesLoaderFactoryClass() {
                //返回给注解类 EnableAutoConfiguration
                return EnableAutoConfiguration.class;
            }
            

            META-INF/spring.factories:自动配置的核心文件

            image-20220501214754474

          2. 这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法

            public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
                ClassLoader classLoaderToUse = classLoader;
                if (classLoader == null) {
                    classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
                }
            
                String factoryTypeName = factoryType.getName();
                //这里调用了loadSpringFactories方法
                return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
            }
            
          3. 继续点击查看 loadSpringFactories 方法

            private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
                //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
                Map<String, List<String>> result = (Map)cache.get(classLoader);
                if (result != null) {
                    return result;
                } else {
                    HashMap result = new HashMap();
            
                    try {
                        //去获取一个资源 "META-INF/spring.factories"
                        Enumeration urls = classLoader.getResources("META-INF/spring.factories");
            
                        //将读取到的资源遍历,封装成为一个Properties
                        while(urls.hasMoreElements()) {
                            URL url = (URL)urls.nextElement();
                            UrlResource resource = new UrlResource(url);
                            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                            Iterator var6 = properties.entrySet().iterator();
            
                            while(var6.hasNext()) {
                                Entry<?, ?> entry = (Entry)var6.next();
                                String factoryTypeName = ((String)entry.getKey()).trim();
                                String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                                String[] var10 = factoryImplementationNames;
                                int var11 = factoryImplementationNames.length;
            
                                for(int var12 = 0; var12 < var11; ++var12) {
                                    String factoryImplementationName = var10[var12];
                                    ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                        return new ArrayList();
                                    })).add(factoryImplementationName.trim());
                                }
                            }
                        }
            
                        result.replaceAll((factoryType, implementations) -> {
                            return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                        });
                        cache.put(classLoader, result);
                        return result;
                    } catch (IOException var14) {
                        throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
                    }
                }
            }
            
  • 自动配置原理分析图解:

    image-20220501224221310

    image-20220501235817074

spring.factories

我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!

image-20220501214754474

我们在上面的自动配置类随便找一个打开看看,比如 :AopAutoConfiguration

image-20220501235312332

可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!

所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。

结论

  • SpringBoot所有的自动配置都是在启动的时候扫描并加载:spring.factories
    • 所有的自动配置类都在这里面,但不一定自动生效,要判断条件是否成立,只要导入对应的start,就有对应的启动器,有了启动器,自动装配就会生效,然后就配置成功了
  1. SpringBoot 在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值
  2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作
  3. 以前我们需要自动配置的东西,现在 springboot 帮我们做了
  4. 整个 JavaEE 的整体解决方案和自动配置都在 springboot-autoconfigure 的jar包中
  5. 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
  6. 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件
  7. 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作

2.4.3、SpringApplication

Run

最初以为是一个main方法,其实是开启了一个服务

@SpringBootApplication
public class Springboot01HelloworldApplication {

    public static void main(String[] args) {
        //将springboot应用启动
        //参数一:应用入口的类	参数二:命令行参数
        SpringApplication.run(Springboot01HelloworldApplication.class, args);
    }

}

SpringApplication.run分析

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

SpringApplication

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

  1. 推断应用的类型是普通的项目还是Web项目

  2. 查找并加载所有可用初始化器 , 设置到initializers属性中

  3. 找出所有的应用程序监听器,设置到listeners属性中

  4. 推断并设置main方法的定义类,找到运行的主类

查看构造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ...
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}
run方法流程分析

640

3、YAML语法

3.1、概述

3.1.1、配置文件

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

  • application.properties
    • 语法结构 :key=value
  • application.yml
    • 语法结构 :key:空格 value

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

3.1.2、YAML概述

YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)

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

YAML is not Markup Language:不是一个标记语言

这种语言以数据作为中心,而不是以标记语言为重点!

以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml

yaml配置:

server:
  prot: 8080

xml配置:

<server>
    <port>8081<port>
</server>
YAML语法

基础语法:

k:(空格) v

以此来表示一对键值对(空格不能省略);以空格的缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的

注意:属性和值的大小写都是十分敏感的。例子:

server:
  port: 8081
  path: /hello
值的写法

字面量:普通的值【数字,布尔值,字符串】

k: v

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

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

比如:name: "D \n t" 输出:D 换行 t

对象的写法
# 普通的写法
student:
  name: Dt
  age: 18
# 对象的行内写法
student: {name: Dt, age: 18}
数组的写法
# 普通的写法
pets:
  - cat
  - dog
  - pig
# 数组的行内写法
pets: [cat,dog,pig]

相比 yamlproperties 只能保存键值对

语法要求严格!

  • 空格不能省略
  • 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的
  • 属性和值的大小写都是十分敏感的

3.2、yaml给实体类赋值

  1. 创建 Person 和 Dog 实体类

    Dog.java

    @Component
    public class Dog {
        private String name;
        private Integer age;
    
        public Dog() {
        }
    
        public Dog(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 "Dog{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    

    Person.java

    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private Integer age;
        private Boolean happy;
        private Date birth;
        private Map<String, Object> maps;
        private List<Object> lists;
        private Dog dog;
    
        public Person() {
        }
    
        public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps,
                      List<Object> lists, Dog dog) {
            this.name = name;
            this.age = age;
            this.happy = happy;
            this.birth = birth;
            this.maps = maps;
            this.lists = lists;
            this.dog = dog;
        }
    
        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;
        }
    
        public Boolean getHappy() {
            return happy;
        }
    
        public void setHappy(Boolean happy) {
            this.happy = happy;
        }
    
        public Date getBirth() {
            return birth;
        }
    
        public void setBirth(Date birth) {
            this.birth = birth;
        }
    
        public Map<String, Object> getMaps() {
            return maps;
        }
    
        public void setMaps(Map<String, Object> maps) {
            this.maps = maps;
        }
    
        public List<Object> getLists() {
            return lists;
        }
    
        public void setLists(List<Object> lists) {
            this.lists = lists;
        }
    
        public Dog getDog() {
            return dog;
        }
    
        public void setDog(Dog dog) {
            this.dog = dog;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", happy=" + happy +
                    ", birth=" + birth +
                    ", maps=" + maps +
                    ", lists=" + lists +
                    ", dog=" + dog +
                    '}';
        }
    }
    
  2. 利用 YAML 注入

    person:
      name: Dt
      age: 3
      happy: false
      birth: 2022/05/02
      maps: {k1: v1, k2: v2}
      list:
        - code
        - music
        - girl
      dog:
        name: 旺财
        age: 3
    
  3. springboot配置注解处理器没有找到

    image-20220502132857997

    解决办法:使用 @ConfigurationProperties 需要导入依赖

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

    @SpringBootTest
    class Springboot02ConfigApplicationTests {
        @Autowired
        private Person person;
    
        @Test
        void contextLoads() {
            System.out.println(person);
        }
    
    }
    
  5. 结果:所有值都注入成功

    image-20220502133750434

3.2.1、配置文件占位符

  • 配置文件还可以编写占位符生成随机数
person:
  name: Dt${random.uuid}
  age: ${random.int}
  happy: false
  birth: 2022/05/02
  maps: {k1: v1, k2: v2}
  lists:
    - code
    - music
    - girl
  dog:
    name: ${person.hello:hhh}_旺财
    age: 3
  • ${random.uuid}:表示随机UUID
  • ${random.int}:表示随机整数
  • ${person.hello:hhh}:表示 person 中的 hello 如果不存在就取默认值 hhh,存在就取存在的值

3.3、回顾properties配置

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

那我们来唠唠其他的实现方式,道理都是相同的;

配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!

  • 【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8

    settings-->FileEncodings 中配置;

    image-20220304154535660

  1. 新建一个实体类 User

    @Component
    public class User {
        private String name;
        private Integer age;
        private String gender;
        
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender='" + gender + '\'' +
                    '}';
        }
    }
    
  2. 编辑配置文件 user.properties

    name=Dt
    age=18
    gender=male
    
  3. 我们在User类上使用 @PropertySource 指定配置文件,@Value 来进行注入

    @Component
    @PropertySource(value = "classpath:user.properties") //加载指定配置文件
    public class User {
        @Value("${name}") //占位符
        private String name;
        @Value("${age}")
        private Integer age;
        @Value("${gender}")
        private String gender;
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender='" + gender + '\'' +
                    '}';
        }
    }
    
  4. Springboot测试

    @SpringBootTest
    class Springboot02ConfigApplicationTests {
        @Autowired
        private User user;
    
        @Test
        void contextLoads() {
            System.out.println(user);
        }
    
    }
    
  5. 结果:注入成功

    image-20220502141125994

3.4、@ConfigurationProperties 与 @Value 对比

@Value 这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;

我们来看个功能对比图

image-20220304154748684

  • @ConfigurationProperties 只需要写一次即可 , @Value 则需要每个字段都添加
  • 松散绑定(Relaxed binding)
    比如例子 user: userName 也可以表示为 user: user-name(大写用-符号),user: username 表示为 user: user_name(小写用_符号),比如支持这种写法的就是支持松散绑定
  • JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
  • 复杂类型封装,yaml 中可以封装对象 , 使用 value 就不支持

结论

配置yml和配置properties都可以获取到值 , 强烈推荐 yml;

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

如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!

4、JSR303校验及多环境切换

4.1、JSR303校验

Springboot中可以用 @Validated 来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。

  1. 使用 @Validated需要导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 我们这里来写个注解让我们的 name 只能支持Email格式

    @Component
    @ConfigurationProperties(prefix = "person")
    @Validated  //数据校验
    public class Person {
    
        @Email(message = "邮箱格式错误") //校验不通过的提示
        private String name;
        
    }
    
  3. 结果:

    image-20220503085206877

使用数据校验,可以保证数据的正确性;

常见参数

@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;

空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.
    
Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  
    
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) string is between min and max included.

日期检查
@Past       验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

.......等等
除此以外,我们还可以自定义一些数据校验规则

4.2、多环境切换

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

4.2.1、配置文档加载位置

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

image-20220503093248593

  • 五种配置文件位置及运行优先级(低->高)

    • classpath:/
    • classpath:/config/
    • file:./
    • file:/config/
    • file:./cofig/*/
  • springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:

    优先级1:项目路径下的config文件夹的直接子目录配置文件
    优先级2:项目路径下的config文件夹配置文件
    优先级3:项目路径下配置文件
    优先级4:资源路径下的config文件夹配置文件
    优先级5:资源路径下配置文件
    

    image-20220503095002326

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

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

4.2.2、多配置文件

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

例如:

  • application-test.properties 代表测试环境配置
  • application-dev.properties 代表开发环境配置

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

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

# springboot 的多环境配置:可以选择激活哪一个配置文件
spring.profiles.active=test

相比 properties 需要写多个 properties文件,yaml 可以实现多文档模块

4.2.3、yaml多文档模块

和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了

server:
  port: 8081
spring:
  profiles:
    active: dev # 选择要激活dev环境块
---
server:
  port: 8082
spring:
  profiles: dev # 配置环境的名称
---
server:
  port: 8083
spring:
  profiles: test # 配置环境的名称

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

5、自动装配原理

5.1、自动装配原理详解

5.1.1、概述

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

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类
原理流程
  1. 我们在 spring.factories里随便点击一个自动装配类看它的源码,例如 HttpEncodingAutoConfiguration

    image-20220503151807071

  2. 查看源码,我们发现@EnableConfigurationProperties是从ServerProperties属性封装类中读取的属性来装配(结合下面的构造器引用的也是ServerProperties

    image-20220503151930630

  3. 我们再点进ServerProperties的源码,我们可以发现这里的原理和yaml给实体类赋值(点击跳转)中给实体类赋值的方式一致:读取配置文件中前缀为 server 的赋值属性

    image-20220503152516539

  4. 通过对比我们可以确认结论正确

    image-20220503153245501

这个就是自动装配的原理,也就是为什么我们在 propertiesyaml 配置文件中配置能生效的原因

B站弹幕总结:自动配置类的配置本质就是配置组件,而自动配置类会依赖properties(这其中有很多属性),在yml文件写上自己想要的属性就好

5.1.2、精髓

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

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

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

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

  • xxxxAutoConfigurartion:自动配置类;给容器中添加组件
  • xxxxProperties:封装配置文件中相关属性;

5.2、@Conditional

了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;

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

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

image-20220304172107032

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

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

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

application.yaml

#开启springboot的调试类
debug: true

开启后就可以在控制台查看日志了。

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

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

  • Unconditional classes: (没有条件的类)

6、SpringBoot Web开发

核心:自动装配

springboot到底帮我们配置了什么?我们能不能进行修改?能修改哪些东西?能不能扩展?

  • xxxxAutoConfiguration.. 向容器中自动配置组件
  • xxxxProperties:自动配置类,装配配置文件中自定义的一些内容

6.1、Web开发静态资源处理

6.1.1、web开发要解决的问题

  • 导入静态资源
  • 首页
  • jsp,模板引擎Thymeleaf
  • 装配扩展SpringMVC
  • 增删改查
  • 拦截器
  • 国际化

6.1.2、静态资源映射规则

首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!

写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?

如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!

我们先来聊聊这个静态资源映射规则:

SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;

我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

有一个方法:addResourceHandlers 添加资源处理

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
       // 已禁用默认资源处理
      logger.debug("Default resource handling disabled");
      return;
   }
    // webjars 配置
   addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    // 静态资源配置
   addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
      registration.addResourceLocations(this.resourceProperties.getStaticLocations());
      if (this.servletContext != null) {
         ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
         registration.addResourceLocations(resource);
      }
   });
}

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
	addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
}

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
		Consumer<ResourceHandlerRegistration> customizer) {
	if (registry.hasMappingForPattern(pattern)) {
		return;
	}
	ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
	customizer.accept(registration);
    //缓存控制
	registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
	registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
	registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
	customizeResourceHandlerRegistration(registration);
}

读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;

6.1.3、什么是webjars

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

使用SpringBoot需要使用Webjars,我们可以去搜索一下:

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

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

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

image-20220504195635404

6.1.4、第二种静态资源映射规律

  1. 在springboot,我们可以使用一下方式处理静态数据

    • webjars localhost:8081/webjars/
    • public,static,/**,resources localhost:8081/
  2. 优先级:resources>static(默认)>public

    优先级和源码里的数组顺序一致:

    image-20220504201249844

6.1.5、自定义静态资源路径

我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;

spring.web.resources.static-locations=/hello/,classpath:/dt/

一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!

6.1.6、拓展:网站图标说明(了解即可)

与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。

1、关闭SpringBoot默认图标

#关闭默认图标
spring.mvc.favicon.enabled=false

2、自己放一个图标在静态资源目录下,我放在 public 目录下

3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!

注:maven pom.xml中的spring-boot-starter-parent的版本要低才行,已经被淘汰(了解即可)

新版本的 springboot 直接把图标命名为 favicon.ico 放在 classpath:/static/下就可以了

注意:favicon.ico 尺寸不能太大

6.2、模板引擎Thymeleaf

6.2.1、模板引擎介绍

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

jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的

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

SpringBoot推荐你可以来使用模板引擎:

模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:

image-20220305094354752

模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。

我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。

6.2.2、引入Thymeleaf

怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:

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

Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf

Spring官方文档:找到我们对应的版本

https://docs.spring.io/spring-boot/docs/2.6.7/reference/htmlsingle/#using.build-systems.starters

找到对应的pom依赖:可以适当点进源码看下本来的包!

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

Maven会自动下载jar包,我们可以去看下下载的东西;

image-20220504214739750

6.2.3、测试Thymeleaf

通过查看ThymeleafProperties的源码,我们了解到 html 文件要存放在 templates

image-20220504215902815

测试

  1. 编写一个indexController

    @Controller
    public class IndexController {
    
        @RequestMapping("/test")
        public String index(Model model) {
            model.addAttribute("msg", "Hello, Test");
            return "test";
        }
    
    }
    
  2. 编写一个测试页面 index.html 放在 templates 目录下(使用thymeleaf需要导入约束:xmlns:th="http://www.thymeleaf.org"

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <!--所有的html元素都可以被 thymeleaf 替换接管: th:元素名-->
    <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
    <div th:text="${msg}"></div>
    
    </body>
    </html>
    
  3. 启动项目请求测试

    image-20220504222233890

6.2.4、Thymeleaf的使用语法

  1. 我们可以使用任意的 th:attr 来替换Html中原生属性的值!

    image-20220305094921146

  2. 我们能写哪些表达式呢?

    Simple expressions:(表达式语法)
    Variable Expressions: ${...}:获取变量值;OGNL;
        1)、获取对象的属性、调用方法
        2)、使用内置的基本对象:#18
             #ctx : the context object.
             #vars: the context variables.
             #locale : the context locale.
             #request : (only in Web Contexts) the HttpServletRequest object.
             #response : (only in Web Contexts) the HttpServletResponse object.
             #session : (only in Web Contexts) the HttpSession object.
             #servletContext : (only in Web Contexts) the ServletContext object.
    
        3)、内置的一些工具对象:
          #execInfo : information about the template being processed.
          #uris : methods for escaping parts of URLs/URIs
          #conversions : methods for executing the configured conversion service (if any).
          #dates : methods for java.util.Date objects: formatting, component extraction, etc.
          #calendars : analogous to #dates , but for java.util.Calendar objects.
          #numbers : methods for formatting numeric objects.
          #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
          #objects : methods for objects in general.
          #bools : methods for boolean evaluation.
          #arrays : methods for arrays.
          #lists : methods for lists.
          #sets : methods for sets.
          #maps : methods for maps.
          #aggregates : methods for creating aggregates on arrays or collections.
    ==================================================================================
    
      Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
      Message Expressions: #{...}:获取国际化内容
      Link URL Expressions: @{...}:定义URL;
      Fragment Expressions: ~{...}:片段引用表达式
    
    Literals(字面量)
          Text literals: 'one text' , 'Another one!' ,…
          Number literals: 0 , 34 , 3.0 , 12.3 ,…
          Boolean literals: true , false
          Null literal: null
          Literal tokens: one , sometext , main ,…
          
    Text operations:(文本操作)
        String concatenation: +
        Literal substitutions: |The name is ${name}|
        
    Arithmetic operations:(数学运算)
        Binary operators: + , - , * , / , %
        Minus sign (unary operator): -
        
    Boolean operations:(布尔运算)
        Binary operators: and , or
        Boolean negation (unary operator): ! , not
        
    Comparisons and equality:(比较运算)
        Comparators: > , < , >= , <= ( gt , lt , ge , le )
        Equality operators: == , != ( eq , ne )
        
    Conditional operators:条件运算(三元运算符)
        If-then: (if) ? (then)
        If-then-else: (if) ? (then) : (else)
        Default: (value) ?: (defaultvalue)
        
    Special tokens:
        No-Operation: _
    

6.2.5、Thymeleaf语法测试

  1. indexController

    @Controller
    public class IndexController {
    
        @RequestMapping("/test")
        public String index(Model model) {
            model.addAttribute("msg", "<h1>Hello, Test</h1>");
            model.addAttribute("users", Arrays.asList("dt","ky"));
            return "test";
        }
    
    }
    
  2. index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <!--所有的html元素都可以被 thymeleaf 替换接管: th:元素名-->
    <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
    <div th:text="${msg}"></div>
    <!--不转义-->
    <div th:utext="${msg}"></div>
    
    <hr>
    
    <!--遍历数据-->
    <!--th:each每次遍历都会生成当前这个标签:官网 #9Local Variables-->
    <h3 th:each="user:${users}" th:text="${user}"></h3>
    
    <!--行内写法:官网 #12Inlining-->
    <span th:each="user:${users}">[[ ${user} ]]</span>
    
    </body>
    </html>
    
  3. 启动项目测试!

    image-20220504224116555

我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!

6.3、MVC配置原理

学习网站:狂神说SpringBoot12:MVC自动配置原理 (qq.com)

在springboot中,有非常多的xxxx Configuration 帮助我们进行扩展配置,只要看到了这个定性,我们就要注意了!

6.3.1、官网阅读

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

只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!

地址:https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.spring-mvc.auto-configuration

Spring MVC Auto-configuration

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

-- Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作

The auto-configuration adds the following features on top of Spring’s defaults:

-- 自动配置在 Spring 默认设置的基础上增加了以下特性:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans

    包含视图解析器

  • Support for serving static resources, including support for WebJars

    支持静态资源,包括 webjars

  • Automatic registration of Converter, GenericConverter, and Formatter beans

    自动注册转换器,通用转换器,格式化器

    • 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
    • 格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象
  • Support for HttpMessageConverters

    支持网页信息转换器

    • pringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串
  • Automatic registration of MessageCodesResolver

    定义错误代码生成规则的

  • Static index.html support

    静态首页支持

  • Automatic use of a ConfigurableWebBindingInitializer bean

    初始化数据绑定器:帮我们把请求数据绑定到JavaBean中

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

-- 如果您希望保留 Spring Boot MVC 定制并进行更多的 MVC 定制(拦截器、格式化程序、视图控制器和其他特性) ,可以添加您自己的 WebMvcConfigurer 类型的@Configuration ,但不要添加@EnableWebMvc

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

-- 如果你想提供自定义的 RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver定制,你可以声明一个类型为 WebMvcRegistrations 的 bean,并使用它来提供这些组件的自定义实例。

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

-- 如果你想完全控制 Spring MVC,你可以添加你自己的注释@Configuration@EnableWebMvc,或者像在@EnableWebMvc 的 Javadoc 中描述的那样添加你自己的@Configuration 注释 DelegatingWebMvcConfiguration

我们来仔细对照,看一下它怎么实现的,它告诉我们SpringBoot已经帮我们自动配置好了SpringMVC,然后自动配置了哪些东西呢?

ContentNegotiatingViewResolver 内容协商视图解析器

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

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

public interface ViewResolver
// 实现了视图解析器的类,我们也可以把它看作视图解析器

我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!

@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
   // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图
   // a view so it should have a high precedence
   // 因此它应该具有较高的优先级
   resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
   return resolver;
}

我们可以点进这类看看!找到对应的解析视图的代码

@Nullable	// 注解说明:@Nullable 即参数可为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
    if (requestedMediaTypes != null) {
        // 获取候选的视图对象
        List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
        // 选择一个最适合的视图对象,然后把这个对象返回
        View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }

    String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
    if (this.useNotAcceptableStatusCode) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }

        return NOT_ACCEPTABLE_VIEW;
    } else {
        this.logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}

我们继续点getCandidateViews进去看,他是怎么获得候选的视图的呢?

getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析!

Iterator var5 = this.viewResolvers.iterator();

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
    List<View> candidateViews = new ArrayList();
    if (this.viewResolvers != null) {
        Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
        Iterator var5 = this.viewResolvers.iterator(); // 视图解析器迭代

        while(var5.hasNext()) {
            ViewResolver viewResolver = (ViewResolver)var5.next();
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                // 添加到候选视图
                candidateViews.add(view);
            }

            Iterator var8 = requestedMediaTypes.iterator();

            while(var8.hasNext()) {
                MediaType requestedMediaType = (MediaType)var8.next();
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                Iterator var11 = extensions.iterator();

                while(var11.hasNext()) {
                    String extension = (String)var11.next();
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
    }

    if (!CollectionUtils.isEmpty(this.defaultViews)) {
        candidateViews.addAll(this.defaultViews);
    }
	// 返回候选视图
    return candidateViews;
}

所以得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的

我们再去研究下他的组合逻辑,看到有个属性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());
        Iterator var3 = matchingBeans.iterator();

        while(var3.hasNext()) {
            viewResolver = (ViewResolver)var3.next();
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    } else {
        for(int i = 0; i < this.viewResolvers.size(); ++i) {
            viewResolver = (ViewResolver)this.viewResolvers.get(i);
            if (!matchingBeans.contains(viewResolver)) {
                String name = viewResolver.getClass().getName() + i;
                this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
            }
        }
    }

    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
}

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

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

  1. 我们在我们的主程序中去写一个视图解析器来试试;

    //扩展 springmvc
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        @Bean
        public ViewResolver myViewResolver() {
            return new MyViewResolver();
        }
    
        //自定义的视图解析器
        public static class MyViewResolver implements ViewResolver {
            @Override
            public View resolveViewName(String viewName, Locale locale) throws Exception {
                return null;
            }
        }
    
    }
    
  2. 怎么看我们自己写的视图解析器有没有起作用呢?

    我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中

    image-20220505100229128

  3. 我们启动我们的项目,然后随便访问一个页面,看一下Debug信息

    找到 this

    image-20220505100432475

    找到视图解析器,我们看到我们自己定义的就在这里了

    image-20220505100538867

    所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了!

Formatter 格式化器

我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索Formatter。找到如下方法!

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 拿到配置文件中的格式化规则
   Format format = this.mvcProperties.getFormat();
   WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
         .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
   addFormatters(conversionService);
   return conversionService;
}

点击format.getDate()去:

public static class Format {
    /**
     * Date format to use, for example 'dd/MM/yyyy'.
     */
    private String date;
	...
    public String getDate() {
        return this.date;
    }
    ...
}

可以看到在我们的Properties文件中,我们可以进行自动配置它!

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

# 自定义配置日期格式化
spring.mvc.format.date=
public void setDate(String date) {
   this.date = date;
}

其余的就不一一举例了,大家可以下去多研究探讨即可!

修改SpringBoot的默认配置

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

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

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

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

扩展使用SpringMVC

官方文档如下:

If you want to keep those Spring Boot MVC customizations and make more MVC customizations(interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;

// 应为类型要求为WebMvcConfigurer,所以我们实现其接口
// 扩展 springmvc
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/dt , 就会跳转到test页面;
        registry.addViewController("/dt").setViewName("test");
    }

}

我们去浏览器访问一下:

image-20220505125246997

确实也跳转过来了,只是没有传参!所以说,我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!

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

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

  2. 这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)

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

    这个父类中有这样一段代码:

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

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

    public void addViewControllers(ViewControllerRegistry registry) {
        Iterator var2 = this.delegates.iterator();
    
        while(var2.hasNext()) {
            // 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
            WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
            delegate.addViewControllers(registry);
        }
    
    }
    

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

全面接管SpringMVC

官方文档:

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

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

只需在我们的配置类中要加一个@EnableWebMvc。

我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;

不加注解之前,访问首页:

image-20220505131704372

给配置类加上注解:@EnableWebMvc

image-20220505131823791

我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;

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

思考问题?为什么加了一个注解,自动配置就失效了!我们看下注解源码:

  1. 这里发现它是导入了一个类,我们可以继续进去看

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import({DelegatingWebMvcConfiguration.class})
    public @interface EnableWebMvc {
    }
    
  2. 它继承了一个父类 WebMvcConfigurationSupport

    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    	...
    }
    
  3. 我们来回顾一下Webmvc自动配置类

    @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 })
    public class WebMvcAutoConfiguration {
    	...
    }
    

    发现@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),只有WebMvcConfigurationSupport.class不存在的时候才生效,而使用了@EnableWebMvc会自动导入 DelegatingWebMvcConfiguration.classDelegatingWebMvcConfiguration本身有继承了WebMvcConfigurationSupport,所以WebMvc自动配置类就没有生效,导致访问首页出现了404

总结一句话:@EnableWebMvcWebMvcConfigurationSupport的组件导入进来了;

而导入的WebMvcConfigurationSupport只是 SpringMVC 最基本的功能!

在SpringBoot中会有非常多的扩展配置,只要看见了这个,我们就应该多留心注意~

7、员工管理系统

7.1、准备工作

bootstrap模板:
https://getbootstrap.com/docs/4.0/examples/

或者 百度链接:https://pan.baidu.com/s/1URaFmN6rt2AzDOwVogWBNQ 提取码:bruc

清理多余的文件或者直接创建新的项目,把 asserts 放 static 下,html 放 templates 下(样式加载不出来可以尝试重启IDEA)

项目结构:

image-20220506002430254

7.1.1、导入依赖

导入 lombok 依赖,还要在 IDEA 上安装 lombok 插件

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

导入 Thymeleaf 模板依赖

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

7.1.2、POJO

Department.java

//部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}

employee.java

//员工表
@Data
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender; // 0:女 1:男
    private Department department;
    private Date birth;

    public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        // 默认的创建日期
        this.birth = new Date();
    }
}

7.1.3、DAO

DepartmentDao.java

//部门dao
@Repository
public class DepartmentDao {

    //模拟数据库中的数据
    private static Map<Integer, Department> departments = null;
    static {
        departments = new HashMap<Integer, Department>(); //创建一个部门表

        departments.put(101, new Department(101, "教学部"));
        departments.put(102, new Department(102, "市场部"));
        departments.put(103, new Department(103, "教研部"));
        departments.put(104, new Department(104, "运营部"));
        departments.put(105, new Department(105, "后勤部"));
    }

    //获得所有部门信息
    public Collection<Department> getDepartments() {
        return departments.values();
    }

    //通过id得到部分
    public Department getDepartmentById(Integer id) {
        return departments.get(id);
    }

}

EmployeeDao.java

//员工dao
@Repository
public class EmployeeDao {

    //模拟数据库中的数据
    private static Map<Integer, Employee> employees = null;
    //员工有所属的部门
    @Autowired
    private DepartmentDao departmentDao;
    static {
        employees = new HashMap<Integer, Employee>(); //创建一个员工表

        employees.put(1001, new Employee(1001, "AA", "A46294093@qq.com", 0, new Department(101, "教学部")));
        employees.put(1002, new Employee(1002, "BB", "B46294093@qq.com", 1, new Department(102, "市场部")));
        employees.put(1003, new Employee(1003, "CC", "C46294093@qq.com", 0, new Department(103, "教研部")));
        employees.put(1004, new Employee(1004, "DD", "D46294093@qq.com", 1, new Department(104, "运营部")));
        employees.put(1005, new Employee(1005, "EE", "E46294093@qq.com", 0, new Department(105, "后勤部")));
    }

    //主键自增
    private static Integer initId = 1006;
    //增加一个员工
    public void save(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(initId++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
        employees.put(employee.getId(), employee);
    }

    //查询全部的员工信息
    public Collection<Employee> getAll() {
        return employees.values();
    }

    //通过Id查询员工
    public Employee getEmployeeById(Integer id) {
        return employees.get(id);
    }

    //通过Id删除员工
    public void deleteEmployeeById(Integer id) {
        employees.remove(id);
    }

}

7.2、首页实现

  1. 视图解析

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    }
    
  2. 修改 html 的引用静态资源 为 Thymeleaf 动态引用,为了防止在不同根目录下找不到静态资源

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
          <title>Signin Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/signin.css}" rel="stylesheet">
       </head>
    
       <body class="text-center">
          <form class="form-signin" action="dashboard.html">
             <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
             <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
             <label class="sr-only">Username</label>
             <input type="text" class="form-control" placeholder="Username" required="" autofocus="">
             <label class="sr-only">Password</label>
             <input type="password" class="form-control" placeholder="Password" required="">
             <div class="checkbox mb-3">
                <label>
              <input type="checkbox" value="remember-me"> Remember me
            </label>
             </div>
             <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
             <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
             <a class="btn btn-sm">中文</a>
             <a class="btn btn-sm">English</a>
          </form>
    
       </body>
    
    </html>
    

    404.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
             <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</a>
             <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
             <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                   <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
                </li>
             </ul>
          </nav>
    
          <div class="container-fluid">
             <div class="row">
                <nav class="col-md-2 d-none d-md-block bg-light sidebar">
                   <div class="sidebar-sticky">
                      <ul class="nav flex-column">
                         <li class="nav-item">
                            <a class="nav-link active" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                                  <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                                  <polyline points="9 22 9 12 15 12 15 22"></polyline>
                               </svg>
                               Dashboard <span class="sr-only">(current)</span>
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                                  <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                                  <polyline points="13 2 13 9 20 9"></polyline>
                               </svg>
                               Orders
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                                  <circle cx="9" cy="21" r="1"></circle>
                                  <circle cx="20" cy="21" r="1"></circle>
                                  <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                               </svg>
                               Products
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                                  <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                                  <circle cx="9" cy="7" r="4"></circle>
                                  <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                                  <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                               </svg>
                               Customers
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                                  <line x1="18" y1="20" x2="18" y2="10"></line>
                                  <line x1="12" y1="20" x2="12" y2="4"></line>
                                  <line x1="6" y1="20" x2="6" y2="14"></line>
                               </svg>
                               Reports
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                                  <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                                  <polyline points="2 17 12 22 22 17"></polyline>
                                  <polyline points="2 12 12 17 22 12"></polyline>
                               </svg>
                               Integrations
                            </a>
                         </li>
                      </ul>
    
                      <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
                  <span>Saved reports</span>
                  <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
                  </a>
                </h6>
                      <ul class="nav flex-column mb-2">
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Current month
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Last quarter
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Social engagement
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Year-end sale
                            </a>
                         </li>
                      </ul>
                   </div>
                </nav>
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <h1>404</h1>
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    

    dashboard.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
             <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</a>
             <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
             <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                   <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
                </li>
             </ul>
          </nav>
    
          <div class="container-fluid">
             <div class="row">
                <nav class="col-md-2 d-none d-md-block bg-light sidebar">
                   <div class="sidebar-sticky">
                      <ul class="nav flex-column">
                         <li class="nav-item">
                            <a class="nav-link active" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                                  <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                                  <polyline points="9 22 9 12 15 12 15 22"></polyline>
                               </svg>
                               Dashboard <span class="sr-only">(current)</span>
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                                  <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                                  <polyline points="13 2 13 9 20 9"></polyline>
                               </svg>
                               Orders
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                                  <circle cx="9" cy="21" r="1"></circle>
                                  <circle cx="20" cy="21" r="1"></circle>
                                  <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                               </svg>
                               Products
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                                  <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                                  <circle cx="9" cy="7" r="4"></circle>
                                  <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                                  <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                               </svg>
                               Customers
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                                  <line x1="18" y1="20" x2="18" y2="10"></line>
                                  <line x1="12" y1="20" x2="12" y2="4"></line>
                                  <line x1="6" y1="20" x2="6" y2="14"></line>
                               </svg>
                               Reports
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                                  <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                                  <polyline points="2 17 12 22 22 17"></polyline>
                                  <polyline points="2 12 12 17 22 12"></polyline>
                               </svg>
                               Integrations
                            </a>
                         </li>
                      </ul>
    
                      <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
                  <span>Saved reports</span>
                  <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
                  </a>
                </h6>
                      <ul class="nav flex-column mb-2">
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Current month
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Last quarter
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Social engagement
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Year-end sale
                            </a>
                         </li>
                      </ul>
                   </div>
                </nav>
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <div class="chartjs-size-monitor" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;">
                      <div class="chartjs-size-monitor-expand" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
                         <div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div>
                      </div>
                      <div class="chartjs-size-monitor-shrink" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
                         <div style="position:absolute;width:200%;height:200%;left:0; top:0"></div>
                      </div>
                   </div>
                   <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
                      <h1 class="h2">Dashboard</h1>
                      <div class="btn-toolbar mb-2 mb-md-0">
                         <div class="btn-group mr-2">
                            <button class="btn btn-sm btn-outline-secondary">Share</button>
                            <button class="btn btn-sm btn-outline-secondary">Export</button>
                         </div>
                         <button class="btn btn-sm btn-outline-secondary dropdown-toggle">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
                    This week
                  </button>
                      </div>
                   </div>
    
                   <canvas class="my-4 chartjs-render-monitor" id="myChart" width="1076" height="454" style="display: block; width: 1076px; height: 454px;"></canvas>
    
                   
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    

    list.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
             <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</a>
             <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
             <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                   <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
                </li>
             </ul>
          </nav>
    
          <div class="container-fluid">
             <div class="row">
                <nav class="col-md-2 d-none d-md-block bg-light sidebar">
                   <div class="sidebar-sticky">
                      <ul class="nav flex-column">
                         <li class="nav-item">
                            <a class="nav-link active" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                                  <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                                  <polyline points="9 22 9 12 15 12 15 22"></polyline>
                               </svg>
                               Dashboard <span class="sr-only">(current)</span>
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                                  <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                                  <polyline points="13 2 13 9 20 9"></polyline>
                               </svg>
                               Orders
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                                  <circle cx="9" cy="21" r="1"></circle>
                                  <circle cx="20" cy="21" r="1"></circle>
                                  <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                               </svg>
                               Products
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                                  <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                                  <circle cx="9" cy="7" r="4"></circle>
                                  <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                                  <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                               </svg>
                               Customers
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                                  <line x1="18" y1="20" x2="18" y2="10"></line>
                                  <line x1="12" y1="20" x2="12" y2="4"></line>
                                  <line x1="6" y1="20" x2="6" y2="14"></line>
                               </svg>
                               Reports
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                                  <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                                  <polyline points="2 17 12 22 22 17"></polyline>
                                  <polyline points="2 12 12 17 22 12"></polyline>
                               </svg>
                               Integrations
                            </a>
                         </li>
                      </ul>
    
                      <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
                  <span>Saved reports</span>
                  <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
                  </a>
                </h6>
                      <ul class="nav flex-column mb-2">
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Current month
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Last quarter
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Social engagement
                            </a>
                         </li>
                         <li class="nav-item">
                            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                               <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                  <polyline points="14 2 14 8 20 8"></polyline>
                                  <line x1="16" y1="13" x2="8" y2="13"></line>
                                  <line x1="16" y1="17" x2="8" y2="17"></line>
                                  <polyline points="10 9 9 9 8 9"></polyline>
                               </svg>
                               Year-end sale
                            </a>
                         </li>
                      </ul>
                   </div>
                </nav>
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <h2>Section title</h2>
                   <div class="table-responsive">
                      <table class="table table-striped table-sm">
                         <thead>
                            <tr>
                               <th>#</th>
                               <th>Header</th>
                               <th>Header</th>
                               <th>Header</th>
                               <th>Header</th>
                            </tr>
                         </thead>
                         <tbody>
                            <tr>
                               <td>1,001</td>
                               <td>Lorem</td>
                               <td>ipsum</td>
                               <td>dolor</td>
                               <td>sit</td>
                            </tr>
                            <tr>
                               <td>1,002</td>
                               <td>amet</td>
                               <td>consectetur</td>
                               <td>adipiscing</td>
                               <td>elit</td>
                            </tr>
                            <tr>
                               <td>1,003</td>
                               <td>Integer</td>
                               <td>nec</td>
                               <td>odio</td>
                               <td>Praesent</td>
                            </tr>
                            <tr>
                               <td>1,003</td>
                               <td>libero</td>
                               <td>Sed</td>
                               <td>cursus</td>
                               <td>ante</td>
                            </tr>
                            <tr>
                               <td>1,004</td>
                               <td>dapibus</td>
                               <td>diam</td>
                               <td>Sed</td>
                               <td>nisi</td>
                            </tr>
                            <tr>
                               <td>1,005</td>
                               <td>Nulla</td>
                               <td>quis</td>
                               <td>sem</td>
                               <td>at</td>
                            </tr>
                            <tr>
                               <td>1,006</td>
                               <td>nibh</td>
                               <td>elementum</td>
                               <td>imperdiet</td>
                               <td>Duis</td>
                            </tr>
                            <tr>
                               <td>1,007</td>
                               <td>sagittis</td>
                               <td>ipsum</td>
                               <td>Praesent</td>
                               <td>mauris</td>
                            </tr>
                            <tr>
                               <td>1,008</td>
                               <td>Fusce</td>
                               <td>nec</td>
                               <td>tellus</td>
                               <td>sed</td>
                            </tr>
                            <tr>
                               <td>1,009</td>
                               <td>augue</td>
                               <td>semper</td>
                               <td>porta</td>
                               <td>Mauris</td>
                            </tr>
                            <tr>
                               <td>1,010</td>
                               <td>massa</td>
                               <td>Vestibulum</td>
                               <td>lacinia</td>
                               <td>arcu</td>
                            </tr>
                            <tr>
                               <td>1,011</td>
                               <td>eget</td>
                               <td>nulla</td>
                               <td>Class</td>
                               <td>aptent</td>
                            </tr>
                            <tr>
                               <td>1,012</td>
                               <td>taciti</td>
                               <td>sociosqu</td>
                               <td>ad</td>
                               <td>litora</td>
                            </tr>
                            <tr>
                               <td>1,013</td>
                               <td>torquent</td>
                               <td>per</td>
                               <td>conubia</td>
                               <td>nostra</td>
                            </tr>
                            <tr>
                               <td>1,014</td>
                               <td>per</td>
                               <td>inceptos</td>
                               <td>himenaeos</td>
                               <td>Curabitur</td>
                            </tr>
                            <tr>
                               <td>1,015</td>
                               <td>sodales</td>
                               <td>ligula</td>
                               <td>in</td>
                               <td>libero</td>
                            </tr>
                         </tbody>
                      </table>
                   </div>
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    

注意点:所有页面的静态资源都需要使用 Thymeleaf 接管;@{}

7.3、国际化

国际化:internationalization(简称 i18n,以 i 为开头,n 为结尾,中间18个字母)

在资讯领域,国际化(i18n)指让产品(出版物,软件,硬件等)无需做大的改变就能够适应不同的语言和地区的需要。对程序来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面。

我们需要使前端页面国际化,在此之前IDEA需要先安装 Resource Bundle Editor 插件来实现 properties 文件的可视化界面

image-20220506081030027

  1. 在 resource 下创建一个 i18n 的包,添加 login.propertieslogin_zh_CN.properties我们发现他们直接被整合起来

    image-20220506081321321

  2. 右键 Resource Bundle 'login' 可以直接创建 properties 文件

    image-20220506081605560

  3. 点开 properties,左下角是可视化配置

    image-20220506081708491
  4. 点开可视化界面,添加 tip,会发现右边有多个语言的配置框,输入不同的参数

    image-20220506082610295

  5. 我们会发现不同的 properties 里有着刚才输入不同的配置

    login.properties

    login.btn=登录
    login.password=密码
    login.remember=记住我
    login.tip=请登录
    login.username=用户名
    

    login_en_US.properties

    login.btn=Sign in
    login.password=Password
    login.remember=Remember me
    login.tip=Please sign in
    login.username=Username
    

    login_zh_CN.properties

    login.btn=登录
    login.password=密码
    login.remember=记住我
    login.tip=请登录
    login.username=用户名
    
  6. application.properties 里配置我们文件的真实路径

    # 我们配置文件的真实位置
    spring.messages.basename=i18n.login
    
  7. 修改首页的国际化部分(Thymeleaf获取国际化内容:#{}

    例如:
    =============
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    改成
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
    ============
    <input type="text" class="form-control" th:placeholder="Username" required="" autofocus="">
    改成
    <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
    ============
    <input type="password" class="form-control" th:placeholder="Password" required="">
    改成
    <input type="password" class="form-control" th:placeholder="#{login.password}" required="">
    ============
    <input type="checkbox" value="remember-me"> Remember me
    改成
    <input type="checkbox" value="remember-me"> [[#{login.remember}]]
    =============
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    改成
    <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
    
  8. 修改首页下面的 a 标签

    <a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
    <a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
    

    Thymeleaf 的携带参数用括号括起来

  9. 在 config 包下创建MyLocaleResolver

    public class MyLocaleResolver implements LocaleResolver {
        //解析请求
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            //获取请求中的语言参数
            String language = request.getParameter("lang");
    
            Locale locale = Locale.getDefault(); //如果没有就使用默认的
    
            //如果请求的链接携带了国际化的参数
            if (!StringUtils.isEmpty(language)) {
                //zh_CN
                String[] split = language.split("_");
                //国家,地区
                locale = new Locale(split[0], split[1]);
            }
    
            return locale;
        }
    
        @Override
        public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
    
        }
    }
    
  10. 接着在容器MyMvcConfig中注册MyLocaleResolver

    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }
    
  11. 测试

    image-20220506095349597

    image-20220506095408571

注意点:

  1. 我们需要配置 i18n 文件
  2. 我们如果需要在项目中进行按钮自动切换,我们需要自定义一个组件LocaleResolver
  3. 记得将自己写的组件配置到 spring 容器中 @Bean
  4. #{}使用国际化内容

7.4、登录功能实现

实现登录时对用户名密码判断,如果错误就返回错误信息,成功就重定向到首页

  1. 修改 index.html ,为用户名和密码输入框添加 name 属性,用来传参。并在主页设置一个 msg 来接收错误参数

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
          <title>Signin Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/signin.css}" rel="stylesheet">
       </head>
    
       <body class="text-center">
          <form class="form-signin" th:action="@{/user/login}">
             <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
             <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
             <!--如果msg的值为空,则不显示消息-->
             <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
             <label class="sr-only">Username</label>
             <input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
             <label class="sr-only">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" th:text="#{login.btn}">Sign in</button>
             <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
             <a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
             <a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
          </form>
    
       </body>
    
    </html>
    
  2. 编写一个 LoginController

    //@Contriller + @ResponseBody = @RestController
    @Controller
    public class LoginController {
    
        @RequestMapping("/user/login")
        public String login(
                @RequestParam("username") String username,
                @RequestParam("password") String password,
                Model model) {
    
            //具体的业务
            if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
                return "redirect:/main.html";
            } else {
                // 告诉用户,登录失败
                model.addAttribute("msg", "用户名或者密码错误");
                return "index";
            }
        }
    
    }
    
  3. 在自定义视图解析器 MyMvcConfig 中添加以下代码,用来实现登录后的重定向

    registry.addViewController("/main.html").setViewName("dashboard");
    

7.5、登录拦截器

想要自定义拦截器,必须实现 HandlerInterceptor 接口

  1. 编写一个 LoginHandlerInterceptor

    public class LoginHandlerInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //登录成功之后,应该有用户的Session
            if (request.getSession().getAttribute("loginUser") == null) {
                request.setAttribute("msg", "没有权限,请先登录");
                request.getRequestDispatcher("/index.html").forward(request, response);
                return false;
            } else {
                return true;
            }
    
        }
    }
    
  2. 修改 LoginController,将登录成功后的用户名放在 Session 中

    @RequestMapping("/user/login")
    public String login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            Model model,
            HttpSession session) {
    
        //具体的业务
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            session.setAttribute("loginUser", username);
            return "redirect:/main.html";
        } else {
            // 告诉用户,登录失败
            model.addAttribute("msg", "用户名或者密码错误");
            return "index";
        }
    }
    
  3. 把该拦截器注册到容器中,并设置拦截规则

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

7.6、账号注销

通过移除 Session 的 loginUser 来实现账号注销

  1. LoginController中添加如下代码,

    @RequestMapping("/user/out")
    public String out(HttpSession session) {
        session.removeAttribute("loginUser");
        return "redirect:/";
    }
    
  2. 修改 dashboard.html

    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    
    <a class="nav-link" th:href="@{/user/out}">Sign out</a>
    

7.7、展示员工列表

7.7.1、提取公共页面

提取公共页面有利于我们代码的复用,简化各个html的代码量

  1. 在 templates 下创建 commons 文件夹,在里面创建一个 commons.html 来存放公共页面(由于头部导航栏和侧边栏在各个页面都存在,我们可以直接提取出来当做公共页面)

    提取:th:fragment="topbar" 
         th:fragment="siderbar(active)"
    
    调用:th:replace="~{commons/commons :: topbar}" 
    
    如果要传递参数,可以直接使用()传参,接收判断即可
        
    (传递不同参数)
    th:replace="~{commons/commons :: siderbar(active='main.html')}"
    th:replace="~{commons/commons :: siderbar(active='list.html')}"
        
    (判断参数内容)
    th:class="${active=='main.html' ? 'nav-link active' : 'nav-link'}"
    th:class="${active=='list.html' ? 'nav-link active' : 'nav-link'}"
    

    commons.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    
    <!--头部导航栏-->
    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
        <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
        <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
        <ul class="navbar-nav px-3">
            <li class="nav-item text-nowrap">
                <a class="nav-link" th:href="@{/user/out}">注销</a>
            </li>
        </ul>
    </nav>
    
    <!--侧边栏-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="siderbar(active)">
        <div class="sidebar-sticky">
            <ul class="nav flex-column">
                <li class="nav-item">
                    <a th:class="${active=='main.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/index.html}">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                            <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                            <polyline points="9 22 9 12 15 12 15 22"></polyline>
                        </svg>
                        首页 <span class="sr-only">(current)</span>
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                            <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                            <polyline points="13 2 13 9 20 9"></polyline>
                        </svg>
                        Orders
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                            <circle cx="9" cy="21" r="1"></circle>
                            <circle cx="20" cy="21" r="1"></circle>
                            <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                        </svg>
                        Products
                    </a>
                </li>
                <li class="nav-item">
                    <a th:class="${active=='list.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/emps}">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                            <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                            <circle cx="9" cy="7" r="4"></circle>
                            <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                            <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                        </svg>
                        员工管理
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                            <line x1="18" y1="20" x2="18" y2="10"></line>
                            <line x1="12" y1="20" x2="12" y2="4"></line>
                            <line x1="6" y1="20" x2="6" y2="14"></line>
                        </svg>
                        Reports
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                            <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                            <polyline points="2 17 12 22 22 17"></polyline>
                            <polyline points="2 12 12 17 22 12"></polyline>
                        </svg>
                        Integrations
                    </a>
                </li>
            </ul>
    
            <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
                <span>Saved reports</span>
                <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
                </a>
            </h6>
            <ul class="nav flex-column mb-2">
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Current month
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Last quarter
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Social engagement
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                            <polyline points="14 2 14 8 20 8"></polyline>
                            <line x1="16" y1="13" x2="8" y2="13"></line>
                            <line x1="16" y1="17" x2="8" y2="17"></line>
                            <polyline points="10 9 9 9 8 9"></polyline>
                        </svg>
                        Year-end sale
                    </a>
                </li>
            </ul>
        </div>
    </nav>
    
    </html>
    
  2. list.html放入 emp 文件夹下,在dashboard.htmllist.html中调用公共页面

    dashboard.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <div th:replace="~{commons/commons :: topbar}"></div>
    
          <div class="container-fluid">
             <div class="row">
    
                <!--传递参数active给组件-->
                <div th:replace="~{commons/commons :: siderbar(active='main.html')}"></div>
    
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <div class="chartjs-size-monitor" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;">
                      <div class="chartjs-size-monitor-expand" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
                         <div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div>
                      </div>
                      <div class="chartjs-size-monitor-shrink" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;">
                         <div style="position:absolute;width:200%;height:200%;left:0; top:0"></div>
                      </div>
                   </div>
                   <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
                      <h1 class="h2">Dashboard</h1>
                      <div class="btn-toolbar mb-2 mb-md-0">
                         <div class="btn-group mr-2">
                            <button class="btn btn-sm btn-outline-secondary">Share</button>
                            <button class="btn btn-sm btn-outline-secondary">Export</button>
                         </div>
                         <button class="btn btn-sm btn-outline-secondary dropdown-toggle">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
                    This week
                  </button>
                      </div>
                   </div>
    
                   <canvas class="my-4 chartjs-render-monitor" id="myChart" width="1076" height="454" style="display: block; width: 1076px; height: 454px;"></canvas>
    
                   
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    

    list.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    	<head>
    		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    		<meta name="description" content="">
    		<meta name="author" content="">
    
    		<title>Dashboard Template for Bootstrap</title>
    		<!-- Bootstrap core CSS -->
    		<link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
    		<!-- Custom styles for this template -->
    		<link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
    		<style type="text/css">
    			/* Chart.js */
    			
    			@-webkit-keyframes chartjs-render-animation {
    				from {
    					opacity: 0.99
    				}
    				to {
    					opacity: 1
    				}
    			}
    			
    			@keyframes chartjs-render-animation {
    				from {
    					opacity: 0.99
    				}
    				to {
    					opacity: 1
    				}
    			}
    			
    			.chartjs-render-monitor {
    				-webkit-animation: chartjs-render-animation 0.001s;
    				animation: chartjs-render-animation 0.001s;
    			}
    		</style>
    	</head>
    
    	<body>
    		<div th:replace="~{commons/commons :: topbar}"></div>
    
    		<div class="container-fluid">
    			<div class="row">
    
    				<!--插入抽取的部分-->
    				<div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div>
    
    
    				<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    					<h2>Section title</h2>
    					<div class="table-responsive">
    						<table class="table table-striped table-sm">
    							<thead>
    								<tr>
    									<th>id</th>
    									<th>lastName</th>
    									<th>email</th>
    									<th>gender</th>
    									<th>department</th>
    									<th>birth</th>
    								</tr>
    							</thead>
    							<tbody>
    								
    							</tbody>
    						</table>
    					</div>
    				</main>
    			</div>
    		</div>
    
    		<!-- Bootstrap core JavaScript
        ================================================== -->
    		<!-- Placed at the end of the document so the pages load faster -->
    		<script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script>
    		<script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script>
    		<script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script>
    
    		<!-- Icons -->
    		<script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script>
    		<script>
    			feather.replace()
    		</script>
    
    		<!-- Graphs -->
    		<script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script>
    		<script>
    			var ctx = document.getElementById("myChart");
    			var myChart = new Chart(ctx, {
    				type: 'line',
    				data: {
    					labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
    					datasets: [{
    						data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
    						lineTension: 0,
    						backgroundColor: 'transparent',
    						borderColor: '#007bff',
    						borderWidth: 4,
    						pointBackgroundColor: '#007bff'
    					}]
    				},
    				options: {
    					scales: {
    						yAxes: [{
    							ticks: {
    								beginAtZero: false
    							}
    						}]
    					},
    					legend: {
    						display: false,
    					}
    				}
    			});
    		</script>
    
    	</body>
    
    </html>
    

7.7.2、修改员工展示列表

  1. 编写 EmployeeController 类

    @Controller
    public class EmployeeController {
    
        //该写法是为了教学更清楚,但不推荐使用
        @Autowired
        EmployeeDao employeeDao;
    
        @RequestMapping("/emps")
        public String list(Model model) {
            Collection<Employee> employees = employeeDao.getAll();
            model.addAttribute("emps", employees);
            return "emp/list";
        }
    
    }
    
  2. 修改点击侧边栏:员工管理 发出的请求为th:href="@{/emps}"

  3. 修改list.html中列表 main

    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
       <h2>Section title</h2>
       <div class="table-responsive">
          <table class="table table-striped table-sm">
             <thead>
                <tr>
                   <th>id</th>
                   <th>lastName</th>
                   <th>email</th>
                   <th>gender</th>
                   <th>department</th>
                   <th>birth</th>
                   <th>操作</th>
                </tr>
             </thead>
             <tbody>
                <tr th:each="emp : ${emps}">
                   <td th:text="${emp.getId()}"></td>
                   <td th:text="${emp.getLastName()}"></td>
                   <td th:text="${emp.getEmail()}"></td>
                   <td th:text="${emp.getGender()==0 ? '女':'男'}"></td>
                   <td th:text="${emp.department.getDepartmentName()}"></td>
                   <td th:text="${#dates.format(emp.getBirth(), 'yyyy-MM-dd HH:mm:ss')}"></td>
                   <td>
                      <button class="btn btn-sm btn-primary">编辑</button>
                       &nbsp;&nbsp;
                      <button class="btn btn-sm btn-danger">删除</button>
                   </td>
                </tr>
             </tbody>
          </table>
       </div>
    </main>
    
  4. 效果

    image-20220506221232032

7.8、增加员工实现

  1. list.html增加添加按钮

    <h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>
    
  2. 在 EmployeeController 类里加入跳转请求

    @GetMapping("/emp")
    public String toAddPage(Model model) {
        //查出所有部门的信息
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments", departments);
        return "emp/add";
    }
    

    注意:这里是 Get 方法

  3. 在 templates/emp 下编写一个 add.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <div th:replace="~{commons/commons :: topbar}"></div>
    
          <div class="container-fluid">
             <div class="row">
    
                <!--插入抽取的部分-->
                <div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div>
    
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <form th:action="@{/emp}" method="post">
                      <div class="form-group">
                         <label>Last Name</label>
                         <input type="text" name="lastName" class="form-control" placeholder="Dt">
                      </div>
                      <div class="form-group">
                         <label>Email</label>
                         <input type="email" name="email" class="form-control" placeholder="746294093@qq.com">
                      </div>
                      <div class="form-group">
                         <label>Gender</label><br>
                         <div class="form-check form-check-inline">
                            <input class="form-check-input" type="radio" name="gender" value="1">
                            <label class="form-check-label">男</label>
                         </div>
                         <div class="form-check form-check-inline">
                            <input class="form-check-input" type="radio" name="gender" value="0">
                            <label class="form-check-label">女</label>
                         </div>
                      </div>
                      <div class="form-group">
                         <label>Department</label>
                          <!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性-->
                         <select class="form-control" name="department.id">
                            <option th:each="dept : ${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
                         </select>
                      </div>
                      <div class="form-group">
                         <label>Birth</label>
                         <input type="text" name="birth" class="form-control" placeholder="yyyy/MM/dd or yyyy/MM/dd HH:mm:ss">
                      </div>
                      <button type="submit" class="btn btn-primary">添加</button>
                   </form>
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    
  4. 在 EmployeeController 类里加入增加员工请求

    @PostMapping("/emp")
    public String addEmp(Employee employee) {
        employeeDao.save(employee); //调用底层业务方法保存员工的信息
        return "redirect:/emps";
    }
    
  5. 我们可以在 application.properties中设置读取日期的格式

    # 格式化时间格式
    spring.mvc.format.date=yyyy-MM-dd
    

7.9、修改员工信息

  1. 修改 list.html的编辑按钮为 a 标签

    <a class="btn btn-sm btn-primary" th:href="@{/emp/} + ${emp.getId()}">编辑</a>
    
  2. 在 EmployeeController 类里加入跳转请求

    @GetMapping("/emp/{id}")
    public String toUpdateEmp(@PathVariable("id") Integer id, Model model) {
        //根据id查出原来的数据
        Employee employee = employeeDao.getEmployeeById(id);
        model.addAttribute("emp", employee);
        //查出所有部门的信息
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments", departments);
        return "emp/update";
    }
    

    注意:这里是 Get 方法

  3. 在 templates/emp 下编写一个 update.html

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <div th:replace="~{commons/commons :: topbar}"></div>
    
          <div class="container-fluid">
             <div class="row">
    
                <!--插入抽取的部分-->
                <div th:replace="~{commons/commons :: siderbar(active='list.html')}"></div>
    
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <form th:action="@{/emp}" method="post">
                      <input type="hidden" th:value="${emp.getId()}" name="id">
                      <div class="form-group">
                         <label>Last Name</label>
                         <input type="text" th:value="${emp.getLastName()}" name="lastName" class="form-control" placeholder="Dt">
                      </div>
                      <div class="form-group">
                         <label>Email</label>
                         <input type="email" th:value="${emp.getEmail()}" name="email" class="form-control" placeholder="746294093@qq.com">
                      </div>
                      <div class="form-group">
                         <label>Gender</label><br>
                         <div class="form-check form-check-inline">
                            <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
                            <label class="form-check-label">男</label>
                         </div>
                         <div class="form-check form-check-inline">
                            <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
                            <label class="form-check-label">女</label>
                         </div>
                      </div>
                      <div class="form-group">
                         <label>Department</label>
                         <!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性-->
                         <select class="form-control" name="department.id">
                            <option th:selected="${emp.getDepartment().getId()==dept.getId()}"
                                  th:each="dept : ${departments}" th:text="${dept.getDepartmentName()}"
                                   th:value="${dept.getId()}"></option>
                         </select>
                      </div>
                      <div class="form-group">
                         <label>Birth</label>
                         <input type="text" th:value="${#dates.format(emp.getBirth(), 'yyyy/MM/dd HH:mm:ss')}"
                               name="birth" class="form-control" placeholder="yyyy/MM/dd or yyyy/MM/dd HH:mm:ss">
                      </div>
                      <button type="submit" class="btn btn-primary">添加</button>
                   </form>
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}"></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}"></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}"></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}"></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    
  4. 由于方法的复用,我们不需要写额外的请求方法来修改员工信息,直接调用增加员工的请求,效果是一致的。

    因为dao层模拟数据库用的是Map,而Map的特点是:主键唯一。所以只要保持主键一致就能达到覆盖修改的效果,就不需要再单独编写新的方法

7.10、删除员工实现

  1. 修改 list.html的删除按钮为 a 标签

    <a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/} + ${emp.getId()}">删除</a>
    
  2. 在 EmployeeController 类里加入增加员工请求

    @GetMapping("/deleteEmp/{id}")
    public String deleteEmp(@PathVariable("id") Integer id) {
        employeeDao.deleteEmployeeById(id);
        return "redirect:/emps";
    }
    

7.11、404处理

  1. 在templates下建立一个error文件夹,把 404.html 放进去

    springboot会自动帮我们配置处理,在出现404的时候会自动跳转到我们写好的404.html

  2. 修改404.html的公共页面

    <!DOCTYPE html>
    <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <meta name="description" content="">
          <meta name="author" content="">
    
          <title>Dashboard Template for Bootstrap</title>
          <!-- Bootstrap core CSS -->
          <link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
    
          <!-- Custom styles for this template -->
          <link th:href="@{/asserts/css/dashboard.css}" rel="stylesheet">
          <style type="text/css">
             /* Chart.js */
             
             @-webkit-keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             @keyframes chartjs-render-animation {
                from {
                   opacity: 0.99
                }
                to {
                   opacity: 1
                }
             }
             
             .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
             }
          </style>
       </head>
    
       <body>
          <div th:replace="~{commons/commons :: topbar}"></div>
    
          <div class="container-fluid">
             <div class="row">
                <!--插入抽取的部分-->
                <div th:replace="~{commons/commons :: siderbar(active='404.html')}"></div>
    
                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                   <h1>404</h1>
                </main>
             </div>
          </div>
    
          <!-- Bootstrap core JavaScript
        ================================================== -->
          <!-- Placed at the end of the document so the pages load faster -->
          <script type="text/javascript" th:src="@{/asserts/js/jquery-3.2.1.slim.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/popper.min.js}" ></script>
          <script type="text/javascript" th:src="@{/asserts/js/bootstrap.min.js}" ></script>
    
          <!-- Icons -->
          <script type="text/javascript" th:src="@{/asserts/js/feather.min.js}" ></script>
          <script>
             feather.replace()
          </script>
    
          <!-- Graphs -->
          <script type="text/javascript" th:src="@{/asserts/js/Chart.min.js}" ></script>
          <script>
             var ctx = document.getElementById("myChart");
             var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                   labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                   datasets: [{
                      data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                      lineTension: 0,
                      backgroundColor: 'transparent',
                      borderColor: '#007bff',
                      borderWidth: 4,
                      pointBackgroundColor: '#007bff'
                   }]
                },
                options: {
                   scales: {
                      yAxes: [{
                         ticks: {
                            beginAtZero: false
                         }
                      }]
                   },
                   legend: {
                      display: false,
                   }
                }
             });
          </script>
    
       </body>
    
    </html>
    

8、如何写一个网站

前端:

  • 模板:别人写好的,我们拿来改成自己需要的
  • 框架:组件:我们自己手动拼接! Bootstrap,Layui,semantic-ui,element-ui
    • 栅格系统
    • 导航栏
    • 侧边栏
    • 表单

8.1、开发网站心得

  1. 前端搞定:页面长什么样:数据
  2. 设计数据库(数据库设计难点!)
  3. 前端让他能够自动运行,独立化工程
  4. 数据接口如何对接:json,对象 all in one!
  5. 前后端联调测试!‘

小心得

  1. 有一套自己熟悉的后台模板:工作必要!——>X-admin
  2. 前端界面:至少自己能够通过前端框架,组合出来一个网站页面
    • index - about - blog - post - user
  3. 让这个页面能够独立运行!

8.2、后面的学习内容

  • 整合JDBC
  • 整合Mybatis 重点
  • 整合Druid 重点
  • Shiro:安全 重点
  • Spring Security:安全 重点
  • 异步任务~,邮件发送,定时任务
  • Swagger
  • Dubbo + Zookeeper

9、SpringBoot整合

9.1、整合JDBC

9.1.1、简介

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

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

Sping Data 官网:https://spring.io/projects/spring-data

数据库相关的启动器 :可以参考官方文档:

https://docs.spring.io/spring-boot/docs/2.6.7/reference/htmlsingle/#using.build-systems.starters

9.1.2、JDBC

去新建一个项目测试:springboot-04-data;引入相关模块!基础模块(这里也要勾选Spring-Web

image-20220508091136242

项目建好后,发现自动帮我们导入了如下的启动器

<!--JDBC-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--MySQL-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--Web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

application.yml配置文件连接数据库

image-20220508092849524

spring:
  datasource:
    username: root
    password: 123456
    # &serverTimezone=UTC 解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    # com.mysql.jdbc.Driver MySQL5.0+
    # com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+
    driver-class-name: com.mysql.cj.jdbc.Driver

测试类测试

@SpringBootTest
class Springboot04DataApplicationTests {

    //DI注入数据源
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //查看默认的数据源
        System.out.println(dataSource.getClass());
        //获得数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        //关闭连接
        connection.close();
    }

}

image-20220508094521498

小结

Spring Boot 2.6.7 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源

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

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

关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 JdbcTemplate

9.1.3、JDBCTemplate

1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;

2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。

3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用

5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateAutoConfiguration 类

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

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

JdbcController类

@RestController
public class JdbcController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询数据库的所有信息
    //没有实体类,数据库中的东西,怎么获取? Map
    @GetMapping("/userList")
    public List<Map<String, Object>> userList() {
        String sql = "select * from mybatis.user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    @GetMapping("/addUser")
    public String addUser() {
        String sql = "insert into mybatis.user(id,name,pwd) values (6,'小明','123456')";
        jdbcTemplate.update(sql);
        return "add-ok";
    }

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id) {
        String sql = "update mybatis.user set name = ?,pwd = ? where id = " + id;

        //封装
        Object[] objects = new Object[2];
        objects[0] = "大明";
        objects[1] = "654321";

        jdbcTemplate.update(sql, objects);
        return "update-ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id) {
        String sql = "delete from mybatis.user where id = ?";
        jdbcTemplate.update(sql, id);
        return "delete-ok";
    }
}
  • 进行测试即可完成CURD的基本操作!

9.2、整合Druid数据源

9.2.1、简介

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

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

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

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

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

Github地址:https://github.com/alibaba/druid/

com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:

image-20220307114416400

img

img

9.2.2、配置数据源

添加上 Druid 数据源依赖

<!--Druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.9</version>
</dependency>

查看项目依赖,导入成功

image-20220508141355353

application.yaml里配置数据源

spring:
  datasource:
    username: root
    password: 123456
    # &serverTimezone=UTC 解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    # com.mysql.jdbc.Driver MySQL5.0+
    # com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+
    driver-class-name: com.mysql.cj.jdbc.Driver
    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

我们还需要导入 Log4j 的依赖

<!--Log4j-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

DruidConfig 配置类

  • 现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要自己添加 DruidDataSource 组件到容器中,并绑定属性

    @Configuration
    public class DruidConfig {
    
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource druidDataSource() {
            return new DruidDataSource();
        }
    
    }
    
  • 配置Druid数据源监控

    Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;

    //后台监控:web.xml ServletRegistrationBean
    //因为SpringBoot 内置了 servlet 容器,所以没有 web.xml,所以只能替代方法:ServletRegistrationBean
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    
        //后台需要有人登录,账号密码配置
        HashMap<String , String > initParameters = new HashMap<>();
        //增加配置
        initParameters.put("loginUsername", "admin");   //登录的key是固定的:loginUsername loginUsername
        initParameters.put("loginPassword", "123456");
    
        //为空表示允许谁可以访问
        initParameters.put("allow", "");
        //只允许本机访问 initParameters.put("allow", "localhost");
    
        //禁止谁能访问 initParameters.put("kuangshen", "192.168.11.123");
    
        bean.setInitParameters(initParameters); //设置初始化参数
        return bean;
    }
    

    访问后台路径:http://localhost:8081/druid/

    image-20220508153247939

  • 配置 Druid web 监控 filter 过滤器

    //filter
    //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
    @Bean
    public FilterRegistrationBean webStatFilter() {
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
    
        bean.setFilter(new WebStatFilter());
    
        //可以过滤哪些请求呢
        HashMap<String , String > initParameters = new HashMap<>();
        //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
        initParameters.put("exclusions", "*.js,*.css,/druid/**");
    
        bean.setInitParameters(initParameters);
        return bean;
    }
    

    平时在工作中,按需求进行配置即可,主要用作监控!

9.3、整合Mybatis

9.3.1、简介

官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.1

9.3.2、测试

导入 MyBatis 所需要的依赖

<!--mybatis-spring-boot-starter-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>

application.yml里编写数据库配置

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
  • 在 test 里测试是否连上数据库

创建一个 User 实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
}

创建一个 UserMapper 类

@Mapper //表示是一个mybatis的mapper类
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);

}

创建以下结构文件

image-20220508214234887

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.dt.mapper.UserMapper">

    <select id="queryUserList" resultType="User">
        select * from mybatis.user
    </select>

    <select id="queryUserById" resultType="User">
        select * from mybatis.user where id = #{id}
    </select>

    <insert id="addUser" parameterType="User">
        insert into mybatis.user(id, name, pwd)
        values (#{id}, #{name}, #{pwd});
    </insert>
    
    <update id="updateUser" parameterType="User">
        update mybatis.user
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="pwd != null">pwd = #{pwd}</if>
        </set>
        where id = #{id}
    </update>
    
    <delete id="deleteUser">
        delete from mybatis.user where id = #{id}
    </delete>

</mapper>

application.yml中配置 Mybatis

mybatis:
  type-aliases-package: com.dt.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml

创建一个 UserController 类

@RestController
public class UserController {

    //这里省略了service层,是为了方便教学
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/queryUserList")
    public List<User> queryUserList() {
        return userMapper.queryUserList();
    }

    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id") int id) {
        return userMapper.queryUserById(id);
    }

    @GetMapping("/addUser")
    public String addUser(User user) {
        userMapper.addUser(new User(6, "小明", "123456"));
        return "add-ok";
    }

    @GetMapping("/updateUser")
    public String updateUser(User user) {
        userMapper.updateUser(new User(6, "大明", "654321"));
        return "update-ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id) {
        userMapper.deleteUser(id);
        return "delete-ok";
    }

}

测试是否成功

10、SpringSecurity

10.1、SpringSecurity简介

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

记住几个类:

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

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

“认证”(Authentication)

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

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

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

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

参考官网:https://spring.io/projects/spring-security,查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/reference/servlet/configuration/java.html

web安全简介

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

市面上存在比较有名的:Shiro,Spring Security !

Shiro 与 Spring Security:相似,除了类不一样,名字不一样

作用:认证,授权

10.2、SpringSecurity环境搭建

  1. 创建一个新项目springboot-06-springsecurity,添加web模块、Thymeleaf模块、SpringSecurity模块

    image-20220510085750172

    或者自己添加依赖:

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
    
  2. 导入静态资源:

    资源地址:https://gitee.com/KyDestroy/spring-boot-study/tree/master

    image-20220510091446703

    注意:由于semantic-ui官网域名改变,html中引用的js链接

    https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css一律改成https://cdn.bootcdn.net/ajax/libs/semantic-ui/2.4.1/semantic.min.css

  3. application.properties中关闭模板引擎的缓存,方便调试

    spring.thymeleaf.cache=false
    
  4. 创建一个 RouterController 类

    @Controller
    public class RouterController {
    
        @RequestMapping({"/","/index"})
        public String index() {
            return "index";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin() {
            return "views/login";
        }
    
        //通过动态传参整合了9个页面的跳转
        @RequestMapping("/level{num}/{id}")
        public String level(@PathVariable("num") int num, @PathVariable("id") int id) {
            return "views/level" + num + "/" + id;
        }
    
    }
    

10.3、认证授权和认证

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能

  1. 确保引入 Spring Security 模块

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 编写 Spring Security 配置类

    • 可以参考https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#:~:text=The%20custom%20DSL%20can%20then%20be%20used%20like%20this%3A来编写

    • 需要基础WebSecurityConfigurerAdapter并重写configure(HttpSecurity http)configure(AuthenticationManagerBuilder auth)方法,使用方法可以直接点开源码查看注释

    @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();
        }
    
        //认证
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //这些数据正常应该从数据库中读
            //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
            //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
            //spring security 官方推荐的是使用 bcrypt加密方式
            auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("dt1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
                    .and()
                    .withUser("dt2").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                    .and()
                    .withUser("dt3").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3");
        }
    }
    
  3. 测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!

  4. 如果需要用数据库来配置认证,就如官网的说明

    image-20220512084637565

10.4、注销及权限控制

10.4.1、注销

  1. 在 SecurityConfig 类里设置注销

    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    
        //开启了注销功能,跳到首页
        http.logout().logoutSuccessUrl("/");
    }
    
  2. 在 index.html 页面添加以下代码

    <!--注销-->
    <a class="item" th:href="@{/logout}">
        <i class="sign-out icon"></i> 注销
    </a>
    
    image-20220512092104082
  3. 测试成功,注销后自动跳回首页

10.4.2、权限控制

  1. 导入 security-thymeleaf 整合包依赖

    <!--security-thymeleaf整合包-->
    <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    注意:高版本 springboot 用的是thymeleaf-extras-springsecurity5而不是thymeleaf-extras-springsecurity4

  2. 修改 index.html 导航栏

    • 导入命名空间

      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      
    • 增加认证判断

      <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
          <div class="ui secondary menu">
              <a class="item"  th:href="@{/index}">首页</a>
      
              <!--登录注销-->
              <div class="right menu">
      
                  <!--如果未登录-->
                  <div sec:authorize="!isAuthenticated()">
                      <a class="item" th:href="@{/toLogin}">
                          <i class="address card icon"></i> 登录
                      </a>
                  </div>
      
                  <!--如果登录:用户名-->
                  <div sec:authorize="isAuthenticated()">
                      <a class="item">
                          用户名:<span sec:authentication="principal.username"></span>&nbsp;
                          角色:<span sec:authentication="principal.authorities"></span>
                      </a>
                  </div>
      
                  <!--如果登录:注销-->
                  <div sec:authorize="isAuthenticated()">
                      <a class="item" th:href="@{/logout}">
                          <i class="sign-out icon"></i> 注销
                      </a>
                  </div>
      
              </div>
          </div>
      </div>
      
    • 角色功能块认证 sec:authorize="hasRole('xxx')"

      <!--菜单根据用户的角色动态的实现-->
      <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>
      
  3. SecurityConfig 配置类

    //防止网站攻击,关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
    http.csrf().disable();
    
    //开启了注销功能,跳到首页
    http.logout().logoutSuccessUrl("/");
    
  4. 测试

    • vip1

      image-20220512124740791

    • vip2

      image-20220512124607498

    • vip3

      image-20220512124512196

10.5、记住我及首页定制

10.5.1、"记住我"功能实现

  1. SecurityConfig 配置类

    • 在 SecurityConfig 配置类中的 configure 方法中定义方法
    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    
        //开启 记住我 功能
        http.rememberMe();
    }
    
  2. 运行测试

    image-20220512125749081

    • 登录时点击记住我功能,之后关闭浏览器,然后重新打开浏览器访问,发现用户依旧存在,因为spring security帮我们将登录信息存储在Cookie中14天!!!
    • 点击注销时,spring security 帮我们自动删除了这个 Cookie!

10.5.2、定制登录页功能实现

  1. SecurityConfig 配置类

    • 在 SecurityConfig 配置类中的 configure 方法中定义方法
    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    
        //没有权限默认会到登录页面,需要开启登录的页面
        //loginPage() 定制登录页
        http.formLogin().loginPage("/toLogin");
    
        ...
    }
    
  2. login.html更改

    • 表单的账号密码部分
    <form th:action="@{/login}" method="post">
        <div class="field">
            <label>Username</label>
            <div class="ui left icon input">
                <input type="text" placeholder="Username" name="username">
                <i class="user icon"></i>
            </div>
        </div>
        <div class="field">
            <label>Password</label>
            <div class="ui left icon input">
                <input type="password" name="password">
                <i class="lock icon"></i>
            </div>
        </div>
        <div class="field">
            <div class="ui checkbox">
                <input type="checkbox" name="remember">
                <label>记住我</label>
            </div>
        </div>
        <input type="submit" class="ui blue submit button"/>
    </form>
    
  3. 修改 SecurityConfig 配置类

    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    
        //没有权限默认会到登录页面,需要开启登录的页面
        //loginPage() 定制登录页
        //loginProcessingUrl() 处理表单请求
        //usernameParameter() 指定接收的用户名参数名
        //passwordParameter() 指定接收的密码参数名
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/toLogin")
                .loginProcessingUrl("/login");
    
        ...
    
        //开启 记住我 功能
        //rememberMeParameter() 指定接收的记住我参数名
        http.rememberMe().rememberMeParameter("remember");
    }
    
  4. 测试

    • 登录页面

      image-20220512134530016

    • 登录成功后进入首页

      image-20220512134545516

总结:Spring Security 的使用多看源码

11、Shiro

11.1、Shiro简介

11.1.1、什么是Shiro

  • Apache Shiro是一个Java 的安全(权限)框架。
  • Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
  • Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等.
  • 下载地址: http://shiro.apache.org/

在这里插入图片描述

11.1.2、Shiro的功能

在这里插入图片描述

  • Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
  • Session Manager: 会话管理,即用户登录后就是第1次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
  • Cryptography: 加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
  • Web Support: Web支持,可以非常容易的集成到Web环境;
  • Caching: 缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
  • Concurrency: Shiro支持多线程应用的并发验证,即,如在一个线程中开启另-一个线程,能把权限自动的传 播过去
  • Testing:提供测试支持;
  • RunAs:允许一个用户假装为另-一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后, 下次再来的话不用登录了

11.1.3、Shiro架构

  1. 外部结构

    从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:

    在这里插入图片描述

    • subject: 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject, Subject代表了当前的用户,这个用户不-定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager; Subject其实是一一个门面, SecurityManageer 才是 实际的执行者
    • SecurityManager: 安全管理器,即所有与安全有关的操作都会与SercurityManager交互, 并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
    • Realm: Shiro从Realm获取安全数据 (如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看DataSource
  2. 内部结构

    在这里插入图片描述

    • Subject: 任何可以与应用交互的用户;
    • Security Manager:相当于SpringMVC中的DispatcherSerlet; 是Shiro的心脏, 所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject, 且负责进行认证,授权,会话,及缓存的管理。
    • Authenticator:负责Subject认证, 是-一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
    • Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中 的那些功能;
    • Realm: 可以有-一个或者多个的realm, 可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以- -般在应用中都需要实现自己的realm
    • SessionManager:管理Session生 命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
    • CacheManager: 缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
    • Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密, 解密等

11.2、Shiro快速开始

11.2.1、准备工作

官方项目路径:shiro/pom.xml at main · apache/shiro · GitHub

  1. 创建一个普通的maven项目,然后删除src目录,创建一个新model——>hello-shiro

    image-20220606145144253

  2. pom.xml

    在这里插入图片描述

    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.9.0</version>
        </dependency>
    
        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.36</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.2</version>
        </dependency>
    </dependencies>
    
  3. log4j2.xmlshiro.ini

    在这里插入图片描述

    • log4j2.xml

      <Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
          <Appenders>
              <Console name="Console" target="SYSTEM_OUT">
                  <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
              </Console>
          </Appenders>
          <Loggers>
              <Logger name="org.springframework" level="warn" additivity="false">
                  <AppenderRef ref="Console"/>
              </Logger>
              <Logger name="org.apache" level="warn" additivity="false">
                  <AppenderRef ref="Console"/>
              </Logger>
              <Logger name="net.sf.ehcache" level="warn" additivity="false">
                  <AppenderRef ref="Console"/>
              </Logger>
              <Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
                  <AppenderRef ref="Console"/>
              </Logger>
              <Root level="info">
                  <AppenderRef ref="Console"/>
              </Root>
          </Loggers>
      </Configuration>
      
    • shiro.ini

      [users]
      # user 'root' with password 'secret' and the 'admin' role
      root = secret, admin
      # user 'guest' with the password 'guest' and the 'guest' role
      guest = guest, guest
      # user 'presidentskroob' with password '12345' ("That's the same combination on
      # my luggage!!!" ;)), and role 'president'
      presidentskroob = 12345, president
      # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
      darkhelmet = ludicrousspeed, darklord, schwartz
      # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
      lonestarr = vespa, goodguy, schwartz
      
      # -----------------------------------------------------------------------------
      # Roles with assigned permissions
      # 
      # Each line conforms to the format defined in the
      # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
      # -----------------------------------------------------------------------------
      [roles]
      # 'admin' role has all permissions, indicated by the wildcard '*'
      admin = *
      # The 'schwartz' role can do anything (*) with any lightsaber:
      schwartz = lightsaber:*
      # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
      # license plate 'eagle5' (instance specific id)
      goodguy = winnebago:drive:eagle5
      
  4. java包下的Quickstart运行类

    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.realm.text.IniRealm;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.subject.Subject;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    
    public class Quickstart {
    
        private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
    
        public static void main(String[] args) {
    
            DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
            IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
            defaultSecurityManager.setRealm(iniRealm);
            SecurityUtils.setSecurityManager(defaultSecurityManager);
    
            // 通过 SecurityUtils 获得当前用户的对象 subject
            Subject currentUser = SecurityUtils.getSubject();
    
            // 通过当前用户拿到Session
            Session session = currentUser.getSession();
            session.setAttribute("someKey", "aValue");
            String value = (String) session.getAttribute("someKey");
            if (value.equals("aValue")) {
                log.info("Subject=>session [" + value + "]");
            }
    
            // 判断当前的用户是否被认证
            if (!currentUser.isAuthenticated()) {
                // Token:令牌
                UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
                token.setRememberMe(true); //设置记住我
                try {
                    currentUser.login(token); //执行登录操作
                } catch (UnknownAccountException uae) {
                    log.info("There is no user with username of " + token.getPrincipal());
                } catch (IncorrectCredentialsException ice) {
                    log.info("Password for account " + token.getPrincipal() + " was incorrect!");
                } catch (LockedAccountException lae) {
                    log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                            "Please contact your administrator to unlock it.");
                }
                // ... catch more exceptions here (maybe custom ones specific to your application?
                catch (AuthenticationException ae) {
                    //unexpected condition?  error?
                }
            }
    
            // 打印他们的识别主体(在本例中为用户名):
            log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
    
            //test a role:
            if (currentUser.hasRole("schwartz")) {
                log.info("May the Schwartz be with you!");
            } else {
                log.info("Hello, mere mortal.");
            }
    
            // 粗粒度
            //test a typed permission (not instance-level)
            if (currentUser.isPermitted("lightsaber:wield")) {
                log.info("You may use a lightsaber ring.  Use it wisely.");
            } else {
                log.info("Sorry, lightsaber rings are for schwartz masters only.");
            }
    
            // 细粒度
            //a (very powerful) Instance Level permission:
            if (currentUser.isPermitted("winnebago:drive:eagle5")) {
                log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                        "Here are the keys - have fun!");
            } else {
                log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
            }
    
            // 注销
            //all done - log out!
            currentUser.logout();
    
            // 结束
            System.exit(0);
        }
    }
    
  5. 运行结果

    image-20220606161230055

11.2.2、Quickstart类简述原理

  • 将在SpringBoot中集成~

    //1、获取当前用户的对象 Subject
    Subject currentUser = SecurityUtils.getSubject();
    //2、通过当前用户拿到Session
    Session session = currentUser.getSession();
    //3、判断当前的用户是否被认证
    currentUser.isAuthenticated()
    //4、获得当前用户的认证
    currentUser.getPrincipal()
    //5、当前用户是否有某个角色
    currentUser.hasRole("schwartz")
    //6、根据不同的角色获取不同的权限
    currentUser.isPermitted("lightsaber:wield")
    //7、注销
    currentUser.logout();
    

11.3、SpringBoot整合Shiro环境搭建

11.3.1、基础配置

  1. 新建SpringBoot项目,勾选webthymeleaf

    image-20220606180109887

  2. templates下新建index.html

    <!DOCTYPE html>
    <html lang="en"
          xmlns:th="https://www.thymeleaf.org"
          xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    </body>
    </html>
    
  3. controller包下新建MyController

    package com.dt.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /**
     * Created with IntelliJ IDEA.
     *
     * @author Dt
     * @Project springboot-08-shiro
     * @Title: MyController
     * @Package com.dt.controller
     * @Description: TODO
     * @date 2022/6/6 22:08
     */
    @Controller
    public class MyController {
    
        @RequestMapping({"/", "/index"})
        public String toIndex(Model model) {
            model.addAttribute("msg", "Hello,Shiro!");
            return "index";
        }
    }
    
  4. 测试

    image-20220606222145202

11.3.2、测试

  1. shiro-spring的maven包

    <!--Shiro整合Spring的包-->
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.9.0</version>
    </dependency>
    
  2. 配置类

    image-20220606224249770

    • UserRealm

      package com.dt.config;
      
      import org.apache.shiro.authc.AuthenticationException;
      import org.apache.shiro.authc.AuthenticationInfo;
      import org.apache.shiro.authc.AuthenticationToken;
      import org.apache.shiro.authz.AuthorizationInfo;
      import org.apache.shiro.realm.AuthorizingRealm;
      import org.apache.shiro.subject.PrincipalCollection;
      
      /**
       * Created with IntelliJ IDEA.
       *
       * @author Dt
       * @Project springboot-08-shiro
       * @Title: UserRealm
       * @Package com.dt.config
       * @Description: TODO
       * @date 2022/6/6 22:38
       */
      // 自定义的Realm
      public class UserRealm extends AuthorizingRealm {
          // 授权
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              System.out.println("执行了=>授权doGetAuthorizationInfo");
              return null;
          }
      
          // 认证
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
              System.out.println("执行了=>认证doGetAuthorizationInfo");
              return null;
          }
      }
      
    • ShiroConfig

      package com.dt.config;
      
      import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
      import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * Created with IntelliJ IDEA.
       *
       * @author Dt
       * @Project springboot-08-shiro
       * @Title: ShiroConfig
       * @Package com.dt.config
       * @Description: TODO
       * @date 2022/6/6 22:33
       */
      @Configuration
      public class ShiroConfig {
      
          //ShiroFilterFactoryBean 第三步
          @Bean
          public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
              ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
              // 设置安全管理器
              bean.setSecurityManager(defaultWebSecurityManager);
              return bean;
          }
      
          //DefaultWebSecurityManager 第二步
          @Bean(name = "securityManager")
          public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
              DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
              // 关联UserRealm
              securityManager.setRealm(userRealm);
              return securityManager;
          }
      
          //创建 realm 对象,需要自定义 第一步
          @Bean
          public UserRealm userRealm() {
              return new UserRealm();
          }
      
      }
      
  3. 导入静态资源

    image-20220606225834370

    • add 和 update页面只有一个普通的h1标签

    • index.html

      <!DOCTYPE html>
      <html lang="en"
            xmlns:th="https://www.thymeleaf.org"
            xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
      
      <h1>首页</h1>
      <p th:text="${msg}"></p>
      
      <a th:href="@{/user/add}">add</a> &nbsp;|&nbsp;
      <a th:href="@{/user/update}">update</a>
      </body>
      </html>
      
  4. MyController控制类添加跳转

    @RequestMapping("/user/add")
    public String add() {
        return "user/add";
    }
    
    @RequestMapping("/user/update")
    public String update() {
        return "user/update";
    }
    
  5. 测试

    image-20220606230756341

  • 可以正常地进行页面跳转!为后面做准备

11.4、Shiro实现登录拦截

具体实现

  1. ShiroConfig类

    • 在 ShiroFilterFactoryBean 方法中加入下面代码

              // 添加Shiro的内置过滤器
              /*
                  anno:无需认证就可以访问
                  authc:必须认证才能访问
                  user:必须拥有 记住我 功能才能用
                  perms:拥有对某个资源的权限才能访问
                  role:拥有某个角色权限才能访问
              */
              Map<String, String> filterMap = new LinkedHashMap<>();
      
      //        filterMap.put("/user/add", "authc");
      //        filterMap.put("/user/update", "authc");
              filterMap.put("/user/*", "authc");
      
              bean.setFilterChainDefinitionMap(filterMap);
      
              // 设置登录的请求
              bean.setLoginUrl("/toLogin");
      
  2. MyController类中添加跳转事务

    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
    
  3. 在templates包下添加login页面

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <h1>登录</h1>
    <hr>
    <form action="">
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="password" name="password"></p>
        <p><input type="submit"></p>
    </form>
    
    </body>
    </html>
    
  4. 测试

    • 点击update时

      image-20220613210011913

    • 页面被拦截,回到登录页面

      image-20220613210038556

11.5、Shiro实现用户认证

实现流程

  1. UserRealm类

    • 认证方法

      // 认证
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
          System.out.println("执行了=>认证doGetAuthorizationInfo");
      
          // 用户名,密码 可以数据库中取
          String name = "root";
          String password = "123456";
      
          UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
          if (!userToken.getUsername().equals(name)) {
              return null; // 抛出异常    UnknownAccountException
          }
      
          // 密码认证,shiro做
      
          return new SimpleAuthenticationInfo("",password,"");
      }
      
  2. MyController页面跳转类

    @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); // 执行登录方法,如果没有异常就说明ok了
            return "index";
        } catch (UnknownAccountException e) {   // 用户名不存在
            model.addAttribute("msg", "用户名不存在");
            return "login";
        } catch (IncorrectCredentialsException e) { // 密码错误
            model.addAttribute("msg", "密码错误");
            return "login";
        }
    }
    
  3. 更改login.html

    <!DOCTYPE html>
    <html lang="en"
          xmlns:th="https://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <h1>登录</h1>
    <hr>
    <p th:text="${msg}" style="color: red"></p>
    <form th:action="@{/login}">
        <p>用户名:<input type="text" name="username"></p>
        <p>密码:<input type="password" name="password"></p>
        <p><input type="submit"></p>
    </form>
    
    </body>
    </html>
    
  4. 测试

    • 当用户成功登录后才有权限使用addupdate功能

      image-20220613214239121

      原理:调用 Shiro 中的认证功能!

11.6、Shiro整合Mybatis

11.6.1、项目框架

  • 项目结构

    image-20220614011138665

  • 数据库的user表

    image-20220614011248534

11.6.2、项目实现流程

  1. maven导入包

    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.9</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
  2. application.properties配置别名mapper位置

    mybatis.type-aliases-package=com.dt.pojo
    mybatis.mapper-locations=classpath:mapper/*.xml
    
  3. application.yml配置 druid

    spring:
      datasource:
        username: root
        password: 123456
        # &serverTimezone=UTC 解决时区的报错
        url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
        # com.mysql.jdbc.Driver MySQL5.0+
        # com.mysql.cj.jdbc.Driver MySQL8.0+兼容5.0+
        driver-class-name: com.mysql.cj.jdbc.Driver
        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
    
  4. POJO层

    • 实体类User
    package com.dt.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String name;
        private String pwd;
    }
    
  5. Mapper层

    • UserMapper接口类
    package com.dt.mapper;
    
    import com.dt.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    @Mapper
    public interface UserMapper {
    
        User queryUserByName(String name);
    
    }
    
    • UserMapper接口实现类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.dt.mapper.UserMapper">
    
        <select id="queryUserByName" resultType="User">
            select * from mybatis.user where name = #{name}
        </select>
    
    </mapper>
    
  6. Service层

    • UserService业务接口类
    package com.dt.service;
    
    import com.dt.pojo.User;
    
    public interface UserService {
    
        User queryUserByName(String name);
    
    }
    
    • UserService业务接口实现类UserServiceImpl
    package com.dt.service;
    
    import com.dt.mapper.UserMapper;
    import com.dt.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        UserMapper userMapper;
    
        @Override
        public User queryUserByName(String name) {
            return userMapper.queryUserByName(name);
        }
    }
    
  7. UserRealm类

    package com.dt.config;
    
    import com.dt.pojo.User;
    import com.dt.service.UserService;
    import com.dt.service.UserServiceImpl;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.subject.Subject;
    import org.springframework.beans.factory.annotation.Autowired;
    
    // 自定义的Realm
    public class UserRealm extends AuthorizingRealm {
    
        @Autowired
        UserServiceImpl userService;
    
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行了=>授权doGetAuthorizationInfo");
            return null;
        }
    
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("执行了=>认证doGetAuthorizationInfo");
    
            UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
            // 连接真实数据库
            User user = userService.queryUserByName(userToken.getUsername());
            if (user == null) {
                return null;    // UnknownAccountException
            }
    
            // MD5 及 MD5盐值加密
            // 密码认证,shiro做,加密了
            return new SimpleAuthenticationInfo("", user.getPwd(), "");
        }
    }
    
  8. 测试

    package com.dt;
    
    import com.dt.pojo.User;
    import com.dt.service.UserServiceImpl;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class ShiroSpringbootApplicationTests {
    
        @Autowired
        UserServiceImpl userService;
    
        @Test
        void contextLoads() {
            User user = userService.queryUserByName("罗翔");
            System.out.println(user);
        }
    
    }
    

    image-20220614012138737

    成功连接上数据库

11.7、Shiro请求授权实现

实现流程

  1. 数据库层面

    • 添加多一个字段,用来存储用户的授权

      image-20220614122424643

      image-20220614122715431

      注意:应该是 perms

    • 项目所要用的数据库

      image-20220614123400672

  2. 实体类User添加新字段

    package com.dt.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String name;
        private String pwd;
        private String perms;
    }
    
  3. ShiroConfig配置类

    • ShiroFilterFactoryBean 方法

      @Bean
          public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
              ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
              // 设置安全管理器
              bean.setSecurityManager(defaultWebSecurityManager);
      
              // 添加Shiro的内置过滤器
              /*
                  anno:无需认证就可以访问
                  authc:必须认证才能访问
                  user:必须拥有 记住我 功能才能用
                  perms:拥有对某个资源的权限才能访问
                  role:拥有某个角色权限才能访问
              */
              Map<String, String> filterMap = new LinkedHashMap<>();
      
              // 权限授权 正常的情况下,没有授权会跳转到未授权页面
              filterMap.put("/user/add", "perms[user:add]");
              filterMap.put("/user/update", "perms[user:update]");
      
      //        filterMap.put("/user/add", "authc");
      //        filterMap.put("/user/update", "authc");
              // 拦截
              filterMap.put("/user/*", "authc");
              bean.setFilterChainDefinitionMap(filterMap);
      
              // 设置登录的请求
              bean.setLoginUrl("/toLogin");
              // 设置未授权页面
              bean.setUnauthorizedUrl("/unauth");
      
              return bean;
          }
      
    • 只要添加授权代码

      // 权限授权 正常的情况下,没有授权会跳转到未授权页面
      filterMap.put("/user/add", "perms[user:add]");
      filterMap.put("/user/update", "perms[user:update]");
      
      // 设置未授权页面
      bean.setUnauthorizedUrl("/unauth");
      
  4. UserRealm配置类

    • 认证方法中返回值修改

      // MD5 及 MD5盐值加密
      // 密码认证,shiro做,加密了
      return new SimpleAuthenticationInfo(user, user.getPwd(), "");
      
    • 授权方法修改

      // 授权
          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
              System.out.println("执行了=>授权doGetAuthorizationInfo");
      
              SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
      //        info.addStringPermission("user:add");
      
              // 拿到当前登录的这个对象
              Subject subject = SecurityUtils.getSubject();
              User currentUser = (User) subject.getPrincipal(); //拿到User对象
      
              // 设置当前用户的权限
              info.addStringPermission(currentUser.getPerms());
      
              return info;
          }
      
  5. MyController类设置无授权处理

    @RequestMapping("/unauth")
    @ResponseBody
    public String unauthorized() {
        return "未经授权无法访问此页面";
    }
    
  6. 测试

    以 KyDestroy 为例

    • 进入add界面成功

      image-20220614135843319

    • 进入update界面失败

      image-20220614135856291

11.8、Shiro整合Thymeleaf

实现流程

  1. 导入shiro-thymeleaf整合包

    <!--shiro-thymeleaf整合-->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.1.0</version>
    </dependency>
    
  2. ShiroConfig类

    //整合ShiroDialect:用来整合shiro thymeleaf
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
    
  3. 修改登陆页面index.html

    <!DOCTYPE html>
    <html lang="en"
          xmlns:th="https://www.thymeleaf.org"
          xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    
    <div shiro:notAuthenticated>
        <a th:href="@{/toLogin}">登录</a>
    </div>
    
    <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>
    
  4. 测试

    • 没登录的状态下

      image-20220614142408284

    • 拥有add权限登录的状态下

      image-20220614142448116

    • 拥有update权限登录的状态下

      image-20220614142536526

12、Swagger

学习目标:

  • 了解Swagger的作用和概念
  • 了解前后端分离
  • 在SpringBoot中集成Swagger

12.1、Swagger简介

image-20220615010616510

1、前后端分离:

  • Vue + Springboot 开发模式
  • 后端时代:前端只用管理静态页面;html—>后端。模板引擎 JSP—>后端是主力

2、前后端分离时代:

  • 前端 :前端控制层、视图层【前端团队】
    • 伪造后端数据,json。已经存在了,不需要后端,前端工程依旧能跑起来
  • 后端:后端控制层、服务层、数据访问层【后端团队】
  • 前后端通过API进行交互【交互方式】
  • 前后端相对独立且松耦合;
  • 前后端甚至可以部署在不同的服务器上

3、产生的问题:

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

4、解决方案

  • 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
  • 早些年:指定word计划文档
  • 前后端分离:
    • 前端测试后端接口:postman
    • 后端提供接口,需要实施更新最新的消息及改动!

5、Swagger诞生

  • 号称世界上最流行的API框架
  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
  • 直接运行,在线测试API
  • 支持多种语言 (如:Java,PHP等)
  • 官网:https://swagger.io/

12.2、第一个Swagger程序

12.2.1、前期准备

SpringBoot集成Swagger => springfox,两个jar包

  • Springfox-swagger2

  • swagger-springmvc

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>
    

    注意:3.0用springfox-boot-starter

12.2.2、项目测试

  1. 创建一个普通的SpringBoot项目,支持web应用

  2. 添加Maven依赖

  3. 编写HelloController,测试确保运行成功!

    package com.dt.swagger.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String toHello() {
            return "Hello";
        }
    
    }
    
  4. 配置Swagger ==》Config

    • SwaggerConfig

      package com.dt.swagger.config;
      
      import org.springframework.context.annotation.Configuration;
      import springfox.documentation.swagger2.annotations.EnableSwagger2;
      
      @Configuration //配置类
      @EnableSwagger2 //开启Swagger2的自动配置
      public class SwaggerConfig {
      }
      
  5. 需要在application.properties中配置

    spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    
  6. 在运行主类添加@EnableOpenApi

    image-20220615015739219

  7. 测试运行:http://localhost:8080/swagger-ui/index.html (Swagger3.0版本的访问链接)

    image-20220615020356419

12.3、Swagger的配置

12.3.1、配置基本页面

  1. SwaggerConfig配置类

    • Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger

    • Docket 实例关联上 apiInfo()

      // 配置了Swagger的Docket的bean实例
      @Bean
      public Docket docket() {
          return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
      }
      
    • 可以通过apiInfo()属性配置文档信息

      // 配置Swagger信息 apiInfo
      public ApiInfo apiInfo() {
          // 作者信息
          Contact contact = new Contact("Dt", "https://gitee.com/KyDestroy", "746294093@qq.com");
          return new ApiInfo(
                  "Dt的SwaggerAPI文档",
                  "正经人谁写文档",
                  "v1.0",
                  "https://gitee.com/KyDestroy",
                  contact,
                  "Apache 2.0",
                  "http://www.apache.org/licenses/LICENSE-2.0",
                  new ArrayList());
      }
      
  2. 测试访问:http://localhost:8080/swagger-ui/index.html

    image-20220615110106777

12.3.2、配置扫描接口

基本方法

  1. SwaggerConfig配置类

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                //enable设置是否启动Swagger
                .enable(false)
                //通过.select()方法,去配置扫描接口
                .select()
                //RequestHandlerSelectors:配置要扫描接口的方式
                .apis(RequestHandlerSelectors.basePackage("com.dt.swagger.controller"))
                //paths() 过滤什么路径,即这里只扫描请求以/dt开头的接口
                .paths(PathSelectors.ant("/dt/**"))
                .build();
    }
    
    • RequestHandlerSelectors配置的一些方法

      basePackage() //指定要扫描的包路径
      any() //扫描全部
      none() //都不扫描
      withClassAnnotation() //扫描方法上的注解,如withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
      withMethodAnnotation() //扫描类上的注解,参数数注解的反射对象,如withMethodAnnotation(GetMapping.class)只扫描get请求
      
    • PathSelectors配置的一些方法

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

12.3.3、配置Swagger开关

Swagger在生产环境中使用,在发布的时候不使用

  • 判断是不是生产环境 flag=false

  • 注入enable(flag)

  • 创建 dev(生产环境)和 prod(发布环境)的配置文件

    application-dev.properties

    spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    

    application-prod.properties

    spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    

    application.properties中选择环境

    spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    # 选择配置环境
    spring.profiles.active=dev
    
  • 修改SwaggerConfig配置类

    @Bean
        public Docket docket(Environment environment) {
    
            // 设置要显示的Swagger环境
            Profiles profiles = Profiles.of("dev");
            // 获取项目的环境,通过environment.acceptsProfiles判断是否处于设定的环境中
            boolean flag = environment.acceptsProfiles(profiles);
    
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                	//通过flag来开启或关闭Swagger
                    .enable(flag)
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.dt.swagger.controller"))
                    .build();
        }
    

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

测试:http://localhost:8080/swagger-ui/index.html

  • 环境为 dev 时,开启Swagger

    image-20220615145645798
  • 环境为 prod 时,关闭Swagger

    image-20220615145751429

12.3.4、配置API分组

实现步骤

  1. 在SwaggerConfig配置类的docket方法中

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

    @Bean
    public Docket docket(Environment environment) {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
          .groupName("hello") // 配置分组
           // 省略配置....
    }
    
  2. 配置多个分组方法

    • 配置多个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");
      }
      
  3. 测试:http://localhost:8080/swagger-ui/index.html

    image-20220615152002694

12.3.5、常见注解

注解的简单说明

  • Swagger的所有注解定义在io.swagger.annotations包下(下面是常见的)

    Swagger注解 简单说明
    @Api(tags = "xxx模块说明") 作用在模块类上
    @ApiOperation("xxx接口说明") 作用在接口方法上
    @ApiModel("xxxPOJO说明") 作用在模型类上:如VO、BO
    @ApiModelProperty(value = "xxx属性说明",hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
    @ApiParam("xxx参数说明") 作用在参数、方法和字段上,类似@ApiModelProperty

举例讲解上述五个注解

  1. HelloController页面控制跳转类

    package com.dt.swagger.controller;
    
    import com.dt.swagger.pojo.User;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.annotations.ApiParam;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @Api(tags = "Hello控制类")
    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello";
        }
    
        // 只要我们的接口中,存在实体类,他就会被扫描到Swagger中
        @PostMapping("/user")
        public User User() {
            return new User();
        }
    
        @ApiOperation("接口方法")
        @GetMapping("/hello2")
        public String hello2(@ApiParam("用户名") String name) {
            return "Hello" + name;
        }
    
    }
    
  2. User类

    package com.dt.swagger.pojo;
    
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    
    @ApiModel("用户实体类")
    public class User {
    
        @ApiModelProperty("用户名")
        public String username;
        @ApiModelProperty("密码")
        public String password;
    
    }
    
  3. 测试

    • 模块类中image-20220615160320875

    • 实体类模型中

      image-20220615160407351

12.3.6、拓展

测试数据

  • 可以在Swagger界面测试请求
  1. 测试前准备

    • HelloController页面控制跳转类

      @ApiOperation("Post测试方法")
      @PostMapping("/post")
      public User post(User user) {
          return user;
      }
      
    • User类中添加Set方法

      package com.dt.swagger.pojo;
      
      import io.swagger.annotations.ApiModel;
      import io.swagger.annotations.ApiModelProperty;
      
      @ApiModel("用户实体类")
      public class User {
      
          @ApiModelProperty("用户名")
          public String username;
          @ApiModelProperty("密码")
          public String password;
      
          public void setUsername(String username) {
              this.username = username;
          }
      
          public void setPassword(String password) {
              this.password = password;
          }
      }
      
  2. 测试步骤

    ①点击Try it outimage-20220615162642528

    ②输入伪造的数据image-20220615162745284

    ③发现响应体中成功传入数据image-20220615162832973

皮肤

  1. 默认的 访问 http://localhost:8080/swagger-ui.html
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
</dependency>

image-20220310143850263

  1. bootstrap-ui 访问 http://localhost:8080/doc.html
<!-- 引入swagger-bootstrap-ui包 /doc.html-->
<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>swagger-bootstrap-ui</artifactId>
   <version>1.9.1</version>
</dependency>

image-20220310143917288

  1. Layui-ui 访问 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
   <groupId>com.github.caspar-chen</groupId>
   <artifactId>swagger-ui-layer</artifactId>
   <version>1.1.3</version>
</dependency>

image-20220310143943192

  1. mg-ui 访问 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
   <groupId>com.zyplayer</groupId>
   <artifactId>swagger-mg-ui</artifactId>
   <version>1.0.6</version>
</dependency>

image-20220310144005581

12.4、Swagger总结

  1. 我们可以通过Swagger给一些比较难理解的属性或者接口,增加注释信息
  2. 接口文档要实时更新
  3. 可以在线测试

Swagger是一个优秀的工具,几乎所有大公司都有使用它!

【注意点】在正式发布的时候,关闭Swagger!!!处于安全考虑,也同时节省运行内存!!!

13、异步、邮件、定时任务

13.1、异步任务

为什么要使用异步处理任务?

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

两个注解:

  • @EnableAsync //SpringBoot启动类
  • @Async //执行方法

测试

  1. 在业务层 service 中定义AsyncService类

    package com.dt.service;
    
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    
    @Service
    public class AsyncService {
    
        //告诉Spring这是一个异步方法
        @Async
        public void hello() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("数据正在处理....");
        }
    
    }
    
  2. 在控制层 controller 中定义AsyncController控制类

    package com.dt.controller;
    
    import com.dt.service.AsyncService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class AsyncController {
    
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping("/hello")
        public String hello() {
            asyncService.hello(); //停止三秒
            return "OK";
        }
    
    }
    
  3. 在主启动类 Springboor09TestApplication 添加开启异步测试

    package com.dt;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableAsync;
    
    @EnableAsync // 开启异步注解功能
    @SpringBootApplication
    public class Sprintboot09TestApplication {
    
       public static void main(String[] args) {
          SpringApplication.run(Sprintboot09TestApplication.class, args);
       }
    
    }
    
  4. 测试:http://localhost:8080/hello

    • 网页瞬间响应,后台代码依旧执行

13.2、邮件任务

13.2.1、概述

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

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

13.2.2、使用步骤

  1. 导入spring-boot-starter-mail依赖

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    
  2. 配置文件

    application.yaml

    spring:
      mail:
        username: 746294093@qq.com
        password: gnbjkeydbntsbdga
        host: smtp.qq.com
        # QQ需要单独开启加密授权验证
        properties:
          mail:
            smtp:
              ssl:
                enable: true
    

    application.properties

    spring.mail.username=746294093@qq.com
    spring.mail.password=gnbjkeydbntsbdga
    spring.mail.host=smtp.qq.com
    # QQ需要单独开启加密授权验证
    spring.mail.properties.mail.smtp.ssl.enable=true
    
    • 获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
  3. 测试类中进行测试

    package com.dt;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.mail.SimpleMailMessage;
    import org.springframework.mail.javamail.JavaMailSenderImpl;
    import org.springframework.mail.javamail.MimeMessageHelper;
    
    import javax.mail.MessagingException;
    import javax.mail.internet.MimeMessage;
    import java.io.File;
    
    @SpringBootTest
    class Sprintboot09TestApplicationTests {
    
       @Autowired
       JavaMailSenderImpl mailSender;
    
       @Test
       void contextLoads() {
          // 一个简单的邮件
          SimpleMailMessage message = new SimpleMailMessage();
    
          message.setSubject("SpringBoot邮件任务(简单邮件)");
          message.setText("Every dog has his day!");
          message.setTo("746294093@qq.com");
          message.setFrom("746294093@qq.com");
    
          mailSender.send(message);
       }
    
       @Test
       void contextLoads2() throws MessagingException {
          // 一个复杂的邮件
          MimeMessage mimeMessage = mailSender.createMimeMessage();
          // 组装
          MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    
          helper.setSubject("SpringBoot邮件任务(复杂邮件)");
          helper.setText("<p style='color:red'>Hello,World</p>", true);
    
          // 发送附件
          helper.addAttachment("1.jpg", new File("C:\\Users\\Administrator\\Pictures\\iTab-g71y17.jpg"));
          helper.addAttachment("2.gif", new File("C:\\Users\\Administrator\\Pictures\\微信图片_20220419124659.gif"));
    
          helper.setTo("746294093@qq.com");
          helper.setFrom("746294093@qq.com");
    
          mailSender.send(mimeMessage);
       }
    
    }
    
  4. 测试:

    • 简单的邮件发送

      image-20220616142119060

    • 复杂的邮件发送

      image-20220616142147134

    查看邮箱,邮件接收成功!

    我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!

拓展:封装成一个方法

@Autowired
JavaMailSenderImpl mailSender;

/**
 *
 * @param html : 是否支持多文本显示
 * @param subject : 标题
 * @param content : 内容
 * @param to : 收件人
 * @param from : 发件人
 * @throws MessagingException
 */
public void setMail(Boolean html, String subject,
               String content, String to, String from) throws MessagingException {
   // 一个复杂的邮件
   MimeMessage mimeMessage = mailSender.createMimeMessage();
   // 组装
   MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, html);

   helper.setSubject(subject);
   helper.setText(content, true);

   // 发送附件
   helper.addAttachment("1.jpg", new File("C:\\Users\\Administrator\\Pictures\\iTab-g71y17.jpg"));
   helper.addAttachment("2.gif", new File("C:\\Users\\Administrator\\Pictures\\微信图片_20220419124659.gif"));

   helper.setTo(to);
   helper.setFrom(from);

   mailSender.send(mimeMessage);
}

13.3、定时执行任务

13.3.1、概述

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

  • TaskExecutor接口
  • TaskScheduler接口

两个注解:

  • @EnableScheduling //SpringBoot启动类
  • @Scheduled //执行方法

13.3.2、使用步骤

  1. ScheduledService业务类

    package com.dt.service;
    
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ScheduledService {
    
        // cron 表达式
        // 秒 分 时 日 月 星期
        @Scheduled(cron = "0 * * * * 0-7")
        public void hello() {
            System.out.println("Hello被执行了");
        }
    
    }
    
  2. 在SpringBoot主启动类中添加注释

    @EnableScheduling //开启定时执行功能
    
  3. 我们来详细了解下cron表达式

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

  4. 常用的表达式

    (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触发
    

14、Dubbo和Zookeeper集成

14.1、分布式理论

14.1.1、什么是分布式系统?

在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

分布式系统(distributed system)是建立在网络之上的软件系统。

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。

14.1.2、Dubbo文档

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。

在Dubbo的官网文档有这样一张图

image-20220311214745570

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

image-20220311214819155

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

image-20220311214936464

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

缺点:公用模块无法重复利用,开发性的浪费

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

image-20220311215008041

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。

image-20220311215040262

14.2、什么是RPC

14.2.1、概述

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;

推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b

14.2.2、RPC基本原理

image-20220311215125107

步骤解析:

image-20220311215143733

RPC两个核心模块:通讯序列化

14.3、Dubbo

14.3.1、什么是dubbo

Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

dubbo官网 http://dubbo.apache.org/zh-cn/index.html

1.了解Dubbo的特性

2.查看官方文档

14.3.2、dubbo基本概念

image-20220311215250368

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

  • 服务容器负责启动,加载,运行服务提供者。

  • 服务提供者在启动时,向注册中心注册自己提供的服务。

  • 服务消费者在启动时,向注册中心订阅自己所需的服务。

  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

14.4、Dubbo环境搭建(Window下安装zookeeper)

点进dubbo官方文档,推荐我们使用Zookeeper 注册中心

下载链接:Apache ZooKeeper

下载版本:3.8.0

image-20220617103517716

  • 注意:3.5版本后要下载bin文件
  1. 解压后打开 conf,将zoo_sample.cfg复制一份改名为zoo.cfg

  2. dataDir=./删掉,加入下列命令

    dataDir=D:\Environment\apache-zookeeper-3.8.0-bin\data
    audit.enable=true
    
  3. 打开bin文件夹的zkServer.cmd(注意要用管理员模式)

    image-20220617104336600
  4. 使用zkCli.cmd测试

    ls /:列出Zookeeper根下保存的所有节点

    [zk: localhost:2181(CONNECTED) 0] ls /
    [zookeeper]
    

    create -e /dt 123:创建一个 /dt 节点,值为123

    image-20220617105722785

    get /dt:获取 /dt 节点的值

    image-20220617105928243

    我们再来看下节点

    image-20220617110033584

14.5、Windows下安装dubbo-admin

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

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

我们这里来安装一下:

  1. 下载dubbo-admin

    地址:https://github.com/apache/dubbo-admin/tree/master-0.2.0

  2. 解压进入目录

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

    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
    # 注册中心的地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    
  3. 在项目目录下打包 dubbo-admin

    mvn clean package -Dmaven.test.skip=true
    
    • 如果打包失败,切换环境变量的jdk版本为1.8
  4. 执行 dubbo/target 下的 dubbo-admin-0.0.1-SNAPSHOT.jar

    java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
    
    • 注意:Zookeeper的服务一定要打开

    执行完毕,我们去访问一下 http://localhost:7001/,这时候我们需要输入登录账号的密码,默认root-root

    image-20220617134953328

    登录成功后,查看界面

    image-20220617135014175

14.6、总结

  1. 小结

    zookeeper:注册中心

    dubbo-admin:一个监控管理后台,查看我们注册了哪些服务,哪些服务被消费了

    Dubbo:jar包

  2. 步骤

    前提:zookeeper服务已经开启!

    1. 提供者提供服务
      1. 导入依赖
      2. 配置注册中心的地址,以及服务发现名,和要扫描的包~
      3. 在想要被注册的服务上面~ 增加一个注解 @DubboService
    2. 消费者如何消费
      1. 导入依赖
      2. 配置注册中心的地址,配置自己的服务名
      3. 从远程注入服务~ @DubboReference

14.7、服务注册与发现实战

14.7.1、步骤

  1. 创建一个空项目

  2. File->New->Module

    image-20220617162846680
  3. 添加web支持

    image-20220617164002159
  4. 删除多余的文件

    image-20220617163034555

  5. 导入依赖

    <!--导入依赖:Dubbo + Zookeeper-->
    <!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.15</version>
    </dependency>
    <!--zkclient-->
    <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
    <dependency>
        <groupId>com.github.sgroschupf</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.1</version>
    </dependency>
    

    注意:dubbo-spring-boot-starter不要用3.0版本以上会报错

    [新版的坑] 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>
    
  6. 项目结构

    image-20220617163555002

    • Service层

      • TicketService

        package com.dt.service;
        
        public interface TicketService {
        
            String getTicket();
        
        }
        
      • TicketServiceImpl

        package com.dt.service;
        
        
        import org.apache.dubbo.config.annotation.DubboService;
        import org.springframework.stereotype.Component;
        
        @Component // 使用了Dubbo后尽量不要用Servic注解是为了与Dubbo的Service区分
        @DubboService //可以被扫描到,在项目启动就自动注册到注册中心
        public class TicketServiceImpl implements TicketService {
            @Override
            public String getTicket() {
                return "Hello Java";
            }
        }
        
        
    • application.properties

      # 应用名称
      spring.application.name=provider-server
      # 应用服务 WEB 访问端口
      server.port=8001
      # 服务应用名字
      dubbo.application.name=provider-server
      # 注册中心地址
      dubbo.registry.address=zookeeper://localhost:2181
      # 哪些服务要被注册
      dubbo.scan.base-packages=com.dt.service
      
  7. 再File->New->Module

    image-20220617163922736
  8. 一样添加添加web支持

  9. 一样删除多余的文件

  10. 给消费者导入依赖(跟生产者一致)

  11. 项目结构

    image-20220617184713734

    • Service层

      • UserService

        package com.dt.service;
        
        import org.apache.dubbo.config.annotation.DubboReference;
        import org.springframework.stereotype.Component;
        
        
        @Component // 放到容器中
        public class UserService {
        
            // 想拿到provider-server提供的票,要去注册中心拿到服务
            @DubboReference // 引用, Pom坐标,可以定义路径相同的接口名
            TicketService ticketService;
        
            public void buyTicket() {
                String ticket = ticketService.getTicket();
                System.out.println("在注册中心拿到=>" + ticket);
            }
        
        
        }
        
      • TicketService

        package com.dt.service;
        
        public interface TicketService {
        
            String getTicket();
        
        }
        
    • application.properties修改端口号

      # 应用名称
      spring.application.name=consumer-server
      # 应用服务 WEB 访问端口
      server.port=8002
      # 消费者去哪里拿服务,需要暴露自己的名字
      dubbo.application.name=consumer-server
      # 注册中心的地址
      dubbo.registry.address=zookeeper://localhost:2181
      
  12. 测试

    • 开启ZooKeeper

      image-20220617185020175
    • 命令行运行dubbo-admin-0.0.1-SNAPSHOT.jar

      java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
      
    • 运行 ProviderServerApplication 启动类(生产者)

    • 访问:http://localhost:7001/governance/providers

      image-20220617185502429

      image-20220617185626795

      我们能发现 TicketServiceImpl已经注册到Dubbo里

    • 运行 ConsumerServerApplicationTests 测试类 (消费者)

      package com.dt;
      
      import com.dt.service.UserService;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      @SpringBootTest
      class ConsumerServerApplicationTests {
      
          @Autowired
          UserService userService;
      
          @Test
          void contextLoads() {
              userService.buyTicket();
          }
      
      }
      
    • 结果

      image-20220617185650502

15、回顾和展望

三层架构 + MVC
	架构 -->解耦

开发框架
    Spring
		IOC		AOP
			IOC : 控制反转
			 	泡温泉,泡茶泡友
				附近的人,打招呼。加微信,聊天,天天聊>约泡
				浴场(容器):温泉,茶庄泡友
				直接进温泉,就有人和你一起了!
原来我们都是自己一步步操作,现在交给容器了!我们需要什么就去拿就可以了
                
             AOP:切面(本质,动态代理)
为了解什么?不影响业本来的情况下,实现动态增加功能,大量应用在日志,事务等等
                    
	Spring是一个轻量级的Java开源框架,容器
        目的:解决企业开发的复杂性问题
        Spring是春天,但配置文件繁琐

    SpringBoot
      	SpringBoot ,新代javaEE的开发标准,开箱即用!>拿过来就可以用,
        它自动帮我们配置了非常多的东西,我们拿来即用,
        特性:约定大于配置!
                    
随着公司体系越来越大,用户越来越多
                    
微服务架构—>新架构
	模块化,功能化!
	用户,支付,签到,娱乐…;
	如果一台服务器解决不了就再增加一台服务器! --横向扩展
	假设A服务器占用98%资源B服务器只占用了10%.–负载均衡;

	将原来的整体项,分成模块化,用户就是一个单独的项目,签到也是一个单独的项目,项目和项目之前需要通信,如何通信
	用户非常多而到十分少给用户多一点服务器,给签到少一点服务器
微服务架构问题?
                    
分布式架构会遇到的四个核心题?
   	1.这么多服务,客户端该如何去访?
    2.这么多服务,服务之间如何进行通信?
    3.这么多服务,如何治理呢?
    4.服务挂了,怎么办?
        
解决方案:                    
	Springcloud是一套生态,就是来解决以上分布式架构的4个问题
    想使用Spring Clould ,必须要掌握 springBoot , 因为Springcloud是基于springBoot ;

	1. spring Cloud NetFlix ,出来了一套解决方案!一站式解决方案。可以直接使用
	Api网关 , zuul组件
	Feign --> Httpclient ---> http通信方式,同步并阻塞
	服务注册与发现 , Eureka
	熔断机制 , Hystrix
                    
2018年年底,NetFlix 宣布无限期停止维护。生态不再维护,就会脱节。

	2. Apache Dubbo zookeeper ,第二套解决方案
	API:没有!要么找第三方组件,要么自己实现
	Dubbo 是一个高性能的基于Java实现的RPC通信框架!2.6.x
	服务注册与发现 , zookeeper :动物管理者 ( Hadoop , Hive )
	没有:借助了Hystrix
                    
不完善,Dubbo

	3. SpringCloud Alibaba 一站式解决方案

目前又提出了新的思路:
	服务网格:也许是下一代维服务标准,Service mesh
	代表解决方案:istio(未来可能需要掌握)

万变不离其宗,一通百通!
	1.API网关 , 服务路由
	2.HTTP,RPC框架,异步调用
	3.服务注册与发现,高可用
	4.熔断机制,服务降级
         
为什么要解决这个问题?因为网络是不可靠的!!!
posted @ 2022-10-08 10:14  D..T  阅读(248)  评论(2编辑  收藏  举报