MySQL问题排查工具介绍

本总结来自美团内部分享,屏蔽了内部数据与工具

知识准备

索引

  • 索引是存储引擎用于快速找到记录的一种数据结构
  • B-Tree,适用于全键值,键值范围或键最左前缀:(A,B,C): A, AB, ABC,B,C,BC
  • 哪些列建议创建索引:WHERE, JOIN , GROUP BY, ORDER BY等语句使用的列
  • 如何选择索引列的顺序:
    1. 经常被使用到的列优先
    2. 选择性高的列优先:选择性=distinct(col)/count(col)
    3. 宽度小的列优先:宽度 = 列的数据类型

慢查询

原因

  1. 未使用索引
  2. 索引不优
  3. 服务器配置不佳
  4. 死锁

命令

看版本

mysql -V 客户端版本 select version 服务器版本

explain 执行计划,慢查询分析神器
  • type

    • const,system: 最多匹配一个行,使用主键或者unique进行索引
    • eq_ref: 返回一行数据,通常在联接时出现,使用主键或者unique索引(内表索引连接类型)
    • ref: 使用key的最左前缀,且key不是主键或unique键
    • range: 索引范围扫描,对索引的扫面开始于某一点,返回匹配的行
    • index:以索引的顺序进行全表扫描,优点是不用排序,缺点是还要全表扫描
    • all: 全表扫描 no no no
  • extra

    • using index : 索引覆盖,只用到索引,可以避免访问表
    • using where: 在存储引擎检索行后再做过滤
    • using temporary:使用临时表,通常在使用GROUP BY,ORDER BY 时出现(严禁)
    • using filesort: 到非索引顺序的额外排序,当order by col未使到索引时发生(严禁)
  • possible_keys: 显示查询可能使用的索引
  • key:优化器决定采用哪个索引来优化对该表的访问
  • rows:MySQL估算的为了找到所需行要检索的数,优化选择key的参考 (不是结果集的行数)
  • key_len: 使用的索引左前缀的长度(字节数),亦可理解为使用了索引中哪些字段
    • 定长字段,int占4个字节、date占3个字节、timestamp占4个字节,char(n)占n个字节
    • NULL的字段:需要加1个字节,因此建议尽亮设计为NOT NULL
    • 变长字段varchar(n),则需要 (n 编码字符所占字节数 + 2 、)个字节,如utf8编码的, 个字符
      占 3个字节,则 度为 n
      3 + 2
  • 强制使用索引: USE INDEX (建议)或 FORCE_INDEX (强制)

show 命令

  • show status
    • 查看select语句的执行数 show global status like ‘Com_select’;
    • 查看慢查询的个数 show global status like ‘Slow_queries’;
    • 表扫描情况 show global status like ‘Handler_read%’; Handler_read_rnd_next / com_select > 4000 需要考虑优化索引
  • show variables
    • 查看慢查询相关的配置 show variables like ‘long_query_time’;
    • 将慢查询时间线设置为2s set global long_query_time=2;
    • 查看InnoDB缓存 show variables like ‘innodb_buffer_pool_size’;
    • 查看InnoDB缓存的使用状态 show status like ‘Innodb_bufferpool%’; 缓存命中率=(1-Innodb_buffer_pool_reads/ Innodb_buffer_pool_read_requests) 100%;缓存率=(Innodb_buffer_pool_pages_data/ Innodb_buffer_pool_pages_total) 100%
    • SHOW PROFILES;该命令可以trace在整个执行过程中各资源消耗情况(会话级)
    • SHOW PROCESSLIST; 查看当前有哪些线程正在运行,并且处在何种状态
    • SHOW ENGINE INNODB STATUS; 可用于分析死锁,但需要super权限

美团分享总结:系统性能优化之道

典型性能问题

  1. 响应慢:你这个服务, 总是响应超时,尽快解决下!
  2. 单机容量低:就这么点量, 还要加机器?
  3. 并发能力弱:这个服务并发怎么上不去呢, 查下为啥

