Singerw's Repository Singerw's Repository
首页
  • 相关文章

    • HTML相关文章
    • CSS相关文章
    • JavaScript相关文章
  • 学习笔记

    • JavaScript笔记
    • ES6笔记
    • Vue笔记
  • 相关文章

    • Spring相关文章
    • SpringBoot相关文章
    • MyBatis相关文章
    • MySQL相关文章
  • 学习笔记

    • SpringBoot笔记
    • Spring笔记
    • MyBatis笔记
    • MySQL笔记
    • JavaWeb笔记
    • JavaCore笔记
  • 学习笔记

    • Linux笔记
    • Git笔记
    • 技术文档
  • 偏门技术

    • GitHub技巧
    • 博客搭建
    • 科学上网
  • 安装教程

    • JDK
    • MySQL
    • Node.js
    • Linux
  • 终身学习
  • 面试人生
  • 心情杂货
  • 生活随笔
  • 归档
  • 标签
GitHub (opens new window)

Singerw

谁能够凭爱意将富士山私有
首页
  • 相关文章

    • HTML相关文章
    • CSS相关文章
    • JavaScript相关文章
  • 学习笔记

    • JavaScript笔记
    • ES6笔记
    • Vue笔记
  • 相关文章

    • Spring相关文章
    • SpringBoot相关文章
    • MyBatis相关文章
    • MySQL相关文章
  • 学习笔记

    • SpringBoot笔记
    • Spring笔记
    • MyBatis笔记
    • MySQL笔记
    • JavaWeb笔记
    • JavaCore笔记
  • 学习笔记

    • Linux笔记
    • Git笔记
    • 技术文档
  • 偏门技术

    • GitHub技巧
    • 博客搭建
    • 科学上网
  • 安装教程

    • JDK
    • MySQL
    • Node.js
    • Linux
  • 终身学习
  • 面试人生
  • 心情杂货
  • 生活随笔
  • 归档
  • 标签
GitHub (opens new window)
  • SpringBoot学习笔记
    • 1、SpringBoot主要优点
    • 2、SpringBoot自动生成的文件解释
    • 1、SpringBootApplication
    • 1、application.properties配置多环境
    • 2、application.yml配置多环境
    • 步骤一:导入依赖
    • 步骤二:配置文件
    • 步骤三:设置自动编译和允许运行时重新自动构建
    • 1、新建一个SpringBoot项目
    • 2、基本的配置设置
      • 配置MyBatis第一种情况
      • 配置MyBatis第二种情况
      • application.yml文件示例
      • pom.xml文件示例
    • 步骤一、导入jar包
    • 步骤二、配置SpringBoot文件
    • 步骤三、使用
    • 1、application.yml和druidConfig.java配置Druid
      • 步骤一:引入相关依赖
      • 步骤二:application.yml中切换数据源
      • 步骤三:application.yml中设置数据源相关配置
      • 步骤四:配置 DruidConfig
      • 步骤五:访问Druid监控
    • 2、application.yml配置Druid
      • 步骤一:引入相关依赖
      • 步骤二:application.yml中配置数据源
      • 步骤三:添加servlet支持
      • 步骤四:访问Druid监控
    • 1、整合Swagger3接口文档
      • 步骤一:引入Swagger3依赖
      • 步骤二:Application启动类上面加入@EnableOpenApi注解
      • 步骤三:配置Swagger3Config.java
      • 步骤四:访问
      • Swagger3自定义Ui
    • 2、整合Swagger2接口文档
      • 步骤一:导入依赖
      • 步骤三:访问
    • 3、Swagger常用注解的使用说明
    • 4、Controller层的配置示例
    • 1、配置Thymeleaf
      • 步骤一:导入thymeleaf依赖
      • 步骤二:在yml中配置thymeleaf
      • 步骤三:处理静态资源
    • 2、常用th属性解读
    • 3、标准表达式语法
    • 1、导入相关依赖并配置文件
    • 2、JPA使用
      • 步骤一:新建实体类并添加JPA注解
      • 步骤二:新建接口ArticleDao
      • 步骤三:测试
    • 3、JPA查询方法命令规范
    • 4、JPQL语法生成
    • 5、JPA URUD示例
    • 6、JPA实现分页和模糊查询
    • 1、Redis是什么?
    • 2、Redis 的一些优点
    • 3、Redis 5种数据结构类型
    • 4、Redis的基本使用
      • 步骤一: 新建springboot项目
      • 步骤二:配置yml文件
      • 步骤三:新建实体类添加JPA注解
      • 步骤四:新建Dao接口
      • 步骤五:1.1 编写服务类接口和实现
      • 步骤六:启动类添加@EnableCaching注解
      • 步骤七:启动测试
    • 5、Redis常用注解小结
      • @CacheConfig
      • @Cacheable
      • @CachePut
    • 6、编写压力测试类
      • 步骤一:引入依赖
      • 步骤二:编写测试类
    • 1、乐观锁和悲观锁的适用场景:
    • 2、悲观锁与其原理
    • 3、乐观锁与其原理
    • 4、悲观锁实现
      • 步骤一:mapper.xml文件
      • 步骤二:Service接口
      • 步骤三:service实现类
      • 步骤四:控制器
      • 步骤五:访问进行测试
      • 总结
    • 5、乐观锁实现
      • 步骤一:添加列和完善实体类
      • 步骤二:mapper.xml
      • 步骤三:Service接口
      • 步骤四:ServiceImpl实现类
      • 步骤五:控制器
      • 步骤六:访问进行测试
      • 总结
    • 1、特征
  • SpringBoot优点与简单介绍
  • SpringBoot源码简单解析
  • 自定义banner
  • 配置文件存储位置分析
  • 多环境配置及配置文件位置
  • 配置热部署
  • SpringBoot自动配置原理
  • SpringBoot整合MyBatis
  • 集成PageHelper分页插件
  • 集成logback日志
  • 整合Druid数据源
  • 整合Swagger接口文档
  • Thymeleaf模板引擎使用
  • JPA的使用
  • JPA+Redis
  • Maven资源导出问题终极版
  • SpringBoot锁 -Mybatis
  • 《SpringBoot》学习笔记
Singerw
2021-09-25

SpringBoot学习笔记

# SpringBoot学习笔记

​ Spring的组织对对spring框架的全系列组件进行了内部封装。对外只是提供maven(jar管理、项目打包工具)或者gradle(新兴jar管理、项目打包工具)的形式来进行引入parent.pom(maven配置文件)或者parent.gradle(gradle配置文件),让每一个spring项目都是以spring的子项目的形式来运行,这样开发人员不用再去注重配置文件的繁琐而是把精力放到业务逻辑以及更深层次的架构方面。自此SpringBoot就诞生了,它有着纯正的开源血统,在此非常感谢spring开源组织给我们java开发人员带来的便利!

# 一、SpringBoot优点与简单介绍

# 1、SpringBoot主要优点

  1. 为所有Spring开发者更快的入门

  2. 开箱即用,提供各种默认配置来简化项目配置,帮助开发者快速整合第三方框架(原理maven依赖特性)

  3. 内嵌式容器【web容器 tomcat】简化Web项目,完全不需要第三方服务器即可运行, 内置了第三方容器(tomcat/jetty/undertom) (原理:tomcat容器使用java开发的)

  4. 没有冗余代码生成和XML配置的要求,使用注解的方式来简化xml书写

  5. 提供一系列大型企业级项目的功能性特性(比如:安全、健康检测、外部化配置、数据库访问、restful搭建等)

# 2、SpringBoot自动生成的文件解释

Application类

Application该类是程序的入口类内有个main方法,可以直接通过run as运行项目

其中的@RestController是@ResponseBody +@Controller

@GetMapping("test")

@GetMapping("test") 就等于@RequestMapping(value="test",method = RequestMethod.Get)

@SpringBootApplication注解

它是声明当前类为springboot的入口类。而一个springboot项目内有且只能有一个这个注解存在。

application.properties

application.properties 该配置文件是项目的核心配置文件,以spring.mvc.view.xx=xx的形式存在,也可以使用yaml得文件形式进行配置。如:application.yaml

# 二、SpringBoot源码简单解析

spring-boot-starter-web :帮我们导入了web模块正常运行所依赖的组件;

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

出厂默认就写好了很多starter,如:

  • spring-boot-starter-activemq
  • spring-boot-starter-aop
  • spring-boot-starter-data-redis
  • spring-boot-starter-data-solr

重要提示:Spring Boot将所有的绝大部分框架整合场景都进行了抽取,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关整合所需的依赖都会导入进来。

# 1、SpringBootApplication

@SpringBootApplication用于标识spring boot应用程序,代表该类是一个spring boot启动类

