SpringBoot 详解

Spring Boot

0. 概述

简介

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

优点

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

准备

开发环境:

  • java 1.8
  • Maven 3.6.3
  • SpringBoot 2.5.0

开发工具:

  • IDEA

1. 第一个 Spring Boot 程序

1.1 创建项目

方法:使用 IDEA 直接创建项目

1、创建一个新项目

2、选择 spring initializr , 其默认会去官网的快速构建工具中实现

3、填写项目信息(包名推荐只保留 com.xxx 即可)

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

5、填写项目路径

6、等待项目构建成功

1.2 项目结构分析

  1. 程序的主启动类 Application.java

  2. 一个核心配置文件 application.properties

  3. 一个测试类 ApplicationTests.java

  4. 一个 pom.xml

1.3 编写 HTTP 接口

步骤如下:

  1. 主程序的同级目录下,新建一个 controller 包

  2. 在包中新建一个 Controller 类

    HelloController.java:

    package com.wmwx.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/hello")
    public class HelloController {
        @RequestMapping("/hello")
        public String hello(){
            return "Hello, world!";
        }
    }
    
  3. 从主程序入口启动程序,并在浏览器地址栏中输入 localhost:8080/hello

    页面显示:Hello, World!
    

1.4 pom 文件分析

pom.xml:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 有一个父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wmwx</groupId>
    <artifactId>springboot-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-01</name>
    <description>Study Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- spring-boot-starter是SpringBoot依赖的前缀 -->
        <!-- web场景启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springboot单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 打jar包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- 跳过项目运行测试用例 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.5 修改配置文件

application.properties:

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

1.6 修改 Banner 图案

字符图案在线生成网站:字符图案在线生成

得到字符图案后,到项目下的 resources 目录下新建一个banner.txt 即可。

2. 自动装配原理

2.1 pom.xml

父依赖 spring-boot-dependencies

  • 管理 Spring Boot 应用里面所有依赖版本的地方,是 Spring Boot 的版本控制中心
  • 以后导入依赖默认不需要再写版本。但如果导入的包没有在依赖中管理,则仍需我们手动配置版本

启动器 spring-boot-starter

  • Spring Boot 的启动场景,会自动导入环境中所有的依赖
  • Spring Boot 会将所有的功能场景变成一个个启动器
  • 我们想要使用什么功能,只需找到对应的启动器即可

2.2 主程序

2.2.1 注解

@SpringBootApplication:标注这个类是一个 Spring Boot 的应用

  • @SpringBootConfiguration:标注这个类是一个 Spring Boot 配置类
    • @Configuration:标注这个类是一个 Spring 配置类
      • @Component:标注这个类是一个 Spring 组件
  • @EnableAutoConfiguration:自动配置
    • @AutoConfigurationPackage:自动配置包
      • @Import({Registrar.class}):导入包注册
    • @Import({AutoConfigurationImportSelector.class}):导入自动配置选择器
  • @ComponentScan:扫描与当前其同类同级的包

结论:Spring Boot 所有的配置都是在启动的时候扫描并加载至 spring.factories 里,当配置对应启动器时就会自动生效

总结:

  1. SpringBoot 在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值
  2. 将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作
  3. 整个 J2EE 的整体解决方案和自动配置都在 springboot-autoconfigure 的 jar 包中
  4. 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件,并配置好这些组件
  5. 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作
2.2.2 SpringApplication

这个类通过 run 方法开启了一个服务,具体执行的动作如下:

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

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

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

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

3. YAML 配置注入

3.1 YAML 概述

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

YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。

YAML 的配置文件后缀为 .yml,如 Spring Boot 的核心配置文件:application.yml

使用 YAML 可以代替原本 .properties 格式的配置文件。例如,修改端口号的写法, 可以参照以下格式:

.properties

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

.yml

# 更改项目端口号
server:
  port: 8081

3.2 基础语法

YAML 对语法的要求十分严格:

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
3.2.1 纯量

纯量是最基本的,不可再分的值,包括:

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

其语法如下:

key: value

即:纯量直接写在冒号的后面即可,中间使用空格隔开。另外, 字符串默认不用加上双引号或者单引号,日期必须使用 yyyy/MM/dd 的格式。

3.2.2 对象

其语法如下:

#对象
name: 
    key1:value1
    key2:value2

或写在行内则为:

#对象(行内写法)
name: {key1: value1,key2: value2}
3.2.3 数组

其语法如下:

key:
 - value1
 - value2
 - value3

或写在行内则为:

key: [value1, value2, ...]

3.3 注入配置文件

YAML 文件强大的地方在于,它可以为实体类直接注入匹配值

之前我们为实体类赋值时,可以采用 @Value() 注解来实现。示例:

  1. 编写实体类

    Dog.java:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    public class Dog {
        @Value("小黑")
        private String name;
        @Value("3")
        private int age;
    }
    
  2. 编写测试类

    package com.wmwx;
    
    import com.wmwx.pojo.Dog;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Dog dog;
    
        @Test
        void contextLoads() {
            System.out.println(dog);
        }
    
    }
    
  3. 输出结果:

    Dog(name=小黑, age=3)
    

但是,在使用 YAML 之后,为实体类的属性赋值就多了一种方案:为实体类添加 @ConfigurationProperties(prefix = “”) 注解,并在 .yml 文件中编写对应的对象即可。

示例:

  1. 编写实体类:

    Person.java:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    @ConfigurationProperties(prefix = "person")
    public class Person {
        private String name;
        private int age;
        private Boolean happy;
        private Date birthday;
        private Map<String, Object> number;
        private List<Object> hobbies;
        private Dog dog;
    }
    
  2. 编写 YAML 配置文件

    application.yml:

    # Person 对象属性赋值
    person:
      name: 刘季恒
      age: 18
      happy: true
      birthday: 1999/07/07
      number:
        # 使用占位符生成随机整数
        phone: ${random.int}
        # 使用占位符生成随机UID
        id: ${random.uuid}
      hobbies:
        - 街机游戏
        - 古风歌曲
      dog:
        name: 小黑
        age: 3
    
  3. 添加依赖:

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

    package com.wmwx;
    
    import com.wmwx.pojo.Dog;
    import com.wmwx.pojo.Person;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Dog dog;
        @Autowired
        private Person person;
    
        @Test
        void contextLoads() {
            System.out.println(person);
        }
    
    }
    
  5. 输出结果:

    Person(name=刘季恒, age=18, happy=true, birthday=Wed Jul 07 00:00:00 CST 1999, number={phone=1012029378, id=25d37c9a-4046-4185-9f8f-bbf6282dbf0d}, hobbies=[街机游戏, 古风歌曲], dog=Dog(name=小黑, age=3))
    

@ConfigurationProperties 对比 @Value

  1. @ConfigurationProperties 只需写一次,@Value 需要对每个字段逐一添加。
  2. @ConfigurationProperties 支持松散绑定,即:first-name 等同于 firstName。
  3. @ConfigurationProperties 支持 JSR303 数据校验
  4. @ConfigurationProperties 可以封装对象,而 @Value 不可以。

3.4 JSR303 数据校验

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

首先,我们为 Person 类添加一个 email 属性,以便于接下来的测试:

Person.java:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private int age;
    //电子邮件,我们希望只接收电子邮箱格式的字符串
    private String email;
    private Boolean happy;
    private Date birthday;
    private Map<String, Object> number;
    private List<Object> hobbies;
    private Dog dog;
}

接着,我们为 Person 类添加 @validated 注解,并在 email 属性的上方添加 @Email 注解:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "person")
@Validated      //数据校验
public class Person {
    private String name;
    private int age;
	@Email(message="电子邮箱格式错误!")
    private String email;
    private Boolean happy;
    private Date birthday;
    private Map<String, Object> number;
    private List<Object> hobbies;
    private Dog dog;
}

这样一来,当 email 属性接收到错误格式的字符串时,就会报出 “电子邮箱格式错误!” 的信息。