性能优化方法论

数据驱动

系统诊断

如何选择工具

性能工具

性能诊断层次

  • 系统层:OS JVM CPU Memory Network Disk | top jstat iftop iostat dstat…
  • 组件层:Jetty DB Driver JSON Lib… | JProfiler Mtrace
  • 业务层: 业务逻辑 数据结构 算法 | 日志 Jstack Greys

例子:首页超时了

  • 排查网关问题
    • 网络延迟数据
  • 排查后端服务问题
    • 从接入层(API层)开始检查, 首页调用链各个环节的延迟, 负载指标
    • 假设其中一个环节(比如POI 服务负载高, 响应异常). 检查服务器系统指标, OCTO 性能指标, CAT 监控数据, 日志数据

参考手册

单机容量上不去

CPU

  • 如何识别:load、cpu使用率、 CPU.Steal()
  • 如何诊断
    • top -bH -p -n 1 | head -n10
    • stack
    • jstat
    • JProfiler (能够精确定位,可以定位到具体代码消耗多少时间)

      内存

  • 如何识别:mem指标、swap、jvm.gc.count …
  • 如何诊断:
    • jstat
    • jmap
  • 精确定位:MAT

    网络

  • 如何识别:net.if.*; TcpExt.ListenOverflows ;
  • 如何诊断:
    • netstat
    • iftop

响应时间慢

下游依赖方

db、缓存、服务

同步调用

逻辑实现

  • 循环调用
  • 本地方法耗时过长 Greys可以分析耗时

并发上不去

  • 资源瓶颈:线程池,连接池 (JProfiler检查线程)
  • 资源竞争:cpu切换,锁 (线程池并发模型 -> 异步并发模型)

Java 快速诊断性能瓶颈, 首选 JProfiler

利用druid sql parser搞一些事情

在最近的项目开发中,有这样一个需求,就是给定一个查询的sql,在where语句中添加几个条件语句。刚开始想的是,是否能用正则去做这个事情呢?其实不用语法树还是有一点困难的。

经过一系列google,看到了我们国产的druid里面sql parse的稳当还是比较详尽。具体参考这个文档SQL Parser

还是回到之前的需求:

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
public List<Map<String, Object>> search(String sql, Map<String, Object> conditions) {
List<Map<String, Object>> result = new ArrayList<>();
// SQLParserUtils.createSQLStatementParser可以将sql装载到Parser里面
SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, JdbcUtils.MYSQL);
// parseStatementList的返回值SQLStatement本身就是druid里面的语法树对象
List<SQLStatement> stmtList = parser.parseStatementList();
SQLStatement stmt = stmtList.get(0);
if (stmt instanceof SQLSelectStatement) {
// convert conditions to 'and' statement
StringBuffer constraintsBuffer = new StringBuffer();
Set<String> keys = conditions.keySet();
Iterator<String> keyIter = keys.iterator();
if (keyIter.hasNext()) {
constraintsBuffer.append(keyIter.next()).append(" = ?");
}
while (keyIter.hasNext()) {
constraintsBuffer.append(" AND ").append(keyIter.next()).append(" = ?");
}
SQLExprParser constraintsParser = SQLParserUtils.createExprParser(constraintsBuffer.toString(), JdbcUtils.MYSQL);
SQLExpr constraintsExpr = constraintsParser.expr();
SQLSelectStatement selectStmt = (SQLSelectStatement) stmt;
// 拿到SQLSelect 通过在这里打断点看对象我们可以看出这是一个树的结构
SQLSelect sqlselect = selectStmt.getSelect();
SQLSelectQueryBlock query = (SQLSelectQueryBlock) sqlselect.getQuery();
SQLExpr whereExpr = query.getWhere();
// 修改where表达式
if (whereExpr == null) {
query.setWhere(constraintsExpr);
} else {
SQLBinaryOpExpr newWhereExpr = new SQLBinaryOpExpr(whereExpr, SQLBinaryOperator.BooleanAnd, constraintsExpr);
query.setWhere(newWhereExpr);
}
sqlselect.setQuery(query);
sql = sqlselect.toString();
Session session = sessionFactory.openSession();
SQLQuery sqlQuery = session.createSQLQuery(sql);
Collection values = conditions.values();
int index = 1;
for (Object value : values) {
sqlQuery.setParameter(index, value);
index++;
}
result = sqlQuery.list();
session.close();
} else {
throw new Exception("not select statement");
}
return result;
}

