SpringBoot

https://spring.io/projects/spring-boot

Reference:

1. 简介

1.1 SpringBoot

  • Spring
    • Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。
    • Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
  • 如何简化?

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

学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;

SpringBoot,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置

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

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

1.2 微服务

微服务是一种架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合;可以通过http的方式进行互通

==高内聚、低耦合==

1.2.1 开发演变

在这里插入图片描述

1.2.2 单体应用架构

将一个应用中的所有服务都封装在一个应用中

  • 优点:易于开发和测试;也十分方便部署;当需要扩展时,只需将war包复制多分,然后放到多个服务器上,再做负载均衡
  • 缺点:修改维护麻烦

1.2.2 微服务架构

把每个功能元素独立出来。把独立出来的功能元素的动态组合,需要的功能元素才去拿来组合,需要多一些时可以整合多个功能元素。所有微服务是对功能元素进行复制,而没有对整个应用进行复制

2. 第一个SpringBoot程序

23-7-28

2.1 项目创建

2.1.1 官网方式

官网提供的快速构建项目的网址:Spring Initializr

image-20230728193109802

  • GEBERATE -> Maven项目
  • 项目结构:image-20230728193453875

2.1.2 IDEA方式

  • 新建项目

image-20230728193749320

  • 添加依赖

image-20230728193825540

  • 直接运行

image-20230728195146256

:warning:

1
2
3
4
java: 无法访问org.springframework.boot.SpringApplication
错误的类文件: /D:/Coding/Java/apache-maven-3.9.3/maven-repo/org/springframework/boot/spring-boot/3.0.9/spring-boot-3.0.9.jar!/org/springframework/boot/SpringApplication.class
类文件具有错误的版本 61.0, 应为 52.0
请删除该文件或确保该文件位于正确的类路径子目录中。
  • 解决方法1 SpringBoot使用了3.0或者3.0以上,因为Spring官方发布从Spring6以及SprinBoot3.0开始最低支持JDK17,所以仅需将SpringBoot版本降低为3.0以下即可。
  • 解决方法2 将JDK版本提高

2.1.3 直接使用

1
2
3
4
5
6
7
@RestController
public class HelloController {
@RequestMapping("/hello") // http://localhost:8080/hello
public String hello() {
return "Hello World!";
}
}

image-20230728195505683

2.1.4 注意点

  1. 新建包要在 HelloworldideaApplication.java 同级目录下建包

  2. 程序入口也是Spring的组件

  3. pom.xml

    • ```xml

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

      </parent>
      com.bayyy
      helloworld
      0.0.1-SNAPSHOT
      helloworld

      Demo project for Spring Boot 8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test

      <!-- 打jar包插件 -->
      <plugins>
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
      </plugins>
      

      </build>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17

      4. 打包

      - ![image-20230728200204481](https://s2.loli.net/2023/07/28/DOjIpbTJCLPV24a.png)
      - ![image-20230728200256244](https://s2.loli.net/2023/07/28/CkLRPGAQpncjbmz.png)

      - 此时可以直接运行 `java -jar .\helloworld-0.0.1-SNAPSHOT.jar`
      - ![image-20230728200657915](https://s2.loli.net/2023/07/28/zBJxOcejn8Dh5Wm.png)

      ## 2.2 基础配置

      ### 2.2.1 修改端口号

      > application.properties

      ```properties
      server.port=8081

2.2.2 修改Banner

resources 目录下新建一个banner.txt

  • 七彩佛祖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
${AnsiColor.BRIGHT_GREEN}$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
${AnsiColor.BRIGHT_YELLOW}$$ _.ooOoo._ $$
${AnsiColor.BRIGHT_RED}$$ o888888888o $$
${AnsiColor.BRIGHT_CYAN}$$ 88" . "88 $$
${AnsiColor.BRIGHT_MAGENTA}$$ (| ^_^ |) $$
${AnsiColor.BRIGHT_GREEN}$$ O\ = /O $$
${AnsiColor.BRIGHT_RED}$$ ____/`-----'\____ $$
${AnsiColor.BRIGHT_CYAN}$$ .' \\| |$$ `. $$
${AnsiColor.BRIGHT_MAGENTA}$$ / \\||| : |||$$ \ $$
${AnsiColor.BRIGHT_GREEN}$$ / _||||| -:- |||||- \ $$
${AnsiColor.BRIGHT_YELLOW}$$ | | \\\ - $$/ | | $$
${AnsiColor.BRIGHT_GREEN}$$ | \_| ''\-----/'' | | $$
${AnsiColor.BRIGHT_YELLOW}$$ \ .-\___ `-` ____/-. / $$
${AnsiColor.BRIGHT_CYAN}$$ ___`. .' /--.--\ `. . ___ $$
${AnsiColor.BRIGHT_RED}$$ ."" '< `.____\_<|>_/____.' >'"". $$
${AnsiColor.BRIGHT_GREEN}$$ | | : `- \`.;`.\ _ /``;.`/ - ` : | | $$
${AnsiColor.BRIGHT_YELLOW}$$ \ \ `-. \_ ___\ /___ _/ .-` / / $$
${AnsiColor.BRIGHT_CYAN}$$ ========`-.____`-.____\_____/____.-`____.-'======== $$
${AnsiColor.BRIGHT_MAGENTA}$$ `=---=' $$
${AnsiColor.BRIGHT_YELLOW}$$ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ $$
${AnsiColor.BRIGHT_GREEN}$$ 佛祖保佑 永无BUG 永不修改 $$
${AnsiColor.BRIGHT_YELLOW}$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
${AnsiColor.BRIGHT_YELLOW}Spring Boot: ${spring-boot.formatted-version}

image-20230728202337762

3. SpringBoot自动装配原理

3.1 pom.xml

  1. 核心依赖在父工程中
  2. spring-boot-dependencies
  3. 我们在引入一些Springboot依赖的时候,不需要版本,就是因为有这些版本仓库

3.1.1 父工程的父工程中管理了大量的 jar

image-20230729145951389

3.1.2 父工程中配置了资源过滤

image-20230729150148686

3.2 启动器

1
2
3
4
5
<!-- 启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
  1. 启动器:SpringBoot的启动场景;
  2. 比如:Spring-boot-starter-web,会帮助我们自动导入web环境所有的依赖!
  3. 使用功能 -> 导入对应启动器(Developing with Spring Boot)

3.3 主程序

1
2
3
4
5
6
7
8
9
@SpringBootApplication      // 标注这个类是一个SpringBoot的应用(组合注解)
public class Springboot01HelloworldApplication {

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

}

3.3.1 注解

未命名文件

  • 所有的配置都是在启动的时候扫描并加载:类路径下/META-INF/spring.factories
    • 判断条件生效(ConditionalOnXXX)
    • 导入对应 start 就回存在对应的启动器 -> 自动装配生效
  • 整合Java2E ,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.2.5.RELEASE.jar 这个包下
    • 所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器

3.3.2 SpringApplication

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

img

4. YML 配置文件

4.1 yml语法及概述

