0%

RediSearch 是一个高性能的全文搜索引擎,它可以作为一个 Redis Module(扩展模块)运行在 Redis 服务器上。

RediSearch

  • Redis 哈希中多个字段的全文索引
  • 无性能损失的增量索引
  • 文档排名(使用tf-idf,具有可选的用户提供的权重)
  • 场加权
  • 使用 AND、OR 和 NOT 运算符的复杂布尔查询
  • 前缀匹配、模糊匹配和精确短语查询
  • 支持双变音拼音匹配
  • 自动完成建议(带有模糊前缀建议)
  • 多种语言中基于词干的查询扩展(使用Snowball
  • 支持中文标记化和查询(使用Friso
  • 数字过滤器和范围
  • 使用Redis 地理空间索引进行地理空间搜索
  • 强大的聚合引擎
  • 支持所有 utf-8 编码文本
  • 检索完整文档、选定字段或仅检索文档 ID
  • 排序结果(例如,按创建日期)

官方文档

运行环境

  • docker
1
2
mkdir -p redisearch/data
docker run -p 6379:6379 -v $PWD/redisearch/data:/data -d redislabs/redisearch:latest
  • 检查安装
1
2
3
4
5
6
7
➜  docker-run redis-cli 
127.0.0.1:6379> MODULE LIST
1) 1) "name"
2) "search"
3) "ver"
4) (integer) 20015
127.0.0.1:6379> exit
  • 测试下
1
2
3
4
5
6
7
127.0.0.1:6379> FT.ADD idx docCn 1.0 LANGUAGE chinese FIELDS txt "Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。从盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。[8]"
OK
127.0.0.1:6379> FT.SEARCH idx "数据" LANGUAGE chinese HIGHLIGHT SUMMARIZE
1) (integer) 1
2) "docCn"
3) 1) "txt"
2) "<b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe5... <b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe8\xbf\x9b\xe8\xa1\x8c\xe5\x86\x99\xe6\x93\x8d\xe4\xbd\x9c\xe3\x80\x82\xe7\x94\xb1\xe4\xba\x8e\xe5\xae\x8c\xe5\x85\xa8\xe5\xae\x9e\xe7\x8e\xb0\xe4\xba\x86\xe5\x8f\x91\xe5\xb8\x83... <b>\xe6\x95\xb0\xe6\x8d\xae</b>\xe5\x86\x97\xe4\xbd\x99\xe5\xbe\x88\xe6\x9c\x89\xe5\xb8\xae\xe5\x8a\xa9\xe3\x80\x82[8... "
  • 对中文支持不够好
阅读全文 »

简单优化的下博客。

优化内容

  • SEO优化
  • 增加站内搜索
  • 增加网站地图
  • 添加字数统计和阅读时长
  • GitHub Fork Me
  • 允许复制代码
  • 图片懒加载

SEO优化

hexo 默认生成文章命名方式,在中文标题下很不友好。可以选择生成永久的链接。

  • 使用abbrlink插件
1
npm install hexo-abbrlink --save
  • 配置_config.yml
1
2
3
4
5
6
#permalink: :year/:month/:day/:title/
#permalink_defaults:
permalink: posts/:abbrlink/
abbrlink:
alg: crc32 #support crc16(default) and crc32
rep: dec #support dec(default) and hex
  • 生成的链接将会是这样的(官方样例):
1
2
3
4
5
6
7
8
9
10
crc16 & hex
https://post.x.com/posts/66c8.html

crc16 & dec
https://post.x.com/posts/65535.html
crc32 & hex
https://post.x.com/posts/8ddf18fb.html

crc32 & dec
https://post.x.com/posts/1690090958.html
阅读全文 »

编写第一个CLI小程序练手。

项目需求

  • 访问一个网站,输出页面内容成md文件。

初始化项目

1
cargo new learning-05
  • 使用vs code 打开项目。

添加依赖

  • 修改Cargo.toml
  • 引入 reqwest 和 html2md
    • reqwest 是一个http客户端
    • html2md 顾名思义就是html转markdown
