SpringBoot笔记

本篇文章的是看狂胜的springboot视频总结的笔记

微服务阶段

javase:OOP

mysql:持久化

html+css+js+jquery+框架:视图,框架不熟练,css不好

javaWeb:独立开发MVC三层架构的网站了,原始

ssm:框架,简化了我们开发的流程,配置也开始较为复杂;

war:tomcat运行

spring再简化:springBoot jar:内嵌tomcat;微服务架构!

服务越来越多:springcloud;
在这里插入图片描述
约定大于配置:maven,spring,springMvc,springBoot

一,SpringBoot的概述

1,传统的spring项目存在的问题

  • 大量的xml文件,配置相当繁琐
  • 整合第三方框架的配置相当复杂
  • 低效的开发效率和部署效率等问题
  • 依赖外部的web服务器
  • 日志管理需要依赖
  • 一堆的依赖在maven中pom.xml中

2,springboot解决了什么问题

  • 创建独立的Spring应用程序
  • 直接嵌入Tomcat、Jetty或Undertow(无需部署WAR文件)
  • 提供固执己见的“启动程序”依赖项以简化构建配置 -starter
  • 尽可能自动配置Spring和第三方库
  • 提供生产准备功能,如度量、运行状况检查和外部化配置
  • 完全没有代码生成,也不需要XML配置

3,SpringBoot缺点

  • 人称版本帝,迭代快,需要时刻关注变化
  • 封装太深,内部原理复杂,不容易精通

总结

Springboot是构建在spring基础之上的一种优化解决方案。

4,微服务

  • 微服务(Microservice Architecture) 是近几年流行的一种架构思想,关于它的概念很难一言以蔽之

  • 究竟什么是微服务呢?我们在此引用ThoughtWorks 公司的首席科学家 Martin Fowler 于2014年提出的一段话

    • 就目前而言,对于微服务,业界并没有一个统一的,标准的定义
    • 但通常而言,微服务架构是一种架构模式,或者说是一种架构风格,它提倡将单一的应用程序划分成一组小的服务,每个服务运行在其独立的自己的进程内,服务之间互相协调,互相配置,为用户提供最终价值,服务之间采用轻量级的通信机制(HTTP)互相沟通,每个服务都围绕着具体的业务进行构建并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应该根据业务上下文,选择合适的语言,工具(Maven)对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3P2Q2XgR-1651305325886)(SpringCloud.assets/image-20211001094352140.png)]

  • 原文

  • 汉化

  • 再来从技术维度角度理解下:

    • 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度看就是一种小而独立的处理过程,类似进程的概念,能够自行单独启动或销毁,拥有自己独立的数据库

后面再聊SpringCloud的时候会具体的将什么叫微服务和微服务架构。

二,第一个SpringBoot程序

1,环境

  • jdk1.8
  • maven 3.6.1
  • SpringBoot:最新版
  • IDEA

2,创建SpringBoot项目的方法

官方:提供了一个快速生成的网站,IDEA集成了这个网站
在这里插入图片描述
目录结构
在这里插入图片描述

//自动装配
@RestController
public class HelloController {
    //接口:http://localhost:8080/hello
    @RequestMapping("/hello")
    public String hello(){
        return "hello,World";
    }
}

通过idea创建(是我们经常用的一种方式)
在这里插入图片描述
彩蛋
如何更改启动时显示的字符拼成的字母,SpringBoot呢?也就是 banner 图案;
只需一步:到项目下的 resources 目录下新建一个banner.txt 即可。
图案可以到:https://www.bootschool.net/ascii 这个网站生成,然后拷贝到文件中即可!

3,原理实现(重点)

自动装配:
pom.xml

  • spring-boot-dependencies:核心依赖在父工程中!
    在这里插入图片描述
    在这里插入图片描述
  • 我们在写或者引入一些SpringBoot依赖的时候,不需要指定的版本,就因为有这些版本仓库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PXO1qgrO-1651311431771)(springBoot.assets/image-20210919192017810.png)]

启动器

  • <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    
  • 启动器:说白了就是SpringBoot的启动场景;

  • 比如说spring-boot-starter-web,他就会帮我们导入web环境所有的依赖

  • SpringBoot会将所有的功能场景,都编程一个个启动器

  • 我们要使用什么功能,就只需要找到对应的启动器就行了 starter

主程序

package com.zhao.springboot01helloworld;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//@SpringBootApplication:标注这个类是一个SpringBoot的应用
@SpringBootApplication
public class Springboot01HelloworldApplication {
    //将SpringBoot应用启动
    public static void main(String[] args) {
        SpringApplication.run(Springboot01HelloworldApplication.class, args);
    }

}
  • 注解

    • @SpringBootConfiguration  //springboot的配置
      	@Configuration:spring配置类
      		@Component:说明这也是一个spring组件
      
      @EnableAutoConfiguration:自动配置
      	@AutoConfigurationPackage:自动配置包
      		@Import(AutoConfigurationPackages.Registrar.class):自动配置‘包注册’
      	@Import({AutoConfigurationImportSelector.class}):自动配置导入选择
              
      //获取所有的配置        
      List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);        
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-04hI7xpK-1651311597062)(springBoot.assets/image-20210919193433276.png)]

      获取候选的配置

      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
         List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
               getBeanClassLoader());
         Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
               + "are using a custom packaging, make sure that file is correct.");
         return configurations;
      }
      

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

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7e67A9y-1651311597063)(springBoot.assets/image-20210825163340757-1647009227546.png)]
      在这里插入图片描述
      这个核心配置文件内的东西我们是可以直接通过maven加入用的。

      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      所有资源加载到配置类中!
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIflgVMx-1651311597064)(springBoot.assets/image-20210919194758527.png)]

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

  1. SpringBoot在启动的时候,从类路径下/META-INF/spring.factories获取指定的值;
  2. 将这些自动配置的类导入容器,自动配置就会生效,帮我进行自动配置
  3. 以前我们需要自动配置的东西,现在SpringBoot帮助我们做了
  4. 整个javaee,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.5.4.jar这个包下
  5. 他会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器;
  6. 容器中也会存在非常多的XXXAutoConfiguration的文件(@Bean),就是这些类给人容器中导入了这个场景需要的所有组件并自动配置,@Configuration,JavaConfig!
  7. 有了自动配置类,免去了我们手动编写文件的工作

不简单的方法

SpringApplication

我最初以为就是运行了一个main方法,没想到却开启了一个服务;

@SpringBootApplication
public class SpringbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.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.setInitializers(this.getSpringFactoriesInstances();
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

同时我们也可以通过这个方法查看容器中所有的组件。
代码如下

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(DemoApplication.class, args);
        //2、查看容器里面的组件
        String[] beanDefinitionNames = run.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }

}

在这里插入图片描述

run方法流程分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EYA0iM4B-1651311597064)(springBoot.assets/image-20220311223419201.png)]

三,yaml配置注入

配置文件

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

  • application.properties

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

    • 语法结构 :key:空格 value

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

比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!

server.port=8081

yaml概述

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

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

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

传统xml配置:

<server>    <port>8081<port></server>

yaml配置:

server:  prot: 8080

yaml基础语法

说明:语法要求严格!

1、空格不能省略

2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

3、属性和值的大小写都是十分敏感的。

字面量:普通的值 [ 数字,布尔值,字符串 ]

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

k: v

注意:

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

    比如 :name: “kuang \n shen” 输出 :kuang 换行 shen

  • ‘’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

    比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

对象、Map(键值对)

#对象、Map格式k:     v1:    v2:

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

student:    
name: qinjiang    age: 3

行内写法

student: {name: qinjiang,age: 3}

数组( List、set )

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

pets: - cat - dog - pig

行内写法

pets: [cat,dog,pig]

总结

# k=v
name: zhao

# 对象
student:
  name: zhao
  age: 20

# 行内写法
student1: {name: zhao,age: 20}

# 数组
pets:
  - dog
  - cat
  - pig

pets1: [dag,cat,pig]

修改SpringBoot的默认端口号

配置文件中添加,端口号的参数,就可以切换端口;

server:  port: 8082

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Seg24JLO-1651321249781)(springBoot.assets/image-20210826085049405.png)]

注入配置文件

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

yaml注入配置文件

1、在springboot项目中的resources目录下新建一个文件 application.yml

2、编写一个实体类 Dog;

package com.zhao.pojo;

//可以让包被扫描
@Component
public class Dog {
    @Value("小雨")
    private String name;
    @Value("1")
    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 +
                '}';
    }
}

3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:

@Component
public class Dog {
    @Value("小雨")
    private String name;
    @Value("1")
    private Integer age;
}

4、在SpringBoot的测试类下注入狗狗输出一下;

package com.zhao;

import com.zhao.pojo.Dog;
import com.zhao.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //@Autowired:自动装配,可以自动装配使用@Component注解的类
    @Autowired
    private Dog dog;
    @Autowired
    private Person person;

    @Test
    void contextLoads() {
        System.out.println(dog);
        System.out.println(person);
    }

}

结果成功输出,@Value注入成功,这是我们原来的办法对吧。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVpX2ZcR-1651322534381)(springBoot.assets/image-20210826090652018.png)]

5、我们在编写一个复杂一点的实体类:Person 类

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

/**
 * @ProjectName: springboot-02-config
 * @ClassName: Person
 * @Description: 请描述该类的功能
 * @Author: 赵先生
 * @Date: 2021/8/26 8:32
 * @version v1.0
 * Copyright (c) All Rights Reserved,山西优逸客科技有限公司,. 
 */
/*
* @ConfigurationProperties(prefix = "xxx")作用:
* 将配置文件中的配置的每一个属性的值,映射到这个组件中:
* 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
* 参数prefix = "person":将配置文件中的person先的所有属性——一一对应
*
* 只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties(prefix = "person")功能
* */
@Component//注册bean
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private Dog dog;

    public Person() {
    }

    public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, Dog dog) {
        this.name = name;
        this.age = age;
        this.happy = happy;
        this.birth = birth;
        this.maps = maps;
        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 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 +
                ", dog=" + dog +
                '}';
    }
}

6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个yaml配置!

person:
  name: 小雨
  age: 20
  happu: ture
  birth: 2001/11/11
  maps: {k1: v1,k2: v2}
  lists: [code,music,boy]
  dog:
    name: 小明
    age: 3

7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;


/*
* @ConfigurationProperties(prefix = "xxx")作用:
* 将配置文件中的配置的每一个属性的值,映射到这个组件中:
* 告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
* 参数prefix = "person":将配置文件中的person先的所有属性——一一对应
*
* 只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties(prefix = "person")功能
* */
@Component//注册bean
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private Dog dog;

    public Person() {
    }

    public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, Dog dog) {
        this.name = name;
        this.age = age;
        this.happy = happy;
        this.birth = birth;
        this.maps = maps;
        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 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 +
                ", dog=" + dog +
                '}';
    }
}

8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AemomWnn-1651322534382)(springBoot.assets/image-20220311223454522.png)]

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

9、确认以上配置都OK之后,我们去测试类中测试一下:

package com.zhao;

import com.zhao.pojo.Dog;
import com.zhao.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //@Autowired:自动装配,可以自动装配使用@Component注解的类
    @Autowired
    private Dog dog;
    @Autowired
    private Person person;

    @Test
    void contextLoads() {
        System.out.println(dog);
        System.out.println(person);
    }

}

结果:所有值全部注入成功!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H17XPhj7-1651322534383)(springBoot.assets/image-20210826090933394.png)]

yaml配置注入到实体类完全OK!

课堂测试:

1、将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败

2、在配置一个person2,然后将 @ConfigurationProperties(prefix = “person2”) 指向我们的person2;

加载指定的配置文件

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

@configurationProperties:默认从全局配置文件中获取值;

1、我们去在resources目录下新建一个person.properties文件

name=ming 

2、然后在我们的代码中指定加载person.properties文件

@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {

    @Value("${name}")
    private String name;

    ......  
}

3、再次输出测试一下:指定配置文件绑定成功!
在这里插入图片描述

配置文件占位符

配置文件还可以编写占位符生成随机数

person:
    name: qinjiang${random.uuid} # 随机uuid
    age: ${random.int}  # 随机int
    happy: false
    birth: 2000/01/01
    maps: {k1: v1,k2: v2}
    lists:
      - code
      - girl
      - music
    dog:
      name: ${person.hello:other}_旺财
      age: 1

回顾properties配置

我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!

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

settings–>FileEncodings 中配置;
在这里插入图片描述
测试步骤:

1、新建一个实体类User

@Component //注册bean
public class User {
    private String name;
    private int age;
    private String sex;
}

2、编辑配置文件 user.properties

user1.name=ming
user1.age=18
user1.sex=男

3、我们在User类上使用@Value来进行注入!

@Component //注册bean
//classpath指定在resource目录下,而后再指定user这个上
@PropertySource(value = "classpath:user.properties")
public class User {
    //直接使用@value
    @Value("${user.name}") //从配置文件中取值
    private String name;
    @Value("#{9*2}")  // #{SPEL} Spring表达式
    private int age;
    @Value("男")  // 字面量
    private String sex;
}

4、Springboot测试

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    User user;

    @Test
    public void contextLoads() {
        System.out.println(user);
    }

}

对比小结

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

在这里插入图片描述
1、@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加

2、松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下

3、JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性

4、复杂类型封装,yml中可以封装对象 , 使用value就不支持

结论:

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

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

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

四,JSR303数据校验及多环境切换

JSR303数据校验

新版SpringBoot不自动支持jsr,所以需要导包

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

先看看如何使用

Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;

@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated  //数据校验
public class Person {

    @Email(message="邮箱格式错误") //name必须是邮箱格式
    private String name;
}

运行结果 :default message [不是一个合法的电子邮件地址];

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QkV01ZBP-1651373360560)(springBoot.assets/image-20220311223557497.png)]

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

常见参数

@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       验证 DateCalendar 对象是否在当前时间之前  
@Future     验证 DateCalendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

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

多环境切换

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

多配置文件

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

例如:

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

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

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

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

#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev

yaml的多文档块

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

server:
  port: 8081
#选择要激活那个环境块
spring:
  profiles:
    active: prod

# 是通过三个-去实现的文件分离
---
server:
  port: 8083
spring:
  profiles: dev #配置环境的名称


---

server:
  port: 8084
spring:
  profiles: prod  #配置环境的名称

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

配置文件加载位置

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

官方外部配置文件说明参考文档

在这里插入图片描述

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

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

项目路径的优先级大于资源路径,config文件夹的优先级大于直接放

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

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

我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;

#配置项目的访问路径
server.servlet.context-path=/kuang

拓展,运维小技巧

指定位置加载配置文件

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

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高

java -jar spring-boot-config.jar --spring.config.location=F:/application.properties

五,自动配置原理

自动配置原理

配置文件到底能写什么?怎么写?

SpringBoot官方文档中有大量的配置,我们无法全部记住
在这里插入图片描述

分析自动配置原理

我们以HttpEncodingAutoConfiguration(Http编码自动配置为例解释自动配置原理;

package com.example.demo.源码解释;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.server.Encoding;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for configuring the encoding to use
 * in web applications.
 *
 * @author Stephane Nicoll
 * @author Brian Clozel
 * @since 2.0.0
 *proxyBeanMethods:代理bean的方法
 *       Full(proxyBeanMethods = true)(保证每个@Bean方法被调用多少次返回的组件都是单实例的)(默认)
 *       Lite(proxyBeanMethods = false)(每个@Bean方法被调用多少次返回的组件都是新创建的)
 */
//proxyBeanMethods = false 就代表这是一个配置类文件
@Configuration(proxyBeanMethods = false)
/**
 * 启动指定类的ConfigurationProperties(配置属性)
 *  进去ServerProperties查看,将配置文件中的内容和ServerProperties中的内容绑定
 *  并把ServerProperties加入到了ioc让其中
 */
@EnableConfigurationProperties(ServerProperties.class)
/**
 * Spring底层@Conditional注解
 *   根据不同的条件判断,如果满足条件,整个配置类里面的配置就会生效
 *   这个意思就是判断当前应用是否是web应用,如果是,当前配置类生效
 */
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
//判断当前项目有没有CharacterEncodingFilter;再SpringMvc中进行乱码解决的过滤器
@ConditionalOnClass(CharacterEncodingFilter.class)
/**
 * 判断配置文件中是否存在某个配置:server.servlet.encoding;
 *      如果不存在,判断也是成立的
 *      即使我们配置文件中不配置pring.server.servlet.encoding=true,也是默认生效的;
 */
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class Http编码自动配置 {
    //他已经和SpringBoot的配置文件映射了
    private final Encoding properties;
    //只有一个有参构造器的情况下,参数的值就会从容器中拿
    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }

    //给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
        return filter;
    }
    
    @Bean
    public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
        return new LocaleCharsetMappingsCustomizer(this.properties);
    }

    static class LocaleCharsetMappingsCustomizer
            implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

        private final Encoding properties;

        LocaleCharsetMappingsCustomizer(Encoding properties) {
            this.properties = properties;
        }

        @Override
        public void customize(ConfigurableServletWebServerFactory factory) {
            if (this.properties.getMapping() != null) {
                factory.setLocaleCharsetMappings(this.properties.getMapping());
            }
        }

        @Override
        public int getOrder() {
            return 0;
        }

    }

}


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

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http") 
public class HttpProperties {    
// .....
}

我们去配置文件里面试试前缀,看提示!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NkicwV4G-1651374253402)(springBoot.assets/image-20220311223623375.png)]

这就是自动装配的原理!

精髓

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGhPs01V-1651374253403)(springBoot.assets/image-20210922101138400.png)]

**xxxxAutoConfigurartion:自动配置类;**给容器中添加组件

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

了解:@Conditional

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

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

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

在这里插入图片描述

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

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

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

#开启springboot的调试类
debug=true

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztKpbD8I-1651374253403)(springBoot.assets/image-20210922101548501.png)]

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

Unconditional classes: (没有条件的类)

在这里插入图片描述

【演示:查看输出的日志】

掌握吸收理解原理,即可以不变应万变!
在这里插入图片描述

六,静态资源问题

其实SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。

使用SpringBoot的步骤:

1、创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好

2、手动在配置文件中配置部分配置项目就可以运行起来了

3、专注编写业务代码,不需要考虑以前那样一大堆的配置了。

要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!

比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?

  • 向容器中自动配置组件 : Autoconfiguration
  • 自动配置类,封装配置文件的内容:Properties

没事就找找类,看看自动装配原理!

我们之后来进行一个单体项目的小项目测试,让大家能够快速上手开发!

静态资源处理

静态资源映射规则

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

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

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

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

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

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

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

@Override
//Registry 登记
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   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);
      }
   });
}

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9P5RYUH7-1651388837393)(springBoot.assets/image-20210826170508589.png)]