4.1.1 配置文件

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

    • application.properties
      • 语法结构 :key=value
    • application.yml ==推荐==
      • 语法结构 :key: value
  • 配置文件作用

    • 修改SpringBoot自动配置的默认值

4.1.2 yaml概述

  • YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)
  • 这种语言以数据作为中心,而不是==以标记语言为重点==

4.1.3 yml语法

  • xml配置
1
2
3
<server>    
<port>8081<port>
</server>
  • yaml配置 ==简单==
1
2
server:
port: 8081

image-20230729163943323

4.2 yml实体类配置

4.2.1 yml配置实体类

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.bayyy.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 Integer age;
private Boolean happy;
private Date birth;
private Map<String, Object> maps;
private List<Object> lists;
private Dog dog;
}

image-20230729165003372

  • 解决方法:

    • ```xml
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
      
      </dependency>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24

      - `@configurationProperties`

      - 将配置文件中的每一个属性的值,映射到这个组件中
      - 告诉SpringBoot将类中的所有属性和配置文件中相关的配置进行绑定
      - 参数:`prefix “person” `:将配置文件中的person 下面的所有属性一一定义
      - 只有这个组件是容器中的组件,才能使用容器提供的`@ConfigurationProperties`功能

      ```yaml
      person:
      name: bayyy
      age: 18
      happy: true
      birth: 2023/7/29
      maps:
      k1: v1
      k2: v2
      lists:
      - code
      - music
      - girl
      dog:
      name: 旺财
      age: 3
  • 测试

1
2
3
4
5
6
7
@Autowired
private Person person;

@Test
void contextLoads() {
System.out.println(person);
}
  • 测试结果:image-20230729165638790

4.2.2 @PropertySource注解

用于加载指定的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
@PropertySource(value = "classpath:dog.properties")
public class Dog {
// SPEL表达式读取配置文件中的值
@Value("${dog.name}")
private String name;
@Value("${dog.age}")
private Integer age;
}
  • dog.properties
1
2
dog.name=旺财
dog.age=3

4.3 松散绑定

  • yml中
    • person_name —> personName
    • 可以有效的转换为驼峰命名法
  • @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
  • JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
  • 复杂类型封装,yml中可以封装对象 , 使用value就不支持

4.4 yml高级应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
person:
name: bayyy${random.uuid}
age: ${random.int}
happy: true
birth: 2023/7/29
maps:
k1: v1
k2: v2
lists:
- code
- music
- girl
dog:
name: ${person.invalid_key:doggy}_旺财 # 如果person.name不存在,则使用doggy
age: 3

4.5 JSR303数据校验 @Validated

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

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

可以写个注解让我们的name只能支持Email格式;

  • 同时可以修改错误消息

  • ```java
    @Email(message=”邮箱错误”)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67

    ### 4.5.1 空检查

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

    ### 4.5.2 Booelan检查

    - @AssertTrue 验证 Boolean 对象是否为 true

    - @AssertFalse 验证 Boolean 对象是否为 false

    ### 4.5.3 长度检查

    - @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
    - @Length(min=, max=) Validates that the annotated string is between min and max included.

    ### 4.5.4 日期检查

    - @Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
    - @Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
    - @Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。

    ### 4.5.5 数值检查

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

    - @Min 验证 Number 和 String 对象是否大等于指定的值
    - @Max 验证 Number 和 String 对象是否小等于指定的值
    - @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
    - @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
    - @Digits 验证 Number 和 String 的构成是否合法
    - @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
    - @Range(min=, max=) 被指定的元素必须在合适的范围内
    - @Range(min=10000,max=50000,message=”range.bean.wage”)
    - @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
    - @CreditCardNumber信用卡验证
    - @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
    - @ScriptAssert(lang= ,script=, alias=)
    - @URL(protocol=,host=, port=,regexp=, flags=)

    ## 4.6 多配置文件

    ### 4.6.1 优先级

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

    ![image-20230729175444128](https://s2.loli.net/2023/07/29/OcxvXagYAkE86Ni.png)

    ![image-20230729173854922](https://s2.loli.net/2023/07/29/ayTrl26Sfw3WRHx.png)

    ### 4.6.2 多配置文件

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

    ![image-20230729174141523](https://s2.loli.net/2023/07/29/U3FGREZ4Qywvukf.png)

    ```yaml
    # 比如在配置文件中指定使用dev环境
    spring:
    profiles:
    active: dev

4.6.3 单文件写多个配置

--- 可以区分多个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8080
spring:
profiles:
active: dev

---
server:
port: 8081
spring:
profiles: dev

---
server:
port: 8082
spring:
profiles: test

4.6.4 互补配置

1
2
3
server:
servlet
context-path: /bayyy

4.7 配置文件内容

4.7.1 配置内容

  • HttpEncodingAutoConfiguration(Http 编码自动配置) 为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@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;
}
//...
}
  • 从配置文件中获取指定值和bean的属性进行绑定
1
2
3
4
5
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}

image-20230729204840443

image-20230729204638424

4.7.2 @Conditional

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

  • 必须是指定条件成立,才给容器添加组件,配置中的内容才会生效

img

4.7.3 配置类输出

控制台打印自动配置报告

1
2
# 开启SpringBoot的调试类
debug: true
  • 测试结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------

AopAutoConfiguration matched:
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
...

Negative matches:
-----------------
...


Exclusions:
-----------

None


Unconditional classes:
----------------------
...
  • Positive matches:(自动配置类启用的:正匹配)
  • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
  • Unconditional classes: (没有条件的类)

5. web开发探究

5.1 静态资源映射规则-webjars

SpringMVC的web配置都在 WebMvcAutoConfiguration 配置类中