1
2
reqwest = { version = "0.11", features = ["blocking"]}
html2md = "0.2"%

编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs;

fn main() {
let url = "https://z201.vip";
let output = "z201.md";
println!("url {} output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();

println!("Converting html to markdown...");
let md = html2md::parse_html(&body);

fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}
  • Console
1
2
3
url https://z201.vip output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

迭代代码

版本一

  • 这里将url 和 output作为参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::fs;
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("args {:?} ",args);
// 获取第一个参数
let url = &args[1];
// 获取第二个参数
let output = &args[2];
// 判断参数是否为空,不知道这个是否有效。
assert_eq!(url.is_empty(),false);
assert_eq!(output.is_empty(),false);

println!("url {} output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();

println!("Converting html to markdown...");
let md = html2md::parse_html(&body);

fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}
  • Console
1
2
3
4
5
6
7
➜  learning-05 git:(master) ✗ cargo run https://z201.vip z201.md
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/learning-05 'https://z201.vip' z201.md`
args ["target/debug/learning-05", "https://z201.vip", "z201.md"]
url https://z201.vip output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

版本二

  • 换一种方式来处理参数
  • 打包测试
增加依赖
1
2
3
reqwest = { version = "0.11", features = ["blocking"]}
html2md = "0.2"
structopt = "0.3.13"
  • 代码
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
use std::fs;
use structopt::StructOpt; // 使用StructOpt传递参数

#[derive(StructOpt, Debug)]
struct Cli{
url:String,
output:String,
}

fn main(){
let args = Cli::from_args();
// println!("args {:?} ",args);
let url = &args.url;
let output = &args.output;
println!("url {}\n output {}",url,output);
let body = reqwest::blocking::get(url).unwrap().text().unwrap();
println!("Converting html to markdown...");
let md = html2md::parse_html(&body);
fs::write(output, md.as_bytes()).unwrap();
println!("Converted markdown has been saved in {}.", output);
}

#[test]
fn check() {
println!("test")
}

打包

1
cargo build --release
  • 慢慢等待

测试下

1
2
3
4
5
6
cd target/release
./learning-05 https://z201.vip z201.md
url https://z201.vip
output z201.md
Converting html to markdown...
Converted markdown has been saved in z201.md.

END

Rust的核心功能之一 ownership ,运行的程序都需要使用计算机管理内存的方式,比如Java 具备垃圾回收,还有一些语言需要手动去释放内存。而Rust则是第三种方式,通过所有权管理内存,编译器在编译时会根据一些列规则检查,在运行时所有权系统的任务功能都不会减慢程序。

  • 所有权和生命周期是 Rust 和其它编程语言的主要区别,也是 Rust 其它知识点的基础。

所有权

  • 栈是程序运行的基础。每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,这块内存被称为帧(frame)。

  • 栈以放入值的顺序存储并以相反的顺序取出值。这也被称为 后进先出 (last in , first out) 。添加数据的时候加 进栈 (pushing anto the stack) ,而移出数据叫 出栈 (poping off th stack)。

  • 在调用的过程中,一个新的帧会分配足够的空间存储寄存器的上下文。在函数里使用到的通用寄存器会在栈保存一个副本,当这个函数调用结束,通过副本,可以恢复出原本的寄存器的上下文,就像什么都没有经历一样。此外,函数所需要使用到的局部变量,也都会在帧分配的时候被预留出来。

  • 栈的操作时非常快的,这主要得益于它存取数据的方式,数据的存取位置总时在栈顶,而不需要重新寻找一个位置去存放或者读取。另一个属性就是,栈中所有的数据都必须占据已知且固定的大小。

  • 但是工作中我们依然要避免大量数据存放栈中,避免栈溢出(stack overfow),运行程序调用栈超出了系统运行的最大空间,就无法创建新的。就会出现溢出现象

在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。

  • 堆主要存放大小未知,可能大小变化的数据,堆事缺乏组织的。当向堆中存放数据时,要申请一个空间,操作系统在堆的某处足够大的空间,把它标记为已经使用。并返回一个指针(pointer),这个过程叫做堆上分配内存(allocating on the heap)。
  • 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。当代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
案例一
1
2
3
4
5
fn say(name : String){
println("name {}" , name);
}
say("tomcat".to_string());
say("jetty".to_string());
  • 字符串的数据结构,在编译器是不确定的,运行期才代码才知道大小。比如tomcatjetty,当say方法执行的时候才知道参数的具体长度。
案例二
1
2
3
let mut arr = Vec::new();
arr.push(1);
arr.push(2);
  • 列表初始化长度是4,而不是2。在堆上内存分配会使用libc提供的mallo函数。系统调用的代价是昂贵的,所以我们要避免频繁地 malloc()。
内存泄漏

如果手工管理堆内存的话,堆上内存分配后忘记释放,就会造成内存泄漏。一旦有内存泄漏,程序运行得越久,就越吃内存,最终会因为占满内存而被操作系统终止运行。

堆越界

如果堆上内存被释放,但栈上指向堆上内存的相应指针没有被清空,就有可能发生使用已释放内存(use after free)的情况。

GC垃圾回收处理
  • 比如Java采用垃圾回收(GC),来自动管理内存,定期标记(mark)找出不在被引用的对象,然后清理(sweep)掉。
  • 但是GC是不确定的,可能引起STW(Stop The World)。
ARC自动引用计数处理方式
  • 在编译时,它为每个函数插入 retain/release 语句来自动维护堆上对象的引用计数,当引用计数为零的时候,release 语句就释放对象。

栈与堆

  • 栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期。
阅读全文 »

函数

函数遍布于 Rust 代码中,Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。

  • Rust 中的函数定义以 fn 开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
  • 可以使用函数名后跟圆括号来调用我们定义过的任意函数。Rust 不关心函数定义于何处,只要定义了就行。

案例

1
2
3
4
5
6
7
8
fn main() {
println!("Hello, world!");
hello();
}

fn hello(){
print!("hello function")
}
  • Console
1
2
Hello, world!
hello function

函数参数

函数也可以被定义为拥有 参数(parameters),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。

  • 在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。
  • 多个参数时,使用逗号分隔。

案例

1
2
3
4
5
6
7
8
fn number(x:i8){
print!("value {} ",x)
}

fn main() {
println!("Hello, world!");
number(1);
}
  • Console
1
2
Hello, world!
value 1
  • number函数声明一个x的参数,x的类型被指定为i8
阅读全文 »

Rust变量与数据类型

Rust 是一种静态类型的语言。 Rust 中的每个值都是某种数据类型。 编译器可以根据分配给它的值自动推断变量的数据类型。

let

使用let关键字声明变量

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
println!("Rust基础语法!");
let str_ = "String"; // string 类型
let f_ = 1.1; // float 类型
let bool_ = true; // boolean 类型
let char_ = 'a'; // unicode character
println!("string {}",str_);
println!("float {}",f_);
println!("boolean {}",bool_);
println!("icon {}",char_);


}
  • Console
1
2
3
4
5
Rust基础语法!
string String
float 1.1
boolean true
icon a
阅读全文 »

学习下Rust

为什么学习Rust

在知乎上看到一个人对Rust的评论。

首先,Rust 是有点反人类,否则不会一直都不火。然后,Rust 之所以反人类,是因为人类这玩意既愚蠢,又自大,破事还贼多。 你看 C++ 就很相信人类,它要求人类自己把自己 new 出来的东西给 delete 掉。 C++:“这点小事我相信你可以的!” 人类:“没问题!包在我身上!” 然后呢,内存泄漏、double free、野指针满世界飘…… C++:“……”

Java 选择不相信人类,但替人类把事办好。 Java:“别动,让我来,我有gc!” 人类:“你怎么做事这么慢呀?你怎么还 stop the world 了呀?你是不是不爱我了呀?” Java:“……”

Rust 发现唯一的办法就是既不相信人类,也不惯着人类。 Rust:“按老子说的做,不做就不编译!” 人类:“你反人类!” Rust:“滚!”

  • C/C++ 完全相信而且惯着程序员,让大家自行管理内存,所以可以编写很自由的代码,但一个不小心就会造成内存泄漏等问题导致程序崩溃。
  • Java/Golang 完全不相信程序员,但也惯着程序员。所有的内存生命周期都由 JVM 运行时统一管理。 在绝大部分场景下,你可以非常自由的写代码,而且不用关心内存到底是什么情况。 内存使用有问题的时候,我们可以通过 JVM 来信息相关的分析诊断和调整。
  • Rust 语言选择既不相信程序员,也不惯着程序员。 让你在写代码的时候,必须清楚明白的用 Rust 的规则管理好你的变量,好让机器能明白高效地分析和管理内存。 但是这样会导致代码不利于人的理解,写代码很不自由,学习成本也很高。
阅读全文 »

背景

权限管理是一个几乎所有大中型 B 端系统都会涉及的重要组成部分,其目的是对整个系统进行权限控制,避免造成误操作及数据泄露等风险问题。

权限与权限管理

名词定义

权限相关的基本概念:

  • 权限:用户可操作行为的最小单位。
  • 用户:每个用户都有唯一标识,并被授予一个或多个角色。
  • 角色:由不同的权限组合而成,最终分配给具体用户。
  • 权限管理:控制用户的权限,只能访问授权内容。

模型选择

  • ACL(Access Control List):基于用户级别的权限控制。
    • 将系统的各种权限直接授予具体的用户。抽象来说,为每个用户维护了单独的权限列表,当需要分配权限、收回权限时,需要修改对应用户的权限信息。
  • RBAC(Role Base Access Control):基于角色级别的权限控制。
    • 与 ACL 对比,RBAC不用给用户单个分配权限,权限与用户之前通过角色关联。通过给不同的角色分配不同的权限,只需要将用户指向对应的角色就会有对应的权限。分配权限、收回权限只需要通过修改用户的角色即可。
  • ABAC(Attribute Base Access Control):基于属性级别的权限控制。
    • 不同于常见的将用户通过某种方式直接关联到权限的方式,ABAC 是通过动态计算一个或一组属性来是否满足某种条件来进行权限判断。属性一般分为四类:用户属性(自然人属性,如年龄、性别等),环境属性(物理环境,如时间、地点、气候),操作属性(读、写)和对象属性(操作对象,如资金、某张图片、某个特定的页面,又称资源属性)。
  • 因此理论上能够实现灵活的权限控制、将在权限与用户之前通过一组或多组属性实现关联,几乎能满足所有类型的需求。

权限管控

抽象来看权限体系可以分为如下两类:功能权限 与 数据权限 两部分。

  • 功能权限指的是在系统中的功能可否使用,通常我们将功能权限分为查看、编辑、删除等,同时编辑、删除权限又包含了查看。通过小的权限点拆分更精细的赋予了员工能否进入某个页面查看信息、编辑信息的能力。
  • 数据权限指数据中存在的数据是否能查看,是一个更细粒度的权限。比如一个页面,不同角色查看不同的数据就需要通过数据权限控制。
    从管理对象维度又可以分为:企业能力 与 员工能力。
  • 企业能力店铺维度的权限,比如开通某服务,可以通过企业能力去体现。
  • 企业能力赋予用户的权限,比如收电子发票、资金管理等。
  • 企业能力优先级绝对高于员工能力,所有场景的权限判断,店铺能力必须先于员工能力。简单地说,企业能力决定了“企业能做什么”,员工能力决定了“用户能做什么”。

jvm调优是日常工作中经常会使用的技巧,整理下。

项目启动的时候加上的参数都是些啥意思?

刚开始工作的时候发现同事会在jar启动的命令上增加很多参数,很长一段时间都不清楚是干啥的。

1
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
  • 简单介绍下参数
1
2
3
4
5
6
7
8
9
-XX:MetaspaceSize=128m (元空间默认大小)
-XX:MaxMetaspaceSize=128m (元空间最大大小)
-Xms1024m (堆最大大小)
-Xmx1024m (堆默认大小)
-Xmn256m (新生代大小)
-Xss256k (棧最大深度大小)
-XX:SurvivorRatio=8 (新生代分区比例 8:2)
-XX:+UseConcMarkSweepGC (指定使用的垃圾收集器,这里使用CMS收集器)
-XX:+PrintGCDetails (打印详细的GC日志)
  • 虽然有了介绍但是依然不清楚具体是干啥的。并且Java虚拟机提供了非常多的参数命令。下面代码可以输出支持的参数数量
1
2
3
4
5
java -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions -version | wc -l
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_275-b01)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.275-b01, mixed mode)
838
  • 堆内存大小配置

建议 -Xms = 最大内存 * [0.6. ~0.8] 这里需要考虑系统损耗内存、和实际物理内存。

  • 堆内存与堆外内存

    • 堆内存

    必须是1024的倍数,且不能低于2M。

    32位机器,最大1G/4G 64位机器最大可以超过 32G/64G

    • 堆外内存

    堆外内存一般指 Direct Memory ,不受GC控制,JVM、Netty都可能使用堆外内存。

1
-XX:MaxDirectMemorySize 限制

如何理解这些参数的含义?

首先我们需要理解java是如何运行的,为什么需要java虚拟机?

我们常用方式一般是安装java运行环境(jre)用命令行的方式启动或者直接双击jar运行。jre包含的java运行的必要环境。

Java 作为一门高级程序语言,它的语法非常复杂,抽象程度也很高。编译出来的也不是机器可以直接直接运行代码。所以使用面向Java语言的虚拟机运行Java编译以后的特定代码。这里的特定代码指的是Java字节指令码。

JVM 内存分配性能问题

  • 在应用服务的特定场景下,JVM 内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。如果没有深入到各项性能指标中去,是很难发现其中隐藏的性能损耗。
  • JVM 内存分配不合理最直接的表现就是频繁的 GC,这会导致上下文切换等性能问题,从而降低系统的吞吐量、增加系统的响应时间。

分析 GC 日志

  • 在进行压测的时候,我们需要对GC日志进行分析。
1
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/log/heap.log
  • -XX:PrintGCTimeStamps:打印 GC 具体时间;
  • -XX:PrintGCDetails :打印出 GC 详细日志;
  • -Xloggc: path:GC 日志生成路径。
  • JVM 内存调优通常和 GC 调优是互补的,基于以上调优,可以对年轻代和堆内存的垃圾回收算法进行调优。
阅读全文 »

SpringBoot测试解决方案

  • mock

    • Mock 的意思是模拟,它可以用来对系统、组件或类进行隔离。
    • 验证组件级别正确性的一大难点在于关于组件与组件之间的依赖关系,这里就需要引出测试领域非常重要的一个概念,即 Mock(模拟)。

  • mvc-mock 测试Controller

  • service-mock 测试 Service

  • repository-mock 测试 Data

  • remote-mock 测试 远程接口

Spring-Boot-Start-Test

Spring Test & Spring Boot Test:为 Spring 和 Spring Boot 框架提供的测试工具。

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
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.4.5:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.4.5:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.4.5:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | | \- net.minidev:json-smart:jar:2.3:test
[INFO] | | \- net.minidev:accessors-smart:jar:1.2:test
[INFO] | | \- org.ow2.asm:asm:jar:5.0.4:test
[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:test
[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:1.2.2:test
[INFO] | +- org.assertj:assertj-core:jar:3.18.1:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.7.1:test
[INFO] | | \- org.junit.jupiter:junit-jupiter-params:jar:5.7.1:test
[INFO] | +- org.mockito:mockito-core:jar:3.6.28:test
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.10.22:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.10.22:test
[INFO] | | \- org.objenesis:objenesis:jar:3.1:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:3.6.28:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-test:jar:5.3.6:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.7.0:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.7.1:test
[INFO] | +- org.apiguardian:apiguardian-api:jar:1.1.0:test
[INFO] | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | \- org.junit.platform:junit-platform-commons:jar:1.7.1:test
[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:5.7.1:test
[INFO] \- org.junit.platform:junit-platform-engine:jar:1.7.1:test
  • JUnit:JUnit 是一款非常流行的基于 Java 语言的单元测试框架,在我们的课程中主要使用该框架作为基础的测试框架。
  • JSON Path:类似于 XPath 在 XML 文档中的定位,JSON Path 表达式通常用来检索路径或设置 JSON 文件中的数据。
  • AssertJ:AssertJ 是一款强大的流式断言工具,它需要遵守 3A 核心原则,即 Arrange(初始化测试对象或准备测试数据)——> Actor(调用被测方法)——>Assert(执行断言)。
  • Mockito:Mockito 是 Java 世界中一款流行的 Mock 测试框架,它主要使用简洁的 API 实现模拟操作。在实施集成测试时,我们将大量使用到这个框架。
  • Hamcrest:Hamcrest 提供了一套匹配器(Matcher),其中每个匹配器的设计用于执行特定的比较操作。
  • JSONassert:JSONassert 是一款专门针对 JSON 提供的断言框架。

mock框架

  • 这里推荐使用mockito 和 阿里开源的TestableMock。
  • 使用spring boot start test 会默认引入mockito

  • mockito常用方法
1
2
3
4
5
6
7
8
9
10
11
Mockito.mock(classToMock)	//模拟对象
Mockito.verify(mock) //验证行为是否发生
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) //触发时第一次返回value1,第n次都返回value2
Mockito.doThrow(toBeThrown).when(mock).[method] //模拟抛出异常。
Mockito.mock(classToMock,defaultAnswer) //使用默认Answer模拟对象
Mockito.when(methodCall).thenReturn(value) //参数匹配
Mockito.doReturn(toBeReturned).when(mock).[method] //参数匹配(直接执行不判断)
Mockito.when(methodCall).thenAnswer(answer)) //预期回调接口生成期望值
Mockito.doAnswer(answer).when(methodCall).[method] //预期回调接口生成期望值(直接执行不判断)
Mockito.doNothing().when(mock).[method] //不做任何返回
reset(mock) //重置mock

演示代码

SpringBoot-mock

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class AppApplicationTest {

@Autowired
private ApplicationContext applicationContext;

@Test
public void testContext() {
Assertions.assertThat(applicationContext).isNotNull();
}
}
  • @ExtendWith(SpringExtension.class) 是junit5的注入方式。
  • @SpringBootTest 注解,SpringBoot 专门提供了一个 @SpringBootTest 注解测试 Bootstrap 类。同时 @SpringBootTest 注解也可以引用 Bootstrap 类的配置,因为所有配置都会通过 Bootstrap 类去加载。
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

/**
* An enumeration web environment modes.
*/
enum WebEnvironment {

/**
* 加载 WebApplicationContext 并提供一个 Mock 的 Servlet 环境,此时内置的 Servlet 容器并没有正式启动。
*/
MOCK(false),

/**
* 加载 EmbeddedWebApplicationContext 并提供一个真实的 Servlet 环境,然后使用一个随机端口启动内置容器。
*/
RANDOM_PORT(true),

/**
* 这个配置也是通过加载 EmbeddedWebApplicationContext 提供一个真实的 Servlet 环境,但使用的是默认端口,如果没有配置端口就使用 8080。
*/
DEFINED_PORT(true),

/**
* 加载 ApplicationContext 但并不提供任何真实的 Servlet 环境。
*/
NONE(false);
}

Controller-mock

  • 接口测试的时候mock controller层,并开启http端口
  • 引入@AutoConfigureMockMvc
    • 请注意 @SpringBootTest 注解不能和 @WebMvcTest 注解同时使用。
  • MockMvc提供部分方法
    • Perform:执行一个 RequestBuilder 请求,会自动执行 SpringMVC 流程并映射到相应的 Controller 进行处理。
    • get/post/put/delete:声明发送一个 HTTP 请求的方式,根据 URI 模板和 URI 变量值得到一个 HTTP 请求,支持 GET、POST、PUT、DELETE 等 HTTP 方法。
    • param:添加请求参数,发送 JSON 数据时将不能使用这种方式,而应该采用 @ResponseBody 注解。
    • andExpect:添加 ResultMatcher 验证规则,通过对返回的数据进行判断来验证 Controller 执行结果是否正确。
    • andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台。
    • andReturn:最后返回相应的 MvcResult,然后执行自定义验证或做异步处理。
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

@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebFlux
public class AppControllerTest {

@Autowired
private WebTestClient webTestClient;

@MockBean
private AppController appController;

@Test
public void testVersion() throws Exception {
// 准备一组测试数据
Map<String, AppVo> testData = new HashMap<>();
testData.put("undefined", new AppVo("", "undefined"));
testData.put("mysql", new AppVo("mysql", "1.0.0"));
testData.put("redis", new AppVo("redis", "2.0.0"));
// 循环测试
for (String s : testData.keySet()) {
Mockito.when(appController.version(s)).thenReturn(testData.get(s));
EntityExchangeResult<String> result = webTestClient.post()
.uri("/" + s)
.contentType(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(String.class)
.returnResult();
log.info("{}",result);
}
}

}
  • Console
1
2
3
[main]  {"key":"mysql","version":"1.0.0"}
[main] {"key":"redis","version":"2.0.0"}
[main] {"key":"","version":"undefined"}

Service-mock

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
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class AppServiceITest {

@MockBean
private AppServiceI appService;

@Test
public void testVersion() {
String key = "mysql";
Mockito.when(appService.version("")).thenReturn(new AppVo("", "undefined"));
Mockito.when(appService.version("mysql")).thenReturn(new AppVo("mysql", "5.7.36"));
Mockito.when(appService.version("redis")).thenReturn(new AppVo("redis", "6.2.6"));
AppVo appVo = appService.version(key);
Assertions.assertThat(appVo).isNotNull();
log.info("{}",JsonTool.toString(appVo));
Assertions.assertThat(appVo.getVersion()).isEqualTo("5.7.36");
appVo = appService.version("redis");
Assertions.assertThat(appVo).isNotNull();
log.info("{}",JsonTool.toString(appVo));
Assertions.assertThat(appVo.getVersion()).isEqualTo("6.2.6");
appVo = appService.version("");
Assertions.assertThat(appVo).isNotNull();
log.info("{}",JsonTool.toString(appVo));
Assertions.assertThat(appVo.getVersion()).isEqualTo("undefined");
}

}
  • Console
1
2
3
[main]  {"key":"mysql","version":"5.7.36"}
[main] {"key":"redis","version":"6.2.6"}
[main] {"key":"","version":"undefined"}
  • remote-mock 测试 远程接口在spring boot cloud test mock中演示。

Data-Mock

日常开发一般都会使用持久层,引入 DataJdbcTest 注解进行mock测试。如果是mybaits可以使用@MybatisTest、如果是MybatisPuls可以使用@MybatisPulsTest。只需要分别引入依赖即可。

  • 演示项目用的jdbc,平时用的是mybaits,针对MybaitsTest单独写一篇文档吧。

Http-Mock

spring 有两个web客户端的实现,一个是RestTemplate另一个是spring5的响应代替WebClient。

  • RestTemplate是阻塞客户端
    • 它基于thread-pre-requset模型。
    • 这意味着线程将阻塞,直到 Web 客户端收到响应。阻塞代码的问题是由于每个线程消耗了一些内存和 CPU 周期。当出现慢速请求的时候,等待结果的线程会堆积起来,将导致创建更多的线程、消耗更多的资源。频繁切换CPU资源也会降低性能。
  • WebClient是异步、非阻塞的方案。
    • WebClient将为每个事件创建类似于“任务”的东西。在幕后,Reactive 框架会将这些“任务”排队并仅在适当的响应可用时执行它们。
    • WebClient是Spring WebFlux库的一部分。因此,我们还可以使用具有反应类型(Mono和Flux的功能性、流畅的 API 作为声明性组合来编写客户端代码。

END