什么是webjars 呢?

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

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

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

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

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

导入完毕,查看webjars目录结构,并访问Jquery.js文件!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkTO92Mu-1651388837394)(springBoot.assets/image-20220311223658802.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsj7ALHN-1651388837394)(springBoot.assets/image-20220311223706780.png)]

第二种静态资源映射规则

那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;

我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:

// 进入方法
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};

ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。

所以得出结论,以下四个目录存放的静态资源可以被我们识别:

"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;

比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;

自定义静态资源路径

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

spring.resources.static-locations=classpath:/coding/,classpath:/kuang/

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

总结:

  • 在SpringBoot,我们可以使用一下方式处理静态资源
    • webjars localhost:8080/webjars/
    • public,static,/**,resources localhost:8080/
  • 优先级:resource>static(默认)>public 就是因为这个顺序我们才可以使用自己的配置

首页和图表设置

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

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService(格式转换服务),ResourceUrlProvider mvcResourceUrlProvider//
资源URL提供程序) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
    //模板可用性提供程序
    	new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}

点进去继续看

private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    // ::是java8 中新引入的运算符
    // Class::function的时候function是属于Class的,应该是静态方法。
    // this::function的funtion是属于这个对象的。
    // 简而言之,就是一种语法糖而已,是一种简写
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

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

比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html

新建一个 index.html ,在我们上面的3个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!

关于网站图标说明

在这里插入图片描述

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

1、关闭SpringBoot默认图标(新版本可以不用这一步)

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

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

3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!
在这里插入图片描述

七,Thymeleaf模板引擎

1,Thymeleaf

模板引擎

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

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

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

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

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

在这里插入图片描述

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

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

引入Thymeleaf

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

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

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

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

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

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

        <!--thymeleaf-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvOW6gyP-1651390904180)(springBoot.assets/image-20220311223733847.png)]

Thymeleaf分析

前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?

我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。

我们去找一下Thymeleaf的自动配置类:ThymeleafProperties

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";//前缀
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
}

我们可以在其中看到默认的前缀和后缀!

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

使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可!

他做了我们spring中的视图解析器

测试

1、编写一个TestController

@Controller
public class TestController {
    
    @RequestMapping("/t1")
    public String test1(){
        //classpath:/templates/test.html
        return "test";
    }
    
}

2、编写一个测试页面 test.html 放在 templates 目录下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>测试页面</h1>

</body>
</html>

3、启动项目请求测试
在这里插入图片描述

Thymeleaf 语法学习

要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;

Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!

我们做个最简单的练习 :我们需要查出一些数据,在页面中展示

1、修改测试请求,增加数据传输;

@RequestMapping("/t1")
public String test1(Model model){
    //存入数据
    model.addAttribute("msg","Hello,Thymeleaf");
    //classpath:/templates/test.html
    return "test";
}

2、我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。

我们可以去官方文档的#3中看一下命名空间拿来过来:

 xmlns:th="http://www.thymeleaf.org"

3、我们去编写下前端页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神说</title>
</head>
<body>
<h1>测试页面</h1>

<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${msg}"></div>
</body>
</html>

4、启动测试!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lKuHpwO1-1651390904180)(springBoot.assets/image-20220311223743693.png)]

OK,入门搞定,我们来认真研习一下Thymeleaf的使用语法!

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NabPIr6B-1651390904180)(springBoot.assets/image-20220311223758569.png)]

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

练习测试:

1、 我们编写一个Controller,放一些数据

@RequestMapping("/t2")
public String test2(Map<String,Object> map){
    //存入数据
    map.put("msg","<h1>Hello</h1>");
    map.put("users", Arrays.asList("qinjiang","kuangshen"));
    //classpath:/templates/test.html
    return "test";
}

2、测试页面取出数据

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>狂神说</title>
</head>
<body>
<h1>测试页面</h1>

<div th:text="${msg}"></div>
<!--不转义-->
<div th:utext="${msg}"></div>

<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签:官网#9-->
<h4 th:each="user :${users}" th:text="${user}"></h4>

<h4>
    <!--行内写法:官网#12-->
    <span th:each="user:${users}">[[${user}]]</span>
</h4>

</body>
</html>

3、启动项目测试!

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

八,MVC自动配置原理

1,MVC自动配置原理

官网阅读

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

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

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

Spring MVC Auto-configuration
// Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自动配置在Spring默认设置的基础上添加了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
// 包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars 
// 自动注册了Converter:
// 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
// Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters:http的信息转换器
// SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
// 定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首页定制
Static index.html support.
// 图标定制
Custom Favicon support (covered later in this document).
// 初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

/*
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己
的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc。如果希望提供
RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver的自定义
实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration 
(interceptors, formatters, view controllers, and other features), you can add your own 
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide 
custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or 
ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

// 如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

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

ContentNegotiatingViewResolver 内容协商视图解析器

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

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

我们去看看这里的源码:我们找到 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使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
    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;
        }
    }
    // .....
}

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

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

Iterator var5 = this.viewResolvers.iterator();

所以得出结论: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());
    }
    // ...............
}

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

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

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

//如果你想diy一些定制化的功能,只需要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配
//全面扩展springmvc  dispatchServlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    //viewResolver 实现了视图解析器接口类,我们就可以把它看做视图解析器
    @Bean
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    //自定义一个自己的视图解析器MyViewResolver
    public static class MyViewResolver implements ViewResolver{

        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }
}

2,怎么看我们写的视图解析器是否起作用了呢?
我们给DispatcherServlet中的doDispatch方法标记一个断点,因为所有的请求都会走 到这个地方。
在这里插入图片描述
3,启动项目,随机开启一个页面,查看Debug的信息
在这里插入图片描述
所以说明,我们如果想要使用自己定义的东西,只需要给容器添加这个组件就可以了,剩下的事情全是springboot的。

2,转换器和格式化器

找到格式化转换器:

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 拿到配置文件中的格式化规则
    WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}

点击去:

public String getDateFormat() {
    return this.dateFormat;
}

/**
* Date format to use. For instance, `dd/MM/yyyy`. 默认的
 */
private String dateFormat;

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a88qZOI1-1651394032007)(springBoot.assets/image-20220311223827837.png)]

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

3,修改SpringBoot的默认配置

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

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

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

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

扩展使用SpringMVC 官方文档如下:

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

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

//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

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

我们去浏览器访问一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hL0JamNL-1651394032007)(springBoot.assets/image-20220311223837706.png)]

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

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

1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter(adapter适配器)

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

3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration(Delegating委派代表)

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

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、我们点进去看一下

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自己的配置类,我们自己的配置类当然也会被调用;

4,全面接管SpringMVC

官方文档:

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXG3mNZB-1651394032008)(springBoot.assets/image-20220311223849766.png)]

给配置类加上注解:@EnableWebMvc

图片

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

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

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

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

@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 {
    
}

总结一句话:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;

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

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

九,创建一个简单的项目

1,导入静态资源

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DexQABkc-1651396215797)(springBoot.assets/image-20210828183749781.png)]

2,创建实体类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cFT7lkc1-1651396215797)(springBoot.assets/image-20210828183813861.png)]

Department.java

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

Employee.java


//员工表
@Data
@NoArgsConstructor
public class Employee {
    private Integer id;  //string类型的api相对较多
    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();
    }
}

3,模拟数据并进行基本操作(CRUD)

EmployeeDao

import com.zhao.pojo.Department;
import com.zhao.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@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","134@qq.com",1,new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"bb","134@qq.com",0,new Department(102,"市场部")));
        employees.put(1003,new Employee(1003,"cc","134@qq.com",1,new Department(103,"教研部")));
        employees.put(1004,new Employee(1004,"dd","134@qq.com",0,new Department(104,"运营部")));
        employees.put(1005,new Employee(1005,"ee","134@qq.com",1,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);
    }
    //删除员工
    public void delete(Integer id){
        employees.remove(id);
    }
}

DepartmentDao

//部门dao
@Repository//将类托管给spring
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);
    }
}

4,首页及其样式修改

MyMvcConfig

通过自定义一个视图控制器来进行跳转,当我的请求为"/“或者”/index.html"时跳转到index

//如果你想diy一些定制化的功能,只需要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配
//全面扩展springmvc  dispatchServlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

这个时候测试时发现css,js样式无法加载,所以导入Thymeleaf模板引擎

    <!--thymeleaf-->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>

再到html页面上加入依赖

<html lang="en" xmlns:th="http://www.thymeleaf.org">

同时将本地的css样式用Thymeleaf导入

  • 简单的表达:
    • 变量表达式: ${...}
    • 选择变量表达式: *{...}
    • 消息表达: #{...}
    • 链接 URL 表达式: @{...}
    • 片段表达式: ~{...}
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/signin.css}" rel="stylesheet">

5,国际化配置

  1. 首页配置:所有页面的静态资源都需要使用thymeleaf接管,所以相关连接和消息需要都需要使用thymeleaf格式

  2. 在资源包下创建一个i18n文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3A6MH44E-1651396215798)(springBoot.assets/image-20210829155756160.png)]

  3. 点击Resource Bundle

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tYxKZmk1-1651396215798)(springBoot.assets/image-20210829155854989.png)]

  4. 进入并填写数据

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHAzmGsh-1651396215798)(springBoot.assets/image-20210829160026637.png)]

  5. 在首页中找到应该替换的数据,进行修改格式。#{}

  6. 如果说我们需要再项目中进行按钮自动切换,我们就需要自定义一个组件LocaleResolver

    这个是SpringBoot中原本的类

    public Locale resolveLocale(HttpServletRequest request) {
        Locale defaultLocale = this.getDefaultLocale();
        if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
            return defaultLocale;
        } else {
            Locale requestLocale = request.getLocale();
            List<Locale> supportedLocales = this.getSupportedLocales();
            if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
                Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
                if (supportedLocale != null) {
                    return supportedLocale;
                } else {
                    return defaultLocale != null ? defaultLocale : requestLocale;
                }
            } else {
                return requestLocale;
            }
        }
    }
    

    我们可以根据这个照猫画虎编辑自己的类

    public class MyLocaleResolver implements LocaleResolver {
        //解析请求
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            //获取请求中的语言参数
            String language = request.getParameter("l");
            Locale locale = Locale.getDefault();//如果没有就使用默认的
            //如果请求的连接携带了国际化的参数
            if (!StringUtils.isEmpty(language)){
                //zh_CN
                //split表示使用——分割
                String[] split = language.split("_");
                //国家,地区
                locale = new Locale(split[0], split[1]);
            }
            return locale;
        }
    
        @Override
        public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
    
        }
    }
    
  7. 然后再将我们自己写的组件配置到spring的容器中

    //如果你想diy一些定制化的功能,只需要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配
    //全面扩展springmvc  dispatchServlet
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    
        //自定义的国际化组件
        @Bean
        public LocaleResolver localeResolver(){
            return new MyLocaleResolver();
        }
    }
    
  8. 如果发生乱码就要去setting——》File Encoding中改编码格式
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U12HeYj9-1651396215798)(springBoot.assets/image-20211008104818876.png)]

6,登录功能

1,实现

  1. 修改index代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNF4IzDv-1651396215799)(springBoot.assets/image-20210829162555909.png)]

  2. 去控制层创建登录的代码实现

    @Controller
    public class LoginController {
        @RequestMapping("/user/login")
        public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){
    
            //具体业务
            if (!StringUtils.isEmpty(username) && password.equals("123456")){
                return "redirect:/main.html";
            }else {
                //告诉用户失败了
                model.addAttribute("msg","用户名密码错误!");
                return "index";
            }
    
    
        }
    }
    
  3. 去编写一个相应的映射

    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    
  4. 因为登录失败以后必须要有提示信息,所以在index上增加提示信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0vyiS2Pi-1651396215799)(springBoot.assets/image-20210829162834318.png)]

2,登录拦截器

  1. 在config文件夹下写一个LoginHandlerInterceptor类

    //拦截器
    public class LoginHandlerInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //登录成功之后有用户的session
            Object loginUser = request.getSession().getAttribute("loginUser");
            if (loginUser==null){
                request.setAttribute("msg","没有权限,请先登录");
                request.getRequestDispatcher("/index.html").forward(request,response);
                return false;
            }else{
                return true;
            }
    
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
        }
    }
    
  2. 在MyMvcConfig中重写拦截器方法

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

7,展示员工列表

  1. 提取公共页面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-72mexS2C-1651396215799)(springBoot.assets/image-20210829184807613.png)]

    <!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" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">注销</a>
            </li>
        </ul>
    </nav>
    
    <!--侧边栏-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
        <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="@{/main.html}">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                            <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                            <polyline points="9 22 9 12 15 12 15 22"></polyline>
                        </svg>
                        首页 <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>
    

    通过<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">中的th:fragment="sidebar"标签标明要插入的片段并为其取名字在需要的地方<div th:replace="··{commons/commons::topbar}"></div>或者<div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>通过这个标签插入如果需要传递参数,可以直接使用()传参即可

  2. 列表循环

    <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>
                                     <button class="btn btn-sm btn-danger">删除</button>
                                 </td>
      </tr>
    </tbody>
    

    通过th:each标签实现循环

8,添加员工

  1. 添加按钮

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fff6XCBU-1651396215800)(springBoot.assets/image-20210907092949957.png)]

  2. 在控制层去写一个对应的toAdd方法

    @RequestMapping("/toAdd")
    public String toAddPage(Model model){
        //查出所有部门的信息
        Collection<Department> departments = departmentDao.getDepartments();
        //然后便利到add中
        model.addAttribute("departments",departments);
        return "add";
    }
    
  3. 编写add页面

    <!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="@{/css/bootstrap.min.css}" rel="stylesheet">
    
        <!-- Custom styles for this template -->
        <link th:href="@{/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:insert="~{commons/commons::topbar}"></div>
    
    <div class="container-fluid">
        <div class="row">
            <div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>
    
            <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                <form th:action="@{/add}" method="post">
                    <div class=" form- group">
                        <label>LastName</label>
                        <input type= "text" name="lastName" class="form- control" placeholder=" kuangshen">
                    </div>
                    <div class="form-group">
                        <label>Email</label>
                        <input type=" email" name="email" class="form-control" placeholder="24736743@qq.com" >
                    </div>
                    <form 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 class=" form-group">
                        <label>department</label>
                        <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" class="form-control" placeholder="kuangstudy" name="birth">
                    </div>
                        <button type="submit" class="btn btn-primary">添加</button>
                </form>
                </form>
            </main>
        </div>
    </div>
    
    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script type="text/javascript" src="asserts/js/jquery-3.2.1.slim.min.js"></script>
    <script type="text/javascript" src="asserts/js/popper.min.js"></script>
    <script type="text/javascript" src="asserts/js/bootstrap.min.js"></script>
    
    <!-- Icons -->
    <script type="text/javascript" src="asserts/js/feather.min.js"></script>
    <script>
        feather.replace()
    </script>
    
    <!-- Graphs -->
    <script type="text/javascript" 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. 点击提交表单的时候会执行add动作

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-crMARqdX-1651396215800)(springBoot.assets/image-20210907093441167.png)]

    @RequestMapping("/add")
    public String add(Employee employee){
        System.out.println("save=>"+employee);
        employeeDao.save(employee);//调用底层业务方法保存
        return "redirect:/emps";
    }
    

9,修改员工信息

  1. 在编辑界面中间添加动作,并获得当前id

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJJDZ2LR-1651396215800)(springBoot.assets/image-20210907102608413.png)]

  2. 编写toupdate

    @GetMapping("/toUpdate/{id}")
    //去到员工的修改页面
    public String toAdd(@PathVariable("id") Integer id,Model model){
        //查出原来的数据
        Employee employeeById = employeeDao.getEmployeeById(id);
        model.addAttribute("updates",employeeById);
        //查出所有部门的信息
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);
        return "update";
    }
    
  3. 编写update页面,取值用th:value,单选框用checks

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <!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="@{/css/bootstrap.min.css}" rel="stylesheet">
    
        <!-- Custom styles for this template -->
        <link th:href="@{/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:insert="~{commons/commons::topbar}"></div>
    
    <div class="container-fluid">
        <div class="row">
            <div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>
    
            <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                <form th:action="@{/update}" method="post">
                    <input type="hidden" name="id" th:value="${updates.getId()}">
                    <div class=" form- group">
                        <label>LastName</label>
                        <input th:value="${updates.getLastName()}" type= "text" name="lastName" class="form- control" placeholder=" kuangshen">
                    </div>
                    <div class="form-group">
                        <label>Email</label>
                        <input th:value="${updates.getEmail()}" type=" email" name="email" class="form-control" placeholder="24736743@qq.com" >
                    </div>
                    <form class="form-group">
                        <label>Gender</label><br/>
                        <div class="form-check form-check-inline">
                            <input class="form-check-input" th:checked="${updates.getGender()==1}" 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" th:checked="${updates.getGender()==0}" type= "radio" name="gender" value="0">
                            <label class=" form-check- label">女</label>
                        </div>
                        <div class=" form-group">
                            <label>department</label>
                            <select class="form-control" name="department.id">
                                <option th:selected="${dept.getId()==updates.getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}"
                                        th:value="${dept.getId()}"></option>
                            </select>
                        </div>
                        <div class="form-group">
                            <label>Birth</label>
                            <input th:value="${updates.getBirth()}" type="text" class="form-control" placeholder="kuangstudy" name="birth">
                        </div>
                        <button type="submit" class="btn btn-primary">修改</button>
                    </form>
                </form>
            </main>
        </div>
    </div>
    
    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script type="text/javascript" src="asserts/js/jquery-3.2.1.slim.min.js"></script>
    <script type="text/javascript" src="asserts/js/popper.min.js"></script>
    <script type="text/javascript" src="asserts/js/bootstrap.min.js"></script>
    
    <!-- Icons -->
    <script type="text/javascript" src="asserts/js/feather.min.js"></script>
    <script>
        feather.replace()
    </script>
    
    <!-- Graphs -->
    <script type="text/javascript" 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>
    </body>
    </html>
    
  4. 再去编写update方法

     @RequestMapping("/update")
        public String update(Employee employee){
            employeeDao.save(employee);
            return "redirect:/emps";
        }
    }
    

10,删除员工信息和404

<a class="btn btn-sm btn-danger" th:href="@{/dele/}+${emp.getId()}">删除</a>
RequestMapping("/out")
public String logout(HttpSession session){
    session.invalidate();
    return "redirect:/index.html";
}

导入错误页面

只需要在资源目录下创建一个error的文件夹把所有的错误页面放进去即可

十,谈一下一个页面该如何去写

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

1,有一套自己熟悉的后台模板:工作必要!x-admin