5.1.1 addResourceHandlers 添加资源处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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/** 都会对应到 classpath:/META-INF/resources/wewbjars/ 找对应的资源

5.1.2 wenjars

WebJars - Web Libraries in Jars

  • 可以通过 Maven 形式引入

image-20230729211250025

image-20230729211513597

image-20230729211630381

5.2 静态资源映射规则-classpath

5.2.1 默认静态资源路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};

  • 即以上路径下的所有文件都可以被访问到
  • 优先级顺序也是按照如上的书写顺序 resources>static>public

5.2.2 自定义静态资源路径

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

:warning: 自定义后,默认的自动配置失效

5.2.3 首页和图标

1)首页

1
2
3
4
5
6
7
8
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService,ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}

其中 getWelcomePage

1
2
3
4
5
6
7
8
9
10
11
12
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

  • 测试结果:image-20230729213938604

2)图标 ==已废弃==

静态资源目录下查找 favicon.ico

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

6. Thymeleaf模板引擎

6.1 模板引擎

前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。jsp支持非常强大的功能,包括能写Java代码

但是 SpringBoot 默认是不支持jsp的

  1. SpringBoot项目首先是以jar的方式,不是war
  2. 用的是嵌入式的Tomcat

SpringBoot推荐使用模板引擎:

  • jsp 就是一个模板引擎,还有 freemarker,包括Thymeleaf

  • img

作用

  • 将页面模板和数据进行组合,进行表达式解析、数据填充等操作

6.2 引入Thymeleaf

image-20230730214250236

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

6.3 Thymeleaf

  • Thymeleaf 自动配置类 - ThymeleafPropert
1
2
3
4
5
6
7
8
9
10
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";
}

:pushpin: 文件放置在 classpath:/templates/ 下,后缀为 .html

  • 测试

    1. TestController

      1
      2
      3
      4
      @RequestMapping("/test")
      public String test() {
      return "test";
      }
    2. test.html - ./templeates/test.html

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>Thymeleaf</title>
      </head>
      <body>
      <h1>模板引擎</h1>

      </body>
      </html>

6.4 基本语法

6.4.1 基本使用

  • Controller

    1
    2
    3
    4
    5
    @RequestMapping("/test")
    public String test(Model model) {
    model.addAttribute("msg", "Hello, SpringBoot");
    return "test";
    }
  • Html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
    </head>
    <body>
    <h1>模板引擎</h1>
    <!-- 所有的html元素都可以被 thymeleaf 解析 -->
    <!-- 通过th:text属性来指定标签体内容 -->
    <div th:text="${msg}"></div>
    </body>
    </html>
    1. 需要加入命名空间 xmlns:th="http://www.thymeleaf.org"
    2. 所有的html元素都可以被 thymeleaf 解析
    3. 通过 th:元素名 进行替换接管
  • 测试结果:image-20230730220150907

6.4.2 使用语法

使用任意的 th:attr 来替换Html中原生属性的值

img

6.4.3 表达式使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:#18
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.

3)、内置的一些工具对象:
      #execInfo : information about the template being processed.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any).
      #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
==================================================================================

Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
Fragment Expressions: ~{...}:片段引用表达式

Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…

Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|

Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -

Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not

Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )

Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)

Special tokens:
No-Operation: _

6.4.4 测试遍历

1
2
3
4
5
6
@RequestMapping("/test")
public String test(Model model) {
model.addAttribute("msg", "<h3>Hello, SpringBoot</h3>");
model.addAttribute("list", Arrays.asList("张三", "李四", "王五"));
return "test";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Thymeleaf</title>
</head>
<body>
<h1>模板引擎</h1>
<!-- 所有的html元素都可以被 thymeleaf 解析 -->
<!-- 通过th:text属性来指定标签体内容 -->
<!--转义-->
<div th:text="${msg}"></div>
<!--不转义-->
<div th:utext="${msg}"></div>

<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签-->
<h4 th:each="item :${list}" th:text="${item}"></h4>
<h4>
<!--行内写法-->
<span th:each="item:${list}">[[${item}]]</span>
</h4>
</body>
</html>

7. MVC自动配置原理

7.1 官网说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
// 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.

7.2 ContentNegotiatingViewResolver 内容协商视图解析器

7.2.1 源码分析

  • WebMvcAutoConfiguration -> ContentNegotiatingViewResolver
1
2
3
4
5
6
7
8
9
10
@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;
}
  • 视图解析代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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循环,解析
1
Iterator var5 = this.viewResolvers.iterator();

7.2.2 自己实现视图解析器

  • ./config/MyMvcConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 扩展 SpringMVC
@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;
}
}
}
  • 验证自己实现的视图解析器,在 DispatcherServlet 中的 doDispatch 方法 加断点 ==所有的请求都会在此处进行分发==

image-20230730222941405

7.3 转换器和格式化器

1
2
3
4
5
public FormattingConversionService mvcConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
addFormatters(conversionService);
return conversionService;
}
1
2
3
4
5
6
7
8
public String getDateFormat() {
return this.dateFormat;
}

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

image-20230730223621869

7.4 修改SpringBoot的默认配置

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;

1
2
3
4
5
6
7
8
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送 /bayyy 请求来到 success
registry.addViewController("/bayyy").setViewName("test");
}
}
  • 测试结果:image-20230730223958418

7.5 全面接管SpringMVC

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

  • 全面接管:SpringBoot对SpringMVC的自动配置清空

    • 其导入了一个类

      1
      @Import({DelegatingWebMvcConfiguration.class})public @interface EnableWebMvc {}
    • 继承父类

      1
      public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {  // ......}
    • 而最基础的Webmvc自动配置类中 ConditionalOnClass 注释说明仅在无指定组件时,自动配置类才会生效

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @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 {

      }

8. 员工管理系统

员工管理系统

9. 整合JDBC

9.1 SpringData简介

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

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

Sping Data 官网:Spring Data

9.2 简单实用

9.2.1 项目创建

image-20230801224723200

9.2.2 yaml配置文件(连接数据库)

1
2
3
4
5
6
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver

9.2.3 测试

  • ```java
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {

    // 查看一下默认的数据源: class com.zaxxer.hikari.HikariDataSource
    System.out.println(dataSource.getClass());
    // 获得数据库连接
    Connection connection = dataSource.getConnection();
    System.out.println(connection);
    connection.close();
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    - ![image-20230801225815471](https://s2.loli.net/2023/08/01/LugJ5n6rqfEY4A7.png)

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

    ### 9.2.4 源码

    - `DataSourceAutoConfiguration`

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

9.3 JDBCTemplate

  • 即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。数据库操作的所有 CRUD 方法都在 JdbcTemplate
  • Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中, 注入即可使用
  • JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration

9.3.1 主要方法

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

9.3.2 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@RestController
public class JDBCController {
/**
* Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
* JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
* 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
*/
@Autowired
JdbcTemplate jdbcTemplate;

//查询employee表中所有数据
@GetMapping("/list")
public List<Map<String, Object>> userList() {
String sql = "select * from user";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}

//新增一个用户
@GetMapping("/add")
public String addUser() {
//插入语句,注意时间问题
String sql = "insert into user(id, username)" + "values(6,'user3')";
jdbcTemplate.update(sql);
//查询
return "addOk";
}

//修改用户信息
@GetMapping("/update/{id}")
public String updateUser(@PathVariable("id") int id) {
//插入语句
System.out.println(id);
String sql = "update user set username=? where id=" + id;
System.out.println(sql);
//数据
String username = "user5";
jdbcTemplate.update(sql, username);
//查询
return "updateOk";
}

//删除用户
@GetMapping("/delete/{id}")
public String delUser(@PathVariable("id") int id) {
//插入语句
String sql = "delete from user where id=?";
jdbcTemplate.update(sql, id);
//查询
return "deleteOk";
}
}

9.3.3 指定数据源

  • spring.datasource.type

  • SpringBoot默认支持以下数据源:

    • com.zaxxer.hikari.HikariDataSource (Spring Boot 2.0 以上,默认使用此数据源)

    • org.apache.tomcat.jdbc.pool.DataSource

    • org.apache.commons.dbcp2.BasicDataSource

10. 整合Druid

10.1 简介

  • Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控;
  • Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
  • Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源
  • Github: https://github.com/alibaba/druid/

10.2 starter配置

apache中已经出了一套完美支持SpringBoot的方案

  1. 引入依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.17</version>
</dependency>
  1. 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