使用数据校验,可以保证数据的正确性。记住,不要相信任何前端传来的数据!

常用注解参数表:

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

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

4. 多配置文件切换

4.1 配置文件加载位置

外部加载配置文件的方式十分多,springboot 启动会扫描以下位置的 application.properties 或者 application.yml 文件作为 Spring boot 的默认配置文件:

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

优先级由高到底,高优先级的配置会覆盖低优先级的配置。然后 SpringBoot 会从这四个位置全部加载主配置文件,并进行互补配置

4.2 多配置文件

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

示例:

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

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

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

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

.properties:

# 更改配置文件
spring.profiles.active = dev

.yml:

# 更改配置文件
spring:
  profiles:
    active: dev

5. 深入理解自动装配原理

配置文件到底写的是什么?应该怎么写?

5.1 分析自动装配原理

HttpEncodingAutoConfiguration(Http编码自动配置) 为例分析自动配置原理:


//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration 

//启动指定类的ConfigurationProperties功能;
  //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
  //并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class}) 

//Spring底层@Conditional注解
  //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
  //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})

//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
  //如果不存在,判断也是成立的
  //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)

public class HttpEncodingAutoConfiguration {
    //它已经和SpringBoot的配置文件映射了
    private final Encoding properties;
    //只有一个有参构造器的情况下,参数的值就会从容器中拿
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }
    
    //给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @Bean
    @ConditionalOnMissingBean //判断容器没有这个组件?
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
}

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

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

例如,如果我们在配置文件中输入 spring.http.encoding. 就会看到 IDEA 的代码补全中,显示的正是 bean 中的属性。这便是自动装配的原理。

5.2 自动装配的精髓

  1. SpringBoot 启动会加载大量的自动配置类
  2. 首先看我们需要的功能有没有在 SpringBoot 默认写好的自动配置类当中
  3. 再来看这个自动配置类中到底配置了哪些组件(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
  4. 给容器中自动配置类添加组件的时候,会从 Properties 类中获取某些属性,我们只需要在配置文件中指定这些属性的值即可

核心:

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

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

5.3 @Conditional

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

而 @Conditional 注解 的作用就是:

  • 必须当 @Conditional 指定的条件成立,才会给容器中添加组件,配置配里面的所有内容才生效。

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

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

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

application.properties:

#开启springboot的调试类
debug=true

其中,可以看到,配置类型的生效情况共有以下三种:

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

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

  • Unconditional classes:没有条件的类

6. Web 开发前的准备

6.1 Spring Boot 的使用步骤

  1. 创建一个 SpringBoot 应用
  2. 选择需要的模块,SpringBoot 就会默认将我们的需要的模块自动配置好
  3. 部分没有被自动装配的模块需要手动装配一下
  4. 专注编写业务代码即可,不需要再像以前那样考虑一大堆的配置了

6.2 加载静态资源

首先,搭建一个普通的 Spring Boot 项目。

在 SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面。可以看到,WebMvcAutoConfigurationAdapter 中有很多配置方法。其中有一个方法叫 addResourceHandlers,正是用来添加资源的处理类:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用默认资源处理
        logger.debug("Default resource handling disabled");
        return;
    }
    // 缓存控制
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    // webjars 配置
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 静态资源配置
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

可以发现,该源码中提供了两种资源配置的方式:**webjars 配置 **和 静态资源映射

6.2.1 webjars 配置

webjars 的本质就是以 jar 包的方式引入静态资源 ,因此,只需在 pom.xml 中引入对应的依赖,即可导入需要的静态资源。

webjars 官网:webjars 官网

例如,当我们想要使用 jQuery 时,只需在 pom.xml 中引入 jQuery 对应版本的依赖即可:

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

访问方式也很简单:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

6.2.2 静态资源映射

webjars 使用起来虽然很方便,但是却有一个重要的问题:不能导入我们自己的静态文件!

因此,我们需要去找 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_RESOURCE_LOCATIONS 数组的内容。

得出结论,按照优先级从高到低的方式,以下四个目录存放的静态资源可以被我们识别:

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

这样一来,我们只需在 resources 根目录下建立对应名称的文件夹,就可以存放我们自己的静态资源文件了。

另外,如果我们在 application.properties 中自己制定了资源路径文件夹,那么原本的自动配置就会失效。这是因为,源码中是这样写的:

if (!this.resourceProperties.isAddMappings()){
    Logger.debug("Default resource handling disabled");
    return;
    //这里返回之后,后续代码中的 webjars 和 资源映射 两种方式都不会再去执行了
}
6.2.3 首页

源码中还有一段关于欢迎页,也就是首页的映射:

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

其中,getWelcomePage() 方法就是获取首页的方法,其源码如下:

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 的文件,它就会自动被映射为首页,通过 localhost:8080 即可进行访问。

6.3 Thymeleaf 模板引擎

6.3.1 模板引擎

模板引擎的作用就是将后台封装的数据按照表达式解析并填充到页面中指定的位置。之前使用的 jsp 其实就是一个模板引擎。而在 SpringBoot 中,它给我们推荐的是 Thymeleaf 模板引擎。它是一个高级语言的模板引擎,它的语法更简单、功能更强大。

6.3.2 引入 Thymeleaf

想要引入 Thymeleaf,只需在 pom.xml 文件中导入相应的启动器即可:

<!-- Thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
6.3.3 使用 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;
}

可以看出,使用 Thymeleaf 的默认前缀是 classpath:/templates/,而默认的后缀是 .html

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

测试:

  1. 编写 ThymeleafController.java:

    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class ThymeleafController {
        @RequestMapping("/test")
        public String test(Model model){
            model.addAttribute("msg", "Thymeleaf 测试");
            return "test";
        }
    }
    
  2. 编写 test.html,并将之放入 templates 目录下:

    <!DOCTYPE html>
    <!-- 引入命名空间 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Thymeleaf 测试</title>
    </head>
    <body>
        <!--th:text就是将div中的内容设置为它指定的值 -->
        <div th:text="${msg}"></div>
    </body>
    </html>
    
  3. 在浏览器地址栏中输入 localhost:8080/test,进入页面可以看到如下输出结果:

    Thymeleaf 测试
    

6.4 MVC 自动配置原理

在进行实战项目编写前,我们还需要知道一个东西,那就是 SpringBoot 对 SpringMVC 还做了哪些配置,包括如何扩展、如何定制。只有把这些都搞清楚了,之后使用起来才会更加得心应手。

想要搞清楚这些,途径有二:

  1. 源码分析
  2. 官方文档:Spring Boot 官方文档 - Spring Web MVC 框架

查阅可知,Spring Boot 在自动配置很多组件的时候,会先去查看容器中有没有用户自己配置的配置类。如果有,就选择用户配置的;如果没有,就启用原本的自动配置。如果有些组件可以存在多个,那么 Spring Boot 就会将用户的配置与默认的配置组合起来进行装配。

6.5 扩展 Spring MVC

官方文档中有这么一段描述:

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

经过解读,我们需要进行如下操作:

  1. 新建 config 目录
  2. config 目录下新建一个类
  3. 令此类实现 WebMvcConfigurer
  4. 为此类添加 @Configuration 注解
  5. 不能为此类添加 @EnableWebMvc 注解,否则自动装配会全部失效

7. 整合 JDBC

7.1 Spring Data

对于数据访问层,无论是 SQL(关系型数据库)还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理,它也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

Sping Data 官网:Spring Data 官网

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