2,前端页面:至少自己能够通过前端框架,组合出来一个网站页面

​ -index

​ -about

​ -blog

​ -post

​ -user

3,让这个网站能够独立运行!

十一,整合jdbc和集成Durid

1,SpringData简介

对于数据访问层,无论是 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.2.5.RELEASE/reference/htmlsingle/#using-boot-starter

2,整合JDBC

创建测试项目和测试数据

  1. 创建一个测试项目:spring-data-jdbc;引入相应的模块!基础模块

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xlZeNMj5-1651505920488)(springBoot.assets/640.webp)]

  2. 创建好项目后,会发现比之前多了如下的启动器

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHpNc3pa-1651505920489)(springBoot.assets/image-20210907221143134.png)]

  3. 编写application.yaml

    spring:
      datasource:
        username: root
        password: 123456
        #?serverTimezone=UTC解决时区的报错
        url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf8
        url可以自动选择最佳的,可以忽略
    
  4. 配置完这些东西以后,就可以直接使用,因为springboot已经帮我们自动配置好了;去测试类进行测试

    package com.zhao;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.SQLException;
    
    @SpringBootTest
    class Springboot04DataApplicationTests {
    
        //获取数据源,DI注入
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() throws SQLException {
            //查看一下默认的数据源:com.zaxxer.hikari.HikariDataSource
            System.out.println(dataSource.getClass());
            //获取数据库连接
            Connection connection = dataSource.getConnection();
            System.out.println(connection);
    
            //xxx Template : SpringBoot已经配置好模板,拿来即用 CRUD
    
            //关闭
            connection.close();
        }
    
    }
    

结束:我们可以看到他给我们默认配置的数据源为class com.zaxxer.hikari.HikariDataSource,我们并没有手动配置

全局搜索一下,找到数据源的所有自动配置都在:DataSourceAutoConfiguration文件:

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

这里导入的类都在DateSourceConfiguration配置类下,可以看出Spring Boot 2.2.5默认使用HikarDataSource数据源

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

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

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

JDBCTemplate

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

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

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

测试

编写一个Controller,注入jdbcTemplate,编写测试方法进行测试

@RestController
public class JDBCController {
    @Autowired
    JdbcTemplate jdbcTemplate;

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

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

    @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] = "小明2";
        objects[1] = "123456";
        jdbcTemplate.update(sql,objects);
        return "update";
    }

    @RequestMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id){
        String sql = "delete from mybatis.user where id=?";
        jdbcTemplate.update(sql,id);
        return "delete";
    }
}

3,集成Durid

Durid简介

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 基本配置参数如下:

配置数据源

  1. 添加上Durid数据源依赖。

    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
    
  2. 切换数据源;可以通过spring.datasource.type指定数据源。

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf8
        type: com.alibaba.druid.pool.DruidDataSource
    
  3. 切换数据源以后,可以在测试类中注入DataSource,然后获取到他,输出一看便知道是否切换成功

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9fa1sJfy-1651505920489)(springBoot.assets/image-20210907234331979.png)]

  4. 切换成功以后我们就可以设置数据源连接初始化大小,最大连接数,等待时间,最小连接数等设置项

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

    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  6. 现在需要程序员手动为自己 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;

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

    @SpringBootTest
    class SpringbootDataJdbcApplicationTests {
    
        //DI注入数据源
        @Autowired
        DataSource dataSource;
    
        @Test
        public void contextLoads() throws SQLException {
            //看一下默认数据源
            System.out.println(dataSource.getClass());
            //获得连接
            Connection connection =   dataSource.getConnection();
            System.out.println(connection);
    
            DruidDataSource druidDataSource = (DruidDataSource) dataSource;
            System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
            System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
    
            //关闭连接
            connection.close();
        }
    }
    

    输出结果,可见配置参数已经生效

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eYqzgj7M-1651505920490)(springBoot.assets/image-20210907235011127.png)]

配置Durid数据监控

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

所以第一步需要设置Durid的后台管理页面,比如密码,账号等;配置后台管理

//配置 Druid 监控管理后台的Servlet;
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

        // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
        // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
        Map<String, String> initParams = new HashMap<>();
        initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
        initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

        //后台允许谁可以访问
        //initParams.put("allow", "localhost"):表示只有本机可以访问
        //initParams.put("allow", ""):为空或者为null时,表示允许所有访问
        initParams.put("allow", "");
        //deny:Druid 后台拒绝谁访问
        //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问

        //设置初始化参数
        bean.setInitParameters(initParams);
        return bean;
    }

设置完毕后可以登录http://localhost:8080/druid/login.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NBOrkSCQ-1651505920490)(springBoot.assets/image-20210907235628083.png)]

进入之后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MRCIxIpu-1651505920490)(springBoot.assets/image-20210907235648308.png)]

配置 Druid web 监控 filter 过滤器

//配置 Druid 监控 之  web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());

    //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
    Map<String, String> initParams = new HashMap<>();
    initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
    bean.setInitParameters(initParams);

    //"/*" 表示过滤所有请求
    bean.setUrlPatterns(Arrays.asList("/*"));
    return bean;
}

十二,整合mybatis

  1. 导入mybatis依赖(因为是用的druid的数据源所以还导入了相关包)

    <!-- mybatis-spring-boot-starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
    
  2. 配置数据库连接

    spring:
      datasource:
        username: root
        password: 123456
        #?serverTimezone=UTC解决时区的报错
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        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
    
  3. 测试数据库是否连接成功

  4. 创建实体类pojo,并使用lombok插件

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private  int id;
        private String name;
        private String pwd;
    }
    
  5. 去编写一个userMappper类

    //这个注解表示了这是一个mybatis的mapper类
    @Mapper
    @Repository //要是没有这个会报错但是实际上并没有影响
    public interface UserMapper {
        List<User> queryUserList();
    
        User queryUserById(int id);
    
        int addUser(User user);
    
        int updateUser(User user);
    
        int deleteUser(int id);
    
    }
    
  6. 对应mapper的映射文件

    位置是在资源目录下编写的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYm0BTsa-1651507800901)(springBoot.assets/image-20210908145159732.png)]

    <?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.zhao.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) value (#{id},#{name},#{pwd})
        </insert>
    
        <update id="updateUser" parameterType="User">
            update mybatis.user set name=#{name},pwd=#{pwd} where id = #{id};
        </update>
    
        <delete id="deleteUser" parameterType="int">
            delete from mybatis.user where id = #{id}
        </delete>
    
    </mapper>
    
  7. 编写控制层

    package com.zhao.controller;/**
     * ************************************************************************
     * 项目名称: springboot-05-mybatis <br/>
     * 文件名称:  <br/>
     * 文件描述: 这里添加您的类文件描述,说明当前文件要包含的功能。 <br/>
     * 文件创建:赵先生 <br/>
     * 创建时间: 2021/9/8 <br/>
     * 山西优逸客科技有限公司 Copyright (c) All Rights Reserved. <br/>
     *
     * @version v1.0 <br/>
     * @update [序号][日期YYYY-MM-DD][更改人姓名][变更描述]<br/>
     * ************************************************************************
     */
    
    import com.zhao.mapper.UserMapper;
    import com.zhao.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    /**
     * @ProjectName: springboot-05-mybatis
     * @ClassName: UserController
     * @Description: 请描述该类的功能
     * @Author: 赵先生
     * @Date: 2021/9/8 9:52
     * @version v1.0
     * Copyright (c) All Rights Reserved,山西优逸客科技有限公司,. 
     */
    @RestController
    public class UserController {
        @Autowired
        private UserMapper userMapper;
    
        @GetMapping("/queryUserList")
        public List<User> queryUserList(){
            List<User> userList = userMapper.queryUserList();
            for (User user : userList) {
                System.out.println(user);
            }
            return userList;
        }
    
        @GetMapping("/addUser")
        public String addUser(){
            userMapper.addUser(new User(5,"小雨","123456"));
            return "ok";
        }
    
        @GetMapping("/updateUser")
        public String updateUser(){
            userMapper.updateUser(new User(5,"小雨","111111"));
            return "ok";
        }
    
        @GetMapping("deleteUser")
        public String deleteUser(){
            userMapper.deleteUser(5);
            return "ok";
        }
    }
    

简单描述:

  1. 导入包
  2. 配置文件
  3. mybatis配置
  4. 边上sql
  5. 业务层调用dao层
  6. controller调用service

十三,SpringSecurity(功能)

1,安全简介

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

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

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

首先我们看下它的官网介绍:Spring Security官网地址

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

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

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

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

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

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

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

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

2,简介

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

记住几个类:

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

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

“认证”(Authentication)

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

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

“授权” (Authorization)

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

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

在web页面使用的时候需要导入语法依赖:xmlns:sec=“http://www.thymeleaf.org/thymeleaf-extras-springsecurity5”

3,认证和授权

目前可以访问所有的页面,我们可以通过SpringSecurity来设置认证和授权的功能。

  1. 添加maven依赖

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

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

    查看我们自己项目中的版本,找到对应的帮助文档:

    https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4

  3. 编写基础配置类,去定制一个请求和认证的授权规则,并打开登录页面,同时要去编写密码的加密格式不然会报错(There is no PasswordEncoder mapped for the id “null”)

    package com.zhao.config;/**
     * ************************************************************************
     * 项目名称: springboot-06-security <br/>
     * 文件名称:  <br/>
     * 文件描述: 这里添加您的类文件描述,说明当前文件要包含的功能。 <br/>
     * 文件创建:赵先生 <br/>
     * 创建时间: 2021/9/8 <br/>
     * 山西优逸客科技有限公司 Copyright (c) All Rights Reserved. <br/>
     *
     * @version v1.0 <br/>
     * @update [序号][日期YYYY-MM-DD][更改人姓名][变更描述]<br/>
     * ************************************************************************
     */
    
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    /**
     * @ProjectName: springboot-06-security
     * @ClassName: SecurityConfig
     * @Description: 请描述该类的功能
     * @Author: 赵先生
     * @Date: 2021/9/8 21:44
     * @version v1.0
     * Copyright (c) All Rights Reserved,山西优逸客科技有限公司,. 
     */
    //AOP: 拦截器
    @EnableWebSecurity// 开启WebSecurity模式
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //链式编程
        //授权
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首页所有人都可以访问,功能页只有对应有权限的人才访问
            //authorizeRequests批准请求,antMatchers 匹配对应的动作,hasRole有作用对象
            //请求授权的规则
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level2/**").hasRole("vip3");
    
            //没有权限默认会到登录页面,需要开启登录页面
            http.formLogin();
        }
    
        //认证
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //这些数据正常应该在数据库中访问,现在是在内存中访问,BCryptPasswordEncoder是一个加密方式
            auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("zhao").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
                    .and()
                    .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3","vip1")
                    .and()
                    .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
        }
    }
    
  4. 测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定

4,记住我功能和首页定制

记住我功能

方法是在配置类中加一个

//开启记住我功能 cookie,保存两周
http.rememberMe();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-luzzu4Hg-1651508223771)(springBoot.assets/image-20210909180346051.png)]

原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zg1ygYqm-1651508223771)(springBoot.assets/image-20210909180901799.png)]

定制首页

当我们调用了http.formLogin()开启了用户登录状态就请求资源将被重定向到登录页面的功能,但是跳转到的登录页面是security自带的,我们想用自己的登录页面,这时就可以查看源码

 * Specifies to support form based authentication. If
 * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page
 * will be generated.
 * 支持用户指定基于表单的身份验证,就是支持我们自己指定一个表单页面来进行身份认证
 * 如果用户没有指定,将使用spring security自带的身份认证表单页面

 * The configuration below demonstrates customizing the defaults.
 * 下面的配置演示如何自定义默认值
 *
 * 
 * @Configuration
 * @EnableWebSecurity
 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
 *
 * 	@Override
 * 	protected void configure(HttpSecurity http) throws Exception {
 * 		http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin()
 * 				.usernameParameter("username") // default is username
 * 				.passwordParameter("password") // default is password
 * //get方式请求的默认login页面,我们就可以通过这个方法指定我们自己的登录页为身份认证页面
 * //注意:loginPage()中指定的是这个页面的请求url,不是资源真实地址
 * 				.loginPage("/authentication/login") // default is /login with an HTTP get 
 * 				.failureUrl("/authentication/login?failed") // default is /login?error
 * 				.loginProcessingUrl("/authentication/login/process"); // default is /login
 * 																		// with an HTTP
 * 																		// post
 * 	}

指定我们自己的登录页面为

//没有权限默认会到登录页面,需要开启登录页面
http.formLogin().loginPage("/toLogin");

那么页面的数据应该去提交到哪里才能进行身份验证呢?

它的源码如下, 但是源码中只有一个new FormLoginConfigurer<>()

所以点开new FormLoginConfigurer<>()看具体的运行结果

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>>...{
	//构造
	public FormLoginConfigurer() {
		super(new UsernamePasswordAuthenticationFilter(), null);
		usernameParameter("username");
		passwordParameter("password");
	}
		/**
	 * <p>
	 * Specifies the URL to send users to if login is required. If used with
	 * {@link WebSecurityConfigurerAdapter} a default login page will be generated when
	 * this attribute is not specified.
	 * </p>
	 *
	 * <p>
	 * If a URL is specified or this is not being used in conjuction with
	 * {@link WebSecurityConfigurerAdapter}, users are required to process the specified
	 * URL to generate a login page. In general, the login page should create a form that
	 * submits a request with the following requirements to work with
	 * {@link UsernamePasswordAuthenticationFilter}:
	 * </p>
	 *
	 * <ul>
	 * <li>It must be an HTTP POST</li>
	 * <li>It must be submitted to {@link #loginProcessingUrl(String)}</li>
	 * <li>It should include the username as an HTTP parameter by the name of
	 * {@link #usernameParameter(String)}</li>
	 * <li>It should include the password as an HTTP parameter by the name of
	 * {@link #passwordParameter(String)}</li>
	 * </ul>
	 *
	 * <h2>Example login.jsp</h2>
	 *
	 * Login pages can be rendered with any technology you choose so long as the rules
	 * above are followed. Below is an example login.jsp that can be used as a quick start
	 * when using JSP's or as a baseline to translate into another view technology.
	 *
	 * <pre>
	 * <!-- loginProcessingUrl should correspond to FormLoginConfigurer#loginProcessingUrl. Don't forget to perform a POST -->
	 * &lt;c:url value="/login" var="loginProcessingUrl"/&gt;
	 * &lt;form action="${loginProcessingUrl}" method="post"&gt;
	 *    &lt;fieldset&gt;
	 *        &lt;legend&gt;Please Login&lt;/legend&gt;
	 *        &lt;!-- use param.error assuming FormLoginConfigurer#failureUrl contains the query parameter error --&gt;
	 *        &lt;c:if test="${param.error != null}"&gt;
	 *            &lt;div&gt;
	 *                Failed to login.
	 *                &lt;c:if test="${SPRING_SECURITY_LAST_EXCEPTION != null}"&gt;
	 *                  Reason: &lt;c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" /&gt;
	 *                &lt;/c:if&gt;
	 *            &lt;/div&gt;
	 *        &lt;/c:if&gt;
	 *        &lt;!-- the configured LogoutConfigurer#logoutSuccessUrl is /login?logout and contains the query param logout --&gt;
	 *        &lt;c:if test="${param.logout != null}"&gt;
	 *            &lt;div&gt;
	 *                You have been logged out.
	 *            &lt;/div&gt;
	 *        &lt;/c:if&gt;
	 *        &lt;p&gt;
	 *        &lt;label for="username"&gt;Username&lt;/label&gt;
	 *        &lt;input type="text" id="username" name="username"/&gt;
	 *        &lt;/p&gt;
	 *        &lt;p&gt;
	 *        &lt;label for="password"&gt;Password&lt;/label&gt;
	 *        &lt;input type="password" id="password" name="password"/&gt;
	 *        &lt;/p&gt;
	 *        &lt;!-- if using RememberMeConfigurer make sure remember-me matches RememberMeConfigurer#rememberMeParameter --&gt;
	 *        &lt;p&gt;
	 *        &lt;label for="remember-me"&gt;Remember Me?&lt;/label&gt;
	 *        &lt;input type="checkbox" id="remember-me" name="remember-me"/&gt;
	 *        &lt;/p&gt;
	 *        &lt;div&gt;
	 *            &lt;button type="submit" class="btn"&gt;Log in&lt;/button&gt;
	 *        &lt;/div&gt;
	 *    &lt;/fieldset&gt;
	 * &lt;/form&gt;
	 * </pre>
	 *
	 * <h2>Impact on other defaults</h2>
	 *
	 * Updating this value, also impacts a number of other default values. For example,
	 * the following are the default values when only formLogin() was specified.
	 *
	 * <ul>
	 * <li>/login GET - the login form</li>
	 * <li>/login POST - process the credentials and if valid authenticate the user</li>
	 * <li>/login?error GET - redirect here for failed authentication attempts</li>
	 * <li>/login?logout GET - redirect here after successfully logging out</li>
	 * </ul>
	 *
	 * If "/authenticate" was passed to this method it update the defaults as shown below:
	 *
	 * <ul>
	 * <li>/authenticate GET - the login form</li>
	 * <li>/authenticate POST - process the credentials and if valid authenticate the user
	 * </li>
	 * <li>/authenticate?error GET - redirect here for failed authentication attempts</li>
	 * <li>/authenticate?logout GET - redirect here after successfully logging out</li>
	 * </ul>
	 *
	 *
	 * @param loginPage the login page to redirect to if authentication is required (i.e.
	 * "/login")
	 * @return the {@link FormLoginConfigurer} for additional customization
	 */
	@Override
	public FormLoginConfigurer<H> loginPage(String loginPage) {
		return super.loginPage(loginPage);
	}
}

上面的源码中我只选出了FormLoginConfigurer的构造和一个成员方法loginPage(),loginPage()显然就是配置spring security的login页面的,他上面的注解的意思是当我们没有配置登录页面时就会自动为我们生成一个登录表单,而且它的表单代码也在其中,由于编码问题,html页面的表单的< >两个尖括号变成了& lt 。

注解中表单的定义为<form action=“${loginProcessingUrl}” method=“post”>

我们可以发现其是以post的方式提交的,提交的地址为${loginProcessingUrl},所以我们也可以把自己的表单定义为这个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ntLtRK4L-1651508223772)(springBoot.assets/image-20210909184528266.png)]

接下来我们就看看传递的用户名和密码后端是如果接收的,这个要看FormLoginConfigure类的构造

public FormLoginConfigurer() {
   super(new UsernamePasswordAuthenticationFilter(), null);
   usernameParameter("username");
   passwordParameter("password");
}

这个意思是我们的名字必须要和这个上所起的名字相互匹配,看看我们的代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsFzA8dZ-1651508223772)(springBoot.assets/image-20210909184854879.png)]