spring:
datasource:
# 数据源基本配置
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
# driver-class需要注意mysql驱动的版本(com.mysql.cj.jdbc.Driver 或 com.mysql.jdbc.Driver)
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# Druid的其他属性配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 连接池的最小空闲数量
min-idle: 5
# 连接池最大连接数量
max-active: 20
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
# 既作为检测的间隔时间又作为testWhileIdel执行的依据
time-between-eviction-runs-millis: 60000
# 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
min-evictable-idle-time-millis: 300000
# 用来检测数据库连接是否有效的sql 必须是一个查询语句(oracle中为 select 1 from dual)
validation-query: SELECT 1 FROM DUAL
# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
# 是否缓存preparedStatement, 也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。
pool-prepared-statements: true
# 置监控统计拦截的filters,去掉后监控界面sql无法统计,stat: 监控统计、Slf4j:日志记录、waLL: 防御sqL注入
filters: stat,wall,log4j
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: 20
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

  1. 配置StatViewServlet -> 监控信息展示html页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
datasource:
druid:
stat-view-servlet:
# 是否启用StatViewServlet默认值true
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台,默认为root
login-username: admin
login-password: 123456
# IP白名单
# allow: 127.0.0.1
# "" 或 null 表示所有
allow:
# IP黑名单(共同存在时,deny优先于allow)
deny:

  1. 配置监控