Spring boot运行这个类的main方法时启动SpringBoot应用。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
1
2
3
4
5
6
7
8
9
10
  • @SpringBootConfiguration: Spring Boot的配置类。标注在类上表示是一个Spring Boot的配置类.

  • @Configuration:配置类上来标注这个注解。配置类相当于配置文件。配置类也是容器中的一个组件。@Component把组件实例化到spring容器中。

  • @EnableAutoConfiguration:开启自动配置功能;当我们需要Spring Boot帮我们自动配置所需要的配置,@EnableAutoConfiguration告诉Spring Boot开启自动配置功能,这样Spring Boot会自动配置好并使之生效。

  • @AutoConfigurationPackage:自动配置包

  • @Import(AutoConfigurationPackages.Registrar.class)

    • Spring的底层注解@Import,给容器中导入一个组件。导入的组件由AutoConfigurationPackages.Registrar.class。将主配置类(标注@SpringBootApplication注解的类)的所在目录的包及下面所有子包里面的所有组件扫描到Spring容器。
  • @Import(EnableAutoConfigurationImportSelector.class) 给容器中导入组件。

  • EnableAutoConfigurationImportSelector:组件的选择器。

    • 将所有需要导入的组件以全类名的方式返回,这些组件就会被添加到容器中。 组件的选择器给容器中导入非常多的自动配置类(xxxAutoConfiguration),给容器中导入这个场景需要的所有组件,并配置好这些组件。

# 三、SpringBoot自定义banner

在Spring Boot工程的/src/main/resources目录下创建一个banner.txt文件,然后将ASCII字符画复制进去,就能替换默认的banner了。

${AnsiColor.BRIGHT_GREEN}
              ,--,
            ,--.'|         ,---,                        __  ,-.      .---. 
  .--.--.   |  |,      ,-+-. /  |  ,----._,.          ,' ,'/ /|     /. ./| 
 /  /    '  `--'_     ,--.'|'   | /   /  ' /   ,---.  '  | |' |  .-'-. ' | 
|  :  /`./  ,' ,'|   |   |  ,"' ||   :     |  /     \ |  |   ,' /___/ \: | 
|  :  ;_    '  | |   |   | /  | ||   | .\  . /    /  |'  :  /.-'.. '   ' . 
 \  \    `. |  | :   |   | |  | |.   ; ';  |.    ' / ||  | '/___/ \:     ' 
  `----.   \'  : |__ |   | |  |/ '   .   . |'   ;   /|;  : |.   \  ' .\    
 /  /`--'  /|  | '.'||   | |--'   `---`-'| |'   |  / ||  , ; \   \   ' \ | 
'--'.     / ;  :    ;|   |/       .'__/\_: ||   :    | ---'   \   \  |--"  
  `--'---'  |  ,   / '---'        |   :    : \   \  /          \   \ |     
             ---`-'                \   \  /   `----'            '---"      
                                    `--`-'                              
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 四、application.properties配置文件存储位置分析

application.properties文件放在src/main/resource目录下,默认的存储位置;

实际上,这个配置文件可以在其他位置出现,在其他位置出现可以进行多环境的配置,或者从外部添加配置,这里采用application.yml举例:

官方文档说明,application.properties或者application.yml可以在其他位置出现,官方文档给出如下四个位置,以执行优先级排序为:

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

# 五、SpringBoot多环境配置及配置文件位置

# 1、application.properties配置多环境

application.properties

#SpringBoot多环境配置,可以选择激活哪一个配置文件
spring.profiles.active=dev
1
2

application-dev.properties

server.port=8081
1

application-test.properties

server.port=8082
1

# 2、application.yml配置多环境

在Spring Boot中多环境配置文件名需要满足application-{profile}.yml的格式,其中{profile}对应的环境标识;

  • application-dev.yml开发环境

  • application-test.yml测试环境

  • application-prod.yml生产环境

如果我们要激活某一个环境,只需要在 application.yml里:

spring:
  profiles:
    active: dev
1
2
3

假设配置一些基本设置如:

application-dev.yml开发环境

server:
  port: 8080
1
2

application-test.yml测试环境

server:
  port: 8081
1
2

application-prod.yml生产环境

server:
  port: 8082
1
2

此时,当我们去修改application.yml:

spring:
  profiles:
    active: test
1
2
3

此时就是8081测试环境运行程序

这些也可以写在一个yaml文件中,如下:

server:
  port: 8080
spring:
  profiles:
    active: dev

---
server:
  port: 8081
spring:
  config:
    activate:
      on-profile: dev

---
server:
  port: 8082
spring:
  config:
    activate:
      on-profile: prod

---
server:
  port: 8083
spring:
  config:
    activate:
      on-profile: 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
25
26
27
28
29

# 六、SpringBoot配置热部署

# 步骤一:导入依赖

通过修改pom文件或者向导中指定支持devtool:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
1
2
3
4
5
6

# 步骤二:配置文件