数据库友好的树结构设计

在最近的系统设计中,涉及到对组织对象或者目录对象组成的树进行重构。我们第一版本的设计参考了这篇文章的设计Managing Hierarchical Data in MySQL

最简单朴素的树结构设计是这样的:

1
2
3
4
5
CREATE TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL
);

但是这种设计对于你想拿一个节点下的子树这种操作不太友好。虽然这种对子树操作比如移动,删除这种还是比较友好的。但是考虑到,组织树这种很少修改,而获取子树这种操作比较多。所以就采取了这篇文章推荐的方案。

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
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
INSERT INTO nested_category VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
SELECT * FROM nested_category ORDER BY category_id;
+-------------+----------------------+-----+-----+
| category_id | name | lft | rgt |
+-------------+----------------------+-----+-----+
| 1 | ELECTRONICS | 1 | 20 |
| 2 | TELEVISIONS | 2 | 9 |
| 3 | TUBE | 3 | 4 |
| 4 | LCD | 5 | 6 |
| 5 | PLASMA | 7 | 8 |
| 6 | PORTABLE ELECTRONICS | 10 | 19 |
| 7 | MP3 PLAYERS | 11 | 14 |
| 8 | FLASH | 12 | 13 |
| 9 | CD PLAYERS | 15 | 16 |
| 10 | 2 WAY RADIOS | 17 | 18 |
+-------------+----------------------+-----+-----+

具体模型如图所示

Nested Set Model

这种你要想获取一个子树,只要限定lft和rgt的范围就好

1
2
3
4
5
6
SELECT node.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = 'ELECTRONICS'
ORDER BY node.lft;

从代码上,简练了很多。但是考虑数据库性能方面就有一个问题。比如,为了数据库加速,是不是要给lft和rgt做索引?虽然我们可以做索引,但是是非聚合索引,也就是说在磁盘上不是连续的。获取多行的时候,还是要涉及随机读写。