1
2
3
4
5
6
7
8
9
10
spring:
datasource:
druid:
web-stat-filter:
# 是否启用StatFilter默认值true
enabled: true
# 添加过滤规则
url-pattern: /*
# 忽略过滤的格式
exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico

10.3 基本配置

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

10.4 配置连接池

  1. 添加pom依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version>
</dependency>
  1. 切换数据源
1
2
3
4
5
6
7
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
  • 测试:

    • ```java
      @Autowired
      DataSource dataSource;

      @Test
      void contextLoads() throws SQLException {

      // 查看一下默认的数据源: class com.zaxxer.hikari.HikariDataSource
      System.out.println(dataSource.getClass());
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35

      - ![image-20230802220355487](https://s2.loli.net/2023/08/02/BJFyPhiZYab8svM.png)

      3. Druid配置

      ```yaml
      spring:
      datasource:
      username: root
      password: 123456
      url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
      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
  1. 导入log4j依赖
1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
  1. 绑定自定义全局配置参数,添加DruidDataSource组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DruidConfig.java
@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();
}
}

10.5 配置Druid数据源监控

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

  1. 设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// DruidConfig.java
// 配置 Druid 监控管理后台的Servlet;
// 内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean<StatViewServlet> 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"); //后台管理界面的登录密码

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

//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
  1. 访问 http://localhost:8080/druid 查看

image-20230802222237740

image-20230802222344034

  • 使用sql后查看

image-20230802222621839

  1. 配置Druid监控filter过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//配置 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;
}

11. 整合MyBatis

11.1 导入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>

11.2 测试数据库连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
DataSource dataSource;

@Test
void contextLoads() throws SQLException {
// 查看一下默认的数据源: class com.zaxxer.hikari.HikariDataSource
System.out.println(dataSource.getClass());
// 获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
System.out.println(connection.getMetaData().getURL());
connection.close();
}

image-20230802230749959

11.3 创建实体类

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String username;
}

11.4 配置Mapper接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//@Mapper : 表示本类是一个 MyBatis 的 Mapper,等价于以前 Spring 整合 MyBatis 时的 Mapper 接口
@Mapper
@Repository
public interface UserMapper {

//选择全部用户
List<User> selectUser();

//根据id选择用户
User selectUserById(int id);

//添加一个用户
int addUser(User user);

//修改一个用户
int updateUser(User user);

//根据id删除用户
int deleteUser(int id);
}

或者在启动文件中添加 MapperScan 注释

image-20230802231218061

11.5 Mapp映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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.bayyy.mapper.UserMapper">
<select id="selectUser" resultType="User">
select *
from user
</select>

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

<insert id="addUser" parameterType="User">
insert into user (id, username)
values (#{id}, #{name})
</insert>

<update id="updateUser" parameterType="User">
update user
set username=#{name}
where id = #{id}
</update>

<delete id="deleteUser" parameterType="int">
delete
from user
where id = #{id}
</delete>
</mapper>

11.6 maven配置资源过滤

1
2
3
4
5
6
7
8
9
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>

11.7 配置整合

1
2
3
4
5
6
7
8
mybatis:
#指定myBatis的核心配置文件与Mapper映射文件
mapper-locations: classpath:mapper/*.xml
# 注意:对应实体类的路径
type-aliases-package: com.bayyy.pojo
configuration:
# 开启驼峰命名
map-underscore-to-camel-case: true

11.8 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;

@GetMapping("/list")
public List<User> userList() {
List<User> users = userMapper.selectUser();
return users;
}

@GetMapping("/list/{id}")
public User userById(@PathVariable("id") int id) {
System.out.println("id = " + id);
return userMapper.selectUserById(id);
}

@GetMapping("/add")
public String addUser() {
System.out.println("add");
User user = new User();
user.setId(6);
user.setUsername("Bayyy");
int res = userMapper.addUser(user);
if (res > 0) {
return "add success";
} else {
return "add failed";
}
}

@GetMapping("/update/{id}")
public String update(@PathVariable("id") int id){
User user = new User();
user.setId(id);
user.setUsername("New_Bayyy");
int res = userMapper.updateUser(user);
if (res > 0) {
return "update success";
} else {
return "update failed";
}
}

@GetMapping("/delete/{id}")
public String delete(@PathVariable("id") int id){
int res = userMapper.deleteUser(id);
if (res > 0) {
return "delete success";
} else {
return "delete failed";
}
}


}
  • 测试结果:image-20230803214221397

12. SpringSecurity

18.1 简介

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

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

    • 用户认证(Authentication)
      • 验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统
      • 要求用户提供用户名和密码
    • 用户授权(Authorization)
      • 验证某个用户是否有权限执行某个操作
      • 系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限
  • SpringSecurity

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

18.2 基本使用

18.2.1 认识SpringSecurity

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

  • 三个基本类

    • WebSecurityConfigurerAdapter:自定义Security策略

    • AuthenticationManagerBuilder:自定义认证策略

    • @EnableWebSecurity:开启WebSecurity模式

  • “认证”(Authentication)

    • 身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
    • 身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
  • “授权” (Authorization)
    • 授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

18.2.2 基本使用

  1. 编写SpringSecurity配置类

参考官网:Spring Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SerurityConfig.java
@EnableWebSecurity
public class SecutityConfig 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();
}
}
  1. 认证规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("bayyy1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("bayyy2").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2")
.and()
.withUser("bayyy3").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
}

18.2.3 权限控制和注销

  1. 开启自动配置的注销的功能
1
2
3
4
5
6
7
8
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
  1. 修改注销后的跳转页面
1
2
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");

18.2.4 权限控制与前端显示结合

  • hymeleaf-security整合包
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 导入命名空间
1
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
  1. 导航栏增加认证判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--登录注销-->
<div class="right menu">

<!--如果未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/login}">
<i class="address card icon"></i> 登录
</a>
</div>

<!--如果已登录-->
<div sec:authorize="isAuthenticated()">
<a class="item">
<i class="address card icon"></i>
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>

<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
</div>
</div>

根据是否登录显示:1)登录按钮;2)用户名和角色

  • 测试结果:image-20230803230853260
  1. 请求改为post表单提交
1
2
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
  1. 根据角色显示模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<div class="ui three column stackable grid">
<!--动态菜单的效果-->
<div class="column">
<div class="ui raised segment" sec:authorize="hasRole('vip1')">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>

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

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

</div>
  • 测试结果:image-20230803232529431

18.2.5 记住用户名/密码

1
2
3
4
5
6
7
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//。。。。。。。。。。。
//记住我
http.rememberMe();
}
  • remember 创建了对应的cookie
  • 测试结果:image-20230803233137660

18.2.6 定制登录页

  1. 登录页面配置指定 loginpage
1
2
// SecutityConfig.java
http.formLogin().loginPage("/toLogin"); //定制登录页
  1. 前端页面指向自定义 login 请求
1
2
3
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
  1. 自定义页面登录信息的发送
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--如果要使用toLogin,则http.formLogin().loginPage("/toLogin");配置一致。
如果非要使用login,则http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");
-->
<form th:action="@{/login}" method="post">
<div class="field">
<label>工号</label>
<div class="ui left icon input">
<input type="text" placeholder="请输入工号..." name="ugh">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>密码</label>
<div class="ui left icon input">
<input type="password" placeholder="请输入密码..." name="upwd">
<i class="lock icon"></i>
</div>
</div>
<div class="field">
<input type="checkbox" name="remember">记住我
</div>
<input type="submit" class="ui blue submit button"/>
</form>
  1. 接收用户名、密码并指定登录表单提交请求
1
2
3
4
5
6
// SecurityConfig.java
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
  1. Remember Me 多选框
1
<input type="checkbox" name="remember"> 记住我
  1. 后端验证处理
1
http.rememberMe().rememberMeParameter("remember");

18.3 Controller配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@EnableWebSecurity
public class SecutityConfig 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();
http.logout().logoutSuccessUrl("/"); // 注销成功后来到首页
http.csrf().disable(); //关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.rememberMe().rememberMeParameter("remember"); //开启记住我功能
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin") // 自定义登录页面
.loginProcessingUrl("/login"); // 登录表单提交请求

}

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("bayyy1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("bayyy2").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2")
.and()
.withUser("bayyy3").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
}
}

13. Shiro

13.1 Shiro简介

13.1.1 概述

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

13.1.2 功能描述

image-20200729114647110

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

13.1.3 Shiro框架-外部

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

image-20200729114702566

  • 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

13.1.4 Shiro-内部

image-20200729114720578

  • 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提高了一些常见的加密组件用于如密码加密/解密。

13.2 简单使用

  1. 参照官网进行配置:shiro/samples/quickstart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Quickstart.java
public class Quickstart {

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


public static void main(String[] args) {

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

// 获取当前的用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();

// 通过当前用户拿到 Session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Subject -> Session" + value);
}

// 判断当前用户是否被认证
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象(token-基于用户名和密码的令牌)
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); // 记住我
try {
currentUser.login(token); // 执行登录操作
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

// 判断当前用户是否有某个角色
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!");
}

// 注销
currentUser.logout();

// 退出
System.exit(0);
}
}
  • 主要操作:

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取当前的用户对象 Subject
    Subject currentUser = SecurityUtils.getSubject();
    Session session = currentUser.getSession();
    currentUser.isAuthenticated()
    currentUser.getPrincipal()
    currentUser.hasRole("schwartz")
    currentUser.isPermitted("lightsaber:wield")
    currentUser.logout();

13.3 SpringBoot继承Shiro

image-20200729190548307

13.3.1 编写导入配置类

  • 导入依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
  • 自定义 UserRealm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义的 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;
}
}
  • 编写配置 ShiroConfig
    1. 创建realm对象,需要自定义类
    2. DefaultWebSecurityManager
    3. ShiroFilterFactoryBean -> 需要命名为 shiroFilterFactoryBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class ShiroConfig {
// ShiroFilterFactoryBean
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager getDefaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(getDefaultWebSecurityManager);
return bean;
}

// DefaultWebSecurityManager
@Bean(name = "getDefaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联 UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}

// 创建 realm 对象,需要自定义类
@Bean(name = "userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
}

13.3.2 登录拦截

  • getShiroFilterFactoryBean 方法中添加指定配置
    • anon: 无需认证就可以访问
    • authc: 必须认证了才能访问
    • user: 必须拥有记住我功能才能用
    • perms: 拥有对某个资源的权限才能访问
    • role: 拥有某个角色权限
1
2
3
4
5
6
7
8
9
10
11
12
/**
* anon: 无需认证就可以访问
* authc: 必须认证了才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
// 授权,正常情况下,没有授权会跳转到未授权页面
filterMap.put("/user/add", "authc");
filterMap.put("/user/update", "authc");
bean.setFilterChainDefinitionMap(filterMap);

此时 addupdate 页面无授权已无法访问

  • 添加拦截成功界面

    • 登录页面 login.html

      image-20230805161640071

    • getShiroFilterFactoryBean 设置登录请求

      1
      2
      3
      // 设置登录的请求
      bean.setLoginUrl("/toLogin");
      return bean;
  • 测试结果:image-20230805162135554

13.3.3 用户认证

  • 处理表单 - controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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";
}
}
  • UserRealm 认证 - doGetAuthenticationInfo
    • 用户名验证错误抛出 null,对应 UnknownAccountException
    • 密码验证是自动进行,返回给上一级即可
1
2
3
4
5
6
7
8
9
10
11
12
13
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证 doGetAuthenticationInfo");
// 用户名+密码
String username = "root";
String password = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
if (!userToken.getUsername().equals(username)) {
return null; // 抛出异常 UnknownAccountException: 用户名错误
}
return new SimpleAuthenticationInfo("", password, "");
}

13.4 Shiro整合Mybatis

13.4.1 基本流程

  1. 导入对应依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- 此阶段用的依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  1. application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
spring:
datasource:
# 数据源基本配置
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
filters: stat,wall,log4j
max-pool-prepared-statement-per-connection-size: 20
use-global-data-source-stat: true
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: 123456
allow:
deny:
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico
mybatis:
type-aliases-package: com.bayyy.pojo
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
  1. pojo.User + mapper.UserMapper + service.UserService

    :warning: 创建 mapper.xml 时模板位置需要修改namespace

    1
    <mapper namespace="com.bayyy.mapper.UserMapper">

image-20230805173240669

image-20230805173520673

  1. UserRealm -> doGetAuthenticationInfo() 连接数据库进行认证
1
2
3
4
5
6
7
8
9
10
11
12
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证 doGetAuthenticationInfo");
// 用户名+密码
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {
return null; // 抛出异常 UnknownAccountException: 用户名错误
}
return new SimpleAuthenticationInfo("", user.getPwd(), "");
}

13.4.2 加密认证

  • 默认是 SimpleCredentialsMatcher 加密认证

    image-20230805173752960

  • MD5 加密

  • MD5 盐值加密 -> 在普通MD 5加密基础上加入一些其他值,避免被简单的暴力尝试破解

13.5 用户授权

  1. ShiroConfiggetShiroFilterFactoryBean()
1
2
3
//授权,正常情况下,没有授权会跳转到为授权页面
filterMap.put("/user/add","perms[user:add]"); // 必须是user用户, 且有add权限
filterMap.put("/user/update","perms[user:update]");
  • 仅对 add 进行授权的添加,测试结果:image-20230805174611870
  1. 添加授权页面

    • controller

      1
      2
      3
      4
      5
      @RequestMapping("/unauthorized")
      @ResponseBody
      public String unauthorized() {
      return "未经授权! 无法访问此页面!";
      }
    • ShiroConfiggetShiroFilterFactoryBean()

      1
      2
      // 设置未授权的请求
      bean.setUnauthorizedUrl("/unauthorized");
    • 测试结果:image-20230805174941782

  2. 给用户授权

实际中用户权限位于数据库中

image-20230805180426492

  • doGetAuthenticationInfo -> 认证(也就是登录时)
  • doGetAuthorizationInfo -> 授权(访问权限资源,我们可以在认证时放入用户、密码和realmName),授权时就可以获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权 doGetAuthorizationInfo");
SimpleAuthorizationInfo auInfo = new SimpleAuthorizationInfo();
// 拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal(); // 拿到 User 对象

// 设置当前用户的权限
auInfo.addStringPermission(currentUser.getPerms());
return auInfo;
}

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证 doGetAuthenticationInfo");
// 用户名+密码
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
User user = userService.queryUserByName(userToken.getUsername());
if (user == null) {
return null; // 抛出异常 UnknownAccountException: 用户名错误
}


// 返回对象为 SimpleAuthenticationInfo,参数分别为:用户名,密码,realmName
return new SimpleAuthenticationInfo(user, user.getPwd(), "");

image-20230805180718893

  • 测试结果:

image-20230805181242115

13.6 logout

  • controller
1
2
3
4
5
6
7
@RequestMapping("/logout")
public String logout(Model model) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
model.addAttribute("msg", "用户已退出");
return "index";
}
  • 测试结果:image-20230805181802677

13.7 Shiro整合Thymeleaf

  1. 整合包依赖
1
2
3
4
5
6
<!-- 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>
  1. ShiroConfig 中添加整合 ShiroDialect
1
2
3
4
5
// 整合 ShiroDialect:用来整合 Shiro 和 thymeleaf
@Bean(name = "shiroDialect")
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
  1. 前端修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="update">
<a th:href="@{/user/update}">update</a>
</div>
<hr>
<div shiro:notAuthenticated=""> <!-- 未认证 -->
<a th:href="@{/toLogin}">login</a>
</div>
<div shiro:authenticated=""> <!-- 已认证 -->
<a th:href="@{/logout}">logout</a>
</div>
</body>
</html>
  • 测试结果:image-20230805183009364

14. Swagger

14.1 Swagger简介

  • 前后端分离
    • 前端 -> 前端控制层、视图层
    • 后端 -> 后端控制层、服务层、数据访问层
    • 前后端通过API进行交互
    • 前后端相对独立且松耦合
  • 前后端分离,无法及时沟通 —> 定义schema[计划的提纲],并实时跟踪最新的API,降低集成风险

  • Swagger

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

14.2 SpringBoot集成Swagger

14.2.1 项目创建

  1. 引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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>

:key: 3.0.0 之后需要额外的依赖

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
  • 另外需要额外的配置
1
2
3
4
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
  1. 基本测试

image-20230805194931903

  1. SwaggerConfig配置
1
2
3
4
5
// SwaggerConfig.java
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
  1. 访问测试

:key: Swagger3.0 后访问地址变为:Swagger UI

image-20230805200632555

14.2.2 配置Swagger

  1. Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger
1
2
3
4
5
6
7
// SwaggerConfig.java

// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2);
}
  1. 通过apiInfo()属性配置文档信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 配置Swagger的apiInfo
private ApiInfo apiInfo() {
// 源码中有默认的apiInfo,可以不用配置
/**
return 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<>());
*/
Contact contact = new Contact("bayyy", "http://www.bayyy.com", "bayyy@bayyy");
return new ApiInfo(
"Bayyy Swagger",
"Bayyy Swagger Api Documentation",
"1.1-beta",
"http://www.bayyy.com",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList<>());
}
  1. Docket 实例关联 apiInfo()