如果他不同的话我们就无法登录,这个时候我们继续看源码

	 *
	 * 	&#064;Override
	 * 	protected void configure(HttpSecurity http) throws Exception {
	 * 		http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin()
	 * 				.usernameParameter(&quot;username&quot;) // default is username
	 * 				.passwordParameter(&quot;password&quot;) // default is password
	 * 				.loginPage(&quot;/authentication/login&quot;) // default is /login with an HTTP get
	 * 				.failureUrl(&quot;/authentication/login?failed&quot;) // default is /login?error
	 * 				.loginProcessingUrl(&quot;/authentication/login/process&quot;); // default is /login
	 * 																		// with an HTTP
	 * 																		// post

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xL1HThAT-1651508223772)(springBoot.assets/image-20210909185201531.png)]

设置接收的变量名

 <form th:action="${loginProcessingUrl}" method="post">

这个时候我们发现注销页面失效了,通过源码我们发现,这个是因为页面触发了CSRF保护,必须使用post方法,但是现在为get方式

 * The URL that triggers log out to occur (default is "/logout"). If CSRF protection
 * is enabled (default), then the request must also be a POST. This means that by
 * default POST "/logout" is required to trigger a log out. If CSRF protection is
 * disabled, then any HTTP method is allowed.
 * 
 * *触发注销的URL(默认为“/logout”),如果启用了CSRF保护(默认),那么请求也必须是POST
 * 这意味着默认情况下需要POST“/logout”来触发注销。如果CSRF保护被禁用,则允许任何HTTP方法
 *
 * <p>
 * It is considered best practice to use an HTTP POST on any action that changes state
 * (i.e. log out) to protect against <a
 * href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If
 * you really want to use an HTTP GET, you can use
 * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code>
 * </p>
 * *最佳做法是在更改状态(即注销)的任何操作上使用HTTP POST来防止
 * <a href=“https://en.wikipedia.org/wiki/Cross-site_请求伪造“>CSRF攻击</a>
 * 如果您真的想使用http get,可以使用
 * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl,“GET”));</code>
 *
 * 
 * @see #logoutRequestMatcher(RequestMatcher)
 * @see HttpSecurity#csrf()
 *
 * @param logoutUrl the URL that will invoke logout.
 * @return the {@link LogoutConfigurer} for further customization
 * 

如果我们想使用get方法提交的话有三种方式:

  • 关闭CSRF攻击,但是这样会有漏洞
  • 所有请求注销的方式都是POST
  • 在config类中使用logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl,“GET”))

方式三的实现

http.logout()	//开启注销功能
	.logoutSuccessUrl("/")	//成功之后跳转的页面
	.logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"));	//使得GET方法也可以成功注销用户

方式二的实现

<form th:action="@{/logout}" method="post" style="display: inline-block">
    <a class="item">
        <input type="submit" value="注销">
    </a>
</form>

方式一:

http.logout().logoutSuccessUrl("/");

增加remember Me功能

我们自定义的页面上没有这个选项,所以我们应该加上他,首先开启remember Me()功能,但是需要我们在页面上写一个开启地方

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6IWrp56-1651508223773)(springBoot.assets/image-20220311224301572.png)]

所以我们只要将新增的记住我的name值设置为remember-me即可

<div class="field">
    <input type="checkbox" name="remember-me">&nbsp;记住我
</div>

小结

到此spring security就学习的差不多了,在spring security中我们学习了它的两个功能:认证+授权,具体学习的知识点如下:

  • 认证:认证用户的角色信息、密码加密
  • 授权:登陆、注销、防跨站攻击、记住我、指定资源访问授权
  • 直接参考下面的代码来记忆和理解
package com.zhao.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


//AOP: 拦截器
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //链式编程
    //授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人都可以访问,功能页只有对应有权限的人才访问
        //authorizeRequests批准请求,antMatchers 匹配对应的动作,hasRole有作用对象
        //请求授权的规则
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level2/**").hasRole("vip3");

        //没有权限默认会到登录页面,需要开启登录页面
        http.formLogin().loginPage("/toLogin");
        //防止网站工具
        http.csrf().disable();//关闭csrf功能,登录失败肯定存在一些问题

        //开启注销功能
        http.logout().logoutSuccessUrl("/");

        //开启记住我功能 cookie,保存两周
        http.rememberMe();
    }

    //认证

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //这些数据正常应该在数据库中访问,现在是在内存中访问
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhao").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3","vip1")
                .and()
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }
}

十四,shiro

1,简介

Apache Shiro 是 Java 的一个安全(权限)框架
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等
下载地址
官网:http://shiro.apache.org/
github:https://codechina.csdn.net/mirrors/apache/shiro?utm_source=csdn_github_accelerator
直接取GitHub上下载压缩包即可

2,作用

在这里插入图片描述

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

3,Shiro架构(外部)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YJPvqC9o-1651509359428)(springBoot.assets/image-20210909193426886.png)]

Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色

Realm:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource

就是说,subject原本应该叫做user,但是名字和其他框架冲突了,而在安全领域方面,对象叫做subject,所以就改为这给名字。这个三个可以看做,Subject是一个对象,而SecurityManager是一个大型的容器,存储了对象和他的操作。Realm是等同于一个数据库,就是对象的所有的信息都在里面放着。

也可以理解为Subject是饭店的前台,我们客户需要啥菜都要和它进行报备,SecurityManager是指饭店的厨房,这才是我们真正做饭的地方,但是做饭需要食材,所以Realm等同于饭店的冷冻库。

4,Shiro架构(内部)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LkVmDKG4-1651509359428)(springBoot.assets/image-20210909193525220.png)]

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

5,Hello World

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JMNBE0y-1651509359429)(springBoot.assets/image-20210909193648783.png)]

按照官方doc只是,下载压缩包之后解压,并进入对应的文件夹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rznVX7Uy-1651509359429)(springBoot.assets/image-20210909193721657.png)]

创建一个普通的maven项目:springboot-07-shiro
老规矩:删除没有文件夹,把这个项目作为一个父项目,在它内部创建子model
创建子model:hello-shiro
直接打开例子中的pom.xml,将依赖导入子model,注意:按照使用最新版本原则,直接粘贴对应依赖去maven仓库中搜最新的,下面的都是最新的

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhao</groupId>
    <artifactId>springboot-07-shiro</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>hello-shiro</module>
    </modules>


    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <classpathScope>test</classpathScope>
                    <mainClass>Quickstart</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>


    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.6.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

</project>
  • 拷贝快速开始例子的resource下的资源文件
  • log4j.properties
log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
  • 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
  • 拷贝Java代码,文件Quickstart.java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Simple Quickstart application showing how to use Shiro's API.
 *
 * @since 0.9 RC2
 */
public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            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?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        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);
    }
}

测试
这里出现了第一个坑:IniSecurityManagerFactory已经过期,不能使用了,我在项目中连jar包都导入不进来
【解决方法1】将下面这3行代码更换为

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

这4行代码

DefaultSecurityManager factory = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
factory.setRealm(iniRealm);
SecurityUtils.setSecurityManager(factory);

再次运行

或者使用以下代码

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;

//import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
//import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Simple Quickstart application showing how to use Shiro's API.
 *
 * @since 0.9 RC2
 */
public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):

        //原本的方法
//        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//        SecurityManager securityManager = factory.getInstance();

        //新方法   shiro更新问题
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        securityManager.setRealm(iniRealm);

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            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?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        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);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KiYL24nP-1651509359429)(springBoot.assets/image-20210909222432929.png)]

小结:

第一个Shiro程序运行成功总共分5步:

  1. 下载Shiro
  2. 创建一个项目,导入pom依赖【当然,按照一贯使用最新的原则,直接去maven上找对应的以来即可】
  3. 粘贴Quickstart中的配置文件,两个:log4j.properties+shiro.ini
  4. 粘贴Quickstart.java代码【会报错,,上文有两种解决方案】
  5. 运行测试

6,shiro的Subject分析

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):

        //原本的方法
//        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//        SecurityManager securityManager = factory.getInstance();

        //新方法   shiro更新问题
        //加载shiro.ini文件,获取SecurityManager工厂,并将工厂实例设置到SecurityUtils中提供给后面使用,就是设置了一个简单的环境
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        securityManager.setRealm(iniRealm);
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        //获取到Subject实例
        //通过SecurityUtils获取到当前正在执行的用户,获取Subject对象
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();//通过用户对象获取session对象,这里是shiro自己的session
        session.setAttribute("someKey", "aValue");//在session中存放了一个键值对
        String value = (String) session.getAttribute("someKey");//将上面存入的值取出来
        if (value.equals("aValue")) {   //判断取出的值是不是aValue
            log.info("Retrieved the correct value! [" + value + "]");//是的话就记录进入日志
        }

        // let's login the current user so we can check against roles and permissions:
        //shiro认证用户使用方法,token为令牌
        if (!currentUser.isAuthenticated()) {//如果当前用户没有被认证
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");//设置用户的账号+密码,获取令牌
            token.setRememberMe(true);//设置记住我
            try {
                currentUser.login(token);//使用令牌认证当前用户
            } catch (UnknownAccountException uae) {//未知账户认证,就是当前用户没有被注册过,即在ini文件中没有和这个用户
                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?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        //打印当前用户的唯一标识符
        //getPrincipal()的作用:返回此时subject在应用程序范围内唯一标识主题
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        //测试用户角色
        if (currentUser.hasRole("schwartz")) {//看这个角色是不是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")) {//测试用户是否有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!
        //注销subject用户
        currentUser.logout();
        //结束运行
        System.exit(0);
    }

小结:

从官方给出的Quickstart例子中,我们可以看出它着重讲述了subject/用户 对象的操作

怎么获取subject:Subject currentUser = SecurityUtils.getSubject();
怎么获取该subject的session对象:Session session = currentUser.getSession();
认证subject:currentUser.login(token);
判断该subject是否通过了认证:currentUser.isAuthenticated()
打印subject的唯一标识符:currentUser.getPrincipal()
判断subject是否拥有指定的角色:currentUser.hasRole(“schwartz”)
判断subject的角色是否拥有指定的权限:currentUser.isPermitted(“lightsaber:wield”)
注销用户:currentUser.logout();
从上面讲解的方法我们可以发现,上面的操作在spring boot中有相同/类似的操作

7,SpringBoot整合Shiro环境

1,搭建环境

  • 创建一个SpringBoot项目,作为父项目即可

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oXU3DaLl-1651509359430)(springBoot.assets/image-20210910164724440.png)]

  • 导入thymeleaf模板引擎的依赖

    <!--thymeleaf依赖-->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
    
  • 在template文件夹下创建一个视图,再创建一个controller,用于视图跳转视图,主要测试整个项目的搭建是否成功

    @Controller
    public class MyController {
    
        @RequestMapping({"/","/index"})
        public String toIndex(Model model){
            model.addAttribute("msg","hello,Shiro");
            return "index";
        }
    }
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    </body>
    </html>
    
  • 但是我们就发现localhost:8080/???不管干什么都会跳转到index.xml页面,所以去看看SpringBoot自动配置欢迎页的源码

    /**
     * An {@link AbstractUrlHandlerMapping} for an application's welcome page. Supports both
     * static and templated files. If both a static and templated index page are available,
     * the static page is preferred.
     *
     * @author Andy Wilkinson
     * @author Bruce Brouwer
     */
    final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
    
       private static final Log logger = LogFactory.getLog(WelcomePageHandlerMapping.class);
    
       private static final List<MediaType> MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL);
    
       WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
             ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
          if (welcomePage != null && "/**".equals(staticPathPattern)) {
             logger.info("Adding welcome page: " + welcomePage);
             setRootViewName("forward:index.html");
          }
          else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
             logger.info("Adding welcome page template: index");
             setRootViewName("index");
          }
       }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nn1ADn51-1651509359430)(springBoot.assets/image-20210910170603373.png)]

  • 从上面的源码看出来我们只要在静态资源或者模板目录下创建一个名为index.html的文件,就会被自动设置为SpringBoot的欢迎页

2,ShiroConfig

  • shiro有三个对象,分别为Subject(用户),SecurityManager(管理所有用户),Realm(连接数据)

  • 导入SpringBoot整合shiro的依赖

    <!--springboot整合shiro的依赖-->
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.6.0</version>
    </dependency>
    
  • 创建一个配置类,要将三个类装配到spring容器中:userRealm,DefaultWebSecurityManager,ShiroFilterFactoryBean

  • 其中userRealm需要我们自己定义

    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("执行力认证=======》doGetAuthenticationInfo");
            return null;
        }
    }
    
  • 定义realm类的时候需要继承AuthorizingRealm,这个类是一个抽象类,它有两个方法没有实现doGetAuthorizationInfo()[授权]和doGetAuthenticationInfo()[认证],在上面的实现中,我们只是输出了这个方法执了

  • 将定义的realm装配到spring容器中(在config中放入)

    @Configuration
    public class ShiroConfig {
        //1,装配realm实例到spring中
        @Bean
        public UserRealm userRealm(){
            return new UserRealm();
        }
        //2,装配DefaultWebSecurityManager实例到spring容器中,DefaultWebSecurityManager需要关联到我们装配到spring容器中的realm实例
        //所以我们先写了realm
        @Bean
        public DefaultWebSecurityManager securityManager(@Qualifier("userRealm") UserRealm userRealm){
            DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
            manager.setRealm(userRealm);//将我们的realm对象与DefaultWebSecurityManager关联起来
            return manager;
        }
        //装配realm实例到spring容器中ShiroFilterFactoryBean,这个实例要关联我们配置到spring容器中的DefaultWebSecurityManager
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            //设置安全管理器
            bean.setSecurityManager(securityManager);
            return bean;
        }
    
    }
    
  • 注意:上面传递参数的时候我们使用了注解@Qualifier(“spring容器中bean的id”),这在我们将spring自动装配的时候,bytyname和bytetype都不能使用的时候讲过,直接使用注解@Qualifier可以指定spring容器中的哪一个bean实例注入给这个对象的引用

3.创建用于测试的页面

  • 创建一个user视图文件夹,在该文件夹下创建两个html文件:add.html和update.html,这两个页面用于我们从index.html跳转

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hgKBCLNK-1651509359430)(springBoot.assets/image-20210910172640997.png)]

  • 在index页面上加上添加跳转标签

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>首页</h1>
    <p th:text="${msg}"></p>
    <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">upDate</a>
    </body>
    </html>
    
  • 添加跳转视图的controller

        @RequestMapping("//user/add")
        public String addPage(){
            return "/user/add";
        }
    
        @RequestMapping("/user/update")
        public String updatePage(){
            return "user/update";
        }
    }
    

4,小结

  • 导入SpringBoot整合shiro的依赖
  • 编写两个核心配置文件:config和realm
  • 对比spring security,使用shiro多写了3个bean,spring security直接在config中配置认证+授权,而shiro在realm中配置认证+授权,并还要使用config向spring容器中注入3个bean(realm、DefaultWebSecurityManager、ShiroFilterFactoryBean)
    这样对比下来使用shiro实现认证+授权成本更大

8,shiro实现登录拦截

  • 实现登陆拦截其实就是使用shiro拦截没有经过认证的用户的请求,当用户在没有认证的情况下就请求资源时,就将其重定向到登陆认证页面,这一点和spring security一样
  • 要实现认证+授权,本质上还是在使用过滤器/拦截器,而spring security只是把这些都封装好了,我们直接调用封装之后的方法就可以使用;而Shiro对于过滤器/拦截器的封装没有spring security那么彻底,所以我们需要在刚刚创建的config中的ShiroFilterFactoryBean中配置我们要加上的过滤器/拦截器来实现认证+授权
  • shiro中有如下5种常用过滤器(shiro常用过滤器)
    • anon:无需认证即可访问
    • authc:必须认证才能访问
    • user:必须有"记住我"功能才能访问【几乎不用】
    • perms:拥有对某个资源的访问权限才能访问【比如某系资源只有管理员可以访问】
    • role:拥有某个角色才能访问
  • shiro中过滤器的使用语法:map集合.put(“需要过滤的URL”,“要使用的过滤器”)

配置过滤器

//装配realm实例到spring容器中ShiroFilterFactoryBean,这个实例要关联我们配置到spring容器中的DefaultWebSecurityManager
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(securityManager);
        return bean;
}

配置我们指定的过滤器需要调用bean.setFilterChainDefinitionMap()方法,即为设置过滤器链需要向这个方法中传入一个Map集合,我们可以查看这个方法的定义

public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
    this.filterChainDefinitionMap = filterChainDefinitionMap;
}

可见需要传入一个Map集合,所以在shiroFilterFactoryBean()中定义一个map集合,将我们要设置的过滤器装入,并传入方法setFilterChainDefinitionMap()中

public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(securityManager);
    /*
    - anon:无需认证即可访问
    - authc:必须认证才能访问
    - user:必须有"记住我"功能才能访问【几乎不用】
    - perms:拥有对某个资源的访问权限才能访问【比如某系资源只有管理员可以访问】
    - role:拥有某个角色才能访问
    */
    Map<String, String> filterMap = new LinkedHashMap<>();
    filterMap.put("/user/add","authc");
    filterMap.put("/user/update","authc");
    //配置过滤器
    bean.setFilterChainDefinitionMap(filterMap);
    return bean;
}

我们只做了:

  • 定义一个mapper
  • 向map中存值,其中key为需要拦截的请求,value为放行的条件,需要条件什么请求的拦截直接put进去
  • 将map集合作为参数传入setFilterChainDefinitionMap

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oihrJ1sE-1651509359431)(springBoot.assets/image-20210910180452731.png)]

由于shiro没有一个登录页面,所以需要写一个

<!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>&nbsp;码:<input type="password" name="password"></p>
    <p><input type="submit"></p>
</form>

</body>
</html>

同时在controller上增加一个跳转

@RequestMapping("/toLogin")
public String loginPage(){
    return "login";
}

现在只是实现了拦截功能,但是还没有具体提交表单

为什么我们没有创建登录页面的时候会被重定向到"/login.jsp"呢?看源码即可

private void applyLoginUrlIfNecessary(Filter filter) {
    String loginUrl = getLoginUrl();
    if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
        AccessControlFilter acFilter = (AccessControlFilter) filter;
        //only apply the login url if they haven't explicitly configured one already:
        //仅当他们尚未明确配置登录url时才应用登录url
        String existingLoginUrl = acFilter.getLoginUrl();
        if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
            acFilter.setLoginUrl(loginUrl);
        }
    }
public static final String DEFAULT_LOGIN_URL = "/login.jsp";
  • 可见,默认设置的登陆页面URL就是"/login.jsp",所以我们没有显式的配置登陆页面的url的时候会被重定向到"/login.jsp"