为了加强连续读写的性能,我们利用前缀树这种方式建立了树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `pe_organ` (
`parent` varchar(255) NOT NULL,
`current` varchar(255) NOT NULL,
`level` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`site_id` varchar(10) NOT NULL,
PRIMARY KEY (`parent`,`current`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
parent current level
NULL /1-1 1
NULL /1-2 1
NULL /1-3 1
/1-1 /1-1/2-1 2
/1-1 /1-1/2-2 2
/1-1/2-1 /1-1/2-1/3-1 3

parent为父亲节点的路径,current表示当前节点的路径。如果我们想拿/1-1的所有子节点的话只要

1
SELECT * FROM pe_organ WHERE parent LIKE '/1-1%'

这个查询是走索引的。

同时由于我们建立了联合主键,获取的子节点在硬盘的安排上是连续的

至于每个级别子路径的设计 我们采取了 level-number的模式

需要注意,这种树的设计在移动树的方面还是比较费劲的,这种设计的主要目的是加速子树查询。

spirng-boot中,基于既有的token验证方式,利用spring-security实现权限系统

用过spring-security的都应该能感觉到,spring-security把authentication和authorization封装的比较死。默认的authorization是基于session的。利用session验证过的信息,保存进SecurityContext,权限系统再根据SecurityContext保存的用户权限相关信息,来进行权限管理。

但是在目前的场景中,服务器端往往要满足多端的验证方式,session的方式不容易和移动端配合的好。更多的是用一个token放在http header中进行验证。这种就需要绕开spring-security默认的authentication直接利用它的authorization。

在这里我演示一个在spring-security做方法级别拦截的方案。

这里就是基于token的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
29
30
31
32
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(1)
public class TokenBasedSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
try {
http.addFilterBefore(... SecurityContextPersistenceFilter.class);
http.securityContext().securityContextRepository(new SecurityContextRepository() {
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
...
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
...
}
@Override
public boolean containsContext(HttpServletRequest request) {
...
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}

在这里,我们要做的其实就是设置重在SecurityContextRepository。这个实体在spring security启动中要传递给SecurityContextPersistenceFilter。这个filter根据request来加载SecurityContext。而SecurityContextPersistenceFilter就是从其内部的SecurityContextRepository来加载SecurityContext的。所以我们就需要重载上面代码中的三个方法,根据request来构造SecurityContext

我们再来看一下SecurityContext到底封装了什么。

1
2
3
4
5
6
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}

Authentication而已。

public interface Authentication extends Principal, Serializable {

Collection<? extends GrantedAuthority> getAuthorities();

Object getCredentials();

Object getDetails();

Object getPrincipal();

boolean isAuthenticated();


void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

在这里我们还要构造一个机遇token的Authentication接口的实现。在实现中对于权限来说很有用的就是getAuthorities方法。我们只要给其封装最简单的SimpleGrantedAuthority就好了。

这样我们就可以给我们的Controller方法做拦截了~

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping(value = "test")
public class TestController {
@PreAuthorize("hasAuthority('super_admin')")
@RequestMapping(value = "hello", method = RequestMethod.GET)
public String superHello(@RequestParam String domain) {
return new String("super hello");
}
}

终于拿到证书了

经过两个月的小努力,终于拿到Cousera机器学习课的证书了。虽然以前也上过模式识别课,但是还是觉得这么课程学到了很多。一方面Andrew Ng教授能用浅显的语言讲出Insight,另一方面本课程的作业确实挺能巩固知识,同时给人成就感的。最后,秀一下证书

证书

利用collectd, influxdb和grafana进行简单的负载预警

本文仅仅是对负载预警的简单尝试,能够预测的场景也比较有限,但是作为预测工作的开始已经比较能够说明问题了。

利用collectd, influxdb和grafana进行监控系统搭建可以参考这篇文章Monitoring hosts with CollectD, InfluxDB and Grafana grafana的操作比nagios和cacti真的友好很多,可定制的能力也强很多。

负载监控

虽然grafana有一定的报警能力(在grafana4.0版本之后,alert模块直接继承进来,所以推荐4.0的版本),但是能够提前十几分钟对于集群负载超标进行预警,一直是我们的一个小目标。所以在这里,我们就开始为这个小目标做了一点小努力。

在实际运行中,我们安装了这个小插件。它能够对应用负载进行批量的显示,节约了好多体力活。

图片描述

通过分析,我们可以看到以下几种负载

简单趋势型

图片描述

周期型

图片描述

规律不明显型

而对于周期型我们也可以看到在短期是可以看到一定趋势的。

在这里我们仅采用线性回归对简单趋势进行预测。这点对于python很好实现

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
from datetime import datetime
from influxdb import DataFrameClient
import numpy as np
from scipy import stats
if __name__ == "__main__":
host = 'localhost'
port = 8086
user = 'root'
password = 'root'
dbname = 'collectd'
client = DataFrameClient(host, port, user, password, dbname)
print("Create pandas DataFrame")
start_time = datetime(2016, 11, 17, 3,10).timestamp()
end_time = datetime(2016, 11, 17, 8,50).timestamp()
query = "select * from load_midterm where host='web153' and time > " + str(int(start_time)) + "s and time < " \
+ str(int(end_time)) + "s"
df = client.query(query)
slope, intercept, r_value, p_value, std_err = stats.linregress(range(df['load_midterm'].shape[0]), df['load_midterm']['value'].values)
print(slope)
print(intercept)
print(r_value)
print(p_value)

在这里我们用的是统计模块得到斜率和截距。需要注意的是,我们还要对R2,p值以及方差进行评价。一般来说r^2 >0.7, p<0.05, std_err<0.1

在这里,为什么没有采用一些更复杂的机器学习方法呢?

无论采用什么模型,关键是要提取一些关键feature。我观察过简单的应用服务器的其他指标,包括tcp连接数,cpu,磁盘io等,与load的协同性比较高,很难成为15min后load的先导预测feature。如果从服务器集群的整体角度去找feature的话,也许是可以做的,未来会关注这块。

在具体实现上,后端基于python flask编写一个预测服务器,而前端开发可以基于grafana的clock插件进行开发:clock-panel

安利一个运维监控平台grafana的插件grafana-influx-dashboard

在这安利一个grafana的监控插件grafana-influx-dashboard 使用方法直接在github主页就能看到

利用grafana+collectd+influxdb快速搭建监控系统是很多创业公司采用的一个方案,网上也有很多相关教程。但是对于管理几十台上百台机器的话,一台一台配置graph确实好麻烦。grafana提供了相应的scripted template编程方式。但是编写起来还是要花一些时间,尤其对于前端经验匮乏的运维来说还是有一定难度。这个grafana的插件利用js模板直接获得所有主机,通过输入不同的get参数能进行多样的监控选择,废话不说,直接上效果。

主机列表
getdash.jpg

某台机器的所有指标

web062.jpg

所有主机的load
load.jpg

有时间了我也会对这个插件做二次开发

angularjs利用ui-route异步加载组件

ui-route相比于angularjs的原生视图路由更好地支持了路由嵌套,状态转移等等。随着视图不断增加,打包的js体积也会越来越大,比如我在应用里面用到了wangeditor里面单独依赖的jquery就300多k。异步加载各个组件就很有必要。在这里我就以ui-route为框架来进行异步加载说明。

首先看一下路由加载文件

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
angular.module('webtrn-sns').config(['$stateProvider', function ($stateProvider) {
$stateProvider.state({
name: 'home.message',
url: '/message',
abstract: true,
templateProvider: ['resources', function (resources) {
return resources.template
}],
controllerProvider: ['resources', (resources)=> {
return resources.controller
}],
onEnter: ['resources', (resources)=>resources.css.use()],
onExit: ['resources', (resources)=>resources.css.unuse()],
resolve: {
resources: ()=> {
return new Promise(
resolve => {
require([], () => {
resolve({
css: require('./css/message_box.css'),
template: require('./html/message_box.html'),
controller: require('./js/message_box.js')
})
})
}
);
}
}
}
).state({
name: 'home.message.add_message',
url: '/add_message?isReply&toUid&title',
params: {isReply: null, toUid: null, title: null},
templateProvider: ['resources', function (resources) {
return resources.template
}],
controllerProvider: ['resources', (resources)=> {
return resources.controller
}],
onEnter: ['resources', (resources)=>resources.css.use()],
onExit: ['resources', (resources)=>resources.css.unuse()],
resolve: {
resources: ()=> {
return new Promise(
resolve => {
require(['./js/message.js'], () => {
resolve({
css: require('./css/add_message.css'),
template: require('./html/add_message.html'),
controller: require('./js/add_message.js')
})
})
}
);
}
}
}
)
}])

这个是路由状态的一个声明文件,name,url,param字段的方式不变,关键是看resolve这个部分。根据ui-route的resolve文档,resolve是为了给state或者controller进行自定义注入对象的。

下面是举出文档中关于resolve的例子:

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
$stateProvider.state('myState', {
resolve:{
// Example using function with simple return value.
// Since it's not a promise, it resolves immediately.
simpleObj: function(){
return {value: 'simple!'};
},
// Example using function with returned promise.
// This is the typical use case of resolve.
// You need to inject any services that you are
// using, e.g. $http in this example
promiseObj: function($http){
// $http returns a promise for the url data
return $http({method: 'GET', url: '/someUrl'});
},
// Another promise example. If you need to do some
// processing of the result, use .then, and your
// promise is chained in for free. This is another
// typical use case of resolve.
promiseObj2: function($http){
return $http({method: 'GET', url: '/someUrl'})
.then (function (data) {
return doSomeStuffFirst(data);
});
},
// Example using a service by name as string.
// This would look for a 'translations' service
// within the module and return it.
// Note: The service could return a promise and
// it would work just like the example above
translations: "translations",
// Example showing injection of service into
// resolve function. Service then returns a
// promise. Tip: Inject $stateParams to get
// access to url parameters.
translations2: function(translations, $stateParams){
// Assume that getLang is a service method
// that uses $http to fetch some translations.
// Also assume our url was "/:lang/home".
return translations.getLang($stateParams.lang);
},
// Example showing returning of custom made promise
greeting: function($q, $timeout){
var deferred = $q.defer();
$timeout(function() {
deferred.resolve('Hello!');
}, 1000);
return deferred.promise;
}
},
// The controller waits for every one of the above items to be
// completely resolved before instantiation. For example, the
// controller will not instantiate until promiseObj's promise has
// been resolved. Then those objects are injected into the controller
// and available for use.
controller: function($scope, simpleObj, promiseObj, promiseObj2, translations, translations2, greeting){
$scope.simple = simpleObj.value;
// You can be sure that promiseObj is ready to use!
$scope.items = promiseObj.data.items;
$scope.items = promiseObj2.items;
$scope.title = translations.getLang("english").title;
$scope.title = translations2.title;
$scope.greeting = greeting;
}
})

我们可以看到resolve的对象是支持Promise的。

再回到我们之前的代码templateProvidercontrollerProvider我们注入了resources的模板对象和controller对象,onEnteronExit注入了css模块。

如果controller中依赖了服务怎么办的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
resolve: {
resources: ()=> {
return new Promise(
resolve => {
require(['./js/message.js'], () => {
resolve({
css: require('./css/add_message.css'),
template: require('./html/add_message.html'),
controller: require('./js/add_message.js')
})
})
}
);
}
}

可以在require里面将服务注入,如代码中的message.js。而为了将服务进行异步加载我们不能用普通的.factory或者.service。而需要调用$provide.factory或者$provide.service

如果采用webpack进行编译打包的话就需要webpack.optimize.CommonsChunkPlugin的支持,这样可以对js进行拆分打包,达到异步加载js的目的。

spring-boot单元测试

本文仅适用spring-boot 1.4版本以后的写法,至于1.4以前的版本,还是建议升级到1.4:)

如果仅仅是直接调用接口函数进行测试的话,非常简单增加。在测试类增加一些注解就好了。利用@Autowired也能将接口给注解进来。

1
2
3
4
5
6
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApiTest {
@Autowired
MessageApi messageApi;
...

同时spring-boot-test也直接支持mockmvc。如果测试controller的话会用得到,像下面这样就就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testControllerMethods() {
MvcResult result = mockMvc.perform(get("/get-receive-message-abstracts").param("siteId", "webtrn").param("uid", "lucy")
.param("limit", "100")).andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(10))).andExpect(jsonPath("$[9].title", is("hello0"))).andReturn();
}

其中MockMvc可以模拟http对于controller的请求主要用到的函数在我的测试用例里面都列出来了。大家开发的时候直接看javadoc就好了。

一个java web程序员,希望自己两年之内能成为data scientist,正在找工作