7.2 搭建整合 JDBC 的项目

  1. 新建一个项目,在创建时选择引入以下模块:

    • Spring Web
    • JDBC API
    • MySQL Driver
  2. 项目创建完成后,可以在 pom.xml 中看到 Spring Boot 已经自动导入了如下的启动器:

    <!-- JDBC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <!-- WEB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MySQL -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
  3. 在 application.yaml 中编写数据库配置

    spring:
      datasource:
        # 数据库用户名&密码:
        username: root
        password: 123456
        # 数据库连接地址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 数据库驱动:
        driver-class-name: com.mysql.cj.jdbc.Driver
    
  4. 由于 SpringBoot 已经默认帮我们进行了自动配置,因此我们直接去使用就可以了。在测试类里面测试一下:

    package com.wmwx;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import javax.sql.DataSource;
    
    @SpringBootTest
    class Springboot03ApplicationTests {
    
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            //查看默认数据源
            System.out.println(dataSource.getClass());
        }
    
    }
    
  5. 输出结果:

    class com.zaxxer.hikari.HikariDataSource
    

这就意味着,在我们没有手动配置的情况下,Spring Boot 的默认数据源为 class com.zaxxer.hikari.HikariDataSource。HikariDataSource 号称是 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀。

如果我们想要自定义数据源,可以使用 spring.datasource.type 指定要使用的连接池实现的完全限定名

7.3 JDBC Template

有了数据源之后,我们就可以拿到数据库连接:

@SpringBootTest
class Springboot03ApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //获取连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();
    }

}

而有了连接,即使不使用第三方第数据库操作框架,如 MyBatis 等,Spring 本身也对原生的 JDBC 做了轻量级的封装,我们也可以使用原生的 JDBC 语句来操作数据库。这个封装便是 Jdbc Template,数据库操作的所有 CRUD 方法都在其中。并且,Spring Boot 不仅提供了默认的数据源,同时默认配置好了 Jdbc Template 放在了容器中。这就意味着,程序员只需自己注入,便可直接使用。

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

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

测试步骤:

  1. 编写一个 Controller,并注入 JDBC Template:

    JDBCController.java:

    package com.wmwx.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/jdbc")
    public class JDBCController {
        @Autowired
        JdbcTemplate jdbcTemplate;
    
        //查询数据库的所有信息
        //没有实体类时,数据库中的数据可以通过Map来获取
        @GetMapping("/userList")
        public List<Map<String, Object>> getUserList(){
            String sql = "select * from user";
            return jdbcTemplate.queryForList(sql);
        }
    }
    
  2. 在浏览器地址栏中输入 localhost:8080/jdbc/userList,可以看到页面中的输出如下:

    [{"id":1,"name":"刘季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉灵","pwd":"101010"},{"id":4,"name":"邓凌光","pwd":"197812"},{"id":5,"name":"周龙","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"}]
    
  3. 为 Controller 添加增加用户的功能:

    JDBCController.java:

    //增加
    @RequestMapping("/addUser")
    public String addUser(){
        String sql = "insert into user values(7, '李章泯', '010101')";
        jdbcTemplate.update(sql);
        return "addUser-success!";
    }
    
  4. 在浏览器地址栏中输入 localhost:8080/jdbc/addUser,可以看到页面中的输出如下:

    addUser-success!
    
  5. 在浏览器地址栏中输入 localhost:8080/jdbc/userList,可以看到页面中的输出如下:

    [{"id":1,"name":"刘季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉灵","pwd":"101010"},{"id":4,"name":"邓凌光","pwd":"197812"},{"id":5,"name":"周龙","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"},{"id":7,"name":"李章泯","pwd":"010101"}]
    

    可见用户的确已经添加成功。

  6. 同理,可为 Controller 添加修改用户和删除用户的功能:

    JDBCController.java:

    //修改
    @RequestMapping("/updateUser/{id}")
    public String updateUser(@PathVariable int id){
        String sql = "update user set name=?,pwd=? where id="+id;
        //封装
        Object[] objects = new Object[2];
        objects[0] = "老金";
        objects[1] = "222222";
        jdbcTemplate.update(sql, objects);
        return "updateUser-success!";
    }
    
    //删除
    @RequestMapping("/deleteUser")
    public String deleteUser(){
        String sql = "delete from user where id=7";
        jdbcTemplate.update(sql);
        return "deleteUser-success!";
    }
    

8. 整合 Druid

8.1 Druid 简介

Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。已经在阿里巴巴部署了超过600个应用,经过了一年多生产环境大规模部署的严苛考验。它可以很好地监控 DB 池连接和 SQL 的执行情况。可以说,它天生就是针对监控而生的 DB 连接池。

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

8.2 使用 Druid

Spring Boot 2.0 以上默认使用 Hikari 数据源,我们需要在配置文件中重新指定为 Druid。

使用步骤:

  1. 在 pom.xml 中添加 Druid 数据源依赖:

    <!-- Druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
    
  2. 在 application.yaml 中切换数据源:

    spring:
      datasource:
        # 数据库用户名&密码:
        username: root
        password: 123456
        # 数据库连接地址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 数据库驱动:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切换数据源为Druid
        type: com.alibaba.druid.pool.DruidDataSource
    
  3. 为数据源配置参数:

    spring:
      datasource:
        # 数据库用户名&密码:
        username: root
        password: 123456
        # 数据库连接地址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 数据库驱动:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切换数据源为Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
  4. 为数据源配置日志监控

    spring:
      datasource:
        # 数据库用户名&密码:
        username: root
        password: 123456
        # 数据库连接地址
        url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 数据库驱动:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切换数据源为Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  5. 在 pom.xml 文件中导入 log4j 的依赖

    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
    
  6. 启动浏览器,测试 7.3 中的增删改查,发现与之前使用 Hikari 时并无不同。

  7. 此时需要创建一个 Config 类,我们自己为 DruidDataSource 绑定全局配置文件中的参数,然后添加到容器中:

    package com.wmwx.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")
        @Bean
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    }
    

8.3 配置 Druid 数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看。想要使用这个功能,需要在 8.2 的代码基础上,为 DruidConfig.java 进行简单的配置:

//后台监控 相当于web.xml
//由于SpringBoot内置了Servlet,所以没有web.xml
//因此当我们想要使用web.xml时,就需要注册ServletRegistrationBean
@Bean
public ServletRegistrationBean statViewServlet(){
    ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
    //后台需要有人登录,账号密码配置
    HashMap<String, String> initParameters = new HashMap<>();
    //增加配置
    //登录的用户名和密码的Key是固定的,即loginUsername和loginPassword,不可以更换
    initParameters.put("loginUsername", "admin");
    initParameters.put("loginPassword", "123456");
    //允许谁可以访问
    initParameters.put("allow",""); //value为空意味着所有人都能访问
    //设置初始化参数
    bean.setInitParameters(initParameters);
    return bean;
}

测试步骤:

  1. 打开浏览器,在地址栏中输入 localhost:8080/druid,进入 Druid 后台登录界面
  2. 用户名输入 admin,密码输入 123456,点击 “login” 按钮即可登入 Druid 监控后台
  3. 浏览器新标签页的地址栏中输入 localhost:8080/jdbc/userList,并刷新监控后台,即可在 SQL 监控中看到被执行的 SQL 语句

8.4 配置 Druid 过滤器

示例:

DruidConfig.java:

//filter 过滤器
@Bean
public FilterRegistrationBean webStatFilter(){
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());
    //配置可以过滤的请求
    HashMap<String, String> initParameters = new HashMap<>();
    //exclusions:不参与统计
    initParameters.put("exclusions", "*.js,*.css,/druid/*");
    bean.setInitParameters(initParameters);
    return bean;
}

9. 整合 MyBatis

9.1 简介

想要在 Spring Boot 内整合 MyBatis,需要一个启动器:mybatis-spring-boot-starter 的支持。与其他大多数启动器不同,这个启动器并非 Spring Boot 官方推出的,而是 MyBatis 官方推出的,作用就是为了整合 MyBatis 与 Spring Boot。也正因此,该启动器的名称以 mybatis 开头,而非以 sping-boot-stater 开头。

官方文档:mybatis-spring-boot-starter 官方文档