1
2
3
4
5
6
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
  • 结果结果:image-20230805223753316

14.3 配置扫描接口

14.3.1 select() 配置扫描

  • 构建Docket时通过select()方法配置怎么扫描接口

14.3.2 apis() 扫描类型

  • apis() 指定扫描的类型

    • any() 扫描全部
    • none()不扫描
    • withClassAnnotation() 扫描类上的注解,参数是一个注解的反射对象
    • withMethodAnnotation() 扫描方法上的注解
    • basePackage() 扫描指定包
  • 链式编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// RequestHandlerSelectors配置要扫描接口的方式
// basePackage指定要扫描的包
/**
* any() 扫描全部
* none() 不扫描
* withClassAnnotation() 扫描类上的注解,参数是一个注解的反射对象
* withMethodAnnotation() 扫描方法上的注解
* basePackage() 扫描指定包
*/
.apis(RequestHandlerSelectors.basePackage("com.bayyy.controller"))
.build();
}
  • 测试结果:image-20230805225028252

14.3.3 paths() 扫描过滤

  • 配置接口扫描过滤
    • ant() 过滤指定路径
    • any() 过滤全部
    • none() 不过滤
    • regex() 正则表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.bayyy.controller"))
/**
* ant() 过滤指定路径
* any() 过滤全部
* none() 不过滤
* regex() 正则表达式
*/
.paths(PathSelectors.ant("/bayyy/**"))
.build();
}
  • 测试结果:image-20230805224848896

14.4 配置Swagger开关

14.4.1 基本开关配置

  • enable()
1
2
3
4
5
6
7
8
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false) // 是否启用Swagger
.build(); // build() 创建Docket实例
}
  • 测试结果:image-20230805225317294

14.4.2 配置特定开发环境开关

image-20230805230057856

1
2
3
4
5
6
7
8
9
10
11
12
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket(Environment environment) {
// 设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev", "test");
// 判断当前是否处于该环境
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag) // 是否启用Swagger
.build(); // build() 创建Docket实例
}
  • 测试结果:image-20230805230329621

14.5 配置API分组

14.5.1 基本分组

没有配置分组,默认是default

  • groupName() 配置分组
1
2
3
4
5
6
7
8
// 配置Swagger2的Docket的bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("group1") // 配置分组
.build();
}
  • 测试结果:image-20230805231023115

14.5.2 多组存在

  • 配置多个 Docket() 即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...

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

@Bean
public Docket docket3() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("group3");
}
@Bean
public Docket docket4() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("group4");
}
  • 测试结果:image-20230805231241180

14.6 实体配置

:key: 只要出现在接口方法的返回值上的实体都会显示

  1. 创建实体类