spring: 
  ##热部署工具
  devtolls:
    restart:
      enabled: true  # 开启热部署
      additional-paths: src/main/java #重启目录
      exclude: templates/**  #classpath目录下的templates文件夹内容修改不重启
1
2
3
4
5
6
7

# 步骤三:设置自动编译和允许运行时重新自动构建

允许运行时重新自动构建(划重点,这是新版调整过的,之前改registry配置,2021版intellij已经取消了)

# 七、SpringBoot自动配置原理

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

XXXAutoConfigurartion:自动装配类,给容器中添加组件

XXXProperties:分装配置文件中相关属性。

# 八、SpringBoot整合MyBatis

# 1、新建一个SpringBoot项目

# 2、基本的配置设置

# 配置MyBatis第一种情况

步骤一:

对于整合mybatis框架,我们需要在SpringBoot配置文件application.yaml中对其进行配置:

server:
  port: 8080
  
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/joint_force?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 795200

mybatis:
  #定义别名包
  type-aliases-package: com.singerw.pojo
  #添加xml文件的依赖
  mapper-locations: classpath:/mybatis/mappers/*.xml
  #开启驼峰映射规则
  configuration:
    map-underscore-to-camel-case: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 定义别名包(type-aliases-package)

    将实体对象pojo路径进行了封装,若在mapper.xml文件中,resulrType不写包名,只写类名,就会自动执行别名包进行拼接;若写了包名+类名,就执行自己写好的;

  2. 第二项mapper-locations是总用的,配置mapper.xml映射文件的路径

  3. 驼峰映射规则(map-underscore-to-camel-case)

    开启驼峰映射规则后,底层会根据切分将数据库用"_"连接的字段名切分组合成由java驼峰规则的命名; 目的:主要为了简化mybatis映射的过程; 规则:user_id-->去除下划线userid-->之后首字母大写userId-->之后属性名一致,就可以正常映射了 注意:如果开启了驼峰规则,必须要满足条件.

步骤二:

​ 我们平常整合mybatis在业务中编写Dao层时,我们通常会在数据层的接口上添加@Mapper注解,让其交由mybatis管理,通过其方法映射的SQL语句来操作数据库;

@Mapper
public interface ArticleMapper {

    List<Article> getArticleByID(Integer aID);

    List<Article> getArticleList();
}
1
2
3
4
5
6
7
  • @mapper或者@repository注解在dao层的应用;
  • @Mapper是mybatis自身带的注解; 使用@mapper后,不需要在spring配置中设置扫描地址,通过mapper.xml里面的namespace属性对应相关的mapper类,spring将动态的生成Bean后注入到Service层。
  • @repository则需要在Spring中配置扫描包地址,然后生成dao层的bean,之后被注入到ServiceImpl中。

也就是说,如果用了@repository,我们的测试类上(程序启动入口)需要加入。

@SpringBootTest
@MapperScan("com.singerw.dao")
class ApplicationTests {

    @Autowired
    private ArticleDao articleDao;

    @Test
    void getArticleTest() {
        List<Article> article = articleDao.getArticleByID(1);
        System.out.println(article);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 配置MyBatis第二种情况

也可以通过在启动类上添加@MapperScan(Mapper接口路径),利用包扫描形式为接口创建代理对象

@SpringBootApplication
@MapperScan("com.singerw.dao")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
1
2
3
4
5
6
7
8

需要注意的是:

在springboot程序入口类处添加注解@MapperScan(“com.singerw.mapper或者dao”),如果程序入口类处没有添加注解@MapperScan,每个dao层接口要添加注解@Mapper。

public interface ArticleDao {

    List<Article> getArticleByID(Integer aID);

    List<Article> getArticleList();

}
1
2
3
4
5
6
7
mybatis:
  #定义别名包
  type-aliases-package: com.singerw.pojo
  #添加xml文件的依赖
  mapper-locations: classpath:/mybatis/mappers/*.xml
  #开启驼峰映射规则
  configuration:
    map-underscore-to-camel-case: true
1
2
3
4
5
6
7
8

# application.yml文件示例

在SpringBoot配置文件application.yaml配置数据库连接,项目端口等信息

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/joint_force?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 795200

mybatis:
  type-aliases-package: com.singerw.pojo
  mapper-locations: classpath*:/mapping/*.xml
  configuration:
    map-underscore-to-camel-case: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# pom.xml文件示例

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!--mybatis-spring的启动器-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    
    <!--数据库驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
</dependencies>
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

# 九、SpringBoot集成PageHelper分页插件

# 步骤一、导入jar包

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.5</version>
</dependency>
1
2
3
4
5

# 步骤二、配置SpringBoot文件

在application.yaml配置文件中添加以下配置:

#pagehelper
pagehelper:
    helperDialect: mysql
    reasonable: true
    supportMethodsArguments: true
    params: count=countSql
    returnPageInfo: check
1
2
3
4
5
6
7
  • pagehelper.helper-dialect : 指定分页插件使用哪种语言
  • pagehelper.offset-as-page-num : 默认为 false, 该参数对使用RowBounds作为分页参数时有效,当为true时,会将RowBounds的offset参数当成pageNum使用
  • pagehelper.row-bounds-with-count : 默认为false,该参数对使用RowBounds作为分页参数时有效,当该参数值为true时,使用RowBounds分页会进行count查询
  • pagehelper.page-size-zero : 默认为false,当该参数为true时,如果pageSize=0或者RowBounds.limit=0就会查询所有结果
  • pagehelper.reasonable : 分页合理化参数,默认为false,当该值为true,pageNum<=0默认查询第一页,pageNum>pages时会查询最后一页,false时直接根据参数进行查询
  • pagehelper.params : 为了支持startPage(Object params)方法,增加该参数来配置参数映射,用于从对象中根据属性名取值,可以配置pageNum,pageSize,pageSizeZero, reasonable, 不配置映射是使用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
  • pagehelper.support-methods-arguments : 支持通过 Mapper接口参数来传递分页参数,默认为false, 分页插件会从查询方法的参数中根据params配置的字段中取值,查找到合适的就进行分页
  • pagehelper.auto-runtime-dialect : 默认为false, 为true时允许在运行时根据多数据源自动识别对应的方言进行分页
  • pagehelper.close-conn : 默认为true, 当使用运行是动态数据源或者没有设置helperDialect属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认为true关闭,false不会自动关闭

# 步骤三、使用

@Mapper
public interface ArticleDao {

    List<Article> getArticleList();
}
1
2
3
4
5
<?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.singerw.dao.ArticleDao">

    <select id="getArticleList" resultMap="articleMap">
    select *
    from article
    </select>


    <resultMap id="articleMap" type="Article">
    <id property="aID" column="a_id"/>
    <result property="articleTitle" column="article_title"/>
    <result property="articleContent" column="article_content"/>
    <result property="headImage" column="head_image"/>
    <result property="articleAuthor" column="article_author"/>
    <result property="typeNumber" column="type_number"/>
    <result property="pageviews" column="pageviews"/>
    <result property="createTime" column="create_time"/>
    <result property="isState" column="is_state"/>
    </resultMap>

    </mapper>
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
public interface ArticleDaoService {
    
    ResponseData<Article> getArticleList(Integer page,Integer limit);
}
1
2
3
4
@Service
public class ArticleDaoServiceImpl implements ArticleDaoService {

    @Autowired
    private ArticleDao articleDao;

    @Override
    public ResponseData<Article> getArticleList(Integer page, Integer limit) {
        PageHelper.startPage(page, limit);
        List<Article> articleList = articleDao.getArticleList();
        PageInfo<Article> pageInfo = new PageInfo(articleList);
        ResponseData<Article> responseData = new ResponseData(0, "success", pageInfo.getTotal(), articleList);
        return responseData;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api")
public class ArticleController {

    @Autowired
    private ArticleDaoService articleDaoService;

    @GetMapping("/artList")
    public ResponseData<Article> getArticleList(
        @RequestParam(name = "page", defaultValue = "1") Integer page,
        @RequestParam(name = "limit", defaultValue = "10") Integer limit) {
        ResponseData<Article> responseData = articleDaoService.getArticleList(page, limit);
        return responseData;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 十、SpringBoot集成logback日志

logback是由log4j创始人设计的又一个开源日志组件,当前分为三个模块:logback-core,logback-classic和logback-access。

  • logback-core:其它两个模块的基础模块
  • logback-classic:是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统
  • log4j或JDK14 Logging
  • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

依赖

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

从依赖树上可以看到,spring-boot-starter 已经引入了 spring-boot-start-loggin。spring boot已经集成logback日志,在application.yaml配置日志和日志输出级别:

<?xml version="1.0" encoding="UTF-8"?>
<!--
    scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
    scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
    debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
    configuration 子节点为 appender、logger、root
-->
<configuration scan="true" scanPeriod="15 seconds" debug="false">

    <!--用于区分不同应用程序的记录-->
    <contextName>logback</contextName>

    <property resource="application.yml"/>
    <springProperty scope="context" name="SERVER_PORT" source="server.port"/>
    <springProperty scope="context" name="LOG_NAME" source="spring.application.name"/>

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式   [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n  -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--控制台-->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger输出日志的logger名 %msg:日志消息,%n是换行符 -->
            <!--            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg%n</pattern>-->
            <pattern>${CONSOLE_LOG_PATTERN}</pattern><!--彩打日志-->
            <!--解决乱码问题-->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--滚动文件-->
    <appender name="infoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/tmp/logs/${LOG_NAME}/info/logback.info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!--            <maxFileSize>100MB</maxFileSize>&lt;!&ndash;单个日志文件最大100M,到了这个值,就会再创建一个日志文件,日志文件的名字最后+1&ndash;&gt;-->
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
            <maxFileSize>50MB</maxFileSize><!--单个文件的最大大小-->
            <totalSizeCap>20GB</totalSizeCap><!--所有的日志文件最大20G,超过就会删除旧的日志-->
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg%n</pattern>
        </encoder>
    </appender>

    <!--滚动文件-->
    <appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>error</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/tmp/logs/${LOG_NAME}/error/logback.error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!--            <maxFileSize>100MB</maxFileSize>&lt;!&ndash;单个日志文件最大100M,到了这个值,就会再创建一个日志文件,日志文件的名字最后+1&ndash;&gt;-->
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
            <maxFileSize>50MB</maxFileSize><!--单个文件的最大大小-->
            <totalSizeCap>20GB</totalSizeCap><!--所有的日志文件最大20G,超过就会删除旧的日志-->
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{36} : %msg%n</pattern>
        </encoder>
    </appender>

    <!--这里如果是info,spring、mybatis等框架则不会输出:TRACE < DEBUG < INFO <  WARN < ERROR-->
    <!--root是所有logger的初始级,均继承root,如果某一个自定义的logger没有指定level,就会寻找父logger看有没有指定级别,直到找到root。-->
    <!--    <root level="debug">-->
    <!--        <appender-ref ref="stdout"/>-->
    <!--&lt;!&ndash;        <appender-ref ref="infoFile"/>&ndash;&gt;-->
    <!--&lt;!&ndash;        <appender-ref ref="errorFile"/>&ndash;&gt;-->
    <!--&lt;!&ndash;        <appender-ref ref="logstash"/>&ndash;&gt;-->
    <!--    </root>-->

    <!--为某个包单独配置logger

    比如定时任务,写代码的包名为:net.add1s.slf4j-logback
    步骤如下:
    1、定义一个appender,取名为task(随意,只要下面logger引用就行了)
    appender的配置按照需要即可


    2、定义一个logger:
    <logger name="net.add1s.slf4j-logback" level="DEBUG" additivity="false">
      <appender-ref ref="task" />
    </logger>
    注意:additivity必须设置为false,这样只会交给task这个appender,否则其他appender也会打印net.add1s.slf4j-logback里的log信息。

    3、这样,在net.add1s.slf4j-logback的logger就会是上面定义的logger了。
    private static Logger logger = LoggerFactory.getLogger(Class1.class);
    -->

    <!--配置多环境日志输出  可以在application.properties中配置选择哪个profiles : spring.profiles.active=dev-->
    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <root level="debug">
            <appender-ref ref="stdout" />
            <appender-ref ref="infoFile" />
            <appender-ref ref="errorFile" />
        </root>
    </springProfile>
    <springProfile name="test">
        <root level="debug">
            <appender-ref ref="stdout" />
            <appender-ref ref="infoFile" />
            <appender-ref ref="errorFile" />
        </root>
    </springProfile>
    <!--生产环境:输出到文件-->
    <springProfile name="prod">
        <root level="info">
            <appender-ref ref="infoFile" />
            <appender-ref ref="errorFile" />
        </root>
    </springProfile>

</configuration>
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
logging:
  level:
    com.singerw.blog.controller: debug
    com.singerw.blog.mapper: debug
  file:
    path: log/blog-test.log
1
2
3
4
5
6
logging:
  level:
    com.singerw.blog.controller: warn
    com.singerw.blog.mapper: debug
  file:
    path: log/blog-dev.log
1
2
3
4
5
6
logging:
  level:
    com.singerw.blog.controller: warn
    com.singerw.blog.mapper: info
  file:
    path: log/blog-prod.log
1
2
3
4
5
6

# 十一、SpringBoot整合Druid数据源

​ Druid是一个关系型数据库连接池,它是阿里巴巴的一个开源项目。Druid支持所有JDBC兼容数据库,包括了Oracle、MySQL、PostgreSQL、SQL Server、H2等。 ​ Druid在监控、可扩展性、稳定性和性能方面具有明显的优势。通过Druid提供的监控功能,可以实时观察数据库连接池和SQL查询的工作情况。使用Druid连接池在一定程度上可以提高数据访问效率。

# 1、application.yml和druidConfig.java配置Druid

# 步骤一:引入相关依赖

<!-- 核心启动器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 必须添加,否则会出现Error creating bean with name 'druidDataSource' -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- druid, 这里需要添加版本号 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>
<!-- 数据库操作需要的mysql 驱动包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
</dependency>
<!-- 使用druid监控页面需要是一个web项目, 引入web启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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
25
26
27

# 步骤二:application.yml中切换数据源

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.10.32:3306/jdbc
    driver-class-name: com.mysql.jc.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
1
2
3
4
5
6
7

# 步骤三:application.yml中设置数据源相关配置

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.10.32:3306/jdbc
    driver-class-name: com.mysql.jc.jdbc.Driver
    
    type: com.alibaba.druid.pool.DruidDataSource

    #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.Properity
    #则导入log4j 依赖就行
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
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

# 步骤四:配置 DruidConfig

/**
 * Created by baidou on 2021/2/26.
 * 
 * class:DruidConfig 
 */