9,Shiro实现用户认证和注销

Shiro实现用户认证

  • shiro中用户的认证需要放在realm中,即请求都在config中进行,但是真正的权限操作需要再realm进行,他会将config和realm类联动起来

  • 但是获取前端的参数需要再controller中进行,我们需要写一个获取前端参数调用shiro进行认证,并返回结果视图的方法

    @RequestMapping("/login")
    public String toLogin(String username,String password,Model model){
        //获取当前subject对象
        Subject subject = SecurityUtils.getSubject();
        //创建一个用户身份令牌
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //使用令牌登录
        try{
            //注意:这里没有叫我们写上shiro的Quickstart案例中的try...cathc,
            //我们可以手动的加上,因为我们需要判断登陆是否出错,从而向前端返回对应的提示信息
            subject.login(token);
            return "index";
        } catch (UnknownAccountException uae) { //未知账户异常,就是当前登陆的用户并没有注册过,在ini文件中没有这个账户的信息
            model.addAttribute("msg","你输入的账户名或密码错误");
            return "login";
        } catch (IncorrectCredentialsException ice) { //账户密码错误异常
            model.addAttribute("msg","你输入的账户名或密码错误");
            return "login";
        } catch (LockedAccountException lae) {  //登陆账户被锁定的异常,比如连续输错5次密码就把你的账户锁定
            model.addAttribute("msg","密码错误超过5次,你的账号已被冻结");
            return "login";
        }
    
    }
    
  • 账号和,密码判断需要去realm中认证方法中进行的

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行力认证=======》doGetAuthenticationInfo");
        //本来需要从数据库中调取的,但是现在是通过伪造的
        String username = "root";
        String password = "123456";
        //使用token获取用户传入需要认证的用户名和密码
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        if (!username.equals(usernamePasswordToken.getUsername())){
            return null;
            //如果用户名和我们数据库中的用户名不一致,即没有查询到用户,就返回空
            //只要返回空就会抛出异常抛出的异常就是UnknownAccountException
        }
        //密码认证shiro不允许我们自己进行,我们只能将我们设置好的密码传进去,具体的操作,大概就是shiro会拿着我们在这里设置的密码和token中传入的密码进行对比,并返回结果
        return new SimpleAuthenticationInfo("",password,"");
    
    }
    

2,实现用户注销

  • 按照shiro的过滤器使用语法,注销功能只需要我们在map集合中加入一个kv键值对即可,但是我们要为对应的注销写一个a标签来发起注销请求,并将a标签的href属性设置为需要注销过滤器处理的url

  • 在controller中编写一个logout处理映射方法

    @RequestMapping("/logout")
    public String logout(){
        return "redirect:/toLogin";
    }
    
  • 在config中添加注销过滤器到map集合中

    filterMap.put("/logout","logout");
    
    • 只要请求"/logout",就会触发shiro的注销过滤器logout
  • 前端增加a标签发起注销请求

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

10,整合mybatis

  1. 导入依赖

    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--Druid数据源依赖-->
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.24</version>
    </dependency>
    <!--log4j的依赖-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!--mybatis和spring boot整合依赖-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    
  2. 配置数据库连接4大参数+spring boot整合Druid数据源

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        driver-class-name: com.mysql.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    #stat:监控统计、log4j:日志记录、wall:防御sql注入
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    #整合mybatis的配置
    mybatis:
      type-aliases-package: com.zhao.pojo #给类取别名
      mapper-locations: classpath:mybatis/mapper/*.xml #mapper注册
    
  3. spring boot整合mybatis,将mybatis配置写在application中即可

    #整合mybatis的配置
    mybatis:
      type-aliases-package: com.zhao.pojo #给类取别名
      mapper-locations: classpath:mybatis/mapper/*.xml #mapper注册
    
  4. 创建pojo,创建resource文件夹下的mybatis/mapper/*.xml文件

    package com.zhao.pojo;/**
     * ************************************************************************
     * 项目名称: springboot-07-shiro <br/>
     * 文件名称:  <br/>
     * 文件描述: 这里添加您的类文件描述,说明当前文件要包含的功能。 <br/>
     * 文件创建:赵先生 <br/>
     * 创建时间: 2021/9/10 <br/>
     * 山西优逸客科技有限公司 Copyright (c) All Rights Reserved. <br/>
     *
     * @version v1.0 <br/>
     * @update [序号][日期YYYY-MM-DD][更改人姓名][变更描述]<br/>
     * ************************************************************************
     */
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * @ProjectName: springboot-07-shiro
     * @ClassName: User
     * @Description: 请描述该类的功能
     * @Author: 赵先生
     * @Date: 2021/9/10 19:40
     * @version v1.0
     * Copyright (c) All Rights Reserved,山西优逸客科技有限公司,. 
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String name;
        private String pwd;
    }
    
  5. 有一个pojo就创建一个mapper/dao

    @Mapper
    @Repository
    public interface UserMapper {
        //根据用户名查询密码
        public User queryUserByName(@Param("username") String name);
    }
    
  6. 在资源目录下创建一个mybatis文件夹下一个mapper

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gOJXUbAx-1651509359431)(springBoot.assets/image-20210910223946249.png)]

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.zhao.mapper.UserMapper">
        <select id="queryUserByName" resultType="user">
            SELECT * FROM mybatis.user
            WHERE name = #{username}
        </select>
    </mapper>
    
  7. 创建service层(完善这个项目的架构)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9LNBlvdt-1651509359431)(springBoot.assets/image-20210910224102490.png)]

    public interface UserService {
        //通过用户名称查询用户对象
        public User queryUserByName(String name);
    }
    
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        UserMapper userMapper;
        @Override
        public User queryUserByName(String name) {
            return userMapper.queryUserByName(name);
        }
    }
    

    记得一定要写@Service标签注入bean

  8. 测试service是否能够成功获取数据

    @SpringBootTest
    class ShiroSpringbootApplicationTests {
    
        @Autowired
        private UserService userService;
        @Test
        void contextLoads() {
            User user = userService.queryUserByName("aaa");
            System.err.println(user);
        }
    
    }
    
  9. 改造上一篇博客中编写的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 token) throws AuthenticationException {
            System.out.println("执行力认证=======》doGetAuthenticationInfo");
    
            //使用token获取用户传入需要认证的用户名和密码
            UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
            User user = userService.queryUserByName(usernamePasswordToken.getUsername());
            if (user == null){
                return null;
                //如果用户名和我们数据库中的用户名不一致,即没有查询到用户,就返回空
                //只要返回空就会抛出异常抛出的异常就是UnknownAccountException
            }
            //密码认证shiro不允许我们自己进行,我们只能将我们设置好的密码传进去,具体的操作,大概就是shiro会拿着我们在这里设置的密码和token中传入的密码进行对比,并返回结果
            return new SimpleAuthenticationInfo("",user.getPwd(),"");
    
        }
    }
    

    与之前相比较,之前是伪造数据,现在呢就是通过查询用户的方法去找到用户对应的数据传入

  10. 测试

11,Shiro请求授权

1,Shiro开启资源授权访问

  • 现在我们的两个连接add和update都只是要求用户认证,认证之后就可以正常访问了,但是在真实业务中资源除了和认证挂钩之外,还与授权有关,对于有些资源,只有你拥有对应的角色你才能去访问,否则你没有权限去访问
  • 在spring security中你只需要在自定义的config类中configure(HttpSecurity http)方法中调用 http.authorizeRequests()方法就可以实现对某一资源的授权访问
  • 在shiro中,对于资源的授权管理通过使用过滤器实现,授权需要使用过滤器perms
    /*
    * - anon:无需认证即可访问
    * - authc:必须认证才能访问
    * - user:必须有"记住我"功能才能访问【几乎不用】
    * - perms:拥有对某个资源的访问权限才能访问【比如某系资源只有管理员可以访问】
    * - role:拥有某个角色才能访问
    * */
filterMap.put("/user/add","perms[user:add]");
//表示用户角色为user,并且拥有add权限的用户可以请求到"/user/add"对应的资源

  • 上面的代码相当于增加了权限控制,对于URL"/user/add"的请求配置了权限控制,没有对应的角色和权限不能对资源进行请求

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADWldlH4-1651509359431)(springBoot.assets/image-20210910225652106.png)]

  • 但是正常情况下未授权也不应该跳转到上面的页面,而是自定义的页面提醒用户未授权

  • 创建未授权页面Unauthorized.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>未授权</title>
    </head>
    <body>
    <h1>对不起,您没有访问该资源的权限,如需访问该资源,请联系管理员</h1>
    </body>
    </html>
    
  • controller编写跳转方法

    @RequestMapping("/Unauthorized")
    public String Unauthorized(){   //未授权页面跳转
        return "Unauthorized"; 
    }
    
  • 设置shiro在未授权的情况下跳转到我们指定的页面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43QphGxA-1651509359432)(springBoot.assets/image-20210910230437367.png)]

2,授权用户权限

  • 我们现在是把授权功能开启了,但是我们并没有将add资源访问需要的权限授予用户,所以现在所有的用户等不能访问我们的add资源,所以我们现在需要对用户进行授权操作

  • 可以授权的地方就是realm中,这个类里面的方法doGetAuthorizationInfo()专门用于向用户授权

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了授权========>doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//用于向用户授权的类对象
        info.addStringPermission("user:add");//通过字符串向用户授权,授予了用户user角色,并且有了add的权限
        return info ;
    }
    
  • 测试,但是现在有一个问题就是所有的对象都可以访问进去这个不是我们想要的结果

  • 正常的做法应该是,我们从数据库中查询用户的权限,符合就可以让他访问,没有的话就跳转到没有去权限的页面

  • 在数据库中加一个一个表示用户权限的字段perms

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KKwhQpRs-1651509359432)(springBoot.assets/image-20210910232050836.png)]

  • 新增pojo的字段

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer id;
        private String name;
        private String pwd;
        private String perms;
    }
    
  • 现在就是要在doGetAuthorizationInfo方法中拿到当前请求该资源的用户的权限,把判断他是否有资格去请求该资源

  • 但是问题是我们的用户数据doGetAuthenticationInfo(AuthenticationToken token)中查询到的,那么如何将方法doGetAuthenticationInfo(AuthenticationToken token)中的数据传入到doGetAuthorizationInfo(PrincipalCollection principalCollection)呢?

    • 这个时候就要看SimpleAuthenticationInfo(user,user.getPwd(),“”);方法了

      //密码认证shiro不允许我们自己进行,我们只能将我们设置好的密码传进去,具体的操作,大概就是shiro会拿着我们在这里设置的密码和token中传入的密码进行对比,并返回结果
      return new SimpleAuthenticationInfo(user,user.getPwd(),"");
      
      public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
          this.principals = new SimplePrincipalCollection(principal, realmName);
          this.credentials = credentials;
      }
      
          * @param principal   从数据库中查询出的用户对象,用于唯一表示用户
          * @param credentials 用户的密码,用于和前端填写的密码进行对比
          * @param realmName   当前realm的名称,不传就写""
          *
          * 这个构造还有多个重载
      
      
  • 注意第一个参数,我们可以将数据库中查询到的user对象传递进去,如何在doGetAuthorizationInfo(PrincipalCollection principalCollection)中获取

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//定义一个授权信息
        Subject subject = SecurityUtils.getSubject();   //获取当前对象
        User principal = (User) subject.getPrincipal(); //获取当前对象的唯一标识符,即我们传入的user对象,从SimpleAuthenticationInfo的构造中传入的
        info.addStringPermission(principal.getPerms()); //获取user权限,并赋予给当前请求需要授权才能访问的资源的用户
        return info;
        //将SimpleAuthorizationInfo,即对于用户的授权信息返回,如果我们在上面进行的授权操作授予的权限/用户本身具有的权限包括了请求当前资源的权限,那么资源请求成功,如果不包含就请求失败
    
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A2mOA7lW-1651509359432)(springBoot.assets/image-20210910232816823.png)]

注意添加权限是在map中

小结

  • shiro中我们需要使用过滤器开启资源访问的权限控制
  • 用户的授权实在用户访问需要权限才能访问的资源的时候授予当前用户的,即用户首先通过认证,认证之后如果要请求需要权限才能访问的资源的时候,就在realm中的doGetAuthorizationInfo()中将用户存在数据库中的权限查询出来授予他,如果他存于数据库中的权限包括了请求当前资源的权限,那么就将请求的资源传给用户,如果不包括,就将用户重定向到未授权页面

12,Shiro整合thymeleaf

1.动态菜单实现

在spring security中,我们还实现了动态菜单的效果,即用户登陆之后不会将所有的资源都展示给用户,而是用户有对应的角色/权限,才将其权限范围内的资源展现出来,在spring security中我们通过整合thymeleaf实现了该功能;在shiro中我们也可以通过整合thymeleaf实现该功能

  • 导入依赖

    <!--shiro整合thymeleaf依赖-->
    <!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    
  • spring security在导入依赖之后就可以直接使用,但是shiro导入依赖之后我们需要进行一些配置才能正常的使用整合之后的功能;配置也很简单,就是将ShiroDialect 类装配到spring容器中即可,ShiroDialect (shiro方言)

    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
    
  • 前端导入命名空间

    xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
    

    注意是导入这个命名空间,其他的没显示

  • 使用整合之后的前缀实现动态菜单,这里使用前缀shiro的时候可以参考切面的spring security

    <div >
        <div shiro:notAuthenticated>
            <a th:href="@{/toLogin}">登陆</a>
        </div>
        <div shiro:authenticated>
            <div shiro:hasPermission="user:add" style="display: inline">
                <a th:href="@{/user/add}">add</a>
            </div>
            <div shiro:hasPermission="user:update" style="display: inline">
                <a th:href="@{/user/update}">update</a>
            </div>
            <div style="display: inline">
                <a th:href="@{/logout}">logout</a>
            </div>
        </div>
    </div>
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NhKf2CUA-1651509359432)(springBoot.assets/image-20210910234025470.png)]

十五,Swagger

1,Swagger介绍和集成

为什么会有Swagger

1,前后端分离

  • 当前最火的前后端分离的技术栈:Vue+SpringBoot

  • 后端时代:前端只用管理和开发静态页面,并将开发好的页面交给后端,后端通过模板引擎JSP重构视图页面(加一些EL表达式等),此时后端是整个开发的主力

  • 前后端分离时代:

    • 后端实现:后端控制层、服务处、数据访问层【后端团队】
    • 前端实现:前端控制层、视图层【前端团队】
      • 伪造后端数据,就是在前端项目中使用JSON格式存储一些不变的数据,然后在项目运行的时候读取JSON数据即可,所以此时前端工程不需要后端提供的数据,也可以运行起来查看最终的效果
    • 前后端怎么进行交互?==?数据API接口(这个接口不是说的我们Java里面的那个接口,而是数据接口,就是我们后端写的controller,前端视图将访问链接对应的绑定到controller中的具体方法上,用户点击前端页面上对应的请求链接,请求就会传入controller中对应的方法,该方法调用service层获取数据,再将数据返回,视图解析器拿到视图+后端返回的数据并将数据渲染上去返回给用户,整个流程结束;当然为了前端可以正确解析后端返回的数据,前后端需要商量数据的格式,只有二者遵守相同的数据格式,数据才能正确的渲染)
    • 前后端分离的好处:前后端相对独立,实现了前后端松耦合(接口实现),前后端甚至可以部署在不同的服务器上,只要前端可以调用到 后端提供的数据接口,是不是部署在一台服务器上没有多大的关系
  • 前后端分离的问题

    • 前后端集成联调问题:前端和后端人员不能及时协商,因为前后端对于某一个数据的CRUD的操作步骤很不一样,前端需要增加一个字段,只需要去页面上写一个即可;但是后端需要从数据库、dao/mapper、service、controller和POJO全部修改一遍,并且一次修改可能涉及多个字段,并且修改之后可能推翻原来的修改又改回去,这就让后端人员很恼火了;
    • 这就导致了问题的爆发,问题出现,工程就要延期,工程延期公司就会亏损
  • 解决方案

    • 首先制定一个schema(计划/纲要/架构),实时更新最新的数据API,降低集成风险
    • 早期:制定word计划文档,但是每次都要下载文件,人多了效果很不好
    • 后来出现了一些工具,比如postman,专门用于测试后端数据接口是否能够正常使用,因为前端需要在确定你的数据接口可以正常使用之后再去将自己的视图模板上的数据模板改成后端提供的数据API;如果前端拿到后端数据之后直接就改了前端视图模板,如果后端数据不能正常请求,那么自己的前端项目也就不能进行正常的测试了,只能等着后端将数据接口修复,前端才能继续自己的工作,继续扩展前端业务,这样显然不满足需求,所以就出现了postman,它可以直接去测试后端数据接口能不能正常的获取到前端想要的数据
    • 现在的解决办法:使用swagger,swagger的好处在于它能实时的向前端展示后端提供了哪些数据接口,并且支持数据接口的测试,不用再去下载例如postman这样的第三方软件进行单独的数据接口测试
  • 总结:就是前后端分离,有一个问题就是我不用知道后端的数据接口是否能够使用,所以我就有了这样一个工具,可以实时向前端展示后端提供了那些数据接口并支持测试

什么是Swagger

  • 号称世界上最流行的API框架,它会实时更新API文档
  • RestFul风格的API,文档在线自动生成工具 => API文档与API定义同步更新(就是后端数据接口/API一旦写完,API文档就实时更新了)
  • 直接运行,并支持在线测试API/数据接口
  • 支持多种语言 (如:Java,PHP等)
  • 官网:https://swagger.io/

在项目中使用Swagger

导入依赖Springfox Swagger2+Springfox Swagger UI

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IbE9oqjz-1651712892101)(springBoot.assets/image-20210911104322495.png)]

<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>3.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>3.0.0</version>
</dependency>