9.2 整合

  1. 在 pom.xml 中导入 MyBatis 所需要的依赖

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    
  2. 在 pom.xml 中导入 lombok 依赖,便于实体类的创建:

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
  3. 创建 pojo 目录,并在其中创建实体类 User.java:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
    
  4. 创建 mapper 目录,并在其中创建接口 UserMapper.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    //@Mapper表明这是一个MyBatis的Mapper类
    @Mapper
    //@Repository是@Component的变形
    @Repository
    public interface UserMapper {
        //查询所有的用户
        public List<User> queryUserList();
    }
    
  5. static 目录下创建 /mybatis/UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wmwx.mapper.UserMapper">
        <select id="queryUserList" resultType="User">
            select * from user;
        </select>
    </mapper>
    
  6. 在 application.yaml 中配置整合 MyBatis

    # 整合 Mybatis
    mybatis:
      # 别名
      type-aliases-package: com.wmwx.pojo
      # Mapper配置文件的位置
      mapper-locations: classpath:/static/mybatis/*.xml
    
  7. controller 目录中新建一个 Controller 类:

    UserController.java:

    package com.wmwx.controller;
    
    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        private UserMapper userMapper;
    
        @RequestMapping("/userList")
        public List<User> queryUserList(){
            List<User> userList = userMapper.queryUserList();
            return userList;
        }
    }
    
  8. 打开浏览器,在地址栏中输入 localhost:8080/user/userList,可以发现页面输出如下:

    [{"id":1,"name":"刘季恒","pwd":"197699"},{"id":2,"name":"周松雅","pwd":"654321"},{"id":3,"name":"沈琉灵","pwd":"101010"},{"id":4,"name":"邓凌光","pwd":"197812"},{"id":5,"name":"周龙","pwd":"000000"},{"id":6,"name":"朱志","pwd":"012345"},{"id":7,"name":"老金","pwd":"222222"}]
    

10. Spring Security

10.1 安全问题

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

目前市面上比较主流的安全框架主要有两个:

  1. Spring Security
  2. Shiro

10.2 Spring Security 简介

Spring Security 是一个功能强大且高度可定制的身份验证访问控制框架。它实际上是保护基于 spring 的应用程序的标准,侧重于为 Java 应用程序提供身份的验证与授权。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它面向切面,可以轻松扩展以满足定制需求。

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

核心类:

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

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

“认证”(Authentication)

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

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

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源的完全权限。

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

10.3 实战测试

10.3.1 环境搭建
  1. 新建一个初始的 Spring Boot 项目,勾选 Web 模块和 Thymeleaf 模块

  2. templates 目录下创建 views 目录,并在 views 目录下创建 level1level2level3 目录

  3. 按照以下结构创建静态资源:

    • views
      • level1
        • 1.html
        • 2.html
        • 3.html
      • level2
        • 1.html
        • 2.html
        • 3.html
      • level3
        • 1.html
        • 2.html
        • 3.html
    • index.html
  4. 编写静态资源:

    index.html:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
    <div>
        <!-- 注意这里的两个路径,不可以改变 -->
        <a th:href="@{/login}">登录</a>
        <a th:href="@{/logout}">注销</a>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level1/1}">level1-1</a></li>
            <li><a th:href="@{/level1/2}">level1-2</a></li>
            <li><a th:href="@{/level1/3}">level1-3</a></li>
        </ul>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level2/1}">level2-1</a></li>
            <li><a th:href="@{/level2/2}">level2-2</a></li>
            <li><a th:href="@{/level2/3}">level2-3</a></li>
        </ul>
    </div>
    <div>
        <ul>
            <li><a th:href="@{/level3/1}">level3-1</a></li>
            <li><a th:href="@{/level3/2}">level3-2</a></li>
            <li><a th:href="@{/level3/3}">level3-3</a></li>
        </ul>
    </div>
    </body>
    </html>
    

    levelm-n.html(m、n均为1~3):

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>levelm-n</title>
    </head>
    <body>
        <h1>levelm-n</h1>
    </body>
    </html>
    
  5. 创建 controller 目录,并在其中创建 RouterController 类:

    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class RouterController {
        @RequestMapping({"/","/index"})
        public String toIndex(){
            return "index";
        }
    
        @RequestMapping("/level1/{id}")
        public String toLevel1(@PathVariable int id){
            return "views/level1/"+id;
        }
    
        @RequestMapping("/level2/{id}")
        public String toLevel2(@PathVariable int id){
            return "views/level2/"+id;
        }
    
        @RequestMapping("/level3/{id}")
        public String toLevel3(@PathVariable int id){
            return "views/level3/"+id;
        }
    }
    
  6. 在浏览器中地址栏输入 localhost:8080 进入首页,测试除登录与注销之外的各个链接之间的跳转,均能成功。

10.3.2 认证和授权
  1. 在 pom.xml 中添加依赖

    <!-- security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 创建 config 目录,在其中新建 SecurityConfig.java,并令其继承 WebSecurityConfigurerAdapter

    package com.wmwx.config;
    
    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;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
        }
    }
    
  3. SecurityController.java 中定制请求授权的规则:

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //授权
        //链式编程
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首页所有人都可以访问,但是功能页必须有权限的人才能访问
            //请求授权的规则
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
        }
    }
    
  4. 在浏览器中地址栏输入 localhost:8080 进入首页,测试各个链接之间的跳转,可以看到所有的 level 页面都会报 403 错误(权限不足)

  5. SecurityController.java 中开启自动配置的登录功能:

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //授权
        //链式编程
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首页所有人都可以访问,但是功能页必须有权限的人才能访问
            //请求授权的规则
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
    
            //开启自动配置的登录功能,若没有权限默认回到登录页
            http.formLogin();
        }
    }
    
  6. 在浏览器中地址栏输入 localhost:8080 进入首页,测试各个链接之间的跳转,可以看到所有的 level 页面都会进入登录页

  7. SecurityController.java 中重新定义认证规则:

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通常从数据库中认证,此处先从内存中认证
        auth.inMemoryAuthentication()
                .withUser("root").password("123456").roles("vip1","vip2","vip3")
                .and()
                .withUser("wmwx").password("123456").roles("vip1","vip2")
                .and()
                .withUser("guest").password("123456").roles("vip1");
    }
    
  8. 在浏览器中地址栏输入 localhost:8080 进入首页,测试各个链接之间的跳转,通过点击 level 页进入登录页,但在登录后却报出 500 错误

  9. 为密码字符串加密:

    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通常从数据库中认证,此处先从内存中认证
        //使用BCryptPasswordEncoder加密
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("wmwx").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }
    
  10. 在浏览器中地址栏输入 localhost:8080 进入首页,测试各个链接之间的跳转,通过点击 level 页进入登录页,输入用户名与密码,发现在具有相应权限的情况下会登录成功,而逾越了权限的情况下则会报出 403 错误

10.3.3 注销
  1. 开启自动配置的注销的功能

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人都可以访问,但是功能页必须有权限的人才能访问
        //请求授权的规则
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");
    
        //开启自动配置的登录功能,没有权限默认回到登录页
        http.formLogin();
        //开启自动配置的注销功能,注销后跳转到首页
        http.logout().logoutSuccessUrl("/");
    }
    
  2. 在浏览器中地址栏输入 localhost:8080 进入首页,登录后再点击注销,可以发现页面自动跳转到了首页,且所有的权限都消失了。

10.3.4 权限控制

现在还有一个问题需要解决:用户登录之后,不应该再看到不具备权限的内容,这样就不会出现 403 报错页面了。

想要解决这个问题,需要借助 Thymeleaf 整合 Spring Security:

  1. 在 pom.xml 中导入整合包依赖:

    <!-- thymeleaf 整合 security -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    
  2. 修改 index.html 中的登录与注销:

    <div>
        <!-- 登录前:显示登录按钮 -->
        <div sec:authorize="!isAuthenticated()">
            <a th:href="@{/login}">登录</a>
        </div>
        <!-- 登录后:显示用户名、权限和注销按钮 -->
        <div sec:authorize="isAuthenticated()">
            用户名:<span sec:authentication="name"></span>
            权限:<span sec:authentication="principal.authorities"></span>
        </div>
        <div sec:authorize="isAuthenticated()">
            <a th:href="@{/logout}">注销</a>
        </div>
    </div>
    
  3. 在浏览器中地址栏输入 localhost:8080 进入首页,只能看到登录按钮;登录后自动跳转到首页,可以看到用户名与权限,测试成功。但是,此时我们再去点击注销按钮,会发现报出了 404 错误。

  4. 出现这个问题是因为 Spring Security 默认防止 csrf 跨站请求伪造,因为它会产生安全问题。我们可以通过将请求改为 post 表单提交,或者在 Spring Security 中关闭 csrf 功能的方式来解决这个问题。这里,我们选择后者:

    //开启自动配置的注销功能,注销后跳转到首页
    //关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
    http.csrf().disable();
    http.logout().logoutSuccessUrl("/");
    
  5. 同理可以修改 index.html 中的三个 level 块:

    <div>
        <!-- 判断用户是否具有vip1的权限 -->
        <ul sec:authorize="hasRole('vip1')">
            <li><a th:href="@{/level1/1}">level1-1</a></li>
            <li><a th:href="@{/level1/2}">level1-2</a></li>
            <li><a th:href="@{/level1/3}">level1-3</a></li>
        </ul>
    </div>
    <div>
        <ul sec:authorize="hasRole('vip2')">
            <li><a th:href="@{/level2/1}">level2-1</a></li>
            <li><a th:href="@{/level2/2}">level2-2</a></li>
            <li><a th:href="@{/level2/3}">level2-3</a></li>
        </ul>
    </div>
    <div>
        <ul  sec:authorize="hasRole('vip3')">
            <li><a th:href="@{/level3/1}">level3-1</a></li>
            <li><a th:href="@{/level3/2}">level3-2</a></li>
            <li><a th:href="@{/level3/3}">level3-3</a></li>
        </ul>
    </div>
    
  6. 在浏览器中地址栏输入 localhost:8080 进入首页,根据登录角色的不同,页面中显示出的内容也不同。

10.3.5 记住登录状态

现在我们在登录后,每次重新打开浏览器进入页面,都需要重新登录,原本的登录状态不会保存。但是,很多网站在登录时都有一个记住登录状态的功能,勾选后短期内就无需再次登录了。这个功能实现起来也很简单,只需一行代码即可:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人都可以访问,但是功能页必须有权限的人才能访问
    //请求授权的规则
    http.authorizeRequests().antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    //开启自动配置的登录功能,没有权限默认回到登录页
    http.formLogin();
    //开启自动配置的注销功能,注销后跳转到首页
    //关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
    http.csrf().disable();
    http.logout().logoutSuccessUrl("/");
    //开启记住登录状态功能,默认保存两周,本质是cookie
    http.rememberMe();
}
10.3.6 登录页定制

到目前为止,我们使用的登录页面都是 Spring Security 默认的。那么,如何才能使用我们自己写的登录页面呢?

  1. views 目录下新建一个 login.html 文件:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
        <form method="post" th:action="@{/login}">
            <div>用户名:<input type="text" placeholder="请输入用户名" name="user"></div>
            <div>密码:<input type="password" name="pwd"></div>
            <div><input type="checkbox" name="remember">记住我的登录状态</div>
            <div><input type="submit" value="登录"></div>
        </form>
    </body>
    </html>
    
  2. 修改首页中登录按钮的路径:

    <a th:href="@{/toLogin}">登录</a>
    
  3. 在 RouterController.java 中添加路由:

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }
    
  4. 在 SecurityConfig.java 中配置登录页和记住登录状态的功能:

    //定制登录页
    http.formLogin()
            .loginPage("/toLogin")
            .usernameParameter("user")	//input标签中用户名的name属性
            .passwordParameter("pwd")	//input标签中密码的name属性
            .loginProcessingUrl("/login");
    
    //开启记住登录状态功能,默认保存两周,本质是cookie
    http.rememberMe().rememberMeParameter("remember");		//input标签中记住登录状态的name属性
    
  5. 在浏览器中测试,登录、注销,一切正常!

11. Shiro

11.1 Shiro 简介

Apache Shiro 是一个 Java 的安全(权限)框架。它可以非常容易地开发出足够好的应用,不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以实现认证、授权、加密、会话管理、Web集成、缓存等功能。

核心:

  • Subject
  • Shiro Security Manager
  • Realm

11.2 Spring Boot 集成 Shiro

  1. 新建一个初始的 Spring Boot 项目,勾选 Web 模块和 Thymeleaf 模块

  2. 在 pom.xml 中导入依赖:

    <!-- shiro-spring -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.7.1</version>
    </dependency>
    
  3. 创建目录 config,并在其中创建 ShiroConfig.java

    package com.wmwx.config;
    
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ShiroConfig {
        //ShiroFilterFactoryBean
        
        //DefaultWebSecurityManager
    
        //创建Realm对象,需要自定义类
    
    }
    
  4. config 目录下创建 UserRealm.java,并令其继承 AuthorizingRealm

    package com.wmwx.config;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    //自定义的Realm
    public class UserRealm extends AuthorizingRealm {
        //授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行了授权方法");
            return null;
        }
        //认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("执行了认证方法");
            return null;
        }
    }
    
  5. 从下至上,补充 ShiroConfig.java

    //创建Realm对象,需要自定义类
    @Bean(name="userRealm")
    public UserRealm userRealm(){
        return new UserRealm();
    }
    
  6. 继续补充 ShiroConfig.java

    //DefaultWebSecurityManager
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联UserRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    
  7. 继续补充 ShiroConfig.java

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(securityManager);
        return bean;
    }
    
  8. templates 目录下创建一个 user 目录,并在其中新建两个 .html 页面:

    add.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>新增用户</title>
    </head>
    <body>
    <h1>新增用户</h1>
    </body>
    </html>
    

    update.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>修改用户</title>
    </head>
    <body>
    <h1>修改用户</h1>
    </body>
    </html>
    
  9. templates 目录下新建一个 index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>首页</h1>
        <a th:href="@{/user/add}">新增用户</a>
        <a th:href="@{/user/update}">修改用户</a>
    </body>
    </html>
    
  10. 创建一个 controller 目录,并在其中新建一个 ShiroController.java

    package com.wmwx.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class ShiroController {
        @RequestMapping({"/","/index"})
        public String toIndex(){
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String toAdd(){
            return "/user/add";
        }
    
        @RequestMapping("/user/update")
        public String toUpdate(){
            return "/user/update";
        }
    }
    
  11. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击链接进行跳转测试,都可以成功进入。

11.3 Shiro 实现登录拦截

  1. ShiroConfig.java 中添加 shiro 的内置过滤器:

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(securityManager);
        //添加shiro的内置过滤器
        /*
        * anon:无需认证即可访问
        * authc:必须认证才能访问
        * user:必须拥有记住我功能才能访问
        * perms:拥有对某个功能的权限才能访问
        * role:拥有某个角色的权限才能访问
        */
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add", "authc");
        filterMap.put("/user/update", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }
    
  2. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击链接进行跳转测试,发现两个链接点进去都报出 404 错误

  3. templates 目录下新建一个 login.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
        <h1>登录</h1>
        <hr />
        <form method="post" th:action="@{/login}">
            <div>用户名:<input type="text" name="username"></div>
            <div>密码:<input type="password" name="password"></div>
            <div><input type="checkbox" name="remember">记住登录状态</div>
            <div><input type="submit" value="登录"></div>
        </form>
    </body>
    </html>
    
  4. ShrioController.java 中新增一个方法:

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
    
  5. ShiroConfig.java 中设置登录请求:

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(securityManager);
        //添加shiro的内置过滤器
        /*
        * anon:无需认证即可访问
        * authc:必须认证才能访问
        * user:必须拥有记住我功能才能访问
        * perms:拥有对某个功能的权限才能访问
        * role:拥有某个角色的权限才能访问
        */
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/add", "authc");
        filterMap.put("/user/update", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
    
        //设置登录请求
        bean.setLoginUrl("/toLogin");
    
        return bean;
    }
    
  6. 修改 index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>首页</h1>
        <a th:href="@{/toLogin}">登录</a>
        <hr />
        <a th:href="@{/user/add}">新增用户</a>
        <a th:href="@{/user/update}">修改用户</a>
    </body>
    </html>
    
  7. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击链接进行跳转测试,发现点击三个链接都会自动跳转到登陆页面