@Configuration
public class DruidConfig {
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    //后台监控
    @Bean
    public ServletRegistrationBean statViewServlet() {
        //注册 ServletBean
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        //设置后台登录的账号密码
        HashMap<String, String> initParameters = new HashMap<>();
        initParameters.put("loginUsername", "admin");
        initParameters.put("loginPassword", "123456");
        //允許谁能访问  initParameters.put("username","password");
        bean.setInitParameters(initParameters);//初始化参数
        return bean;
    }

    //Filter
    public FilterRegistrationBean webStatFilter() {
        //注册 FilterBean
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());
        //可以过滤那些请求?
        HashMap<String, String> initParameters = new HashMap<>();
        //这些东西不进行统计
        initParameters.put("exclusions", "*.js,*.css,/druid/*");
        bean.setInitParameters(initParameters);//初始化参数
        return bean;
    }
}
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

# 步骤五:访问Druid监控

http://localhost:8000/druid/login.html
http://localhost:8000/druid/
1
2

# 2、application.yml配置Druid

# 步骤一:引入相关依赖

<!-- 核心启动器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 必须添加,否则会出现Error creating bean with name 'druidDataSource' -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- druid, 这里需要添加版本号 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
</dependency>
<!-- 数据库操作需要的mysql 驱动包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
</dependency>
<!-- 使用druid监控页面需要是一个web项目, 引入web启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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
25
26
27

# 步骤二:application.yml中配置数据源