SpringBoot集成Swagger

  • 创建一个springBoot项目:Swagger-demo

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RHJq1xFl-1651712892106)(springBoot.assets/image-20210911120224420.png)]

  • 导入web依赖

  • 导入swagger依赖

  • 编写一个hello过程(就是创建一个跳转页面)

    @RestController
    public class HelloController {
        @RequestMapping("/hello")
        public String hello(){
            return "Hello Swagger";
        }
    }
    
  • 测试

  • 配置swagger,配置之后才能使用,因为我们可以观察导入的两个依赖的名称Springfox Swagger2+Springfox Swagger UI,没有一个和springBoot启动器相关,所以springBoot不会帮我们进行自动配置,所以我们需要自己配置之后才能使用

  • 在springBoot中,要新增spring容器中可以使用的依赖,需要使用javaconfig,所以我们应该去创建一个SwaggerConfig[外链图片转存失败,源站可能有防盗链机制,
    在这里插入图片描述

    @Configuration
    @EnableSwagger2//开启Swagger2
    public class SwaggerConfig {
    }
    
  • 创建的SwaggerConfig只需要写上上面两个注解,就可以测试运行了,之所以写上@EnableSwagger2就可以使用swagger的原因就是它本身就有默认的配置,所以开启之后就可以直接使用

  • 测试运行,访问localhost:8080/swagger-ui.html[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jgw2lY5l-1651712892107)(springBoot.assets/image-20210911120506981.png)]

  • 原因:引入的jar包太新了,是swagger2 3.0.0,这个版本的依赖有了很多的改动,我们可以先学老版本,然后再学习新版本,这样就会有很好的兼容性,如果想直接看swagger2 3.0.0的使用

  • 我这里先学习swagger老版本用法,所以对于导入的依赖做降级处理,两个依赖都降级为2.9.2,这个版本使用的人最多(这里就先不使用新版本的了😂,毕竟需要先入门,后面新版本改动比较大,公司使用的必然不多,所以还是只有迎合市场的需求去使用当前使用最多的一个版本)

  • 重启项目测试在这里插入图片描述

  • 这个页面可以实时查看我们项目中的数据API接口

  • 页面结构[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7kbsPGiA-1651712892109)(springBoot.assets/image-20210911120827663.png)]

2,配置Swagger信息

  • 配置swagger需要使用swagger的配置类Docket,并将这个类注入到spring容器中,所以去config中装配Docket实例

  • 但是Docket类在配置类中new的时候,需要参数,需要什么参数?看源码

    public Docket(DocumentationType documentationType) {
        this.apiInfo = ApiInfo.DEFAULT;
        this.groupName = "default";
        this.enabled = true;
        this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
        this.applyDefaultResponseMessages = true;
        this.host = "";
        this.pathMapping = Optional.absent();
        this.apiSelector = ApiSelector.DEFAULT;
        this.enableUrlTemplating = false;
        this.vendorExtensions = Lists.newArrayList();
        this.documentationType = documentationType;
    }
    
  • 显然,我们需要传入一个DocumentationType 的对象作为参数才能new Docket(),但是我们可以发现这个类只是在构造的最后一行使用的传入的DocumentationType 对象,前面的一系列操作都是在为类Docket赋初始值

  • 第一行操作this.apiInfo = ApiInfo.DEFAULT;使用了一个常量ApiInfo.DEFAULT,我们可以去看看这个常量的值

    public static final ApiInfo DEFAULT;
    static {
        DEFAULT = new ApiInfo("Api Documentation", "Api Documentation", "1.0", "urn:tos", 
        DEFAULT_CONTACT, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", 
        new ArrayList());
    }
    
  • 看到这个常量值是不是眼熟

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dfN2TnjX-1651712892112)(springBoot.assets/image-20210911123020547.png)]

    可见,页面上的swagger信息部分显示的数据就是这里配置的

  • 再来看第二个参数this.groupName = “default”,是不是也在页面上见过?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ec6YFK4v-1651712892113)(springBoot.assets/image-20210911123107539.png)]

  • 看到这里我们就可以确定,类Docket就是用来配置Swagger的

  • 回到Docket构造需要的参数DocumentationType类,我们也可以看一下这个类的源码

    public class DocumentationType extends SimplePluginMetadata {
      public static final DocumentationType SWAGGER_12 = new DocumentationType("swagger", "1.2");
      public static final DocumentationType SWAGGER_2 = new DocumentationType("swagger", "2.0");
      public static final DocumentationType SPRING_WEB = new DocumentationType("spring-web", "1.0");
    }
    
  • 我们可以发现这个类内部本来就new了3个对象,并且是静态的,所以我们在new DocumentationType()的时候可以使用这3个静态常量,仔细看一看它们之间的区别就是new的时候传入的参数值不一样,我们现在使用的依赖为swagger 2.0,所以我们使用常量2即可

    @Configuration
    @EnableSwagger2 //开启Swagger2
    public class SwaggerConfig {
        @Bean
        public Docket docket(){
            return new Docket(DocumentationType.SWAGGER_2);
        }
    }
    
  • 但是就这么就把配置对象存入spring容器中和使用swagger默认值又有什么区别,所以我们一般会在这里配置一些配置参数,怎么配置呢?刚刚在上面不是说到了一个类ApiInfo吗?它内部配置的信息不就是显示在swagger页面上的数据吗?所以我们可以通过修改这个类的参数实现配置swagger页面显示的数据

  • 怎么通过Docket类修改ApiInfo类中数据的信息呢?Docket类内部有一个方法

    public Docket apiInfo(ApiInfo apiInfo) {
    	 this.apiInfo = defaultIfAbsent(apiInfo, apiInfo);
    	 return this;
    }
    
  • 可见这个方法内部调用了另一个方法defaultIfAbsent(),我们可以看一看这个方法的源码

    public static <T> T defaultIfAbsent(T newValue, T defaultValue) {
        return Optional.fromNullable(newValue).or(Optional.fromNullable(defaultValue)).orNull();
    }
    
  • 这个方法显然就是在判断我们有没有为ApiInfo设置新的配置,如果没有,那么ApiInfo将采用原来的默认配置,所以我们就可以使用Docket.apiInfo()修改apiInfo类的配置,并且这个方法返回值也是一个Docket对象,所以我们就可以直接在@Bean方法的返回值处直接"."方法进行参数配置了

  • 那么我们可以配置哪些参数呢?可以参考ApiInfo的源码

    public class ApiInfo {
        public static final Contact DEFAULT_CONTACT = new Contact("", "", "");
        public static final ApiInfo DEFAULT;
        
        //以下8个成员变量为可以配置的参数
        private final String version;
        private final String title;
        private final String description;
        private final String termsOfServiceUrl;
        private final String license;
        private final String licenseUrl;
        private final Contact contact;
        private final List<VendorExtension> vendorExtensions;
    }
    
  • 上面就是我们可以配置的8个属性,我们可以直接调用ApiInfo的构造方法配置我们自己想要设置的属性

    @Configuration
    @EnableSwagger2 //开启Swagger2
    public class SwaggerConfig {
        @Bean
        public Docket docket(){
            return new Docket(DocumentationType.SWAGGER_2).apiInfo(new ApiInfo(
                    "thhh的SwaggerAPI文档",    //标题
                    "thhh学习Swagger",         //描述
                    "v1.0",                   //版本
                    "www.termsOfServiceUrl.com",           //服务条款URL
                    new Contact("thhh", "www.thhh.com", "www.thhh@email.com") ,   //作者信息
                    "Apache 2.0",
                    "http://www.apache.org/licenses/LICENSE-2.0",
                    new ArrayList()
            ));
        }
    
    }
    

在这里插入图片描述

  • 所以所谓的配置Swagger信息,就是在配置一些当前这个swagger的描述信息

  • 注意:在配置Swagger信息的时候我们只能通过new ApiInfo()的方式,因为ApiInfo类并没有为属性配置对应的set方法,所以我们只能通过构造来实现信息配置,对于一些我们不想修改的配置需要我们自己将源码中的配置粘贴上去,或者直接传""
    注意
    可能会报下面的错误
    在这里插入图片描述
    这时表示springboot版本太高了,导致swagger依赖缺失。降到2.5.3就行了

3,配置扫描接口及开关swagger

1.为什么要配置扫描接口

  • 之前配置的只是一些基本信息,在看swagger页面的时候,我们可以发现它一共有4个板块,这一篇用于配置接口信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S8xpNjDd-1651712892117)(springBoot.assets/image-20210911145833841.png)]

  • 我们可以发现上图中接口信息已经有两个了

    • 一个是basic-error-controller(基本错误控制器,这个controller是我们的springBoot项目自带的,我们每一个出现了4xx和5xx的错误的时候都是走的这个controller,所以swagger将它扫描了出来,这个我们可以不关心)

    • 一个是hello-controller(这个控制器就是我们自己在测试项目是否搭建成功的时候自己写的一个controller)

      @RestController
      public class HelloController {
          @RequestMapping("/hello")
          public String hello(){
              return "Hello Swagger";
          }
      }
      
  • 我们可以点开看一下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WEuiICg-1651712892118)(springBoot.assets/image-20210911145942037.png)]

    可见hello-controller下拉菜单中有多个请求方式,这是因为我们的controller中方法的映射地址写的是@RequestMapping(“/hello”),并没有指定使用哪一种请求方式去映射,所以swagger为我们把所有的请求方式都列了出来,如果我们使用@RequestMapping注解的变体,指定了这个方法对应的映射地址的请求方式,就不会有这么多的选项了

    @GetMapping
    @PostMapping
    @PutMapping
    @DeleteMapping
    @PatchMapping
    
  • 所以接下来我们需要学习swagger怎么配置扫描的数据接口/API

2.swagger配置扫描的数据接口

  • 在默认情况下,我们可以发现swagger把项目中的所有的数据接口都扫描出来了,但是我们并没有在controller的方法上自定义一些设置,这显然不是我们想要的,我们想要的是swagger扫描的是我们想让它扫描到的数据API,而不是它自己就把所有的数据接口都扫描了展示在swagger页面上

自定义扫描的接口

  • 要实现自定义扫描,我们需要使用Docket类的select()方法,要了解这个方法的作用,看源码

    /**
     * Initiates a builder for api selection.
     *
     * @return api selection builder. To complete building the api selector, the build method of the api selector
     * needs to be called, this will automatically fall back to building the docket when the build method is called.
     */
    public ApiSelectorBuilder select() {
      return new ApiSelectorBuilder(this);
    }
    
    

    注解大大意为

    *启动api选择的生成器
    
    *调用select()将返回一个api选择生成器。要完成api选择器的构建,需要调用api选择器的build方法,
    它将自动返回帮助构建docket对象
    
  • 所以我们还需要调用select().build(),我们可以查看build()的源码,从这个方法的返回值我们也可以发现,select()返回值为new ApiSelectorBuilder(),而ApiSelectorBuilder的构造为

    public class ApiSelectorBuilder {
      private final Docket parent;
    	  public ApiSelectorBuilder(Docket parent) {
    	    this.parent = parent;
    	  }
      }
    
  • 可见,new ApiSelectorBuilder()其实就是返回了一个Docket对象

  • 我们可以好好看看ApiSelectorBuilder这个类的定义,这个类很简单

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gOmp4j7-1651712892119)(springBoot.assets/image-20210911155044411.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTaOJ88a-1651712892119)(springBoot.assets/image-20210911155419791.png)]

  • 所以在调用build()之前,我们可以调用apis() / paths()来设置build()方法返回的Docket对象的参数,apis()需要传入Predicate< RequestHandler >对象,所以我们需要调用RequestHandlerSelectors.basePackage(“需要被扫描的API接口所在的包”)作为参数传入apis()中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbCaYIRG-1651712892120)(springBoot.assets/image-20210911160005016.png)]

  • 原因就是我们在apis()方法中传入的RequestHandlerSelectors.basePackage()指明了swagger要扫描的数据API的包,所以除了这个包里面的数据接口,swagger将不会去扫描其他包中的数据接口,而我们的"com.zhao.controller"包中又只有我们自己定义的一个"/hello"数据接口,所以swagger页面上只有一个数据接口展示

  • RequestHandlerSelectors除了有方法basePackage()之外,还有其他几个方法可以使用,但是只有basePackage()最常用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wQlVJn9-1651712892121)(springBoot.assets/image-20210911160117361.png)]

  • paths()方法用于规定只要哪些路径请求的API

  • 比如在HelloController中再定义一个方法test()

    @RequestMapping("/test")
    public String test(){
        return "Hello Swagger";
    }
    
  • 调用select.().paths()

@Configuration
@EnableSwagger2//开启Swagger2
public class SwaggerConfig {
    @Bean
    public Docket docket(){
        ApiInfo apiInfo = new ApiInfo(
                "ming的SwaggerAPI文档",
                "ming学习Swagger",
                "v1.0",
                "www.termsOfServiceUrl.com",
                new Contact("ming","www.ming.com", "www.1356858620@qq.com"),
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList<>()
        );
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select().apis(RequestHandlerSelectors.basePackage("com.zhao.controller")).paths(PathSelectors.ant("/test"))
                .build();
    }


}
  • 注意:虽然我已经使用apis()指定了要扫描"com.thhh.swagger.controller"下的所有数据API接口,但是由于我又使用了PathSelectors.ant(“/test”),此时swagger就只会扫描并加载显示"/test"对应的数据API接口信息
    -重启项目测试

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wOu4HoQB-1651712892121)(springBoot.assets/image-20210911160716294.png)]

  • 去掉paths()的调用再次启动项目

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqwHOCAv-1651712892122)(springBoot.assets/image-20210911160741491.png)]

2.配置是否启动swagger

  • 对于swagger是否启动就直接在swagger的配置类中使用,我们可以回顾一下在swagger配置类Docket中可以配置的参数

    public Docket(DocumentationType documentationType) {
        this.apiInfo = ApiInfo.DEFAULT;
        this.groupName = "default";
        this.enabled = true;
        this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
        this.applyDefaultResponseMessages = true;
        this.host = "";
        this.pathMapping = Optional.absent();
        this.apiSelector = ApiSelector.DEFAULT;
        this.enableUrlTemplating = false;
        this.vendorExtensions = Lists.newArrayList();
        this.documentationType = documentationType;
    }
    
  • 我们可以在上面的可以配置的参数中发现一个参数 this.enabled = true,从属性名称(启用)就可以知道,这个属性就是用来控制swagger服务是否可用的,我们可以尝试在config中配置该参数为false,再来看看能不能使用swagger服务

  • Docket类中有专门设置enable属性的方法

      /**
       * Hook to externally control auto initialization of this swagger plugin instance.
       * Typically used if defer initialization.
       *
       * @param externallyConfiguredFlag - true to turn it on, false to turn it off
       * @return this Docket
       */
      public Docket enable(boolean externallyConfiguredFlag) {
        this.enabled = externallyConfiguredFlag;
        return this;
      }
    
    
  • 我们可以直接通过链式编程的方式在new Docket后面直接"."使用enable()方法设置enabled参数

    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(false)
                .apiInfo(this.apiInfo())
                .select().apis(RequestHandlerSelectors.basePackage("com.zhao.controller")).paths(PathSelectors.ant("/test"))
                .build();
    }
    
  • 测试

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMXNFnST-1651712892122)(springBoot.assets/image-20210911180320175.png)]

  • 所以swagger功能是否开启直接使用Docket.enable()即可设置,true为开启(默认),false为关闭

3.需求:根据环境自动开启/关闭swagger功能

  • 要求swagger在生产环境中可以使用,但是在发布的时候不能使用

  • 怎么解决?

    • 分析:能不能使用swagger,肯定是要设置上面学习的Docket类的enable,即在生产环境中让enable=true,在发布的时候让enable=false

    • 再者,我们的生产环境和发布环境在springBoot项目中怎么区分的?就是通过配置文件区分的,我们配置一套dev环境,再配置一套pro环境,然后在application中选择激活哪一套环境即可

    • 最后,在配置swagger的config怎么判断当前的项目环境是生产环境还是发布环境?可以使用springBoot核心包中的一个类Environment,它有一个方法acceptsProfiles()接收配置文件,这个方法专门用来判断当前环境中指定的配置文件是否激活,可以看看源码,主要看注解

      /**
      * Return whether one or more of the given profiles is active or, in the case of no
      * explicit active profiles, whether one or more of the given profiles is included in
      * the set of default profiles. If a profile begins with '!' the logic is inverted,
      * i.e. the method will return {@code true} if the given profile is <em>not</em> active.
      * For example, {@code env.acceptsProfiles("p1", "!p2")} will return {@code true} if
      * profile 'p1' is active or 'p2' is not active.
      * @throws IllegalArgumentException if called with zero arguments
      * or if any profile is {@code null}, empty, or whitespace only
      * @see #getActiveProfiles
      * @see #getDefaultProfiles
      * @see #acceptsProfiles(Profiles)
      * @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)}
      */
      boolean acceptsProfiles(Profiles profiles);
      

      注解大意为:

      *返回一个或多个给定配置文件是否处于活动状态,或者在没有显式配置活动配置文件的情况下,
      判断是否将一个或多个指定的配置文件包含在默认配置文件集中。如果配置文件名称以“!”开头,则判断逻辑反转/颠倒,
      也就是说,如果给定的配置文件没有被激活,那么该方法将返回 true
      
      *例如,env.acceptsProfiles("p1", "!p2")},则当环境中p1配置文件被激活,p2配置文件没有激活
      的情况下这个方法返回为true
      
  • 动手解决

    • 创建两套环境的配置文件,一个配置文件为application-dev.properties,端口配置为8081;一个配置文件为application-pro.properties,端口配置为8082

    • 在swagger的config类中调用Environment .acceptsProfiles()

          public Docket docket(Environment environment){
              Profiles profiles = Profiles.of("dev","test");//如果当前的配置文件是dev/test,就返回一个实例
              boolean flag = environment.acceptsProfiles(profiles);//如果这个Profiles实例/参数代表的配置文件被激活,就返回true,否则就返回false
              return new Docket(DocumentationType.SWAGGER_2)
                      .enable(flag)
                      .apiInfo(this.apiInfo())
                      .select().apis(RequestHandlerSelectors.basePackage("com.zhao.controller")).paths(PathSelectors.ant("/test"))
                      .build();
          }
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fC7Umxsx-1651712892123)(springBoot.assets/image-20210911193925236.png)]

    • 去application中激活文件dev

      spring.profiles.active=dev
      
    • 测试

4,分组和接口注释