1
2
3
4
5
6
7
8
9
@ApiModel("用户实体")
public class User {
@ApiModelProperty("用户id")
private int id;
@ApiModelProperty("用户名")
private String name;
@ApiModelProperty("用户密码")
private String pwd;
}
  1. 将实体类作为请求接口的返回值 (泛型),都能映射到实体项中
1
2
3
4
@RequestMapping("/user")
public User user() {
return new User();
}
  • 测试结果:image-20230805231641198

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

14.7 常用注释

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

Swagger注解 简单说明
@Api(tags = “xxx模块说明”) 作用在模块类上
@ApiOperation(“xxx接口说明”) 作用在接口方法上
@ApiModel(“xxxPOJO说明”) 作用在模型类上:如VO、BO
@ApiModelProperty(value = “xxx属性说明”,hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam(“xxx参数说明”) 作用在参数、方法和字段上,类似@ApiModelProperty
  • 示例:
1
2
3
4
5
6
@GetMapping("/test")
@ApiOperation("测试接口")
public String test(@ApiParam("测试输入数据") String text) {
text = text + "test";
return text;
}

image-20230805232604065

:white_check_mark: 正式环境部署前需要关闭Swagger

14.8 Swagger在线测试

image-20230805232711858

14.9 扩展:其他皮肤

  • 默认

    1
    2
    3
    4
    <dependency> 
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    </dependency>
  • bootstrap-ui

    1
    2
    3
    4
    5
    <!-- 引入swagger-bootstrap-ui包 /doc.html-->
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    </dependency>
  • Layui-ui

    1
    2
    3
    4
    5
    <!-- 引入swagger-ui-layer包 /docs.html-->
    <dependency>
    <groupId>com.github.caspar-chen</groupId>
    <artifactId>swagger-ui-layer</artifactId>
    </dependency>
  • mg-ui

    1
    2
    3
    4
    5
    <!-- 引入swagger-ui-layer包 /document.html-->
    <dependency>
    <groupId>com.zyplayer</groupId>
    <artifactId>swagger-mg-ui</artifactId>
    </dependency>

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

15.1 异步任务

15.1.1 耗时任务等待问题

  1. 创建耗时的测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
// AsyncService.java
@Service
public class AsyncService {

public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中....");
}
}
  1. AsyncController
1
2
3
4
5
6
7
8
9
10
@GetMapping("/async")
@ResponseBody
public String async(){
// 输出总用时
long start = System.currentTimeMillis();
asyncService.hello();
long end = System.currentTimeMillis();
String time = "总用时:" + (end - start) + "ms";
return time;
}
  • 测试结果:image-20230805235520018

15.1.2 异步任务

  1. @Async
1
2
3
4
5
6
7
8
9
@Async  // 告诉Spring这是一个异步方法
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("处理数据中...");
}
  1. EnableAsync
    • 需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableAsync // 开启异步注解功能
public class Springboot08TaskApplication {

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

}
  • 测试结果:image-20230805235821368

15.2 定时任务

15.2.1 相关类和注释

  • 异步执行任务调度

    • | 接口 | 注释 |
      | ———————- | ———————————————— |
      | TaskExecutor | @EnableScheduling (主程序添加) |
      | TaskScheduler | @Scheduled |

15.2.2 cron表达式

  • | 字段 | 允许值 | 允许的特殊字符 |
    | ——————————— | ——————————————————— | ————————————— |
    | 秒(Seconds) | 0~59的整数 | , - / 四个字符 |
    | 分(Minutes) | 0~59的整数 | , -
    / 四个字符 |
    | 小时(Hours) | 0~23的整数 | , - / 四个字符 |
    | 日期(DayofMonth) | 1~31的整数(但是你需要考虑你月的天数) | ,-
    ? / L W C 八个字符 |
    | 月份(Month) | 1~12的整数或者 JAN-DEC | , - / 四个字符 |
    | 星期(DayofWeek) | 1~7的整数或者 SUN-SAT (1=SUN) | , -
    ? / L C # 八个字符 |
    | 年(可选,留空)(Year) | 1970~2099 | , - * / 四个字符 |

  • | 特殊字符 | 代表含义 |
    | ———— | —————————————————————————————— |
    | | 任意 可用在所有字段中,表示对应时间域的每一个时刻,例如,在分钟字段时,表示“每分钟” |
    | ? | 日/星期冲突匹配 该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符 |
    | - | 区间 表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12 |
    | , | 枚举 表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五 |
    | / | 步长 x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用/y,它等同于0/y |
    | L | 最后 该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五 |
    | W | 工作日 该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五 |
    | LW | 在日期字段可以组合使用LW,它的意思是当月的最后一个工作日 |
    | C | 和calendr联系后计算过的值 该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天 |
    | # | *星期(4#2 - 第2个星期三)
    该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发 |

15.2.3 使用

  1. ScheduledService 添加 @Scheduled 注释
1
2
3
4
5
6
7
8
9
10
@Service
public class ScheduleService {
// 秒 分 时 日 月 周几
// 表示每天每时的49-50分钟,从0秒开始每隔5秒执行一次
@Scheduled(cron = "0/5 49-50 * * * *")
public void scheduleHello() {
System.out.println("====================");
System.out.println("Hello, Scheduling! "+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
  1. 主程序添加 @EnableScheduling 注释
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableScheduling // 开启定时任务注解功能
@EnableAsync // 开启异步注解功能
public class Springboot08TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot08TaskApplication.class, args);
}
}
  • 测试结果:image-20230806005122293

15.2.4 一些cron表达式

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

15.3 邮件任务

15.3.1 基本步骤

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

15.3.2 发送邮件

1)引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

其内部为:

1
2
3
4
5
6
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>1.6.7</version>
<scope>compile</scope>
</dependency>
  • 自动配置类

    • image-20230806000527220

    • 配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @ConfigurationProperties(prefix = "spring.mail")
      public class MailProperties {
      private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
      private String host;
      private Integer port;
      private String username;
      private String password;
      private String protocol = "smtp";
      private Charset defaultEncoding = DEFAULT_CHARSET;
      private Map<String, String> properties = new HashMap<>();
      private String jndiName;
      }
    • MailSenderJndiConfiguration

      • image-20230806000735225

2)配置文件

1
2
3
4
5
6
7
8
9
10
11
spring:
mail:
host: smtp.qq.com
username: 111@qq.com
password: xxxxxxxxxxxxxxxxx
# 由于QQ邮箱的SSL证书不是标准的,所以需要设置为true,否则会报错
properties:
mail:
smtp:
ssl:
enable: true