spring:
  datasource:
    druid:
      # 数据源类型
      db-type: com.alibaba.druid.pool.DruidDataSource
      # 连接数据库的url,不同数据库不一样。例如:
      # mysql : jdbc:mysql://ip:port/database
      # oracle : jdbc:oracle:thin:@ip:port:database
      url: jdbc:mysql://127.0.0.1:3306/joint_force?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: 795200
      # 驱动(根据url自动识别)这一项可配可不配,如果不配置druid会根据url自动识别dbType,然
      # 后选择相应的driverClassName(建议配置下)
      driver-class-name: com.mysql.cj.jdbc.Driver
      # 始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时(缺省值:0)
      initial-size: 2
      # 最小连接池数量
      min-idle: 5
      # 最大连接池数量
      max-active: 10
      # 程序向连接池中请求连接时,超过maxWait的值后,认为本次请求失败,即连接池
      max-wait: 5000
      # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。(缺省值:false)
      # 默认是false
      pool-prepared-statements: false
      # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
      # 在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
      max-open-prepared-statements: -1
      # 每个连接最多缓存多少个SQL
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      max-evictable-idle-time-millis: 900000
      # 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
      validation-query: SELECT now()
      # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      test-while-idle: true
      # 程序申请连接时,进行连接有效性检查(低效,影响性能)
      test-on-borrow: false
      # 程序返还连接时,进行连接有效性检查(低效,影响性能)
      test-on-return: false
      # 物理连接初始化的时候执行的sql
      connection-init-sqls: SELECT now()
      # 程序没有close连接且空闲时长超过 minEvictableIdleTimeMillis,则会执行validationQuery指定的SQL,以保证该程序连接不会池kill掉,其范围不超过minIdle指定的连接个数。
      keep-alive: true
      # 要求程序从池中get到连接后, N 秒后必须close,否则druid 会强制回收该false,当发现程序有未连接,不管该连接中是活动还是空闲, 以防止进程不会进行close而霸占连接。
      remove-abandoned: true
      # 当druid强制回收连接后,是否将stack trace 记录到日志中
      log-abandoned: true
      # 设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此值后,druid将强制回收该连接,单位秒。应大于业务运行最长时间
      remove-abandoned-timeout: 6000
      # 连接属性。比如设置一些连接池统计方面的配置。
      connection-properties: druid.stat.mergeSql=true; druid.stat.slowSqlMillis=5000

      # 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
      filters: stat, wall, slf4j
      filter:
        stat: # 监控统计
          enabled: true
          log-slow-sql: true # 慢SQL记录
          slow-sql-millis: 1000 # 慢SQL执行时间
          merge-sql: true # 是否合并sql
          db-type: mysql # 数据库类型
        wall: # 防御SQL注入
          enabled: true
          db-type: mysql
          config:
            delete-allow: false
            drop-table-allow: false
            multi-statement-allow: true

      # statViewServlet配置
      stat-view-servlet:
        enabled: true # 是否启用
        allow: 127.0.0.1 # 设置白名单,不填则允许所有访问
        deny: 192.168.0.1 # 设置黑名单, 如果allow与deny同时存在,deny优先于allow
        reset-enable: false # 禁止手动重置监控数据
        url-pattern: /druid/*
        login-username: admin  # 设置监控页面的登陆密码
        login-password: admin  # 设置监控页面的登陆名


      # WebStatFilter配置
      web-stat-filter:
        enabled: true # 是否启用
        url-pattern: "/*"  # 访问路径
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" # 排除不必要的url
        session-stat-max-count: 1000 # 监控最大session数,默认是1000
        session-stat-enable: true # 是否启用session的统计功能
        profile-enable: true # 是否启用监控单个Url调用的sql列表
        principalSessionName: session_user_key # 使druid当前session的用户是谁,session_user_key是你保存到session中的sessionName
        principalCookieName: cookie_user_key # 使druid只当当前的user是谁,cookie_user_key是你保存在cookie中的cookieName
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 步骤三:添加servlet支持

因druid监控页面是一个servlet,需要让SpingBoot支持servlet,所以在程序入口添加注解@ServletComponentScan,否则会出现404错误。

@SpringBootApplication
@ServletComponentScan
public class SingerwblogApplication {

    public static void main(String[] args) {
        SpringApplication.run(SingerwblogApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

# 步骤四:访问Druid监控

启动项目后,访问http://localhost:8080/druid/login.html,输入刚才配置文件配置的用户名和密码!

可以看到大致包含了如下几个模块:数据源、SQL监控、SQL防火墙、Web应用、URI监控、Session监控、JSONAPI等。

QL监控:可以查看所有的执行sql语句

SQL防火墙:druid提供了黑白名单的访问,可以清楚的看到sql防护情况。

Web应用:可以看到目前运行的web程序的详细信息。

URI监控:可以监控到所有的请求路径的请求次数、请求时间等其他参数。

Session监控:可以看到当前的session状况,创建时间、最后活跃时间、请求次数、请求时间等详细参数。

JSONAPI:通过api的形式访问Druid的监控接口,api接口返回Json形式数据。

# 十二、SpringBoot整合Swagger接口文档

# 1、整合Swagger3接口文档

前后端分离的项目,接口文档的存在十分重要。与手动编写接口文档不同,swagger是一个自动生成接口文档的工具,在需求不断变更的环境下,手动编写文档的效率实在太低。与新版的swagger3相比swagger2配置更少,使用更加方便。

# 步骤一:引入Swagger3依赖

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>
1
2
3
4
5

# 步骤二:Application启动类上面加入@EnableOpenApi注解

@EnableOpenApi
@SpringBootApplication
@MapperScan(basePackages = {"com.singerw.dao"})
public class SingerwblogApplication {
    public static void main(String[] args) {
        SpringApplication.run(SingerwblogApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

# 步骤三:配置Swagger3Config.java

@Configuration
public class Swagger3Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.etc.swagger.controller"))
            .paths(PathSelectors.any())
            .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("Swagger3接口文档")
            .description("个人博客项目文档。")
            .contact(new Contact("ZhangSingerw。", "http://www.singerw.com", "zhangsingerw@gmail.com"))
            .version("1.0")
            .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 步骤四:访问

Swagger的访问路径由http://127.0.0.1:8080/swagger-ui.html改成了http://127.0.0.1:8080/swagger-ui/ 或http://127.0.0.1:8080/swagger-ui/index.html

# Swagger3自定义Ui

Swagger有很多可以自定义的ui,这里只举例以下两种:

<dependency>
    <groupId>com.zyplayer</groupId>
    <artifactId>swagger-mg-ui</artifactId>
    <version>1.0.6</version>
</dependency>
1
2
3
4
5

导入包后访问:http://localhost:8080/document.html

<!--老版本引用-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency>

<!--新版本引用-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-ui</artifactId>
    <version>3.0.3</version>
</dependency>

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>${knife4j.version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

导入包后访问:http://localhost:8080/doc.html

# 2、整合Swagger2接口文档

# 步骤一:导入依赖

<!--swagger2 start-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.1</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.3.5</version>
</dependency>
<!--swagger2 end-->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

步骤二:新建Swagger2Config配置类

@Configuration
@EnableSwagger2
public class Swagger2Config {

    //swagger2的配置文件,这里可以配置swagger2的一些基本的内容,比如扫描的包等等
    @Bean  创建一个bean
        public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            //为当前包路径
            .apis(RequestHandlerSelectors.basePackage("com.singerw.controller"))
            .paths(PathSelectors.any())
            .build();
    }
    
    //构建 api文档的详细信息函数,注意这里的注解引用的是哪个
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            //页面标题
            .title("Spring Boot 测试使用 Swagger2 构建RESTful API")
            //创建人
            .contact(new Contact("小白", "http://singerw.com", ""))
            //版本号
            .version("1.0")
            //描述
            .description("API 描述")
            .build();
    }

}
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

# 步骤三:访问

http://127.0.0.1:8080/swagger-ui.html

# 3、Swagger常用注解的使用说明

@Api:用在请求的类上,表示对类的说明
    tags="说明该类的作用,可以在UI界面上看到的注解"
    value="该参数没什么意义,在UI界面上也看到,所以不需要配置"

@ApiOperation:用在请求的方法上,说明方法的用途、作用
    value="说明方法的用途、作用"
    notes="方法的备注说明"

@ApiImplicitParams:用在请求的方法上,表示一组参数说明
@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
    name:参数名
    value:参数的汉字说明、解释
    required:参数是否必须传
    paramType:参数放在哪个地方
    · header --> 请求参数的获取:@RequestHeader
    · query --> 请求参数的获取:@RequestParam
    · path(用于restful接口)--> 请求参数的获取:@PathVariable
    · body(不常用)
    · form(不常用)  
    dataType:参数类型,默认String,其它值dataType="Integer"    
    defaultValue:参数的默认值

@ApiResponses:用在请求的方法上,表示一组响应
@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息
    code:数字,例如400
    message:信息,例如"请求参数没填好"
    response:抛出异常的类

@ApiModel:用于响应类上,表示一个返回响应数据的信息
    (这种一般用在post创建的时候,使用@RequestBody这样的场景,
    请求参数无法使用@ApiImplicitParam注解进行描述的时候)
@ApiModelProperty:用在属性上,描述响应类的属性
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

# 4、Controller层的配置示例

@Api(tags = "用户信息管理")
@RestController
@RequestMapping("userRecord")
public class UserRecordController extends ApiController {
    /**
   * 服务对象
   */
    @Resource
    private UserRecordService userRecordService;

    /**
   * 分页查询所有数据
   * @param page    分页对象
   * @param userRecord 查询实体
   * @return 所有数据
   */
    @ApiOperation("分页查询所有数据")
    @GetMapping("page")
    public R selectAll(Page<UserRecord> page, UserRecord userRecord) {
        return success(this.userRecordService.page(page, new QueryWrapper<>(userRecord)));
    }

    /**
   * 通过主键查询单条数据
   * @param id 主键
   * @return 单条数据
   */
    @ApiOperation("通过主键查询单条数据")
    @GetMapping("{id}")
    public R selectOne(@PathVariable Serializable id) {
        return success(this.userRecordService.getById(id));
    }

    /**
   * 新增数据
   * @param userRecord 实体对象
   * @return 新增结果
   */
    @ApiOperation("新增数据")
    @PostMapping("insert")
    public R insert(@RequestBody UserRecord userRecord) {
        return success(this.userRecordService.save(userRecord));
    }

    /**
   * 修改数据
   * @param userRecord 实体对象
   * @return 修改结果
   */
    @ApiOperation("修改数据")
    @PutMapping("update")
    public R update(@RequestBody UserRecord userRecord) {
        return success(this.userRecordService.updateById(userRecord));
    }

    /**
   * 删除数据
   * @param idList 主键结合
   * @return 删除结果
   */
    @ApiOperation("删除数据")
    @DeleteMapping("delete")
    public R delete(@RequestParam("idList") List<Long> idList) {
        return success(this.userRecordService.removeByIds(idList));
    }
}
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

# 十三、Thymeleaf模板引擎的使用

Thymeleaf 模板引擎功能强大,使用简单,Spring Boot推荐使用。

# 1、配置Thymeleaf

# 步骤一:导入thymeleaf依赖

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

# 步骤二:在yml中配置thymeleaf

spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    cache: false
1
2
3
4
5
6

# 步骤三:处理静态资源

html静态页面放在路径classpath:/templates下,在静态页面中导入thymeleaf的名称空间

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

步骤四:使用thymeleaf语法渲染数据即可

<!DOCTYPE html>
<!--名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Thymeleaf 语法</title>
    </head>
    <body>
        <h2>hymeleaf 语法</h2>
        <!--th:text 设置当前元素的文本内容,常用,优先级不高-->
        <p th:text="${thText}" />
        <p th:utext="${thUText}"/>

        <!--th:value 设置当前元素的value值,常用,优先级仅比th:text高-->
        <input type="text" th:value="${thValue}" />

        <!--th:object 声明变量,和*{} 一起使用-->
        <div th:object="${thObject}">
            <p>ID: <span th:text="*{id}" /></p><!--th:text="${thObject.id}"-->
            <p>TH: <span th:text="*{thName}" /></p><!--${thObject.thName}-->
            <p>DE: <span th:text="*{desc}" /></p><!--${thObject.desc}-->
        </div>

        <!--th:each 遍历列表,常用,优先级很高,仅此于代码块的插入-->
        <!--th:each 修饰在div上,则div层重复出现,若只想p标签遍历,则修饰在p标签上-->
        <div th:each="message : ${thEach}"> <!-- 遍历整个div-p,不推荐-->
            <p th:text="${message}" />
        </div>
        <!--只遍历p,推荐使用-->
        <div>
            <p th:each="user : ${ulist}" th:object="${user}">
                <span th:text="*{id}"></span>
                <span th:text="*{name}"></span>
                <span th:text="*{phone}"></span>
            </p>
        </div>

        <!--th:if 条件判断,类似的有th:switch,th:case,优先级仅次于th:each, 其中#strings是变量表达式的内置方法-->
        <p th:text="${thIf}" th:if="${not #strings.isEmpty(thIf)}"></p>

        <!--th:insert 把代码块插入当前div中,优先级最高,类似的有th:replace,th:include,~{} :代码块表达式 -->
        <div th:insert="~{template/footer :: copy}"></div>
        <div th:replace="~{template/footer :: copy}"></div>
        <div th:include="~{template/footer :: copy}"></div>


    </body>
</html>
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

后台给负责给变量赋值,和跳转页面。