1.数据接口分组配置

  • 配置API文档的分组

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibxtv2aY-1651712892124)(springBoot.assets/image-20210911194306266.png)]

  • 我们可以看到swagger页面上有一个下拉列表,组信息;就是我们需要对不同的数据接口API进行分组,通过组信息/选择某一个组别,我们可以方便查看不同 类别/组中 的数据API

  • 一看这就是在配置swagger页面上的东西,所以我们还是需要使用到swagger的配置类Docket,我们可以再次回顾一下Docket类中可以配置的参数

    public Docket(DocumentationType documentationType) {
        this.apiInfo = ApiInfo.DEFAULT;
        this.groupName = "default";
        this.enabled = true;
        this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
        this.applyDefaultResponseMessages = true;
        this.host = "";
        this.pathMapping = Optional.absent();
        this.apiSelector = ApiSelector.DEFAULT;
        this.enableUrlTemplating = false;
        this.vendorExtensions = Lists.newArrayList();
        this.documentationType = documentationType;
    }
    
  • 第1参数我们见过了,就是通过ApiInfo类配置页面上的swagger信息,就是对于这个swagger中数据接口的一个概述+作者信息

  • 第3个参数我们也使用过了,通过设置参数enabled的值,我们可以自定义开启还是关闭swagger功能

  • 我们可以看看第二个参数groupName ,见名知意,组名称,那么和组信息相关的配置应该就是通过这个属性来配置了

  • 和enabled属性同理,Docket类中有专门的方法可以配置组名称

    /**
      * If more than one instance of Docket exists, each one must have a unique groupName as
      * supplied by this method. Defaults to "default".
      *
      * @param groupName - the unique identifier of this swagger group/configuration
      * @return this Docket
      */
     public Docket groupName(String groupName) {
       this.groupName = defaultIfAbsent(groupName, this.groupName);
       return this;
     }
    
  • 调用Docket.groupName()

    @Bean
    public Docket docket(Environment environment){
        Profiles profiles = Profiles.of("dev","test");//如果当前的配置文件是dev/test,就返回一个实例
        boolean flag = environment.acceptsProfiles(profiles);//如果这个Profiles实例/参数代表的配置文件被激活,就返回true,否则就返回false
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(flag)
                .groupName("zhao")
                .apiInfo(this.apiInfo())
                .select().apis(RequestHandlerSelectors.basePackage("com.zhao.controller")).paths(PathSelectors.ant("/test"))
                .build();
    }
    
  • 测试

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bY3i73Vl-1651712892124)(springBoot.assets/image-20210911195201892.png)]

  • 从上面的测试结果我们可以发现,只有一个组别信息,这显然不是组信息设置的初衷,它的设计初衷应该是有多个组别可以选择,我们可以通过选择不同的组别查看不同类别的组中对应的数据接口,而不用将整个项目的数据接口放在一个页面上进行展示,这样要精确定位一个数据接口很麻烦

  • 那么此时我们需要做的就是多搞几个组出来,好让swagger页面上的组信息可选>1

  • 思考:为什么会有这个组别信息?因为我们在注入的spring容器的Docket类中配置了我们的组信息

  • 猜测:一个注入spring容器中的Docket对象代表了一个组别信息,那么我们想要在swagger页面上选择切换多个组别的数据接口进行查看,我们需要向spring容器中注入多个Docket对象

  • 实验:在config中多写几个注册Docket类的@Bean方法

    @Bean
    public Docket docket1(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("B");
    }
    @Bean
    public Docket docket2(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("C");
    }
    @Bean
    public Docket docket3(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("D");
    }
    
  • 注意:上面新增的3个@Bean方法由于除了组信息之外的其他什么信息都没有配置,所以它们显示的数据就是swagger默认配置的,扫描所有的数据接口进行展示,所以必然3个页面会有两个controller的数据接口信息;并且swagger信息也是默认的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2efRkEzX-1651712892125)(springBoot.assets/image-20210911195557346.p

  • 分组的好处:一个人开发的模块就是一个组,我们的组名称就可以取当前开发这个模块的人的名称/这个模块的功能名称,这样别人在查看swagger中接口信息的时候就可以很清楚的找打你开发的数据API的信息了;这样就有了协同开发的基础了

  • 但是我们发现swagger默认页面有一个model模块,他是用来存储当前实体类信息的

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wx3YwTfZ-1651712892125)(springBoot.assets/image-20210911195717971.png)]

2.实体类注解信息配置

  • 创建pojo user

    package com.zhao.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private Integer id;
        private String username;
        private String password;
    }
    
  • 我们创建的实体类怎么才能被swagger扫描到呢?

  • 首选我们回忆一下swagger是做什么的?数据接口,它会扫描的只有数据接口,所以我们想要实体类被swagger扫描到,我们只有将实体类和接口关联起来;做法:只要我们的controller中的方法返回了实体类对象,那么这个实体类就会被swagger扫描到

  • 所以我们可以去controller中定义一个返回User对象的方法

    @GetMapping("/user")
    public User user(){
        return new User();
    }
    
  • 测试

  • 所以想要swagger获取实体类的属性信息,①写上属性的get() ②属性public

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-13Xv2ZB2-1651712892126)(springBoot.assets/image-20210911201214241.png)]

  • 在实体类上我们还常用5个注解,注解主要用于对我们的实体类和实体类的属性进行注释,这个注释就是解释的意思,因为实际开发中有些类和类的属性非常复杂,所以我们需要使用swagger的注解为类和类的数据加上注释,帮助使用数据API的人理解实体类和实体类的属性

  • @ApiModel(“实体类的描述信息”),就是为了帮助别人理解这个实体类的作用的,即文档注释

  • @ApiModelProperty(“属性的描述信息”),就是为了帮助别人理解这个属性的作用的

    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @ApiModel("User-用户实体类")
    public class User {
        @ApiModelProperty("用户ID")
        private Integer id;
        @ApiModelProperty("用户名")
        private String username;
        @ApiModelProperty("用户密码")
        private String password;
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3tldwHl-1651712892127)(springBoot.assets/image-20210911201426459.png)]

3.数据接口注解信息配置

  • @Api(tags = “接口信息名称”,description = “接口信息描述”)

    @Api(tags = "接口信息名称,默认就是Controller类的名称",description = "接口信息描述,默认也是Controller类的名称,并且这个属性过期声明了")
    @RestController
    public class HelloController {}
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rll696eo-1651712892128)(springBoot.assets/image-20210911202704329.png)]

  • @ApiOperation,用于说明controller中的方法的作用,或者说在swagger中说明某一个数据接口的作用

    @ApiOperation("用于让swagger扫描到User实体类")
        @GetMapping("/user")
        public User user(){
            return new User();
        }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d3jT3SL-1651712892128)(springBoot.assets/image-20210911203210346.png)]

  • @ApiParam,用于说明数据接口的方法中参数列表中的参数的作用

    @ApiOperation("用于让swagger扫描到User实体类")
    @GetMapping("/user")
    public User user(@ApiParam("用于说明数据接口对应的方法的参数的作用") String name){
        return new User();
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9UeWx8mF-1651712892129)(springBoot.assets/image-20210911203259544.png)]

  • 可以发现,我们常用的5个注解都是在配置一些描述信息,用于别人调用我们的数据API的时候可以明确我们的数据接口(@ApiOperation)、数据接口集(@Api)、接口中的参数(@ApiParam)、实体类的作用(@ApiModel)、实体类中成员属性(@ApiModelProperty)的作用

  • 其实对于我们本身实现功能没有什么意义,主要还是方便别人调用我们的数据接口

4.在线测试数据接口(swagger强大之处)

  • 将刚刚使用的user()方法进行改造,让他传递的参数为一个User对象,并最后将则个User对象返回

    @ApiOperation("用于让swagger扫描到User实体类")
    @PostMapping("/user")
    public User user(@ApiParam("用于说明数据接口对应的方法的参数的作用") User user){
        return user;
    }
    

在这里插入图片描述

  • 假设我们故意在数据接口中制造错误

    @ApiOperation("用于让swagger扫描到User实体类")
    @PostMapping("/user")
    public User user(@ApiParam("用于说明数据接口对应的方法的参数的作用") User user){
        int i = 5/0;	//除0错误
        return user;
    }
    
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsG8uTm7-1651712892131)(springBoot.assets/image-20210911204312852.png)]

  • 可见即使是错误也是可以将信息提示出来的

  • 上面的在线测试数据接口就是swagger的强大之处

小结:

  • 我们可以通过swagger的注解给一些难以理解的实体类、实体类的属性、数据接口、接口的参数等等加上注释信息
  • swagger页面上的数据接口/API是实时更新的
  • 可以在线测试数据接口/API
  • 每个人开发的数据接口可以作为一个组分开管理

以上swagger的特点就可以很好的解决前后端分离造成的沟通不及时问题,国内几乎所有大厂都在使用
唯一的注意点:出于安全考虑,在正式发布的时候关闭swagger,并且可以节约使用内存

  • 我们需要掌握的swagger的知识点
    • 使用swagger的5个常用注释
    • 会修改swagger页面上swagger信息
    • 会操作swagger页面上的接口信息
    • 会操作swagger页面上的分组信息
    • 会操作swagger页面上的实体类/model信息
    • 会使用swagger在线测试数据接口

十六,异步任务

1.异步任务/多线程

  • 为什么需要异步任务?或者说为什么我们需要多线程?为了提高用户体验

  • 就用邮件发送来举例,邮件发送需要时间,如果从邮件发送到完成之间的时间,我们都让用户等待,前端页面白屏/转圈提示加载中的话,结果就是用户体验及其不好

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8GvvkOJ-1651767800951)(springBoot.assets/image-20210911213927435.png)]

  • 解决办法就是使用异步任务/多线程,即用户点击发送之后,我们的主线程开启子线程,然后主线程直接向用户返回发送成功的提示信息,用户就结束了页面的使用,让子线程去做完耗时的邮件发送操作;这样做的好处就是用户直接就能得到发送的结果,提高了用户的体验(和用户体验相关的都是大事)

  • 但是,要使用多线程的地方,我们都需要自己去手动的编写它,即在controller中手动的开启一个新的子线程,然后在其内部调用发送邮件的任务;但其实每次需要编写开启新线程的代码大体上是不变的,就是开启一个新的线程,然后将要执行的任务交给它,主线程继续向下执行;springBoot为我们想到了简化开发的做法:使用在需要开启新线程执行的方法上使用注解@Async,并且在springBoot的启动程序上使用注解@EnableAsync

  • 使用传统多线程实现

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-53ymjR6j-1651767800952)(springBoot.assets/image-20220311224904036.png)]
    在这里插入图片描述

    这个时候呢,他的数据时秒出的,但是控制台的输出是三秒后有的

  • 使用springBoot提供的多线程注解@Async实现

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8HJ4XFKK-1651767800952)(springBoot.assets/image-20210911214748164.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJNsEOgg-1651767800953)(springBoot.assets/image-20220311224924497.png)]

结果同上

  • 相比较而言,使用springBoot的注解实现异步任务/多线程,只是加了一个注解,没有改变原来的service层的结构,而使用传统的多线程实现,我们需要service层实现接口runnable/Thread,并实现run(),相比之下,我们应该更愿意选择使用springBoot的注解实现异步任务/多线程

  • 相比没有开启多线程的时候,整个流程中要实现的功能是没有变的,就是调用service层发送邮件,但是用户的体验有了很大的改观,开启线程之前我们需要用户等待服务器的响应,开启多线程之后用户操作完点击发送服务器立马返回结果,用户就不用等待服务器响应时间,而真正的耗时的发送邮件的任务还是要被执行;相比之下,开启了异步任务/多线程的时候,用户体验好得多(和用户体验挂钩的都是整个项目中的大事)

十七,邮件任务

1,环境搭建

  • 导入依赖

    <!--邮件发送依赖-->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    
  • 可见,我们直接使用的是springBoot中集成的邮件发送的启动器,既然是springBoot启动器导入的依赖,那么在导入的依赖中,必然有一个mailautoconfig的类,我们可以搜索一下,来看看springBoot为邮件发送配置了哪些默认的参数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wl16gxz8-1651767800953)(springBoot.assets/image-20210911232707215.png)]

  • 这个自动配置类果不其然有一个配置参数类MailProperties,我们可以点击进去查看配置参数详情

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwCILft6-1651767800953)(springBoot.assets/4.png)]

  • 从上图可知,参数配置的就和原来javaweb中配置的参数一样,在配置参数类中我们可以发现,我们可以在application配置文件中使用"spring.mail"来配置邮件发送的参数

  • 那么邮件发送的具体具体实现springBoot帮我们封装好了吗?当然,springBoot就是想让我们集中自己的注意力在核心功能的开发上,对于固定步骤的操作它都有封装

  • 在MailSenderAutoConfiguration类中还导入了一个类MailSenderJndiConfiguration,我们可以点击去看看这个类

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Session.class)
    @ConditionalOnProperty(prefix = "spring.mail", name = "jndi-name")
    @ConditionalOnJndi
    class MailSenderJndiConfiguration {
    
    	private final MailProperties properties;
    
    	MailSenderJndiConfiguration(MailProperties properties) {
    		this.properties = properties;
    	}
    
    	@Bean
    	JavaMailSenderImpl mailSender(Session session) {
    		JavaMailSenderImpl sender = new JavaMailSenderImpl();
    		sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
    		sender.setSession(session);
    		return sender;
    	}
    
    	@Bean
    	@ConditionalOnMissingBean
    	Session session() {
    		String jndiName = this.properties.getJndiName();
    		try {
    			return JndiLocatorDelegate.createDefaultResourceRefLocator().lookup(jndiName, Session.class);
    		}
    		catch (NamingException ex) {
    			throw new IllegalStateException(String.format("Unable to find Session in JNDI location %s", jndiName), ex);
    		}
    	}
    
    }
    
  • 可见这个类的mailSender()已经帮我们实现了邮件发送的流程了,所以我们在使用的时候只需去application中配置参数,并从spring容器中将这个实例获取到,调用mailSender()即可发送邮件

2.简单邮件发送

  • 这里还是使用QQ邮箱

  • 获取邮箱登陆授权码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAt8CDVC-1651767800953)(springBoot.assets/image-20210911234141898.png)]

  • 去application中配置参数

    spring.mail.username=qq邮箱
    spring.mail.password=授权码
    spring.mail.host=smtp.qq.com
    #QQ邮箱需要开启加密验证
    spring.mail.properties.mail.smtl.ssl.enable=true
    
  • 代码实现

    @SpringBootTest
    class Springboot09TestApplicationTests {
    
        @Autowired
        JavaMailSenderImpl mailSender;
        @Test
        void contextLoads() {
            mailSender.send();
        }
    
    }
    
  • mailSender.send()需要传入参数,具体参数我们可以查看一下send()的源码

    public interface MailSender {
        void send(SimpleMailMessage var1) throws MailException;
    
        void send(SimpleMailMessage... var1) throws MailException;
    }
    
  • 所以我们需要传入SimpleMailMessage对象作为参数,所以我们需要new一个SimpleMailMessage对象

    @SpringBootTest
    class Springboot09TestApplicationTests {
    
        @Autowired
        JavaMailSenderImpl mailSender;
        @Test
        void contextLoads() {
            SimpleMailMessage mailMessage = new SimpleMailMessage();
            mailSender.send(mailMessage);
        }
    }
    
  • 那么我们邮件发送的信息、从哪里发送、发送给谁的信息怎么配置呢?当然是在SimpleMailMessage对象中配置了,我们可以查看该类的源码

    public class SimpleMailMessage implements MailMessage, Serializable {
        @Nullable
        private String from;//发件方
        @Nullable
        private String replyTo;
        @Nullable
        private String[] to;//收件方
        @Nullable
        private String[] cc;
        @Nullable
        private String[] bcc;
        @Nullable
        private Date sentDate;
        @Nullable
        private String subject;//邮件主题
        @Nullable
        private String text;//邮件内容
    }
    
  • 可见发送邮件常用的配置参数都可以在这个类里面配置,并且每一个成员属性都有对应的set(),所以我们可以直接在测试方法中调用

    @SpringBootTest
    class Springboot09TestApplicationTests {
    
        @Autowired
        JavaMailSenderImpl mailSender;
        @Test
        void contextLoads() {
            SimpleMailMessage mailMessage = new SimpleMailMessage();
            mailMessage.setFrom("发件方邮箱");//发件方
            mailMessage.setTo("收件方邮箱");//收件方
            mailMessage.setSubject("测试springBoot邮件发送");//邮件主题
            mailMessage.setText("这是使用springBoot封装的对象JavaMailSenderImpl发送的邮件");//邮件信息
            mailSender.send(mailMessage);
        }
    }
    
  • 效果

3.复杂邮件发送

  • 但是上面的邮件是简单邮件的发送,即邮件中只有文字信息,我们还可以使用springBoot发送复杂邮件,比如图片和附件;在原来发送复杂邮件的时候我们实现的方式为使用MIME(多用途互联网邮件扩展类型),复杂邮件发送回忆

  • springBoot将发送复杂邮件的步骤也为我们进行了封装,我们需要使用封装好的类MimeMessage +MimeMessageHelper

  • MimeMessage 表示这是一个复杂邮件对象MimeMessageHelper由于帮助复杂邮件对象组装各个组成部分,对于复杂邮件中简单邮件部分的操作和简单邮件调用的方法一样,而添加附件的时候需要调用MimeMessageHelper.addAttachment()

  • 代码实现

    @Test
    void contextLoads2() throws MessagingException {
        MimeMessage mimeMessage = mailSender.createMimeMessage();   //创建复杂邮件对象
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
        //获取发送复杂邮件的帮助类,参数1为要发生的复杂邮件对象,参数2为multipart,即是否支持多文件/复杂邮件由多个部分拼装而成,一般复杂邮件都要设置为true
    
    
        helper.setFrom("发件方邮箱");//发件方
        helper.setTo("收件方邮箱");//收件方
        helper.setSubject("测试springBoot复杂邮件发送");//邮件主题
        helper.setText("<p style='color:red'>这是使用springBoot封装的对象JavaMailSenderImpl发送的<strong>复杂邮件</strong></p>",true);
        //邮件信息,参数1为要发送的邮件消息,参数2表示是否支持解析前面邮件信息中的html标签
    
        helper.addAttachment("附件1.png",new File("附件1的地址"));//添加一个附件,参数1为附件名称,参数2为附件在本地的file对象
        helper.addAttachment("附件2.png",new File("附件2的地址"));//添加一个附件
    
        mailSender.send(mimeMessage);
    }
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NoIaJnd0-1651767800954)(springBoot.assets/image-20210924151526043.png)]

4.将邮件发送抽象成一个可以复用的工具方法

  • 这里就直接封装发送复杂邮件的步骤

    /**
     * 复杂邮件发送方法
     * @param from  发送发邮箱
     * @param to    接收方邮箱
     * @param subject   邮件主题
     * @param text  邮件正文
     * @param html  是否支持解析邮件中的html标签
     * @param attachmentName    附件名称
     * @param attachmentUrl     附件地址
     * @throws MessagingException
     * @author thhh
     */
    @Test
    void contextLoads3(String from, String to, String subject, String text, Boolean html, String attachmentName, String attachmentUrl) throws MessagingException {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(text,html);
        helper.addAttachment(attachmentName,new File(attachmentUrl));
        mailSender.send(mimeMessage);
    }
    
    

十八,定时任务

1.定时任务

  • 定时任务在实际开发中很常用,比如在某一个时间点就开启执行某一段代码,最常见的就是淘宝折扣,一到凌晨00:00,某一件商品的折扣就生效,到了活动结束的时候,这个商品就恢复到原价
  • 我们也可以使用定时任务实现对于这些东西的抢购,或者其他一些功能,我们需要使用到两个接口:TaskExecutor(函数式接口,可以使用lambda表达式实现)+TaskScheduler
    • TaskExecutor【任务执行者】
    • TaskScheduler【任务调度程序】
  • 除了上面两个接口之外我们还需要使用两个注解
    • @Scheduled:表示这个功能什么时候执行
    • @EnableScheduling:用于开启定时任务功能
  • 除了上面的两个接口+两个注解之外,我们还需要学习一个表达式:Cron表达式