11.4 Shiro 实现用户认证

  1. ShrioController.java 中新增一个方法:

    @RequestMapping("/login")
    public String login(String username, String password, Model model){
        //获取当前用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户的登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //执行登录的方法
        try {
            subject.login(token);
            return "index";
        }catch (UnknownAccountException e){
            model.addAttribute("msg", "用户名不存在!");
            return "login";
        }catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误!");
            return "login";
        }
    }
    
  2. 修改 login.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
        <h1>登录</h1>
        <p th:text="${msg}" style="color: red;"></p>
        <hr />
        <form method="post" th:action="@{/login}">
            <div>用户名:<input type="text" name="username"></div>
            <div>密码:<input type="password" name="password"></div>
            <div><input type="checkbox" name="remember">记住登录状态</div>
            <div><input type="submit" value="登录"></div>
        </form>
    </body>
    </html>
    
  3. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击任意链接进入登录页,输入任意字符后点击登录按钮,发现页面中用红字输出以下内容:

    用户名不存在!
    

    而在控制台中则输出以下内容:

    执行了认证方法
    
  4. 修改 UserRealm.java

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //用户名、密码可以从数据库中获取,此处先预先写死
        String username = "root";
        String password = "123456";
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        if (!token.getUsername().equals(username)){
            return null;    //抛出UnknownAccountException异常,即用户名不存在的异常
        }
        //IncorrectCredentialsException异常,即密码错误的异常交给Shiro来检测
        return new SimpleAuthenticationInfo("",password,"");
    }
    
  5. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击任意链接进入登录页,故意输错用户名后点击登录按钮,可以看到红字输出:

    用户名不存在!
    

    在用户名框中输入 “root”,在密码框中故意输错,再次点击登录按钮,可以看到红字输出:

    密码错误!
    

    在用户名框中输入 “root”,并在密码框中输入 “123456”,点击登录按钮,链接自动跳转至首页。此时,再次点击 “新增用户” 或 “修改用户”,都可以进入到对应的页面中,而不会再次跳转至登录页面。