@RequestMapping("/admin")
public String toAdmin(Model model){
    PageHelper.startPage(1,10);
    PageHelper.orderBy("id desc");
    List<InterUser> ulist = interUserService.selectInterUserList();
    model.addAttribute("ulist",ulist);
    model.addAttribute("thText","thText1");
    model.addAttribute("thUText","thUText1");
    model.addAttribute("thValue","thValue1");
    model.addAttribute("thEach", Arrays.asList("th:each", "遍历列表"));
    model.addAttribute("thIf", "msg is not null");
    model.addAttribute("thObject", new ThObject(1L, "th:object", "用来偷懒的th属性"));
    return "/admin/admin";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

官方文档:https://www.thymeleaf.org/documentation.html

# 2、常用th属性解读

html有的属性,Thymeleaf基本都有,而常用的属性大概有七八个。

  1. th:text : 设置当前元素的文本内容,相同功能的还有th:utext,两者的区别在于前者不会转义html标签,后者会。
  2. th:value : 设置当前元素的value值,类似修改指定html标签属性的还有th:src,th:href。
  3. th:each: 遍历循环元素,和th:text或th:value一起使用。注意该属性修饰的标签位置,详细看后文。
  4. th:if: 条件判断,类似的还有th:unless,th:switch,th:case。
  5. th:insert : 代码块引入,类似的还有th:replace,th:include,三者区别很大,若使用不恰当会破坏html结构,常用于公共代码块的提取复用。
  6. th:fragment: 定义代码块,方便被th:insert引用。
  7. th:object : 声明变量,一般和*{}一起配合使用,达到偷懒的效果。
  8. th:attr: 修改任意属性,实际开发中用的较少,因为有丰富的其他th属性帮忙。

作者:荆辰曦 链接:https://www.jianshu.com/p/5bbac20348ec 来源:简书

# 3、标准表达式语法

变量表达式:

${...}
1

链接表达式:

@{...}
1

消息表达式:

#{...}
1

代码块表达式:

~{...}
1

选择变量表达式

*{...}
1

作者:荆辰曦 链接:https://www.jianshu.com/p/5bbac20348ec 来源:简书

# 十四、Spring Data JPA的使用

JPA顾名思义就是Java Persistence API的意思,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

  • SpringBoot使用SpringDataJPA完成CRUD操作. 数据的存储以及访问都是最为核心的关键部分,现在有很多企业采用主流的数据库,如关系型数据库:MySQL,Oracle,SQLServer。非关系型数据库:redis,mongodb等.
  • Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。

# 1、导入相关依赖并配置文件

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url:
    username:
    password:
  jpa:
    database: mysql
    # 日志中显示sql语句
    show-sql: true
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2、JPA使用

# 步骤一:新建实体类并添加JPA注解

@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

    @Id
    @GeneratedValue
    @Column(name = "a_id")
    private Integer aId;
    @Column(name = "article_title")
    private String articleTitle;
    @Column(name = "article_content")
    private String articleContent;
    @Column(name = "head_image")
    private String headImage;
    @Column(name = "article_author")
    private String articleAuthor;
    @Column(name = "type_number")
    private Integer typeNumber;
    @Column(name = "pageviews")
    private Integer pageViews;
    @Column(name = "create_time")
    private String createTime;
    @Column(name = "is_state")
    private Integer isState;
    
}
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

# 步骤二:新建接口ArticleDao

/**
 * JpaRepository<T,ID>  提供简单的数据操作接口
 *     Article 实体类类型
 *     Integer 主键类型
 *
 * JpaSpecificationExecutor<T> 提供复杂查询接口
 *     Article 实体类类型
 *
 * Serializable 序列化
 */
@Repository
public interface ArticleDao extends JpaRepository<Article,Integer>,JpaSpecificationExecutor<Article>,Serializable{

    //这里没有代码,注意没有代码..........

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

# 步骤三:测试

@SpringBootTest
class Springboot07JpaApplicationTests {

    @Autowired
    private ArticleDao articleDao;

    @Test
    void contextLoads() {
        List<Article> articleList = articleDao.findAll();
        articleList.forEach(System.out::println);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3、JPA查询方法命令规范

关键字 方法命名 sql where字句
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like '?%'
EndingWith findByNameEndingWith where name like '%?'
Containing findByNameContaining where name like '%?%'
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?)
NotIn findByIdNotIn(Collection<?> c) where id not in (?)
True findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

# 4、JPQL语法生成

public interface StandardRepository extends JpaRepository<Standard, Long> {

    // JPA的命名规范
    List<Standard> findByName(String name);

    // 自定义查询,没有遵循命名规范
    @Query("from Standard where name = ?")
    Standard findByNamexxxx(String name);

    // 遵循命名规范,执行多条件查询
    Standard findByNameAndMaxLength(String name, Integer maxLength);

    // 自定义多条件查询
    @Query("from Standard where name = ?2 and maxLength = ?1")
    Standard findByNameAndMaxLengthxxx(Integer maxLength, String name);

    // 使用”标准”SQL查询,以前mysql是怎么写,这里继续
    @Query(value = "select * from T_STANDARD where C_NAME = ? and C_MAX_LENGTH = ?",
           nativeQuery = true)
    Standard findByNameAndMaxLengthxx(String name, Integer maxLength);

    // 模糊查询
    Standard findByNameLike(String name);

    @Modifying // 代表本操作是更新操作
    @Transactional // 事务注解
    @Query("delete from Standard where name = ?")
    void deleteByName(String name);

    @Modifying // 代表本操作是更新操作
    @Transactional // 事务注解
    @Query("update Standard set maxLength = ? where name = ?")
    void updateByName(Integer maxLength, String name);
}
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

# 5、JPA URUD示例

modle:Article.java

@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "a_id")
    private int aId;
    @Column(name = "article_title")
    private String articleTitle;
    @Column(name = "article_content")
    private String articleContent;
    @Column(name = "head_image")
    private String headImage;
    @Column(name = "article_author")
    private String articleAuthor;
    @Column(name = "type_number")
    private int typeNumber;

    private int pageviews;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "create_time")
    private Date createTime;
    @Column(name = "is_state")
    private int isState;
}
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

dao:ArticleDao.java

public interface ArticleDao extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article>, Serializable {

    List<Article> findByArticleTitleContaining(String keywords);

    //自定义方法
    @Query("select art from Article art where art.articleTitle like %?1% or art.articleContent like %?1%")
    Page<Article> findByLike(String keywords, Pageable pageable);
}
1
2
3
4
5
6
7
8

service:ArticleService.java

public interface ArticleService {

    Page<Article> findByLike(String keywords, int page, int pageSize);

    public void delArticle(int aId);

    public void updateArticle(Article article);

    public void addArticle(Article article);
}
1
2
3
4
5
6
7
8
9
10

serviceImpl:ArticleServiceImpl.java

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleDao articleDao;

    @Override
    public Page<Article> findByLike(String keywords, int page, int pageSize) {
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        PageRequest pageable = PageRequest.of(page - 1, pageSize, sort);
        Page<Article> pageResult = articleDao.findByLike(keywords, pageable);
        return pageResult;
    }

    @Override
    public void delArticle(int aId) {
        articleDao.deleteById(aId);
    }

    @Override
    public void updateArticle(Article article) {
        articleDao.save(article);
    }

    @Override
    public void addArticle(Article article) {
        articleDao.save(article);
    }
}
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

controller:ArticleController.java

@RestController
@Api(value = "文章的接口", description = "文章的接口")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    @GetMapping("Article")
    public ResponseData<Article> selAllArticle(
        @RequestParam(value = "keywords", required = true, defaultValue = "") String keywords,
        @RequestParam(value = "page", required = true, defaultValue = "1") Integer page,
        @RequestParam(value = "pageSize", required = true, defaultValue = "10") Integer pageSize) {

        Page<Article> pageResult = articleService.findByLike(keywords, page, pageSize);
        ResponseData<Article> rd = new ResponseData<Article>(200, "success", pageResult.getTotalElements(), pageResult.getContent());
        return rd;
    }
    
    @DeleteMapping("Article/{aId}")
    public void delArticleById(@PathVariable int aId) {
        articleService.delArticle(aId);
    }
    
    @PutMapping("Article")
    public void updateArticleById(@RequestBody Article article) {
        articleService.updateArticle(article);
    }
    
    @PostMapping("Article")
    public void addArticleById(@RequestBody Article article) {
        articleService.addArticle(article);
    }
}
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

# 6、JPA实现分页和模糊查询

modle:Article.java

@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "a_id")
    private int aId;
    @Column(name = "article_title")
    private String articleTitle;
    @Column(name = "article_content")
    private String articleContent;
    @Column(name = "head_image")
    private String headImage;
    @Column(name = "article_author")
    private String articleAuthor;
    @Column(name = "type_number")
    private int typeNumber;

    private int pageviews;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "create_time")
    private Date createTime;
    @Column(name = "is_state")
    private int isState;
}
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

dao:ArticleDao.java

public interface ArticleDao extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article>, Serializable {

    @Query("select art from Article art where art.articleTitle like %?1% or art.articleContent like %?1%")
    Page<Article> findByLike(String keywords, Pageable pageable);
}
1
2
3
4
5

service:ArticleService.java

public interface ArticleService {

    Page<Article> findByLike(String keywords, int page, int pageSize);
}
1
2
3
4