2.Corn表达式

  • Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

    • Seconds Minutes Hours DayofMonth Month DayofWeek [Year] - 7个
    • Seconds Minutes Hours DayofMonth Month DayofWeek - 6个

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AyCPx3UV-1651820812651)(springBoot.assets/image-20210912152312333.png)]

    '*' 字符可以用于所有字段,表示定义域内的任意一个值
    
    '?' 它用来指定 '不明确的值'
    
    '-' 字符被用来表示一个取值的范围,[a,b]
    
    ',' 字符指定枚举值,即这个字段的值只能是这里面的其中一个
    
    '/' 字符表示间隔触发,比如5/10在分钟位置上,表示从第5分钟执行一次,到第15分钟的时候再执行一次,
    后面的时间点分别是25,35...
    
    'L'字符表示最后last,比如在周位置上使用5L,表示这个月最后一周的周4执行一次;这里注意:周的数字是1-7,
    但是1表示星期天,2表示星期一,以此类推
    
    'W' 字符表示工作日(workday)触发,只能用在月份上,比如10W,表示这个月的10号"可能"会触发,
    可能的意思就是要看10号这一天是不是工作日,如果不是,它将找到"最近"的一个工作日促发,比如10号是周6,
    那么这个事件将在周5即9号触发,如果10号是周日,那么这个事件将在下周1即11号触发,前提是下周1不在下一个月,
    如果是下一个月,事件将在周5即9号触发,即,W的最近寻找不会跨过月份 
    
    'LW' 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五
    
    '#' 用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三
    
  • ?的作用

    • 问号 ? 的作用是指明该字段‘没有特定的值’(比如null),星号 * 和其它值,比如数字,都是给该字段指明特定的值,只不过用星号(*)代表所有可能值
    • 官方doc指出:cronExpression对日期和星期字段的处理规则是它们必须互斥,即只能且必须有一个字段有特定的值(可以是*),另一个字段必须是‘没有特定的值’(必须是?)
    • 自我理解:这个符号主要是配合指定具体日期和星期几的时候使用;如:说了是这个月的哪一天,星期几就没有必要指出,指出了反而容易出错;说了是这个月的星期几,就表示我希望这个月的每一周的星期X都执行操作,而指定了日期是不是限定了某一天执行了,是不是就和原意冲突了呢?并且可能指定的那一天还不符合我们设置的星期,这也就会报错了;所以官方为了避免报错,就给出了?这样一个null值,表示这个位置上没有值,或者说执行的时候不用关心这个值,即忽略它

3.定时任务实现

1.步骤

  • 首先我们需要一个需要定时执行的方法
  • 然后在这个方法上面写上注解@Scheduled(cron = “”)
  • 最后在整个springBoot项目的入口程序上面使用注解@EnableScheduling开启项目对于定时任务的支持

2.代码

  • 在web项目中定义一个需要定时执行的方法

    @Scheduled(cron = "10 * * * * ?")
    public void send(){
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()));
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FKoxF40X-1651820812653)(springBoot.assets/image-20210912154134338.png)]

十九,分布式系统理论

1.什么是分布式系统

  • 在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个计算机系统
  • 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据
  • 分布式系统(distributed system)是建立在网络之上的软件系统
  • 首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统(所以,我们不要一上来就考虑使用分布式,由于一开始项目的规模很小,单机就可以运行,那么我们就只用单机运行即可;只有当单机不能解决问题的时候,我们才考虑使用分布式系统)。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题
  • 其实把分布式系统就是让的多台电脑去完成一个项目,实现的途径是通过网络

2.服务架构

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

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tiYQdL3t-1651820812653)(springBoot.assets/image-20210912154659092.png)]

1.单一应用架构(All in One)

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b7x4EKjU-1651820812653)(springBoot.assets/image-20210912154734599.png)]

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

缺点

  1. 性能扩展比较难
  2. 协同开发问题
  3. 不利于升级维护

2.垂直应用架构(MVC)

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7bZAxTMo-1651820812654)(springBoot.assets/image-20210912155154444.png)]

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

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

3.分布式服务架构(RPC)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8E6bRub-1651820812654)(springBoot.assets/image-20210912155257498.png)]

4.流动计算架构(SOA)

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUMaZZPu-1651820812654)(springBoot.assets/image-20210912155331432.png)]

  • 相当于有一个放在云端的服务器,这个服务器专门用来注册后端服务器提供的服务,前端有请求来了,先到注册中心,注册中心决定将前端请求转发到哪一台服务器上进行响应,所以你服务器上的服务器一定要在注册中心注册之后才会被使用,不注册就根部不会被调用

二十,什么是RPC&Dubbo&安装zookeeper

1.什么是RPC

  • RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,它是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同
  • 也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数
  • https://www.jianshu.com/p/2accc2840a1b

2.RPC原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpBMBmKl-1651820812654)(springBoot.assets/image-20210912155739659.png)]

步骤分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbsutHOS-1651820812655)(springBoot.assets/image-20210912155802099.png)]

  • 所以RPC两个核心模块为:通讯,序列化(方便数据传输)

3.Dubbo概念

1.什么是Dubbo

  • Dubbo官网

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KVtrj1Fw-1651820812655)(springBoot.assets/image-20210912160724082.png)]

  • 从上图我们就可以知道Dubbo和RPC的关系了,RPC用于实现远程过程调用,但是我们每次使用它的时候都要经过一些固定的步骤,所以就出现了Dubbo,帮助我们简化这些步骤,让我们专心传输调用和接收结果

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmvTVfFo-1651820812655)(springBoot.assets/image-20210912160947354.png)]

  • 服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
  • 注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
  • 监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

  • 服务容器负责启动,加载,运行服务提供者
  • 服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务消费者在启动时,向注册中心订阅自己所需的服务
  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

4.Dubbo环境搭建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eW4JhTh-1651820812656)(springBoot.assets/image-20210912161309205.png)]

1.什么是zookeeper

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SvmyANEy-1651820812656)(springBoot.assets/image-20210912161455539.png)]

2,下载zookeeper

  • 下载zookeeper :地址, 下载最新版zookeeper-3.6.2,解压zookeeper(直接下载3.4.14,下面这个最新版本会翻车)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kDSFGZxe-1651820812656)(springBoot.assets/image-20210912161857880.png)]

  • 可能遇到问题:闪退 !

  • 解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因

在这里插入图片描述

  • 报错解决,下载-bin的压缩包

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x2XZKPM2-1651820812657)(springBoot.assets/image-20210912163129599.png)]

  • 解压运行上面的步骤

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nbFUBu8f-1651820812657)(springBoot.assets/image-20210912165300678.png)]

  • 解决

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mNlCgnO6-1651820812657)(springBoot.assets/image-20210912165318266.png)]

  • 解决,降级版本3.4.14

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WEEMuCN4-1651820812658)(springBoot.assets/image-20210912165402990.png)]

  • 成功启动service之后,可以启动客户端zkCli.cmd测试服务器是否能够正常连接

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mQI6LQEG-1651820812658)(springBoot.assets/image-20210912165416364.png)]

  • 测试最基础的命令:ls /,查看当前根目录下面有什么\

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CJBXMHPO-1651820812658)(springBoot.assets/image-20210912165435230.png)]

  • 我们可以尝试着新建自己的节点,语法:create -e /新节点的名称 新节点的值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1baZ1Ky-1651820812658)(springBoot.assets/image-20210912165455728.png)]

  • 从上图可见,我们新建一个节点数据之后,再次使用 ls /,可以发现我们新创建的节点已经加入到了/ 路径下

  • 创建之后我们可以获取刚刚创建的节点数据,语法:get /节点名称

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GnUdfO1E-1651820812658)(springBoot.assets/image-20210912165535650.png)]

  • 通过上面的例子我们就实现了通过zookeeper存值取值,zookeeper就是存储我们后台服务请求接口的地方,我们有了一个新的服务可以对外提供,我们就需要将请求注册到zookeeper中,前端请求的时候,会将请求发送给zookeeper,由zookeeper决定调用哪个服务请求进行响应;所以zookeeper就是用来充当Dubbo中的注册中心的

二十一,Dubbo-admin安装测试

1.window下安装dubbo-admin

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

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

  • 下载dubbo-admin

  • 下载地址,直接download

  • 解压进入目录[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-plKunbBM-1651820812659)(springBoot.assets/image-20210912185805021.png)]

  • 如果修改了zookeeper的端口号,就需要去修改 dubbo-admin\src\main\resources \application.properties ,将修改之后的zookeeper的端口号写上

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upzjSh90-1651820812659)(springBoot.assets/image-20210912185823841.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDpVmsyo-1651820812659)(springBoot.assets/image-20210912185838410.png)]

  • 在文件解压路径下进入管理员CMD,进行项目打包,把它打成jar包,这样我们才能运行它,使用它[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4E9qDOXx-1651820812659)(springBoot.assets/image-20210912185848978.png)]

  • 打包命令

    mvn clean package -Dmaven.test.skip=true

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8OcIiHG-1651820812660)(springBoot.assets/image-20210912185917662.png)]

  • 上面的打包其实就是用的maven的打包,所以打包结果的输出也是和maven一样的一个target文件夹,我们只需要去找这个打包完的jar包即可[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iSj0fK6W-1651820812660)(springBoot.assets/image-20210912185929809.png)]

  • 启动zookeeper,然后运行这个jar包

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTkggVLv-1651820812660)(springBoot.assets/image-20210912185943028.png)]

  • 项目启动成功之后就是访问这个项目的页面,端口号、登陆账户+密码都在刚刚的那个配置文件中展示过[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mt8AuyAG-1651820812660)(springBoot.assets/image-20210912185956355.png)]

  • 访问localhost:7001

二十二,SpringBoot+Dubbo+zookeeper整合

1.环境搭建

  • 创建一个空项目

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4dLuMpR-1651820812660)(springBoot.assets/image-20210912225424399.png)]

  • 项目命名dubbo+zookeeper

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G1xl5BfC-1651820812661)(springBoot.assets/image-20210912225440065.png)]

  • 创建一个新的springBoot模块

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-su4z1paw-1651820812661)(springBoot.assets/image-20210912225459855.png)]

  • 这样这个项目中的一个子model就创建好了,然后就需要清理项目结构

2.代码部分

  • 注意:上面创建的子model是一个provider,即服务提供者的子model,如果想要使用这个服务,我们应该再创建一个consumer的子model来消费/使用这个服务;注意:我们分开两个model创建的原因就是想要模拟真实情况中分布式开发的情况,让消费者通过注册中心调用对应的服务,只有在分布式开发的情况下我们才会需要使用RPC,才会使用我们的Dubbo、zookeeper和Dubbo-admi接口定义

    package com.thhh.service;
    
    //服务器上的票务服务
    public interface TicketService {
        //提供票的服务
        public String getTicket();
    }
    
    
  • 接口实现

    package com.thhh.service;
    
    public class TicketServiceImpl implements TicketService{
        @Override
        public String getTicket() {
            return "你获得了服务器提供的一张票";
        }
    }
    
  • 再创建一个子model,作为消费者,来消费/使用前面的服务者提供的服务

在这里插入图片描述

  • 创建一个类UserService,将用户可以进行的操作都写在这里面,这里由于服务者只提供了获取票的服务,所以这个类中最多只有一个买票的功能

    package com.thhh.service;
    
    public class UserService {
        //获取服务者提供的票
        
    }
    
  • 这是两个不同的子model,是两个不同的springboot项目,所以我们需要配置两个不同的服务接口,否则两个项目同时启动的时候会冲突,我们可以把提供者设为8081,消费者设为8082

  • 此时Dubbo结构的图中两个角色:消费者和提供者就有

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXSoogTW-1651820812661)(springBoot.assets/image-20210912225711511.png)]

  • 现在我们要做的就是配置项目连接注册中心,也就是我们本地的zookeeper

1.提供者

  • 由于我们的项目需要连接使用Dubbo进行RPC通信,并且Dubbo的注册中心由zookeeper充当,且要使用zookeeper的前提是需要连接上它,所以我们需要导入必要的依赖帮助我们在springBoot项目启动的时候就去自动的连接zookeeper,而Dubbo依赖用于我们的项目中的服务向注册中心注册服务

  • 首先导入依赖:Dubbo+zookeeper客户端依赖(连接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.8</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
    <dependency>
        <groupId>com.github.sgroschupf</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.1</version>
    </dependency>
    
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FKyHkMgI-1651820812662)(springBoot.assets/image-20210912225818916.png)]

  • 除了上面的两个依赖之外,我们还需要导入3个依赖

    <!-- 下面两个依赖是Dubbo运行的时候需要的依赖 -->
    <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>
    <!-- zookeeper依赖,为了防止报错,排除了slf4j -->
    <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>
    
  • 配置provider子model的参数

    server.port=8081
    #配置要注册的服务的名称
    dubbo.application.name=provider-server
    #配置注册中心的地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    #我们要注册项目中的哪些服务,这里也是基于包扫描注册的,对应的需要在要注册的服务上面加上注解@component
    dubbo.scan.base-packages=com.thhh.service
    
  • 编写服务接口

    package com.thhh.service;
    
    //服务器上的票务服务
    public interface TicketService {
        //提供票的服务
        public String getTicket();
    }
    
    
  • 服务接口实现类

    package com.thhh.service;
    
    import org.apache.dubbo.config.annotation.Service;
    import org.springframework.stereotype.Component;
    
    @Component
    @Service
    public class TicketServiceImpl implements TicketService{
        @Override
        public String getTicket() {
            return "你获得了服务器提供的一张票";
        }
    }
    
  • 注意:我们在实现类上使用了两个注解:@Component+@Service,其中@Component是spring自己的注解,用于将这个实现类装配到spring容器中,@Service注解来自于org.apache.dubbo.config.annotation.Service,注意它来自apache.dubbo包,所以我们需要引入Dubbo的依赖,这个注解的作用为Dubbo用于暴露要注册到注册中心的服务,当项目启动的时候,DubboAutoConfiguration进行自动配置并装配对应的bean实例到spring容器中,这些实例将会被自动的调用,用于实现Dubbo的功能,其中就包括扫描项目中暴露出来的服务,将其自动注册到注册中心中去,而要暴露服务使其能够被自动注册到注册中zookeeper中,就必须要使用Dubbo的注解@Service

  • 启动zookeeper+提供者项目

  • 这里需要多配置一个参数,即springBoot中连接zookeeper的默认超时时间,默认为3s,因为本来连接zookeeper就比较慢,3s太短了,很容易就报错连接不上,所以我们可以在配置文件中追加一条配置,将连接超时时间设为10s

    dubbo.config-center.timeout=10000
    
  • 设置之后就可以正常连接了,如果10s都没有连接上,就真的是zookeeper的问题了

  • 启动Dubbo-admin监控页面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4858CCsZ-1651820812662)(springBoot.assets/image-20210912230104137.png)]

  • 点击可以查看服务详情

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Epy8b3w8-1651820812662)(springBoot.assets/image-20210912230121340.png)]

  • 现在提供者已经将提供的服务注册到了注册中心,消费者要使用服务直接去服务中心获取即可使用

2.消费者

  • 导入依赖,基本和提供者的依赖相同

    <!--dubbo-->
    <!-- Dubbo Spring Boot Starter -->
    <dependency>
       <groupId>org.apache.dubbo</groupId>
       <artifactId>dubbo-spring-boot-starter</artifactId>
       <version>2.7.3</version>
    </dependency>
    <!--zookeeper-->
    <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
    <dependency>
       <groupId>com.github.sgroschupf</groupId>
       <artifactId>zkclient</artifactId>
       <version>0.1</version>
    </dependency>
    <!-- 引入zookeeper -->
    <dependency>
       <groupId>org.apache.curator</groupId>
       <artifactId>curator-framework</artifactId>
       <version>2.12.0</version>
    </dependency>
    <dependency>
       <groupId>org.apache.curator</groupId>
       <artifactId>curator-recipes</artifactId>
       <version>2.12.0</version>
    </dependency>
    <dependency>
       <groupId>org.apache.zookeeper</groupId>
       <artifactId>zookeeper</artifactId>
       <version>3.4.14</version>
       <!--排除这个slf4j-log4j12-->
       <exclusions>
           <exclusion>
               <groupId>org.slf4j</groupId>
               <artifactId>slf4j-log4j12</artifactId>
           </exclusion>
       </exclusions>
    </dependency>
    
  • 配置消费者的配置文件

    server.port=8082
    
    #配置消费者的名称,用于zookeeper记录哪个消费者来消费了
    dubbo.application.name=consumer-server
    #配置zookeeper地址/注册中心的地址,这样消费者才知道去哪获取服务
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    
  • 调用服务

    • 首先,消费者在自己的代码中调用的是提供者的提供的服务,所以调用服务的时候是不是需要一个提供者提供服务的接口对象,不然你怎么调用服务者提供的服务接口中的方法?这里的解决办法就是在消费者项目中创建一个和提供者提供的功能接口一模一样的文件路径,并在该文件路径下中创建一个和提供者相同的服务接口,必须一模一样,直接粘贴最保险
    • 其次,怎么将提供者接口实现类注入消费者调用服务类中的接口引用中?需要使用注解@Reference引用远程实例注入,注意注解@Reference来自org.apache.dubbo.config.annotation,所以我们需要引入Dubbo的包
    • 最后就是在消费者中调用实例提供的服务即可
  • 代码

    package com.thhh.service;
    
    import org.apache.dubbo.config.annotation.Reference;
    import org.springframework.stereotype.Service;
    
    @Service //注入到spring容器中
    public class UserService {
        //获取服务者提供的票
        @Reference  //引用dubbo注解,引用远程的实例注入
        TicketService ticketService;
    
        public void buyTicket(){
            String ticket = ticketService.getTicket();
            System.out.println(ticket+"《thhh study Dubbo+zookeeper+SpringBoot》");
        }
    }
    
  • 测试

    @SpringBootTest
    class ConsumerServerApplicationTests {
    
        @Autowired
        UserService userService;
        @Test
        void contextLoads() {
            userService.buyTicket();
        }
    
    }
    
  • 注意:虽然只是输出了一个字符串,但是我们需要明白,这一个字符串的意义,字符串的前半段来自服务者提供的服务,后半段来自消费者自己提供的数据,所以它的输出代表了我们使用Dubbo+zookeeper成功实现了分布式中不同主机上运行的项目之间的方法调用

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值