3)发送测试

  • 简单邮件

    • ```java
      @Autowired
      JavaMailSenderImpl mailSender;

      @Test
      void SimpleMail() {

      // 简单邮件
      SimpleMailMessage message = new SimpleMailMessage();
      message.setSubject("简单邮件测试");
      message.setText("这是一封简单邮件");
      message.setTo("111@qq.com");
      message.setFrom("111@qq.com");
      mailSender.send(message);
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27

      -

      - ![image-20230806001621865](https://s2.loli.net/2023/08/06/RgDvohyz8amMCYX.png)

      - 复杂邮件

      - ```java
      @Test
      void ComplexMail() throws MessagingException {
      MimeMessage mimeMessage = mailSender.createMimeMessage();
      MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);

      helper.setSubject("复杂邮件测试");
      // 第二个参数为true表示开启HTML
      helper.setText("<p style='color:red'>这是一封复杂邮件</p>", true);

      // attachmentFilename 表示附件名
      // File("文件路径") 表示附件路径
      // static目录下的文件可以直接访问
      helper.addAttachment("warning.jpg", new File("file_path"));

      helper.setTo("111@qq.com");
      helper.setFrom("111@qq.com");

      mailSender.send(mimeMessage);
      }
    • image-20230806002858598

16. 分布式Dubbo和Zookeeper

Dubbo:Apache Dubbo

Zookeeper:Apache ZooKeeper

16.1 分布式系统

  • 分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统

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

    • 建立在网络之上的软件系统

:key: 只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,才需要考虑分布式系统

16.2 Dubbo文档

在这里插入图片描述

16.2.1 单一应用架构 ORM

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

img

  • 适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
  • 缺点:
    1. 性能扩展比较难
    2. 协同开发问题
    3. 不利于升级维护

16.2.2 垂直应用架构 MVC

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

img

  • 通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
  • 缺点:公用模块无法重复利用,开发性的浪费

16.2.3 分布式服务架构 RPC

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

img

  • RPC(Remote Procedure Call)- 远程过程调用
    • 一种进程间通信方式,是一种技术的思想,而不是规范
    • 允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同
    • RPC就是要像调用本地的函数一样去调远程函数
  • 核心:通讯序列化
  • RPC基本原理
    • RPC基本原理
  • 步骤解析
    • img

16.2.4 流动计算架构 SOA

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

img

16.3 Dubbo简介

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

img

16.3.1 Dubbo基本概念

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

16.3.2 调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者
  2. 服务提供者在启动时,向注册中心注册自己提供的服
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

16.3.3 环境搭建 -> Zookeeper

ZooKeeper是一个集中式服务,用于维护配置信息、命名、提供分布式同步、提供组服务。所有这些类型的服务都以某种形式由分布式应用程序使用。每次实施它们时,都需要进行大量工作来修复不可避免的错误和竞争条件。由于实现此类服务很困难,应用程序最初通常会忽略它们,这使得它们在发生变化时变得脆弱并且难以管理。即使正确完成,这些服务的不同实现也会导致部署应用程序时的管理复杂

  • 下载:Apache ZooKeeper

  • 运行:/bin/zkServer.cmd

    • 没有zoo.cfg配置文件会闪退

      image-20230806232224669

  • 修改 zoo.cfg 配置文件

    image-20230806232404035

    • 最新版本需要添加 audit.enable=true
  • 重要位置

    • dataDir=./ 临时数据存储的目录(可写相对路径)
    • clientPort=2181 zookeeper的端口号
  • 再次启动,使用 /bin/zkCli.cmd 进行测试

    • image-20230806233019448

16.3.4 基本使用

  • 打开 Server端和Client端
  • ls / 列出zookeeper根下保存的所有节点
    • image-20230806233319173
  • create –e /bayyy 123 创建一个bayyy节点,值为123
    • image-20230806233409696
  • get /bayyy 获取bayyy节点的值
    • image-20230806233446893

16.3.5 安装dubbo-admin

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

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

  • Github:apache/dubbo-admin at master-0.2.0 (github.com)
    • image-20230806234030777
  • 修改配置

    • 修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址
      • 如果 Zookeeper 未做修改则无需修改
    • image-20230806234102219
  • 项目目录下打包dubbo-admin

    • mvn clean package -Dmaven.test.skip=true
    • image-20230806234234425
    • image-20230806234400663
    • image-20230806234423001
  • 执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar

    • 此时 Zookeeper服务需要打开
    • java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
      • 运行会报错
      • image-20230806235619527
    • 添加 --add-opens java.base/java.lang=ALL-UNNAMED
      • 由于 JDK 8 中有关反射相关的功能自从 JDK 9 开始就已经被限制了,为了兼容原先的版本,需要在运行项目时添加 --add-opens java.base/java.lang=ALL-UNNAMED 选项来开启这种默认不被允许的行为
    • java -jar --add-opens java.base/java.lang=ALL-UNNAMED dubbo-admin-0.0.1-SNAPSHOT.jar
  • 访问 http://localhost:7001/

    • 默认用户名/密码 均为 root
    • image-20230806235844294

image-20230806235935759

16.4 SpringBoot+Dubbo+Zookeeper

16.4.1 项目搭建

  • 创建两个模块分别为

    • provider-server
    • consumer-server
  • provider

    • ```java
      public class TicketServiceImpl implements TicketService{
      @Override
      public String getTicket() {
          return "《厉害了,我的国》";
      }
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      - consumer

      ### 16.4.2 服务提供者

      1. 导入依赖

      ```xml
      <!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>3.2.4</version>
      </dependency>
      <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
      <dependency>
      <groupId>com.github.sgroschupf</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.1</version>
      </dependency>
  1. 新版本需要解决日志冲突,以及提出日志依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!-- 引入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>
  2. springboot配置dubbo相关属性

    1
    2
    3
    4
    5
    6
    #当前应用名字
    dubbo.application.name=provider-server
    #注册中心地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    #扫描指定包下服务
    dubbo.scan.base-packages=com.bayyy.provider.service
  3. service的实现类中配置服务注解,发布服务

    导包要是 org.apache.dubbo.config.annotation.Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.apache.dubbo.config.annotation.Service;
    import org.springframework.stereotype.Component;

    @Component // 放在容器中,不要用原来的Service,因为会和dubbo的Service重名,容易误操
    @Service // 将服务发布
    public class TicketServiceImpl implements TicketService{
    @Override
    public String getTicket() {
    return "《厉害了,我的国》";
    }
    }

16.4.3 服务消费者

  1. 导入依赖(同上)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.2.4</version>
    </dependency>
    <!-- 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>
  2. 配置参数 - 表明从何处哪服务,以及自己的身份

    1
    2
    3
    4
    # 当前应用名字
    dubbo.application.name=consumer-server
    # 注册中心地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181

    运行发现需要在启动类上增加@EnableDubbo 注解

  3. 启动 Dubbo + Zookeeper + SpringBoot-provider

    image-20230807004217918

    image-20230807004227089

  4. 将服务接口复制,需要保证路径正确,即与服务提供者相同

    image-20230807004849645

  5. 服务者消费类 @Reference

    此处可以使用正常SpringBoot的 @Service 注解进行容器注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import org.apache.dubbo.config.annotation.Reference;
    import org.springframework.stereotype.Service;

    @Service //注入到容器中
    public class UserService {

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

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

    }
  6. 测试

    1
    2
    3
    4
    5
    6
    7
    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
    userService.buyTicket();
    }
  • 测试结果:image-20230807005143057
  1. dubbo-admin

image-20230807005449024

image-20230807005512758