1. 项目概述:为什么JDBC Request的Query Type如此关键?
如果你正在用JMeter做数据库性能测试,或者想验证某个后端服务的数据库操作瓶颈,那你肯定绕不开JDBC Request这个采样器。很多朋友在初次使用时,可能会觉得:这不就是写个SQL语句吗?把SQL往“Query”框里一贴,运行,完事。但实际跑起来,你可能会遇到各种“诡异”的问题:为什么我的插入语句没生效?为什么参数化查询结果不对?为什么明明调用了存储过程却拿不到输出参数?这些问题,十有八九都出在“Query Type”这个看似不起眼的下拉框上。
我见过不少测试脚本,因为Query Type选错,导致整个压测场景的数据模型完全失真。比如,你以为在压测一个复杂的多表关联查询,结果因为Query Type设置不当,JMeter可能只执行了第一条语句,或者把所有语句当成一个更新操作来处理,响应时间和TPS数据自然就失去了参考价值。所以,今天我们就来彻底拆解JDBC Request中的Query Type。这不是一个简单的配置项,它直接决定了JMeter如何与数据库驱动交互、如何解析你的SQL、以及最终如何定义一次“请求”的边界。理解它,是写出可靠、准确的数据库测试脚本的第一步。
2. Query Type深度解析:不只是“选择”那么简单
2.1 Query Type的完整清单与核心定义
打开JDBC Request的配置界面,在“Query”文本框下方,你会看到一个下拉列表,里面通常包含以下选项(不同版本的JMeter或不同数据库驱动可能略有差异,但核心如下):
- Select Statement
- Update Statement
- Callable Statement
- Prepared Select Statement
- Prepared Update Statement
- Commit
- Rollback
- AutoCommit(false)
- AutoCommit(true)
光看名字,似乎“Select”对应查询,“Update”对应增删改,“Callable”对应存储过程。但实际区别远不止于此。它们的本质差异,在于JMeter底层调用的是JDBC API中不同的
Statement
接口。
-
Select Statement / Update Statement
:对应JDBC的
Statement接口。这是最基础、最“原始”的方式。JMeter会把你写在“Query”框里的字符串,原封不动地交给数据库驱动去执行。如果你的SQL是动态拼接的(比如用JMeter变量),那么每次执行都会生成一条全新的SQL语句发送给数据库。 -
Prepared Select Statement / Prepared Update Statement
:对应JDBC的
PreparedStatement接口。这是 强烈推荐 在性能测试中使用的方式。你需要使用?作为占位符,然后在下方的“Parameter values”和“Parameter types”中绑定具体的值和数据类型。数据库会对带占位符的SQL进行预编译,后续即使传入不同的参数值,也无需再次编译SQL,极大提升了执行效率,同时也是防范SQL注入的标准做法。 -
Callable Statement
:对应JDBC的
CallableStatement接口,专用于调用数据库的存储过程或函数。你需要按照数据库方言编写调用语句,例如{call my_procedure(?, ?)},并可以处理输入(IN)、输出(OUT)和输入输出(INOUT)参数。 - Commit / Rollback / AutoCommit :这几个是事务控制类型。它们并不执行用户SQL,而是向数据库发送事务控制指令。这在测试需要显式控制事务边界的业务场景时至关重要。
注意 :
Statement和PreparedStatement的选择,在性能测试中是天壤之别。使用Statement进行压测,数据库可能会因为反复解析、编译大量相似的SQL而耗尽CPU资源,这本身就成了一个巨大的性能瓶颈,导致你的测试结果反映的不是业务逻辑的瓶颈,而是数据库SQL解析的瓶颈。因此, 只要SQL结构固定、仅参数变化,务必使用Prepared Statement类型。
2.2 不同Query Type的底层执行机制与性能影响
为了更直观地理解,我们来看看在不同Query Type下,JMeter、JDBC驱动和数据库之间发生了什么。
假设我们要查询用户状态,SQL逻辑是:
SELECT * FROM users WHERE status = ‘ACTIVE’ AND region = ?
。其中
region
是变量。
-
场景A:使用“Select Statement”
-
JMeter端
:每次线程迭代时,使用
${region}变量拼接SQL,得到如SELECT * FROM users WHERE status = ‘ACTIVE’ AND region = ‘North’的完整字符串。 - 网络传输 :每次发送的都是完整的、不同的SQL字符串。
- 数据库端 :每次收到SQL,都需要进行 语法解析、语义检查、生成执行计划(可能包含硬解析) ,然后才执行。在高并发下,数据库的解析开销巨大。
-
JMeter端
:每次线程迭代时,使用
-
场景B:使用“Prepared Select Statement”
-
JMeter端
:Query中写入
SELECT * FROM users WHERE status = ‘ACTIVE’ AND region = ?。在“Parameter values”中设置值为${region},“Parameter types”设为VARCHAR。 - 网络传输 :首次执行时,发送带占位符的SQL模板。后续执行,通常只发送参数值和参数类型标识。
- 数据库端 :首次收到模板时,进行 一次性的解析和编译,生成执行计划并缓存 。后续即使参数值变化,数据库直接使用缓存的计划,仅绑定新参数即可执行(软解析)。并发压力下,性能优势极其明显。
-
JMeter端
:Query中写入
我曾经在一个用户登录验证的场景中做过对比测试,该场景需要根据用户名查询密码盐值和哈希。使用
Prepared Select Statement
相比
Select Statement
,在500并发下,数据库服务器的CPU使用率降低了近40%,平均响应时间减少了约60%。这个差距在真实的压测中是不可忽视的。
3. 核心细节解析与实操要点
3.1 Prepared Statement的参数绑定:细节决定成败
使用Prepared Statement时,参数绑定是核心操作,也是最容易出错的地方。
1. 参数值(Parameter values):
这是一个多行文本框,每一行对应SQL中一个
?
占位符的值。值可以是常量,也可以是JMeter变量(如
${user_id}
)。这里有个关键点:
参数值本身不需要加引号
。即使数据库字段类型是字符串(VARCHAR),你也不应该在值两边写单引号。JMeter和JDBC驱动会根据参数类型自动处理。
-
错误示例
:SQL:
SELECT * FROM products WHERE name = ?; Parameter values:'Laptop' -
正确示例
:SQL:
SELECT * FROM products WHERE name = ?; Parameter values:Laptop
2. 参数类型(Parameter types):
同样是一个多行文本框,必须与“Parameter values”逐行对应。它告诉JDBC驱动以何种JDBC类型(
java.sql.Types
中的常量)来传递参数。常见的类型有:
-
VARCHAR或CHAR(字符串) -
INTEGER(整数) -
BIGINT(长整数) -
DOUBLE或FLOAT(浮点数) -
DATE,TIME,TIMESTAMP(日期时间) -
BOOLEAN(布尔值,注意并非所有数据库都原生支持)
类型必须匹配
。如果你在数据库中定义了一个
INT
类型的
user_id
字段,那么在Parameter types里就应该写
INTEGER
,而不是
VARCHAR
。类型不匹配可能导致索引失效(数据库进行隐式类型转换),在压测中这会严重扭曲性能表现。
3. 参数化变量与CSV数据集配置器的联动: 这是实战中最常用的模式。通常,我们会使用“CSV Data Set Config”元件来读取测试数据文件。
-
在CSV文件中定义好列,如
userId,userName。 -
在JDBC Request的“Parameter values”中,直接引用CSV变量:
${userId}。 -
在“Parameter types”中,根据数据库表结构,写上对应的JDBC类型,如
INTEGER,VARCHAR。 这样,每个虚拟用户(线程)在每次循环时,都会自动获取一组新的参数值并执行预编译的SQL,完美模拟真实场景。
3.2 Callable Statement:调用存储过程的完整流程
当你的业务逻辑封装在数据库存储过程中时,就需要使用
Callable Statement
。它的配置比Prepared Statement更复杂一些。
1. SQL调用格式: 你需要按照JDBC标准编写调用语句。不同数据库的格式高度统一:
-
调用无返回值的存储过程
:
{call proc_name(?, ?)} -
调用有返回值的函数
:
{? = call func_name(?, ?)}大括号{}是必须的,这是JDBC转义语法,驱动会将其转换为数据库原生的调用语句。
2. 参数注册(Parameter values & types):
对于
IN
参数,填写方式和Prepared Statement完全一样。
对于
OUT
或
INOUT
参数,这里是关键!你需要在“Parameter values”中,为输出参数
预留位置
,但填写的不是具体值,而是一个
标识符
。通常,我们可以简单地填写
OUT
或
INOUT
作为提示,但实际起作用的配置在另一个地方。
3. 输出参数的类型注册(容易被忽略!): JDBC Request面板上的“Parameter values”和“Parameter types”对于输出参数是不够的。你必须使用一个名为**“JDBC Request”采样器自带的“Output Variables”功能(在较新版本中可能在高级选项里),或者更通用、更可靠的做法是:使用“用户参数”或“JSR223 PreProcessor”来预先注册输出参数。** 更常见的实战做法是使用**“JSR223 PreProcessor”**配合Groovy脚本,在采样器执行前动态注册输出参数。因为JMeter的GUI对Callable Statement的输出参数支持并不直观。
一个典型的Callable Statement实战步骤:
-
添加一个
JSR223 PreProcessor到JDBC Request采样器之下。 -
在PreProcessor中编写Groovy脚本,获取当前采样器的
CallableStatement对象,并使用registerOutParameter方法注册输出参数。
实际上,由于JMeter架构的限制,在采样器执行前很难直接操作其内部的Statement对象。因此, 更简单可靠的实战方案是:将调用存储过程并处理输出参数逻辑,写在一个单独的“JSR223 Sampler”中,使用纯Java JDBC代码来实现 。这样可以获得完全的控制权。对于复杂的、依赖输出参数的存储过程测试,我通常建议绕过JMeter的JDBC Request图形界面,直接使用JSR223 Sampler编写JDBC代码,虽然失去了图形化配置的便利,但换来了绝对的准确性和灵活性。import java.sql.Types // 假设你的JDBC Request采样器变量名为‘jdbcSampler’ def sampler = ctx.getCurrentSampler() if (sampler instanceof org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler) { // 这里比较棘手,因为JMeter在PreProcessor阶段并未创建Statement对象。 // 更实际的做法是在一个前置的JDBC Request中调用存储过程,并将结果存入变量。 }
3.3 事务控制类型:Commit, Rollback, AutoCommit
这三个类型用于管理数据库事务,在测试转账、订单创建等需要原子性操作的业务时非常重要。
- Commit :提交当前事务。执行此操作后,之前所有未提交的DML(增删改)操作将永久生效。
- Rollback :回滚当前事务。执行此操作后,之前所有未提交的DML操作将被撤销。
-
AutoCommit(false)
:将数据库连接的自动提交模式设置为
false。此后,你需要显式地执行Commit或Rollback来结束事务。这是测试多语句事务场景的 前提 。 -
AutoCommit(true)
:将自动提交模式恢复为
true(默认值)。每条SQL语句都会作为一个独立的事务立即提交。
实战应用模式: 假设你要测试一个“下订单并扣库存”的事务。
-
首先添加一个JDBC Request,Query Type选择
AutoCommit(false)。 -
然后添加一系列
Prepared Update Statement,模拟插入订单、更新库存等操作。 -
最后,再添加一个JDBC Request,Query Type选择
Commit。 -
为了模拟失败场景,你可以在某些线程中,在更新库存后,不执行
Commit而执行Rollback。
这样,你就能精确控制事务边界,测试数据库在事务处理下的性能和锁竞争情况。 务必注意 :在压测过程中,如果开启了未提交的事务且未正确关闭,可能会导致数据库连接被占用、锁堆积,从而耗光连接池或造成死锁。因此,务必在事务型测试场景的线程组或逻辑控制器末尾,确保事务被正确提交或回滚。
4. 实操过程与核心环节实现
4.1 构建一个完整的JDBC性能测试计划
让我们从一个具体的例子出发,构建一个测试用户登录验证的JDBC测试计划。假设我们有一个
users
表,结构为
(id INT, username VARCHAR, password_hash VARCHAR, salt VARCHAR)
。登录时,后端会根据用户名查询盐值和哈希密码进行验证。
步骤1:环境准备与驱动配置
-
将你的数据库驱动JAR包(如
mysql-connector-java-8.0.33.jar)放入JMeter的/lib目录下。 - 启动JMeter,新建一个测试计划。
步骤2:创建线程组与连接配置
- 添加一个 线程组 ,设置线程数(虚拟用户数)、Ramp-Up时间和循环次数。
-
在线程组下添加一个
JDBC Connection Configuration
。
-
Variable Name
:
user_db(这个名称后面会用到,必须唯一)。 -
Database URL
: 根据你的数据库填写,如
jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC。这里添加连接参数是为了避免常见的时区和编码问题。 -
JDBC Driver Class
:
com.mysql.cj.jdbc.Driver(MySQL 8.x驱动类,注意与5.x不同)。 - Username/Password : 你的数据库账号密码。
-
其他配置
:
- Max Number of Connections : 连接池最大大小,建议设置为略大于你的并发线程数,比如线程数*1.2。
-
Transaction Isolation
: 保持默认
DEFAULT即可,除非你需要测试特定隔离级别。 -
Test While Idle
和
Validation Query
: 对于长时间压测,建议勾选
Test While Idle,并设置一个简单的Validation Query,如SELECT 1,以便连接池定期验证连接有效性。
-
Variable Name
:
步骤3:准备测试数据(CSV文件)
-
创建一个
test_users.csv文件,内容如下:username,expected_password alice,hashed_pwd_1 bob,hashed_pwd_2 charlie,hashed_pwd_3 -
在线程组下添加一个
CSV Data Set Config
。
-
Filename
: 指向你的
test_users.csv文件。 -
Variable Names
:
username,expected_pwd -
Delimiter
:
,(逗号) -
Recycle on EOF?
:
True(如果数据量小于循环次数,则从头开始) -
Stop thread on EOF?
:
False -
Sharing mode
:
All threads(所有线程共享同一份数据,顺序读取)
-
Filename
: 指向你的
步骤4:添加核心的JDBC Request采样器
-
添加一个
JDBC Request
。
-
Name
:
Query User by Username (Prepared) -
Variable Name
:
user_db(与连接配置中的名称一致)。 -
Query Type
:
Prepared Select Statement(这是性能测试的最佳选择)。 -
SQL Query
:
SELECT id, password_hash, salt FROM users WHERE username = ? -
Parameter values
:
${username} -
Parameter types
:
VARCHAR -
Variable names
(这是一个非常重要的字段!): 在此处填写
user_id, pwd_hash, salt。JMeter会将查询结果集的第一行的各列,依次赋值给这些变量。如果查询返回多行,只有第一行会被赋值。 -
Result variable name
: 可以留空,或者填写一个变量名如
queryResult。如果填写,整个结果集对象会被存储到这个变量中,供后续的JSR223元件进行更复杂的处理。
-
Name
:
步骤5:添加断言与监听器
-
在JDBC Request下添加一个
响应断言
,来验证查询是否成功返回了数据。
-
Apply to
:
Main sample and sub-samples -
Field to Test
:
Response Code(JDBC请求的响应码是消息,如OK) -
Pattern Matching Rules
:
Equals -
Patterns to Test
:
OK
-
Apply to
:
-
再添加一个
BeanShell断言
或
JSR223断言
(推荐JSR223,性能更好),来验证查询到的密码哈希是否与预期一致。
-
语言
:
groovy -
脚本
:
// 从JMeter变量中获取查询结果和CSV中的预期值 def actualHash = vars.get("pwd_hash"); def expectedHash = vars.get("expected_pwd"); // 简单的字符串比较(实际中可能是更复杂的密码验证逻辑) if (actualHash == null || !actualHash.equals(expectedHash)) { Failure = true; FailureMessage = "Password hash mismatch for user: " + vars.get("username") + ". Expected: " + expectedHash + ", Actual: " + actualHash; }
-
语言
:
- 最后,添加你需要的监听器,如 聚合报告 、 查看结果树 (调试用,压测时请禁用)、 图形结果 等。
4.2 复杂查询与结果集处理
上面的例子是单行结果。如果查询返回多行多列,该如何处理?
1. 使用“Variable names”处理固定列数的多行结果:
“Variable names”字段在处理多行结果时能力有限。假设查询返回3列,你设置了
var1,var2,var3
。JMeter会这样处理:
-
var1= 第一行第一列的值 -
var2= 第一行第二列的值 -
var3= 第一行第三列的值 -
var1_#= 结果集的行数(#是行数计数器) -
var1_1,var2_1,var3_1= 第一行的三列值 -
var1_2,var2_2,var3_2= 第二行的三列值 - ...以此类推。
你可以通过
${var1_#}
获取总行数,然后通过循环控制器(如
ForEach
控制器)配合计数器来遍历所有行。但这比较繁琐。
2. 使用“Result variable name”配合JSR223进行灵活处理(推荐): 这是处理复杂结果集更强大的方式。
-
在JDBC Request的“Result variable name”中填入一个变量名,例如
rs。 - 在JDBC Request后添加一个 JSR223 PostProcessor 。
-
编写Groovy脚本处理结果集:
import java.sql.ResultSet // 获取JDBC采样器返回的结果集对象 def resultSet = prev.getObject("rs") as ResultSet def userList = [] if (resultSet != null) { while (resultSet.next()) { def userId = resultSet.getInt("id") def userName = resultSet.getString("username") userList.add("$userId:$userName") // 也可以将数据存入JMeter变量数组,供后续使用 // vars.put("userId_" + resultSet.getRow(), userId.toString()) // vars.put("userName_" + resultSet.getRow(), userName) } // 将处理后的数据存入一个变量,方便查看或传递给其他元件 vars.put("processedUserList", userList.join(";")) log.info("Processed users: " + vars.get("processedUserList")) }
这种方式给了你完全的控制权,可以进行过滤、转换、聚合等任何操作。
5. 常见问题与排查技巧实录
在实际使用JDBC Request进行压测时,你会遇到各种各样的问题。下面是我总结的一些高频问题及其排查思路。
5.1 连接类问题
问题1:
Cannot create PoolableConnectionFactory (Communications link failure)
- 现象 :测试启动时报错,无法创建数据库连接。
-
排查
:
-
检查网络与端口
:确认Database URL中的IP和端口是否正确,数据库服务器是否可达(
telnet ip port)。 -
检查驱动类名
:尤其是MySQL 5.x和8.x的驱动类名不同(
com.mysql.jdbc.Drivervscom.mysql.cj.jdbc.Driver)。查看驱动JAR包的META-INF/services/java.sql.Driver文件确认。 -
检查驱动JAR位置
:确保驱动JAR在JMeter的
/lib目录下,并且没有版本冲突。 -
检查URL格式
:JDBC URL格式必须正确。例如MySQL 8.x可能需要添加时区参数:
serverTimezone=UTC。 - 检查数据库权限 :确认使用的用户名和密码有从测试机连接到目标数据库的权限。
-
检查网络与端口
:确认Database URL中的IP和端口是否正确,数据库服务器是否可达(
问题2:
Connection is closed
或
No operations allowed after connection closed
- 现象 :压测运行一段时间后,开始大量报错。
-
排查
:
-
检查连接池配置
:在JDBC Connection Configuration中,合理设置
Max Wait(获取连接的最大等待时间)、Validation Query和Test While Idle。网络不稳定或数据库主动断开空闲连接时,这些配置能帮助连接池自动恢复。 -
检查防火墙或中间件超时
:数据库服务器或中间的负载均衡器、代理可能有空闲连接超时设置,时间短于你的测试间隔。尝试在JDBC URL中添加连接保活参数,如MySQL的
autoReconnect=true&failOverReadOnly=false(注意:此参数有局限性,并非万能)。 -
检查测试逻辑
:是否在某个采样器中执行了
Connection.close()?或者在JSR元件中错误地关闭了连接?JMeter的连接池管理应该交给JDBC Configuration元件。
-
检查连接池配置
:在JDBC Connection Configuration中,合理设置
5.2 SQL执行与结果处理类问题
问题3:查询结果为空,但数据库中明明有数据
- 现象 :断言失败,查询到的变量为空。
-
排查
:
-
检查参数绑定
:确认Prepared Statement的
?占位符数量与“Parameter values”的行数完全一致。多一个或少一个都会导致错误。 -
检查参数值
:使用
Debug Sampler或View Results Tree查看${username}等变量在请求发出时的实际值是否正确,是否包含意外的空格或特殊字符。 -
检查参数类型
:确认“Parameter types”设置正确。例如,查询一个整数ID字段,类型用了
VARCHAR,可能导致类型转换失败或索引失效返回空集。 - 检查SQL本身 :将JMeter最终拼接的SQL(对于Statement类型)或带参数的SQL(查看结果树的请求体)复制出来,直接在数据库客户端执行,看是否能返回结果。
-
检查参数绑定
:确认Prepared Statement的
问题4:
Parameter index out of range (X > number of parameters, which is Y)
- 现象 :执行Prepared Statement时报错。
-
原因
:SQL语句中的
?占位符数量是Y,但你在“Parameter values”或“Parameter types”中提供的参数数量X大于Y。通常是多写了参数行,或者SQL注释中也包含了?字符(JDBC驱动可能会将其误认为占位符)。 -
解决
:仔细核对SQL语句中的
?数量,并确保参数行与之严格匹配。避免在SQL中使用?作为注释或运算符的一部分。
问题5:更新语句(INSERT/UPDATE/DELETE)不生效
- 现象 :脚本执行无报错,但数据库数据未变化。
-
排查
:
-
检查自动提交(AutoCommit)
:这是最常见的原因!如果JDBC Connection Configuration或前序采样器将
AutoCommit设置为false,并且后续没有执行Commit,那么所有更新都在一个未提交的事务中,对其他会话不可见,最终可能被回滚。 -
检查Query Type
:你确定选的是
Update Statement或Prepared Update Statement吗?误选为Select类型,语句不会被执行。 -
检查WHERE条件
:UPDATE和DELETE语句的WHERE条件是否过于宽泛或错误,导致影响了0行数据。使用
Debug Sampler输出受影响的行数变量(${updateCount})来验证。
-
检查自动提交(AutoCommit)
:这是最常见的原因!如果JDBC Connection Configuration或前序采样器将
5.3 性能与稳定性类问题
问题6:压测时响应时间越来越慢,甚至出现大量超时
- 现象 :TPS逐渐下降,平均响应时间飙升。
-
排查
:
- 检查数据库服务器监控 :CPU、内存、磁盘IO是否饱和?慢查询日志是否激增?
-
检查JMeter自身
:使用
jp@gc - PerfMon Metrics Collector监听器监控JMeter测试机本身的资源使用情况。JMeter也可能成为瓶颈。 - 检查连接池耗尽 :如果“Max Number of Connections”设置过小,而线程数很多,会导致大量线程等待获取数据库连接。观察JDBC采样器的“Connect Time”是否异常高。
- 检查是否使用了Statement而非PreparedStatement :这会导致数据库硬解析飙升,CPU打满。务必确认所有高频执行的、结构固定的SQL都使用了Prepared Statement类型。
-
检查是否存在连接泄漏
:在长时间压测后,检查数据库的
SHOW PROCESSLIST,是否存在大量来自JMeter的sleep状态连接。这可能意味着连接没有正确返还给连接池。
问题7:如何验证JDBC请求是否真的成功了?
除了看响应码为
OK
,更可靠的验证是:
- 使用“断言”检查响应数据 :如上文所述,使用响应断言检查响应码,使用JSR223断言检查业务逻辑(如查询结果非空、字段值符合预期)。
-
检查“影响行数”
:对于Update/Insert/Delete操作,JMeter会将被影响的行数存入变量
${updateCount}。你可以在断言中检查这个值是否大于0。 - 在监听器中查看样本结果 :在“查看结果树”中,选择“Sampler result”选项卡,可以看到详细的执行信息,包括执行时间、返回的结果集行数、影响行数等。
5.4 一个高级技巧:使用JSR223 Sampler替代复杂JDBC操作
当遇到以下情况时,放弃JDBC Request图形界面,转而使用JSR223 Sampler直接编写Java/Groovy JDBC代码,往往是更优解:
- 需要调用带有多个OUT/INOUT参数的复杂存储过程。
- 需要在一个采样器内执行多个SQL语句,并基于中间结果进行逻辑判断。
- 需要处理非常复杂的结果集,如游标、CLOB/BLOB大对象。
- 需要实现特定的连接管理或事务隔离级别控制。
在JSR223 Sampler中,你可以获得完整的编程能力,直接使用
java.sql
包下的API。这样虽然牺牲了一点配置的便捷性,但换来了无与伦比的灵活性和可控性。代码结构也更清晰,易于版本管理和团队协作。对于企业级复杂的数据库性能测试场景,这通常是最终的选择。

2279

被折叠的 条评论
为什么被折叠?