11.5 Shiro 整合 MyBatis

  1. 在 pom.xml 中导入依赖:

    <!-- mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!-- Druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.6</version>
    </dependency>
    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
  2. 编写 application.yaml 配置文件:

    spring:
      datasource:
        # 数据库用户名&密码:
        username: root
        password: 123456
        # 数据库连接地址
        url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
        # 数据库驱动:
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 切换数据源为Druid
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 默认是不注入这些属性值的,需要自己绑定
        #druid 数据源专有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    mybatis:
      # 为实体类起别名
      type-aliases-package: com.wmwx.pojo
      # 绑定Mapper配置文件
      mapper-locations: classpath:mapper/*.xml
    
  3. 创建 pojo 目录,并在此目录下创建 User.java 文件:

    package com.wmwx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
    }
    
  4. 创建 mapper 目录,并在此目录下创建 UserMapper.javaUserMapper.xml

    UserMapper.java:

    package com.wmwx.mapper;
    
    import com.wmwx.pojo.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    @Repository
    @Mapper
    public interface UserMapper {
        //通过用户名查询用户
        public User queryUserByUsername(String name);
    }
    

    UserMapper.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.wmwx.mapper.UserMapper">
        <!--
            //通过用户名查询用户
            public User queryUserByUsername(String name);
        -->
        <select id="queryUserByUsername" resultType="User">
            select * from user where name=#{name}
        </select>
    </mapper>
    
  5. 创建 service 目录,并在此目录下创建 UserService.javaUserServiceImpl.java

    UserService.java:

    package com.wmwx.service;
    
    import com.wmwx.pojo.User;
    
    public interface UserService {
        //通过用户名查询用户
        public User queryUserByUsername(String name);
    }
    

    UserServiceImpl.java:

    package com.wmwx.service;
    
    import com.wmwx.mapper.UserMapper;
    import com.wmwx.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserServiceImpl implements UserService{
        @Autowired
        UserMapper userMapper;
    
        @Override
        public User queryUserByUsername(String name) {
            return userMapper.queryUserByUsername(name);
        }
    }
    
  6. 编写测试类:

    package com.wmwx;
    
    import com.wmwx.pojo.User;
    import com.wmwx.service.UserServiceImpl;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class Springboot05ApplicationTests {
        @Autowired
        UserServiceImpl userService;
    
        @Test
        void contextLoads() {
            User user = userService.queryUserByUsername("root");
            System.out.println(user);
        }
    
    }
    
  7. 运行 contextLoads() 方法,控制台中出现如下输出:

    User(id=1, name=root, pwd=123456)
    
  8. 修改 UserRealm.java

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //用户名、密码从数据库中获取
        User user = userService.queryUserByUsername(token.getUsername());
        //user==null说明没有找到
        if (user==null){
            //抛出UnknownAccountException异常,即用户名不存在的异常
            return null;
        }else{
            //IncorrectCredentialsException异常,即密码错误的异常交给Shiro来检测
            return new SimpleAuthenticationInfo("",user.getPwd(),"");
        }
    }
    
  9. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,点击任意链接进入登录页,输入数据库中存在的用户名和密码,登录成功!

11.6 Shiro 实现请求授权

  1. 修改 ShiroConfig.java

    LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    //授权
    filterMap.put("/user/add", "perms[user:add]");
    //拦截
    filterMap.put("/user/*", "authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //设置登录请求
    bean.setLoginUrl("/toLogin");
    
  2. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,登录后点击 “修改用户”,可以正常访问;点击 “新增用户”,报出 401 错误

  3. ShiroController.java 中新增以下方法:

    @RequestMapping("/noauth")
    @ResponseBody
    public String unauthorized(){
        return "未经授权无法访问此页面!";
    }
    
  4. 修改 ShiroConfig.java

    LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
    //授权
    filterMap.put("/user/add", "perms[user:add]");
    //拦截
    filterMap.put("/user/*", "authc");
    bean.setFilterChainDefinitionMap(filterMap);
    //设置登录请求
    bean.setLoginUrl("/toLogin");
    //设置未授权页面
    bean.setUnauthorizedUrl("/noauth");
    
  5. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,登录后点击 “新增用户”,页面输出以下内容:

    未经授权无法访问此页面!
    
  6. 修改 UserRealm.java

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //对所有用户进行授权
        info.addStringPermission("user:add");
        return info;
    }
    
  7. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,登录后点击 “新增用户”,可以正常访问。

  8. 修改 UserRealm.java

    //认证
    //IncorrectCredentialsException异常,即密码错误的异常交给Shiro来检测
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    
  9. 修改 shiroConfig.java

    //授权
    filterMap.put("/user/add", "perms[user:add]");
    filterMap.put("/user/update", "perms[user:update]");
    
  10. 修改 UserRealm.java

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //拿到当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal();
        //设置当前用户的权限为数据库中的权限
        //数据库中有两条数据:
        //id name pwd     perms
        // 1 root 123456  user:add
        // 2 wmwx 010101  user:update
        // 3 guest 222333 none
        info.addStringPermission(currentUser.getPerms());
        return info;
    }
    
  11. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,登录账号 “root”,点击 “新增用户,可以正常访问;点击 “修改用户”,页面输出如下:

    未经授权无法访问此页面!
    

11.7 Shiro 整合 Thymeleaf

  1. pom.xml 中导入依赖:

    <!-- thymeleaf-shiro -->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    
  2. 修改 ShiroConfig.java

    //配置ShiroDialect:用来整合Shiro和Thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
    
  3. 修改 index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:shiro="http://www.polix.at/thymeleaf/shiro">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>首页</h1>
        <a th:href="@{/toLogin}">登录</a>
        <a th:href="@{/}">注销</a>
        <hr />
        <a th:href="@{/user/add}" shiro:hasPermission="user:add">新增用户</a>
        <a th:href="@{/user/update}" shiro:hasPermission="user:update">修改用户</a>
    </body>
    </html>
    
  4. 打开浏览器,在地址栏中输入 localhost:8080 进入首页,“注销”、“新增用户” 和 “修改用户” 的链接都未显示;登录账号 “root”,显示 “新增用户”;登陆账号 “wmwx”,显示 “修改用户”;登录用户 “guest”,两个链接都不显示;并且无论登录哪个账号,“登录” 按钮都会消失,“注销” 按钮都会出现。

12. Swagger

12.1 简介

  • Swagger 号称是世界上最流行的 API 框架
  • Restful 风格的 Api 文档在线自动生成
  • 直接运行,可以在线测试 API 接口
  • 支持多种语言(如:Java、PHP等)

官网:Swagger 官网

12.2 Spring Boot 集成 Swagger

  1. 新建一个 Spring Boot 项目,勾选 Spring Web

  2. 导入相关依赖(注意版本号,本处引用 2.9.2,从 3.0 开始 Swagger 进行了大更新)

    <!-- springfox-swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <!-- springfox-swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  3. 创建 config 目录,并在此目录下创建 SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2     //开启Swagger2
    public class SwaggerConfig {
        
    }
    
  4. 在浏览器中输入 localhost:8080/swagger-ui.html,可以看到默认提供的文档界面

12.3 Swagger 配置

12.3.1 配置 Swagger 信息
  1. 补充 SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2     //开启Swagger2
    public class SwaggerConfig {
        //配置了 Swagger 的 Docket Bean 实例
        @Bean
        public Docket getDocket(){
            return new Docket(DocumentationType.SWAGGER_2);
        }
    }
    
  2. 继续补充 SwaggerConfig.java

    package com.wmwx.springboot06.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.Contact;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    import java.util.ArrayList;
    
    @Configuration
    @EnableSwagger2     //开启Swagger2
    public class SwaggerConfig {
        //配置了 Swagger 的 Docket Bean 实例
        @Bean
        public Docket getDocket(){
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo());
        }
    
        //配置 Swagger 文档信息 => ApiInfo
        public ApiInfo apiInfo(){
            Contact contact = new Contact("妙霄", "mxblog.top", "19852821117@163.com");
    
            return new ApiInfo(
                    "妙霄的Swagger文档",
                    "腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。",
                    "v1.0",
                    "mxblog.top",
                    contact,
                    "Apache 2.0",
                    "http://www.apache.org/licenses/LICENSE-2.0",
                    new ArrayList()
            );
        }
    }
    
  3. 在浏览器中输入 localhost:8080/swagger-ui.html,可以看到自己配置了个人信息的文档界面

12.3.2 配置 Swagger 扫描与过滤
  1. 创建 controller 目录,并在其中新建 HelloController.java 文件:

    package com.wmwx.springboot06.controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
        @RequestMapping("/hello")
        public String hello(){
            return "Hello Swagger!";
        }
    }
    
  2. 在浏览器中输入 localhost:8080/swagger-ui.html,可以看到 Swagger 帮我们自动生成了两个 Controller 接口文档和一个 Models 文档。

  3. 继续补充 SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 实例
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文档信息
                .apiInfo(apiInfo())
                .select()
                //RequestHandlerSelectors 配置扫描接口的方式
                //basePackage:指定要扫描的包(最常用)
                //any:全都扫描
                //none:都不扫描
                //withClassAnnotation:扫描类上的注解
                //withMethodAnnotation:扫描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                .build();
    }
    
  4. 在浏览器中输入 localhost:8080/swagger-ui.html,发现原本的文档中显示的两个 Controller 和一个 Models,现在只剩下一个 hello-controller 了。

  5. 继续补充 SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 实例
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文档信息
                .apiInfo(apiInfo())
                .select()
                //RequestHandlerSelectors 配置扫描接口的方式
                //basePackage:指定要扫描的包(最常用)
                //any:全都扫描
                //none:都不扫描
                //withClassAnnotation:扫描类上的注解
                //withMethodAnnotation:扫描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置过滤的路径
                //ant:指定要过滤的路径(最常用)
                //any:全都过滤
                //none:都不过滤
                .paths(PathSelectors.ant("/controller/**"))
                .build();
    }
    
  6. 在浏览器中输入 localhost:8080/swagger-ui.html,发现现在的文档中只显示 No operations defined in spec!,即没有自动生成的接口文档了。

  7. 注释掉第 5 步中新增的代码,取消过滤,让 Swagger 中只生成 controller 包下接口的文档。

  8. 继续补充 SwaggerConfig.java

    //配置了 Swagger 的 Docket Bean 实例
    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文档信息
                .apiInfo(apiInfo())
                //定义是否启动swagger,若为false则不能在浏览器中访问
                .enable(false)
                .select()
                //RequestHandlerSelectors 配置扫描接口的方式
                //basePackage:指定要扫描的包(最常用)
                //any:全都扫描
                //none:都不扫描
                //withClassAnnotation:扫描类上的注解
                //withMethodAnnotation:扫描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置过滤的路径
                //ant:指定要过滤的路径(最常用)
                //any:全都过滤
                //none:都不过滤
                //.paths(PathSelectors.ant("/controller/**"))
                .build();
    }
    
  9. 在浏览器中输入 localhost:8080/swagger-ui.html,发现文档不见了,页面中有如下输出:

    😱 Could not render e, see the console.
    
12.3.3 配置 Swagger 开关
  1. 删除项目中原本自动生成的 application.properties 文件,并建立以下三个配置文件:

    application-dev.yaml:

    # 应用服务 WEB 访问端口
    server:
      port: 8080
    

    application-prod.yaml:

    # 应用服务 WEB 访问端口
    server:
      port: 8082
    

    application.yaml:

    spring:
      # 应用名称
      application:
        name: springboot-06
      # 使用的配置环境
      profiles:
        active: dev
    

    这样一来,项目中就同时存在 devprod 两个配置环境了,并且此时正在使用的是 dev 环境。

  2. 修改 SwaggerConfig.java (注意 getDocket 方法添加了参数):

    //配置了 Swagger 的 Docket Bean 实例
    @Bean
    public Docket getDocket(Environment environment){
        //设置要启动Swagger的环境
        Profiles profile = Profiles.of("dev");
        //判断当前环境是否是要启动Swagger的环境
        boolean isEnv = environment.acceptsProfiles(profile);
        return new Docket(DocumentationType.SWAGGER_2)
                //apiInfo 配置文档信息
                .apiInfo(apiInfo())
                //定义是否启动swagger,若为false则不能在浏览器中访问
                .enable(isEnv)
                .select()
                //RequestHandlerSelectors 配置扫描接口的方式
                //basePackage:指定要扫描的包(最常用)
                //any:全都扫描
                //none:都不扫描
                //withClassAnnotation:扫描类上的注解
                //withMethodAnnotation:扫描方法上的注解
                .apis(RequestHandlerSelectors.basePackage("com.wmwx.springboot06.controller"))
                //PathSelectors 配置过滤的路径
                //ant:指定要过滤的路径(最常用)
                //any:全都过滤
                //none:都不过滤
                //.paths(PathSelectors.ant("/controller/**"))
                .build();
    }
    

    这样一来,我们只需要改变 Profiles.of 里的参数,即可指定哪个环境才会去开启 Swagger。

12.3.4 配置 Swagger 分组

在 Swagger 生成的文档的右上角可以选择分组,想要配置 Swagger 分组可以通过以下方式实现:

@Bean
public Docket getDocket(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("groupName");
}

而如果想要配置多个分组,也只需编写多个 Docket 即可:

//多个分组
@Bean
public Docket getDocketA(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket getDocketB(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket getDocketC(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}

12.4 Swagger 注解

12.4.1 实体类注解
  1. 创建 pojo 目录,并在其中创建 User.java,并为 User 类添加 @ApiModel 注解,为其属性添加 @ApiModelProperty 注解:

    package com.wmwx.springboot06.pojo;
    
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    
    @ApiModel("用户类")
    public class User {
        @ApiModelProperty("编号")
        private int id;
        @ApiModelProperty("用户名")
        private String name;
        @ApiModelProperty("密码")
        private String pwd;
    
        public User() {
        }
    
        public User(int id, String name, String pwd) {
            this.id = id;
            this.name = name;
            this.pwd = pwd;
        }
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPwd() {
            return pwd;
        }
    
        public void setPwd(String pwd) {
            this.pwd = pwd;
        }
    }
    
  2. SwaggerController.java 中添加一个方法,令其返回值为 User 类型:

    @RequestMapping("/user")
    public User user(){
        return new User();
    }
    
  3. 在浏览器中输入 localhost:8080/swagger-ui.html,发现现在的文档中显示了 Models 模块。点击展开后看到以下输出内容:

    用户类	{
        id	integer($int32)
        	编号
    
        name	string
        		用户名
    
        pwd	string
        	密码
    }
    

    由此可见,@ApiModel 的作用是在文档中为实体类添加注释,而 @ApiModelProperty 则是为其属性添加注释。

    除此之外,还可以使用 @Api 注解,它与 @ApiModel等价的。

12.4.2 方法注解
  1. SwaggerController.java 添加注解:

    @ApiOperation("hello方法")
    @RequestMapping("/hello")
    public String hello(@ApiParam("用户名") String username){
        return "Hello " + username + "!";
    }
    
  2. 在浏览器中输入 localhost:8080/swagger-ui.html,可以看到文档中的 “/hello” 旁边被标注了 “hello方法”,展开参数列表可以看到 “用户名” 的字样。

13. 任务

13.1 异步任务

  1. 创建一个 Spring Boot 项目,勾选 Spring Web 即可

  2. 创建 service 目录,并在其中创建 AsyncService.java

    package com.wmwx.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class AsyncService {
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("数据正在处理......");
        }
    }
    
  3. 创建 controller 目录,并在其中创建 AsyncController.java

    package com.wmwx.controller;
    
    import com.wmwx.service.AsyncService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class AsyncController {
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping("/hello")
        public String hello(){
            asyncService.hello();
            return "方法执行完毕!";
        }
    }
    
  4. 在浏览器中输入 localhost:8080/hello,可以发现,需要等待3秒,控制台才会输出 “数据处理完毕!”,屏幕上才会显示出 “方法执行完毕!”的字样。

  5. 修改 AsyncService.java,添加 @Async 注解:

    @Service
    public class AsyncService {
        //告诉Spring这是一个异步方法
        @Async
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("数据处理完毕!");
        }
    }
    
  6. 修改启动类,添加 @EnableAsync 注解:

    @EnableAsync    //开启异步注解
    @SpringBootApplication
    public class Springboot07Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot07Application.class, args);
        }
    
    }
    
  7. 在浏览器中输入 localhost:8080/hello,可以发现,页面立即显示出 “方法执行完毕!”,3秒后控制台才会输出 “数据处理完毕! ”的字样。

13.2 邮件任务

  1. pom.xml 中导入依赖:

    <!-- mail -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    
  2. application.properties 中编写邮箱配置:

    # 你的邮箱地址
    spring.mail.username=
    # 你的邮箱授权码
    spring.mail.password=
    # smtp.163.com 或 smtp.qq.com 等
    spring.mail.host=
    # 如果是QQ邮箱,需要配置SSL开启
    spring.mail.properties.mail.smtp.ssl.enable=true
    
  3. 编写测试类,测试简单邮件:

    @SpringBootTest
    class Springboot07ApplicationTests {
        @Autowired
        JavaMailSenderImpl mailSender;
        @Test
        void contextLoads() {
            //一个简单的邮件
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("测试简单邮件功能");
            message.setText("这里是邮件的正文");
            message.setTo("");	//收信人地址
            message.setFrom("");	//寄信人地址
            mailSender.send(message);
        }
    }
    
  4. 运行测试类,打开收信人邮箱,就可以看到我们发出的邮件了。

  5. 在测试类中新建一个方法,测试复杂邮件:

    @Test
    void mailTest() throws MessagingException {
        //一个复杂的邮件
        MimeMessage message = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setSubject("测试复杂邮件功能");
        helper.setText("<p style='color:red;'>这里是邮件的正文</p>", true);
        helper.setTo("");	//收信人地址
        helper.setFrom("");	//寄信人地址
        //附件
        helper.addAttachment("1.jpg", new File(""));	//文件路径,可以是本地的绝对路径
        helper.addAttachment("2.jpg", new File(""));
        //发送
        mailSender.send(message);
    }
    
  6. 运行测试类,打开收信人邮箱,可以看到我们带格式的邮件连带附件一起发出了。

13.3 定时任务

  1. 在启动类中添加 @EnableScheduling 注解:

    @EnableScheduling   //开启定时功能的注解
    @SpringBootApplication
    public class Springboot07Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Springboot07Application.class, args);
        }
    
    }
    
  2. service 目录下创建 ScheduledService.java

    package com.wmwx.service;
    
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ScheduledService {
        //在一个特定的时间执行这个方法
        //cron表达式
        @Scheduled(cron = "0/2 * * * * ?")  //每两秒执行一次
        public void hello(){
            System.out.println("Hello!");
        }
    }
    
  3. 运行启动类(定时任务是异步的,只要配置了自动就会运行,不需要额外去调用),在控制台中看到每两秒输出一次 “Hello!”

附录 - cron 表达式示例:

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

14. 分布式简介

14.1 概念

分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。它由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是通过利用更多的机器来处理更多的数据。

互联网架构迭代:

  • 单一应用架构:当网站流量很小时,只需通过一个应用将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的**数据访问框架(ORM)**是关键。这种架构适只用于小型网站、小型管理系统,因为它将所有功能都部署到一个功能里,简单易用。但它的缺点也很明显:性能扩展比较难、不易于协同开发、不利于升级维护等。
  • 垂直应用架构:当访问量逐渐增大时,单一应用增加机器带来的加速度会越来越小,因此可以将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 **Web 框架(MVC)**是关键。这种架构通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。但同时,它的公用模块无法重复利用,会造成开发性的浪费。
  • 分布式服务架构:当垂直应用越来越多时,应用之间交互不可避免。为了避免因此带来的问题,可以将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
  • 流动计算架构:当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture ] 是关键。

14.2 RPC

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

RPC 的两个核心模块是:

  1. 通讯
  2. 序列化

14.3 Dubbo

14.3.1 简介

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

Dubbo 官网:Dubbo 官网

14.3.2 基本构成

Dubbo 由以下几大模块构成:

  • 服务提供者(Provider):暴露服务的服务提供方。服务提供者在启动时,向注册中心注册自己提供的服务。
  • 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务。然后从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用。如果调用失败,再选另一台调用。
  • 注册中心(Registry):注册中心返回服务提供者地址列表给消费者。如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 监控中心(Monitor):服务消费者和服务提供者在内存中的累计调用次数和调用时间,会定时每分钟发送一次统计数据到监控中心。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值