1. 为什么说“写完测试还能跑通”才是真本事
我先描述一个场景,你看看是不是似曾相识:UserService里有个方法registerUser,内部依赖UserMapper做数据库写入、EmailClient发欢迎邮件、KafkaProducer发注册事件,还要做用户名唯一性校验。让你为这个方法写单元测试,你大概率会打开 IDE,右键 → Generate → Test,看着 IDEA 自动生成一个测试类骨架——里面所有依赖都是null,方法体全是TODO。然后你开始手动补Mockito.mock()、when().thenReturn()、verify(),一个方法写完,三四十行代码没了,而且一半时间花在“怎么把UserMapper那个@Autowired依赖注入进去”这种体力活上。
这就是我接触到 Squaretest 之前的状态:单元测试覆盖率报表上数字挺好看,但实际写起来特别痛苦,一个新来的同事光是学会怎么给LocalDateTime.now()打桩就要折腾一个小时。
Squaretest 是 IntelliJ IDEA 上的一个付费插件,干的事情可以概括成一句话:选中一个类,右键一点,自动生成基于 Mockito 的可运行的单元测试代码。它分析你类里的构造器、字段依赖、方法签名、返回值类型,然后自动创建@Mock、@InjectMocks,给每个被调用的依赖方法生成when().thenReturn()打桩,甚至主动生成verify()断言来校验方法间的交互顺序。
这篇文章我把自己从安装到实测、再到现在日常使用的完整过程拆开讲,适合正在为 Java 单元测试秃头的后端开发,也适合那些“知道要写测试但一直没时间系统学 Mockito”的团队新人。我尽量把每个关键操作都解释清楚为什么这样做,而不是只给一步抄一步的清单。
2. Mock 单元测试的三种写法,为什么 Squaretest 选了最难的那个
深入插件本身之前,先聊清楚一个根本问题:Mock 单元测试到底该怎么生成。市面上其实有三条技术路线,Squaretest 选的是第三条,也是技术上最激进的那条。
2.1 反射注入式——最简单的路,但代码质量最差
IDEA 自带的测试生成器走的是这条路。它扫描被测类的字段,生成@Before方法,然后在里面写一堆unsafe方式的反射赋值,或者干脆留空让你自己填。
这种方式生成的测试类有个通病:测试方法和被测方法之间没有“契约感”。你调用userService.registerUser(user),但userMapper是null,跑到userMapper.insert(user)那行直接 NullPointerException,你还得手动去 mock 所有依赖。写得多了你会发现,所谓“自动生成测试”其实就是“自动生成了个测试文件”,九成代码还是要自己写。
2.2 轻量占位式——看着能用,实际上跑不起来
有段时间我试过 IDEA 的Generate Test Methods结合 Live Template 的方式,它在测试类里给每个被测方法生成空方法壳,然后配合org.mockito.Mockito.mock()在字段声明处直接初始化:
private UserMapper userMapper = Mockito.mock(UserMapper.class);这段代码能编译能运行,但你运行测试时看到的全是FAILED,因为所有 mock 对象的方法调用默认返回null或原始值,而你没有写任何when().thenReturn()。这种生成器的价值约等于帮你敲好了类名和方法名,仅此而已。
2.3 静态分析 + 交互式打桩——Squaretest 的路
Squaretest 的生成逻辑是第一代和第二代的深度融合:它先对被测类做字节码级别的静态分析,识别出所有实例字段和构造器参数,确定哪些需要 mock。然后针对每个测试方法,追踪被测方法内部对依赖对象的调用链,为每个调用生成合理的when打桩。
举个例子,UserService.registerUser里调用了userMapper.selectByUsername(username),Squaretest 会给这个方法生成类似这样的打桩:
User user = new User(); when(userMapper.selectByUsername(anyString())).thenReturn(null);它甚至分析出selectByUsername返回的是User对象,所以自动new了一个User实例作为返回值,还贴心地生成了anyString()匹配器。这就是“自动生成”四个字的真实含金量:它不仅输出结构,还在替你思考“这个依赖方法需要返回什么东西,测试才能往下走”。
| 生成方式 | 依赖分析能力 | 打桩生成 | 断言生成 | 可运行性 |
|---|---|---|---|---|
| IDEA 原生生成器 | 无,仅生成文件壳 | 无 | 无法生成 | 不可直接运行 |
| Live Template 轻量生成 | 弱,仅 mock 字段 | 无 | 无法生成 | 编译通过但运行失败 |
| Squaretest | 强,字节码级分析依赖调用 | 自动生成 when/thenReturn | 生成 verify 和基本 assert | 多数情况下可直接运行 |
Squaretest 在技术上更接近“读取你代码的语义,然后映射成对应的 Mockito 语句”,而不是简单拼接代码字符串。它能做到这一点,核心原因是它分析的是编译后的字节码,而 IDEA 原生生成器只做了源码层面的简单匹配。这也是为什么它生成出来的测试比 IDEA 自带的“像样”很多。
3. 环境准备和首次生成:从安装到跑通第一个 Mock 测试
工具选型聊完,进入实操环节。这一节我把自己第一次从零配置到跑通测试的过程原原本本写出来,包括中途踩的几个小坑。
3.1 依赖配置:JUnit + Mockito + 必要的辅助库
Squaretest 只为 IDEA 插件市场提供安装包,它本身不做测试框架。它生成的代码默认依赖 JUnit 4 和 Mockito,所以你项目的build.gradle里需要先有这些基础依赖:
dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito:mockito-inline:4.11.0' testImplementation 'org.springframework:spring-test:5.3.30' }这里有几个选型细节值得展开:
- JUnit 4 是 Squaretest 的默认目标框架。虽然新版 Squaretest 也支持 JUnit 5(Jupiter),但我实际用下来,JUnit 4 + MockitoJUnitRunner 的生成代码在稳定性上表现最好。如果项目是纯 JUnit 5,注意在生成选项里手动切换 Test Framework,否则生成代码虽然看着正常,但
@RunWith注解会直接报红。 - mockito-inline 这个依赖,很多人忽略,但它非常关键。它让 Mockito 支持 mock 静态方法和
final类,这两个能力在测试 Java 后端的工具类、配置类时几乎是刚需。不加这个依赖,Squaretest 生成的某些mockStatic代码会直接抛MockitoException,报错信息还很隐蔽,说“static mocking is not supported”。 - spring-test 不是必需项,但如果你被测类里有
@Value注入、@Autowired字段这类 Spring 注解,Squaretest 可能会用到ReflectionTestUtils来辅助注入,事先引入能避免生成代码里出现 import 报错。
注意:如果项目里同时存在 JUnit 4 和 JUnit 5 的依赖,建议在测试代码里统一用 JUnit 4 的
org.junit.Test注解,避免 Squaretest 生成时产生混合依赖的混乱。
3.2 插件安装:Community 版装不了,别白费功夫
Squaretest 在 IntelliJ IDEA 插件市场搜得到,安装过程本身没什么好说的,但有一个特别容易踩的坑:IntelliJ IDEA Community Edition(社区版)不支持这个插件。
当年我就是被这一点坑过。公司配的电脑上装的是社区版,我兴致勃勃地在插件市场搜到 Squaretest,点击 Install,结果按钮直接置灰,下方一行小字提示:Only available in IntelliJ IDEA Ultimate。社区版本质上是个轻量级 IDE,不包含商业插件的基础框架支持,这类测试生成类插件全都用不了。
所以如果你现在是社区版,想要原生体验 Squaretest,就得让公司申请 Ultimate 授权。如果实在没有条件,退而求其次的替代方案是:把项目代码帮你生成测试的人装一个 Ultimate,生成后的测试代码直接提交到仓库,其他人就能在同一份代码上继续开发。
插件装好后,Preferences → Plugins → Squaretest里能看到配置项。需要注意两个选项:
- Mockito Static Mocking:默认开启,推荐保持打开。关闭后生成代码里遇到静态方法会直接忽略,测试很容易在静态方法调用处报 NPE。
- Detect Spring @Autowired Fields:默认开启。如果不开启,Squaretest 会将
@Autowired字段当成普通字段处理,生成的@InjectMocks注入顺序可能出问题,某些依赖 mock 进去了但when打桩指向错误对象。
3.3 第一次生成:右键的一小步,单元测试的一大步
在 IDE 里打开被测类,右键类名 → Generate → Squaretest → OK,插件会在src/test/java的对应包路径下生成一个测试类。
我用一个典型的三层应用服务类做演示,被测类是UserService:
public class UserService { private final UserMapper userMapper; private final EmailClient emailClient; private final KafkaProducer kafkaProducer; public UserService(UserMapper userMapper, EmailClient emailClient, KafkaProducer kafkaProducer) { this.userMapper = userMapper; this.emailClient = emailClient; this.kafkaProducer = kafkaProducer; } public User registerUser(UserRegistrationRequest request) { String username = request.getUsername(); User existing = userMapper.selectByUsername(username); if (existing != null) { throw new BusinessException("USERNAME_ALREADY_EXISTS"); } User user = User.createNewUser(request); userMapper.insert(user); emailClient.sendWelcomeEmail(user.getEmail()); kafkaProducer.send("USER_REGISTERED", user.getId()); return user; } }Squaretest 生成的测试类长这样(截取核心部分):
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserMapper userMapper; @Mock private EmailClient emailClient; @Mock private KafkaProducer kafkaProducer; @InjectMocks private UserService userService; @Before public void setUp() { // Squaretest generated setup } @Test public void testRegisterUser_Success() throws Exception { UserRegistrationRequest request = new UserRegistrationRequest(); // setUp for request when(userMapper.selectByUsername(anyString())).thenReturn(null); User result = userService.registerUser(request); // assert result verify(userMapper).insert(any(User.class)); verify(emailClient).sendWelcomeEmail(anyString()); verify(kafkaProducer).send(eq("USER_REGISTERED"), anyString()); } }看到了吧,这就是前面说的“静态分析 + 交互式打桩”的成果。@Mock自动创建了三个依赖的 mock 对象,@InjectMocks把 mock 注入到被测类的构造器里,when(userMapper.selectByUsername(...)).thenReturn(null)对应被测方法里第一个依赖调用,verify系列是插件根据被测方法内部所有依赖交互自动生成的断言。
不过这里有个细节我非常想提醒:Squaretest 生成的断言偏“交互验证”型,而不是“状态验证”型。也就是说,它更关注“你有没有调用 emailClient.sendWelcomeEmail”,而不是“返回值里的 email 字段是否等于 request.getEmail()”。前者适合验证流程,后者适合验证数据正确性。实际使用中我一般会手动在assert result注释处补上状态断言,比如:
assertNotNull(result.getId()); assertEquals(request.getUsername(), result.getUsername());4. 生成代码的边界和常见问题:为什么有些测试跑不过去
工具再聪明,它也不会读心术。Squaretest 生成的测试类里有很多地方需要人工介入,这部分才是真正区分“会用插件”和“用好插件”的分界线。
4.1 数据对象创建:new出来的对象字段全空
Squaretest 没有办法知道你的请求对象哪个字段对业务逻辑至关重要,它只能创建一个对象,然后让所有字段保持默认值(null、0、false)。
这里的问题在于:很多业务代码里,registerUser内部第一行就是String username = request.getUsername();,然后selectByUsername(username)打桩用了anyString(),这没问题。但如果被测方法内部对某个字段做了非空判断:
if (request.getEmail() == null) { throw new IllegalArgumentException("email is required"); }那么 Squaretest 生成的测试就会在这里直接抛异常,断言失败,测试标红。这不是插件 bug,而是它无法理解“email 是必填项”这种领域规则。
我的处理方式是把数据准备抽成一个 helper 方法,每次生成后统一调用:
private UserRegistrationRequest createValidRequest() { UserRegistrationRequest request = new UserRegistrationRequest(); request.setUsername("alice"); request.setEmail("alice@example.com"); request.setPassword("secret"); return request; }生成完代码后,把new UserRegistrationRequest()替换成createValidRequest(),全局一次性修完。实测下来效率提升非常明显。
4.2 被测方法里同步调用静态方法或 final 方法
Squaretest 生成的测试代码能处理一部分LocalDate.now()、UUID.randomUUID()这类 JDK 静态方法,但它们的处理方式比较粗暴——直接不打桩,保留真实调用。
这在大多数情况下没问题:UUID.randomUUID()的真实行为是稳定且幂等的,不会影响测试结果。但如果你被测方法内部有:
public String generateUsername(UserRegistrationRequest request) { return request.getUsername() + "_" + RandomStringUtils.randomAlphanumeric(6); }那么每次运行测试,RandomStringUtils都会生成不同随机值,如果断言依赖这个生成结果,测试就会变成“间歇性通过/失败”的 flaky test。
这种情况下,我推荐的方案分两步:
第一,给generateUsername方法内部引入一个可注入的RandomString服务,而不是直接在方法里new一个静态工具:
public class UserService { private final RandomStringGenerator randomStringGenerator; // 构造器注入 }第二层思路才是用mockito-inline配合try-with-resources做静态方法的局部 mock:
try (MockedStatic<RandomStringUtils> mocked = mockStatic(RandomStringUtils.class)) { mocked.when(() -> RandomStringUtils.randomAlphanumeric(6)) .thenReturn("abc123"); // 调用被测方法并断言 }这里我想拎出来单说一句:能用依赖注入解决的问题,不要依赖静态 mock。静态 mock 写起来麻烦,而且只对当前线程有效,多线程测试容易出问题。Squaretest 对这类场景通常只能做到“生成一个建议性的注释”,没有能力自动写出可靠的静态打桩,所以人工介入不可避免。
4.3 私有方法无法直接测试
Squaretest 生成的测试方法只覆盖被测类里的public方法。如果你的业务类里有一个逻辑复杂的private void validateUser(User user)方法,Squaretest 不会生成针对它的直接测试。
这倒不能怪插件,private方法本身就不是单元测试的主要目标。业界主流观点是:私有方法的逻辑需要通过它的 public 调用方来间接覆盖。如果你发现某个私有方法逻辑特别复杂、分支特别多,而你又特别想单独验证,最干净的方案是把它抽成独立的package-private类或工具类,再对它写测试。
我不建议用反射去调私有方法。反射测试私有方法的本质是把测试代码和实现细节耦合在一起,一旦你重命名了私有方法,测试代码立刻编译失败,这种维护成本远大于重构代码结构来换取可测试性。
4.4 构造器入口复杂:@InjectMocks的局限
Squaretest 的@InjectMocks对构造器注入、Setter 注入、字段注入都有一定的兼容能力。但有一个场景它处理得不好:构造器里存在非 mock 类型的入参。
比如你有一个配置类,构造器长这样:
public UserService(UserMapper userMapper, AppConfig config) { this.userMapper = userMapper; this.config = config; }AppConfig是一个普通配置 POJO,不需要 mock,保持真实对象比较好。Squaretest 默认会把所有入参都 mock 掉,这也无可厚非,但生成出来的when(config.getSomeParam()).thenReturn("...")这种打桩反而显得多余。
针对这种情况,我一般是在生成测试类后手动删掉@Mock AppConfig,然后在@Before里 new 一个真实的AppConfig对象并设置必要字段,再替换@InjectMocks为手动构造:
private UserService userService; @Before public void setUp() { AppConfig config = new AppConfig(); config.setMaxRetryTimes(3); userService = new UserService(userMapper, config); }这样处理的好处是测试意图更清晰:UserMapper是外部依赖需要 mock,AppConfig是基础配置直接使用真实值。
5. 从“跑通”到“跑稳”:生成代码的质量增强三板斧
如果你想拿 Squaretest 生成完的测试代码直接塞进 CI 流水线,多半会吃一堆“间歇性失败”的苦头。这一节分享我自己在生产和社区项目里调出来的三条增强策略,它们本质上是让生成代码更接近“一个懂业务的人手写的测试”。
5.1 断言向量化:给 verify 加上你真正关心的状态
前面提到过 Squaretest 生成的断言偏交互型。我建议在最终交付前对代码做一轮断言增强,思路是给目标状态建立“断言向量”:尽可能同时验证 switch 逻辑的分支走向和最终状态值。
以testRegisterUser_Success为例,除了 verify 三个依赖交互,至少要补上这几条断言:
assertNotNull(result.getId()); assertEquals("alice", result.getUsername()); assertTrue(result.getStatus().isActive()); assertEquals("USER_REGISTERED_PENDING", result.getWelcomeEmailStatus());这里的核心逻辑是:一个测试用例,至少要有一个“反向可证伪”的状态断言。如果测试跑通了但完全没有状态断言,你根本不知道这个方法是不是真的把注册用户状态改成正确的值了。
另外,分支覆盖是一个常见的短板。Squaretest 会在被测方法每个分支都生成一个测试方法,比如testRegisterUser_WhenUsernameExists、testRegisterUser_WhenEmailInvalid等等。但它生成了分支测试方法不代表它能测试对分支条件。实际使用中,我把生成出来所有test*_When*的方法过一遍,给每个方法补上对应的 mock 打桩。
testRegisterUser_WhenUsernameExists里需要把:
when(userMapper.selectByUsername(anyString())).thenReturn(null);改成:
User existingUser = new User(); existingUser.setUsername("alice"); when(userMapper.selectByUsername("alice")).thenReturn(existingUser);这样这个测试方法才是真正运行在“用户名已存在”的分支上,不然它和成功分支的测试代码几乎一样,但又跑不过去。
5.2 填充数据准备区:给随机时间、金额、状态码留出可控入口
很多业务方法里会用到时间。LocalDateTime.now()如果不打桩,Sweetest 生成的方法在早上跑和晚上跑结果可能不一样,尤其是那些有“过期时间”“创建时间”限定的逻辑。
Squaretest 不会自动打桩时间相关代码,但业务代码如果设计成接收一个Clock参数,就能轻松解决:
public class UserService { private final Clock clock; public UserService(UserMapper userMapper, EmailClient emailClient, KafkaProducer kafkaProducer, Clock clock) { // ... this.clock = clock; } public User registerUser(UserRegistrationRequest request) { LocalDateTime now = LocalDateTime.now(clock); // 用 now 设置创建时间、过期时间 } }单元测试里写:
Clock fixedClock = Clock.fixed( Instant.parse("2025-01-15T10:00:00Z"), ZoneId.of("Asia/Shanghai") );把fixedClock传入构造器,所有时间相关断言都变成确定性的了。这个方法不复杂,但能省掉一大把“为什么早上跑测试就挂了”的 Debug 时间。
5.3 依赖边界划分:识别哪些类不该用 Squaretest 生成测试
Squaretest 适合生成业务服务类、调用外部接口的 Manager/Client、包含判断逻辑的领域服务这类对象的测试。但有两类代码我不建议用它生成,硬用反而浪费时间:
- 纯 DTO / POJO:没有逻辑,生成测试没有意义。
- 包含了直接
new多个复杂协作者的类:比如一个OrderService的构造器里new OrderValidator()、new OrderPriceCalculator()这种非注入的方式创建依赖,Squaretest 没法分析这些内部new出来的对象,生成的测试很容易出现“实际运行的是真实依赖而不是 mock”的状态,测试结果不可控。
遇到第二种情况,我最直接的方案是先重构被测类,把内部new的地方全部改成构造器注入,再跑 Squaretest。这个顺序不能反,反了生成的测试就是给自己埋雷。
6. 覆盖率和测试数据的真实关系:自动生成能拉高多少分
说完代码生成本身,最后聊一个我在团队里感触最深的话题:Squaretest 自动生成的测试对覆盖率数字的影响,以及这个数字背后的真实意义。
6.1 行覆盖率提升很快,但分支覆盖率才是硬骨头
Squaretest 生成一个测试类后,行覆盖率(Line Coverage)通常能直接拉到 60%~80%。原因是它给每个可调用的依赖都打了桩,被测方法里的主线逻辑基本都会被执行到。我拿一个约 300 行代码的服务类试过,生成 15 个测试方法后,行覆盖率从 8% 涨到 74%,速度非常惊人。
但分支覆盖率(Branch Coverage)就没那么乐观了。原因是 Squaretest 对if分支的处理策略是“每个分支生成一个测试方法”,但生成方法内部需要手动补打桩和断言,不补无法通过。所以分支覆盖率完全取决于你后续人工修改的深度。
这也是我想重点强调的:Squaretest 的定位是帮你敲代码,不是帮你思考测试策略。它把一张白纸变成一张有线条的画,但最重要的那些触笔,比如“用户名已存在分支的selectByUsername要返回已存在用户”“insert失败时会不会回滚”这些基于业务语义的判断,必须你来完成。
6.2 当覆盖率达到目标后,下一步该做什么
我自己用下来,Squaretest 拉满覆盖率之后,我反而会去关注那些插件没法生成的东西:
- 异常路径和不合法数据:比如
request.getUsername()是空字符串、超长字符串、包含特殊字符的时候,被测方法是否依然行为正确。 - 并发场景:两个请求同时注册相同用户名,是否只有一个能成功。这个类场景单元测试很难测,直接用集成测试跑会更合适。
- 幂等与重复调用:同一个
registerUser调用两次,第二次应该报业务异常而不是插入重复数据。这类逻辑 Squaretest 生成的测试往往不能覆盖。
覆盖率是一个门槛性指标,不是质量指标。Squaretest 能让你的代码快速跨过 CI 门槛,但最终测试有没有价值,还是看你有没有在上面补足“业务语义”那一层。
6.3 可测试性设计的反哺:用起来之后,你的代码会变好
这是我最意外的收获。以前写业务代码,构造器注入写不写无所谓,反正 Spring 能自动装配;私有方法抽不抽取无所谓,反正编译能过就行。但如果你的团队把 Squaretest 作为日常开发工具,大家会不自觉地往“可测试性”方向调整代码风格:
- 减少内部
new,改成构造器注入; - 减少静态方法里的随机值和
Thread.sleep,改成参数传入或依赖注入; - 把复杂私有方法抽成独立的公开工具类。
这些调整和设计模式无关,纯粹是“为了让 Squaretest 生成质量更高”这个诉求倒逼出来的好习惯。工具不会改变你的架构能力,但它会无声地引导你把代码写到更易被自动化测试检查的形态里。
7. 线程池和限量问题:生成大量测试类时容易忽略的性能细节
这一节算是我个人使用过程中的一个补充踩坑记录。项目代码达到一定规模后,我一次性对一个包下十几个 service 类生成测试,接着跑全量单测,发现构建时间从几分钟拉长到十几分钟,而且频繁出现OOM或MockitoException: Mockito is currently self-attaching之类的报错。
排查之后,问题出在几个地方:
- mockito-inline 的静态 mock 机制导致每个测试类加载时都会做额外的字节码插桩,类一多,内存占用显著上升。如果你的项目里生成了几十个测试类,建议在
gradle.properties里调大测试 JVM 内存:
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1g测试类的并行执行冲突。
Gradle默认maxParallelForks=1,多数项目不会遇到。但如果你开了并行测试,多个测试类同时使用mockStatic会互相干扰。我的建议是:凡是try (MockedStatic...)这种静态 mock 代码,统一放到一个单独的测试类里串行执行,不要和普通并行任务混在一起。IDEA 本地生成时卡顿。Squaretest 在做字节码分析时对大型类会占用较多 CPU,如果你执行“Generate Tests for All Methods”且被测类有大量私有的复杂分支,界面可能会假死几秒。这不是死机,是分析过程。建议对复杂类分批生成,先生成单个方法,确认结果无误后再全量生成。
这些细节别人写教程的时候很少提,但实际到了多人协作的规模,不处理真的会拖垮构建流水线。
8. 最后:把自动生成当作“第一版草稿”而非最终交付物
我到现在用 Squaretest 已经大半年,最深的感受是:它最强的能力不是帮你把测试写完,而是帮你把“从无到有”这段最枯燥、最劝退新人的路程压缩到几秒钟。过去你要花一下午各种求助同事才会写的 Mockito 结构,现在右键一下就能得到 80 分的基础版本。剩下那 20 分——关键分支的 mock 数据、业务含义的状态断言、环境相关的特殊处理——这才是你作为开发者真正要动脑的地方。
如果你正打算引入这个插件,我的建议是:先拿一个你们系统里最典型的 service 类试跑一次,把生成的代码从头到尾读懂,感受一下插件对依赖分析的处理方式。然后挑一个不那么重要的方法,手工改断言、补分支、跑测试跑稳,体会一下“人工增强”和“自动生成”的配合节奏。等你完全理解了这套工作流,再把它推到团队里。
跑稳一波之后你会发现,单元测试门槛低了,大家更愿意写了,而更意外的是,你自己写业务代码的时候,会下意识开始考虑“这个类生成测试方不方便”。就冲这一点,这个插件的价值就已经超出了“省时间”本身。