serviceImpl:ArticleServiceImpl.java

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleDao articleDao;

    @Override
    public Page<Article> findByLike(String keywords, int page, int pageSize) {
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        PageRequest pageable = PageRequest.of(page - 1, pageSize, sort);
        Page<Article> pageResult = articleDao.findByLike(keywords, pageable);
        return pageResult;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

controller:ArticleController.java

@RestController
@Api(value = "文章的接口", description = "文章的接口")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    @GetMapping("Article")
    public ResponseData<Article> selAllArticle(
        @RequestParam(value = "keywords", required = true, defaultValue = "") String keywords,
        @RequestParam(value = "page", required = true, defaultValue = "1") Integer page,
        @RequestParam(value = "pageSize", required = true, defaultValue = "10") Integer pageSize) {

        Page<Article> pageResult = articleService.findByLike(keywords, page, pageSize);
        ResponseData<Article> rd = new ResponseData<Article>(200, "success", pageResult.getTotalElements(), pageResult.getContent());
        return rd;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 十五、SpringBoot之JPA+Redis

# 1、Redis是什么?

No(Not Only)SQL数据库,内存数据库,提高数据访问的效率.

可用于缓存,事件发布或订阅,高速队列等场景。该数据库使用ANSI C语言编写,支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。

什么场景用到了redis? 验证码。 有效期.缓存.

长期不变的数据:省市区级联[MySQL] ->[Redis]->[直接存储在前端]

# 2、Redis 的一些优点

  • 异常快 - Redis 非常快,每秒可执行大约 110000 次的设置(SET)操作,每秒大约可执行 81000 次的读取/获取(GET)操作。
  • 支持丰富的数据类型 - Redis 支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。这使得 Redis 很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。
  • 操作具有原子性 - 所有 Redis 操作都是原子操作,这确保如果两个客户端并发访问,Redis 服务器能接收更新的值。
  • 多实用工具 - Redis 是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis 本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。

# 3、Redis 5种数据结构类型

结构类型 结构存储的值 结构的读写能力
String 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement)
List 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素
Set 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
Hash 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
Zset 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素

# 4、Redis的基本使用

# 步骤一: 新建springboot项目

此时pom.xml中导入的主要的包:

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10

# 步骤二:配置yml文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/joint_force?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 795200
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

  redis:
    host: 42.193.104.128
    port: 6379
    username:
    password: 795200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 步骤三:新建实体类添加JPA注解

@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

    @Id
    @GeneratedValue
    @Column(name = "a_id")
    private int AId;
    @Column(name = "article_title")
    private String articleTitle;
    @Column(name = "article_content")
    private String articleContent;
    @Column(name = "head_image")
    private String headImage;
    @Column(name = "article_author")
    private String articleAuthor;
    @Column(name = "type_number")
    private int typeNumber;

    private int pageviews;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Column(name = "create_time")
    private Date createTime;
    @Column(name = "is_state")
    private int isState;
}
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

# 步骤四:新建Dao接口

public interface ArticleDao extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article>, Serializable {

    List<Article> findByArticleTitleContaining(String keywords);


    @Query("select art from Article art where art.articleTitle like %?1% or art.articleContent like %?1%")
    Page<Article> findByLike(String keywords, Pageable pageable);



    List<Article> findByAId(Integer Aid);

}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 步骤五:1.1 编写服务类接口和实现

public interface ArticleService {

    List<Article> findByAId(Integer Aid);
}
1
2
3
4

接口的实现类中配置注解 @Cacheable(cacheNames = "com.singerw.serviceImpl.findByAId")表示启用缓存,并命名(命名可自定义,按要求命名即可);

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleDao articleDao;

    @Override
    @Cacheable(cacheNames = "findByAId",key = "#Aid")
    public List<Article> findByAId(Integer Aid) {
        return articleDao.findByAId(Aid);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。

@Cacheable可以指定三个属性:

  • value
  • key
  • condition

@Cacheable(value=”accountCache”):当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的key就是参数 Aid,value 就是Article对象。

@Override
@Cacheable(cacheNames = "findByAId",key = "#Aid")
public List<Article> findByAId(Integer Aid) {
    return articleDao.findByAId(Aid);
}
1
2
3
4
5

# 步骤六:启动类添加@EnableCaching注解

@SpringBootApplication
@EnableCaching
public class RedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class, args);
    }
}
1
2
3
4
5
6
7
8

# 步骤七:启动测试

第一次访问该接口,会查询我们的mysql来获取记录,查询成功后会将数据存储再redis中;则:

第二次访问该接口,没有查询我们的mysql,而是查询缓存数据库redis中的记录。

# 5、Redis常用注解小结

# @CacheConfig

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
1
2

默认key的生成按照以下规则:

  • 如果没有参数,则使用0作为key

  • 如果只有一个参数,使用该参数作为key

  • 如果又多个参数,使用包含所有参数的hashCode作为key

自定义key的生成:

当目标方法参数有多个时,有些参数并不适合缓存逻辑

比如:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {...}
1
2

其中checkWarehouse,includeUsed并不适合当做缓存的key.针对这种情况,Cacheable 允许指定生成key的关键属性,并且支持支持SpringEL表达式。(推荐方法示例)

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {...}

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {...}

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) {...}

@Cacheable(cacheNames="books", key="#map['bookid'].toString()")
public Book findBook(Map<String, Object> map) {...}
1
2
3
4
5
6
7
8
9
10
11

# @Cacheable

主要的参数:

  • value

value是缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:

@Cacheable(value=”mycache”)
@Cacheable(value={”cache1”,”cache2”}
1
2

key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:

@Cacheable(value=”testcache”,key=”#userName”)
1

condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:

@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
1

@CachePut 的作用 主要针对方法配置,如果缓存需要更新,且不干扰方法的执行,可以使用注解@CachePut。@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CachePut @Cacheable 使用,注意返回值需要一致;

@CacheEvict @Cacheable 不用管返回值

注意:如果使用是@CachePut那么需要和@Cacheable的方法的返回值是一致的;

# @CachePut

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
1
2

注意:应该避免@CachePut 和 @Cacheable同时使用的情况。

# 6、编写压力测试类

# 步骤一:引入依赖

<!-- https://mvnrepository.com/artifact/org.databene/contiperf -->
<dependency>
    <groupId>org.databene</groupId>
    <artifactId>contiperf</artifactId>
    <version>2.3.4</version>
    <scope>test</scope>
</dependency> 
1
2
3
4
5
6
7

# 步骤二:编写测试类

单元测试类里面使用 @Rule 注解激活 ContiPerf

@Rule
public ContiPerfRule i = new ContiPerfRule();
1
2

在具体测试方法上使用 @PerfTest 指定调用次数/线程数,使用 @Required 指定每次执行的最长时间/平均时间/总时间等

@Test
@PerfTest(invocations = 30000, threads = 20)
@Required(max = 1200, average = 250, totalTime = 60000)
public void test1() throws Exception {
    snowflakeSequence.nextId();
}
1
2
3
4
5
6

注意我们使用的是junt4,在pom.xml配置文件内已经添加了性能测试的依赖contiperf(康迪泼妇),测试从 Redis内读取数据与 数据库内读取输出的性能差异。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootJpaRedisApplicationTests_Contiperf {

    @Rule
    public ContiPerfRule  cr = new ContiPerfRule();

    @Autowired
    private ArticleService articleService;

    /**
    * invocations 1w次查询,200个线程同时操作getArticle方法
    */
    @Test
    @PerfTest(invocations = 10000,threads = 200)
    public void contextLoads() {

        List<Article> list= articleService.getArticle();

        list.forEach(System.out::println);
    }

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

​ 测试结论:测试有加入@Cacheable缓存时所用的时间和没有加入缓存时所用的时间进行对比,在数据库很多或并发性很高的情况下,加入缓存的测试结果要快得多。大致得到的一个效果是,数据量大,并发比较多,加上redis效率比较高的.

注:PerfTest参数

  • @PerfTest(invocations = 300):执行300次,和线程数量无关,默认值为1,表示执行1次;
  • @PerfTest(threads=30):并发执行30个线程,默认值为1个线程;
  • @PerfTest(duration = 20000):重复地执行测试至少执行20s。

2)Required参数

  • @Required(throughput = 20):要求每秒至少执行20个测试;
  • @Required(average = 50):要求平均执行时间不超过50ms;
  • @Required(median = 45):要求所有执行的50%不超过45ms;
  • @Required(max = 2000):要求没有测试超过2s;
  • @Required(totalTime = 5000):要求总的执行时间不超过5s;
  • @Required(percentile90 = 3000):要求90%的测试不超过3s;
  • @Required(percentile95 = 5000):要求95%的测试不超过5s;
  • @Required(percentile99 = 10000):要求99%的测试不超过10s;
  • @Required(percentiles = "66:200,96:500"):要求66%的测试不超过200ms,96%的测试不超过500ms。

# 十六、SpringBoot锁 -Mybatis

数据库加锁,是为了处理并发问题,在并发下保证数据一致性。

虽然我们在发开的过程中,没有使用锁的情况下,我们的程序也可以正常的执行,那是因为数据库帮我们隐式的添加了锁,按照使用方式可以讲锁分为:悲观锁和乐观锁;按照粒度可分为:全局锁,表锁和行锁,并在出现死锁后如何解决。

# 1、乐观锁和悲观锁的适用场景:

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,频繁读取使用乐观锁,频繁写入使用悲观锁。

# 2、悲观锁与其原理

悲观锁:

悲观锁则认为拿到的数据一定会被别人修改,为了防止被人修改,当我一拿到数据,我就对它进行加锁,这样别人就无法获得我的数据,只有等我操作完成释放锁后,后面的人才能获得我的数据。悲观锁的实现直接在语句后面加for update即可.

就是独占锁,不管读写都上锁了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

# 3、乐观锁与其原理

乐观锁:

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

乐观锁的原理:

乐观锁,大多是基于数据版本 ( Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现(表中添加一个字段 version)。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁的实现:

在数据库和实体类中添加一个int型的version字段,生成getter和setter方法。

# 4、悲观锁实现

# 步骤一:mapper.xml文件

需要使用select for update,即select * from A Where id=1 for update;

在mapper.xml中sql查询语句中加入for update

<?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.singerw.dao.ArticleMapper">
    
    <sql id="Base_Column_List">
        <!--@mbg.generated-->
        a_id, article_title, article_content, head_image, article_author, type_number, pageviews,
        create_time, is_state, version
    </sql>

    <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        <!--@mbg.generated-->
        select
        <include refid="Base_Column_List"/>
        from article
        where a_id = #{aId,jdbcType=INTEGER} for update
    </select>
    
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 步骤二:Service接口

public interface ArticleService {

    void testUptateLock1(Integer aId);

    void testUptateLock2(Integer aId);

}
1
2
3
4
5
6
7

# 步骤三:service实现类

@Service
public class ArticleServiceImpl implements ArticleService {

    @Resource
    private ArticleMapper articleMapper;

    @Override
    @Transactional
    public void testUptateLock1(Integer aId) {
        // 1 先做查询
        Article article = articleMapper.selectByPrimaryKey(aId);

        // 2 修改对象的属性值
        article.setArticleTitle("悲观锁_lock测试的Title_1");

        // 模拟其他的业务
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 3 调用修改的方法
        int result = articleMapper.updateByPrimaryKeySelective(article);

        // 自定义空指针异常
        String str = null;
        System.out.println(str.length());
    }

    @Override
    @Transactional
    public void testUptateLock2(Integer aId) {
        // 1 先做查询
        Article article = articleMapper.selectByPrimaryKey(aId);

        // 2 修改对象的属性值
        article.setArticleTitle("悲观锁_lock测试的Title_2");

        // 3 调用修改的方法
        int result = articleMapper.updateByPrimaryKeySelective(article);
    }
}
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

这里在testUptateLock1方法中模拟了一个线程休眠来代替业务的执行时间,在10秒内,我们再尝试去控制器调用testUptateLock2,等待10秒的时间时为了后续测试方便能发现测试结果。

# 步骤四:控制器

@RestController
@RequestMapping("/api")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    @GetMapping("/updateLock01/{aId}")
    public String updateLock01(@PathVariable("aId") Integer aId){
        articleService.testUptateLock1(aId);
        return "updateLock01 OK!";
    }

    @GetMapping("/updateLock02/{aId}")
    public String updateLock02(@PathVariable("aId") Integer aId){
        articleService.testUptateLock2(aId);
        return "updateLock02 OK!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 步骤五:访问进行测试

首先访问控制器的updateLock01的/updateLock01/{aId},访问的时候,需求等待十秒才能执行结束,

同时,我们再控制器的updateLock01执行等待的10秒内去访问updateLock02的/updateLock02/{aId},

而updateLock02需求等待updateLock01执行结束后才会执行。

# 总结

在SQL写了一个查询 : … for update,加了这个后,只有当控制器的updateLock01执行结束后updateLock02才有机会执行。这是典型的悲观锁的具体实现。

# 5、乐观锁实现

# 步骤一:添加列和完善实体类

数据库表中加一个字段 version =>int 初始值为0,实体类中也有一个private int version

model:Article.java

@ApiModel(value = "article")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Article implements Serializable {
    /**
     * 文章ID
     */
    @ApiModelProperty(value = "文章ID")
    private Integer aId;

    ...

    /**
     * 乐观锁
     */
    @ApiModelProperty(value = "乐观锁")
    private Integer version;

    private static final long serialVersionUID = 1L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 步骤二:mapper.xml

数据库sql语句层面,我们每次提交数据(update) 的时候,我们需要将版本号更新;原有版本号上+1; 修改语句的条件也要加上…. And version =#{version}

<update id="updateByPrimaryKey" parameterType="com.singerw.pojo.Article">
    <!--@mbg.generated-->
    update article
    set article_title   = #{articleTitle,jdbcType=LONGVARCHAR},
    article_content = #{articleContent,jdbcType=LONGVARCHAR},
    head_image      = #{headImage,jdbcType=VARCHAR},
    article_author  = #{articleAuthor,jdbcType=VARCHAR},
    type_number     = #{typeNumber,jdbcType=INTEGER},
    pageviews       = #{pageviews,jdbcType=INTEGER},
    create_time     = #{createTime,jdbcType=TIMESTAMP},
    is_state        = #{isState,jdbcType=INTEGER},
    version         = #{version,jdbcType=INTEGER}
    where a_id = #{aId,jdbcType=INTEGER}
    and version = #{version,jdbcType=INTEGER}
</update>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 步骤三:Service接口

public interface ArticleService {

    void testUptateLock3(Integer aId);

    void testUptateLock4(Integer aId);

}
1
2
3
4
5
6
7

# 步骤四:ServiceImpl实现类

@Service
public class ArticleServiceImpl implements ArticleService {

    @Resource
    private ArticleMapper articleMapper;


    @Override
    @Transactional
    public void testUptateLock3(Integer aId) {
        // 1 先做查询
        Article article = articleMapper.selectByPrimaryKey(aId);

        // 2 修改对象的属性值
        article.setArticleTitle("乐观锁_lock测试的Title_3");

        // 模拟其他的业务
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 3 调用修改的方法
        int result = articleMapper.updateByPrimaryKeySelective(article);
    }

    @Override
    @Transactional
    public void testUptateLock4(Integer aId) {
        // 1 先做查询
        Article article = articleMapper.selectByPrimaryKey(aId);

        // 2 修改对象的属性值
        article.setArticleTitle("乐观锁_lock测试的Title_4");

        // 3 调用修改的方法
        int result = articleMapper.updateByPrimaryKeySelective(article);
    }
    
}
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

# 步骤五:控制器

@RestController
@RequestMapping("/api")
public class ArticleController {

    @Autowired
    private ArticleService articleService;

    @GetMapping("/updateLock03/{aId}")
    public String updateLock03(@PathVariable("aId") Integer aId){
        articleService.testUptateLock3(aId);
        return "updateLock03 OK!";
    }

    @GetMapping("/updateLock04/{aId}")
    public String updateLock04(@PathVariable("aId") Integer aId){
        articleService.testUptateLock4(aId);
        return "updateLock04 OK!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 步骤六:访问进行测试

优先访问:updateLock03 ===>/updateLock03/{aId}

再访问下:updateLock04===> /updateLock04/{aId}

# 总结

发现实际的效果,第二个请求updateLock04并没有等第一个请求updateLock03 结束后才响应;直接响应,并更新成功;

然后,当我们第二个请求提交成功后,第一个请求经过10s,也尝试提交,但是,修改失败,因为version版本号已经被updateLock04给修改了,所有updateLock03 无法再进行提交。

对于mybatis中实现乐观锁,就是sql语句的实现,如果没有更新成功,不会报异常,直接就是受影响的行为0而已;

# 十七、SpringSecurity(安全)

身份认证和权限认证的安全框架

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

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义要求。

# 1、特征

  • 对身份验证和授权的全面且可扩展的支持
  • 防止会话固定、点击劫持、跨站点请求伪造等攻击
  • Servlet API 集成
  • 与 Spring Web MVC 的可选集成
编辑 (opens new window)
#SpringBoot
SpringBoot优点与简单介绍

SpringBoot优点与简单介绍→

最近更新
01
Maven资源导出问题终极版
10-12
02
《MyBatis-Plus》学习笔记
10-07
03
MyBatis-Plus—配置日志
10-07
更多文章>
Theme by Vdoing | Copyright © 2020-2021 版权所有 | repository.singerw.com
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×