开心一刻

今天心情不好,给哥们发语音

我:哥们,晚上出来喝酒聊天吧

哥们:咋啦,心情不好?

我:嗯,刚刚在公交车上看见前女友了

哥们:然后呢?

我:给她让座时,发现她怀孕了...

哥们:所以难受了?

我:不是她怀孕让我难受,是她怀孕还坐公交车让我难受

哥们:不是,她跟着你就不用坐公交车了?不还是也要坐,有区别吗?

我默默的挂断了语音,心情更难受了

Java开发手册

作为一个 javaer,我们肯定看过 AlibabaJava开发手册,作为国内Java开发领域的标杆性编码规范,我们或多或少借鉴了其中的一些规范,其中有一点

我印象特别深,也一直在奉行,自己还从未试过用 is 作为布尔类型变量的前缀,不知道会有什么坑;正好前段时间同事这么用了,很不幸,他挖坑,我踩坑,阿西吧!

is前缀的布尔变量有坑

为了复现问题,我先简单搞个 demo;调用很简单,服务 workflow 通过 openfeign 调用 offline-sync,代码结构如下

qsl-data-govern-common:整个项目的公共模块

qsl-offline-sync:离线同步

  • qsl-offline-sync-api:向外提供 openfeign 接口
  • qsl-offline-sync-common:离线同步公共模块
  • qsl-offline-sync-server:离线同步服务

qsl-workflow:工作流

  • qsl-workflow-api:向外提供 openfeign 接口,暂时空实现
  • qsl-workflow-common:工作流公共模块
  • qsl-workflow-server:工作流服务

完整代码:qsl-data-govern

qsl-offline-sync-server 提供删除接口

/**
* @author 青石路
*/
@RestController
@RequestMapping("/task")
public class SyncTaskController { private static final Logger LOG = LoggerFactory.getLogger(SyncTaskController.class); @PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody SyncTaskDTO syncTask) {
// TODO 删除处理
LOG.info("删除任务[taskId={}]", syncTask.getTaskId());
return ResultEntity.success("删除成功");
}
}

qsl-offline-sync-api 对外提供 openfeign 接口

/**
* @author 青石路
*/
@FeignClient(name = "data-govern-offline-sync", contextId = "dataGovernOfflineSync", url = "${offline.sync.server.url}")
public interface OfflineSyncApi { @PostMapping(value = "/task/delete")
ResultEntity<String> deleteTask(@RequestBody SyncTaskDTO syncTaskDTO);
}

qsl-workflow-server 调用 openfeign 接口

/**
* @author 青石路
*/
@RestController
@RequestMapping("/definition")
public class WorkflowController { private static final Logger LOG = LoggerFactory.getLogger(WorkflowController.class); @Resource
private OfflineSyncApi offlineSyncApi; @PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody WorkflowDTO workflow) {
LOG.info("删除工作流[workflowId={}]", workflow.getWorkflowId());
// 1.查询工作流节点,查到离线同步节点(taskId = 1)
// 2.删除工作流节点,删除离线同步节点
ResultEntity<String> syncDeleteResult = offlineSyncApi.deleteTask(new SyncTaskDTO(1L));
if (syncDeleteResult.getCode() != 200) {
LOG.error("删除离线同步任务[taskId={}]失败:{}", 1, syncDeleteResult.getMessage());
ResultEntity.fail(syncDeleteResult.getMessage());
}
return ResultEntity.success("删除成功");
}
}

逻辑是不是很简单?我们启动两个服务,然后发起 http 请求

POST http://localhost:8081/data-govern/workflow/definition/delete

Content-Type: application/json

{

"workflowId": 99

}

此时 qsl-offline-sync-server 日志输出如下

2025-06-30 14:53:06.165|INFO|http-nio-8080-exec-4|25|c.q.s.s.controller.SyncTaskController :删除任务[taskId=1]

至此,一切都很正常,第一版也是这么对接的;后面 offline-sync 进行调整,删除接口增加了一个参数:isClearData

public class SyncTaskDTO {

    public SyncTaskDTO(){}

    public SyncTaskDTO(Long taskId, Boolean isClearCache) {
this.taskId = taskId;
this.isClearData = isClearCache;
} private Long taskId;
private Boolean isClearData = false; public Long getTaskId() {
return taskId;
} public void setTaskId(Long taskId) {
this.taskId = taskId;
} public Boolean getClearData() {
return isClearData;
} public void setClearData(Boolean clearData) {
isClearData = clearData;
}
}

然后实现对应的逻辑

/**
* @author 青石路
*/
@RestController
@RequestMapping("/task")
public class SyncTaskController { private static final Logger LOG = LoggerFactory.getLogger(SyncTaskController.class); @PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody SyncTaskDTO syncTask) {
// TODO 删除处理
LOG.info("删除任务[taskId={}]", syncTask.getTaskId());
if (syncTask.getClearData()) {
LOG.info("清空任务[taskId={}]历史数据", syncTask.getTaskId());
// TODO 清空历史数据
}
return ResultEntity.success("删除成功");
}
}

调整完之后,同事通知我,让我做对 qsl-workflow 做对应的调整。调整很简单,qsl-workflow 删除时直接传 true 即可

/**
* @author 青石路
*/
@RestController
@RequestMapping("/definition")
public class WorkflowController { private static final Logger LOG = LoggerFactory.getLogger(WorkflowController.class); @Resource
private OfflineSyncApi offlineSyncApi; @PostMapping("/delete")
public ResultEntity<String> delete(@RequestBody WorkflowDTO workflow) {
LOG.info("删除工作流[workflowId={}]", workflow.getWorkflowId());
// 1.查询工作流节点,查到离线同步节点(taskId = 1)
// 2.删除工作流节点,删除离线同步节点
// 删除离线同步任务,isClearData直接传true
ResultEntity<String> syncDeleteResult = offlineSyncApi.deleteTask(new SyncTaskDTO(1L, true));
if (syncDeleteResult.getCode() != 200) {
LOG.error("删除离线同步任务[taskId={}]失败:{}", 1, syncDeleteResult.getMessage());
ResultEntity.fail(syncDeleteResult.getMessage());
}
return ResultEntity.success("删除成功");
}
}

调整完成之后,发起 http 请求,发现历史数据没有被清除,看日志发现

LOG.info("清空任务[taskId={}]历史数据", syncTask.getTaskId());

没有打印,参数明明传的是 true 吖!!!

offlineSyncApi.deleteTask(new SyncTaskDTO(1L, true));

这是哪里出了问题?

问题排查

因为 qsl-offline-sync-api 是直接引入的,并非我实现的,所以我第一时间找到了其实现者,反馈了问题后让其自测下;一开始他还很自信,说这么简单怎么会有问题

当他启动 qsl-offline-sync-server 后,发起 http 请求

POST http://localhost:8080/data-govern/sync/task/delete

Content-Type: application/json

{

"taskId": 123,

"isClearData": true

}

发现 isClearData 的值是 false

此刻,疑问从我的额头转移到了他的额头上,他蒙蔽了,我轻松了。为了功能能够正常交付,我还是决定看下这个问题,没有了心理压力,也许更容易发现问题所在。第一眼看到 isClearData,我就隐约觉得有问题,所以我决定仔细看下 SyncTaskDTO 这个类,发现 isClearDatasettergetter 方法有点不一样

private Boolean isClearData = false;

public Boolean getClearData() {
return isClearData;
} public void setClearData(Boolean clearData) {
isClearData = clearData;
}

方法名是不是少了 Is?带着这个疑问我找到了同事,问他 settergetter 为什么要这么命名?他说是 idea 工具自动生成的(也就是我们平时用到的idea自动生成setter、getter方法的功能)

我让他把 Is 补上试试

private Boolean isClearData = false;

public Boolean getIsClearData() {
return isClearData;
} public void setIsClearData(Boolean isClearData) {
this.isClearData = isClearData;
}

发现传值正常了,他回过头看着我,我看着他,两人同时提问

他:为什么加了 Is 就可以了?

我:布尔类型的变量,你为什么要加 is 前缀?

问题延申

作为一个严谨的开发,不只是要知其然,更要知其所以然;关于

为什么加了 Is 就可以了

这个问题,我们肯定是要会上一会的;会这个问题之前,我们先来捋一下参数的流转,因为是基于 Spring MVC 实现的 Web 应用,所以我们可以这么问 deepseek

Spring MVC 是如何将前端参数转换成POJO的

能够查到如下重点信息

RequestResponseBodyMethodProcessorresolveArgument

/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
} return adaptArgumentIfNecessary(arg, parameter);
}

正是解析参数的地方,我们打个断点,再发起一次 http 请求

很明显,readWithMessageConverters 是处理并转换参数的地方,继续跟进去会来到 MappingJackson2HttpMessageConverterreadJavaType 方法

此刻我们可以得到,是通过 jackson 完成数据绑定与数据转换的。继续跟进,会看到 isClearData 的赋值过程

通过前端传过来的参数 isClearData 找对应的 setter方法是 setIsClearData,而非 setClearData,所以问题

为什么加了 Is 就可以了

是不是就清楚了?

问题解决

  1. 按上述方式调整 isClearDatasettergetter 方法

    带上 is

    public Boolean getIsClearData() {
    return isClearData;
    } public void setIsClearData(Boolean isClearData) {
    this.isClearData = isClearData;
    }
  2. 布尔类型的变量,不用 is 前缀

    可以用 if 前缀

    private Boolean ifClearData = false;
    
    public Boolean getIfClearData() {
    return ifClearData;
    } public void setIfClearData(Boolean ifClearData) {
    this.ifClearData = ifClearData;
    }
  3. 可以结合 @JsonProperty 来处理

    @JsonProperty("isClearData")
    private Boolean isClearData = false;

总结

  1. Spring MVC 对参数的绑定与转换,内容不同,采用的处理器也不同

    1. form表单数据(application/x-www-form-urlencoded)

      处理器:ServletModelAttributeMethodProcessor

    2. JSON 数据 (application/json)

      处理器:RequestResponseBodyMethodProcessor

      转换器:MappingJackson2HttpMessageConverter

    3. 多部分文件 (multipart/form-data)

      处理器:MultipartResolver

  2. POJO 的布尔类型变量,不要加 is 前缀

    命名不符合规范,集成第三方框架的时候就很容易出不好排查的问题

    成不了规范的制定者,那就老老实实遵循规范!

都说了布尔类型的变量不要加 is 前缀,非要加,这不是坑我了嘛的更多相关文章

  1. POJO类中的任何布尔类型的变量,都不要加is

    POJO类中的任何布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误. 定义为基本数据类型boolean isSuccess:的属性,它的方法也是isSuccess(),HSF框架在反向解 ...

  2. C++中对一个布尔类型的变量按位取反结果不变

    C++中对一个bool类型的变量按位取反是无效的.例如: bool a = true; bool b = ~a; // b的值还是true

  3. 016 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 10 布尔类型和字符串的字面值

    016 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 10 布尔类型和字符串的字面值 本文知识点:字面值 关于字面值的概念,需要注意:很多地方,我们可能就把字面值 ...

  4. Python 学习 第一篇:数据类型(数字,集合,布尔类型,操作符)

    Python语言最常用的对象是变量和常量,常量的值是字面意思,其值是不可变的,变量的值是可变的,例如,123,"上海"是常量,而a=1,a=2,其中a是变量名.内置的核心数据类型有 ...

  5. POJO类中布尔类型为啥不让用isXxx命名

    源码面前,了无秘密 <阿里开发规范泰山版>(2020.04.22)-->编程规约-->(一) 命名风格-->第8条规定: [强制]POJO 类中的任何布尔类型的变量,都不 ...

  6. BOOL布尔类型

    1.BOOL数据类型,是一种表示非真即假的数据类型,布尔类型的变量只有YES和NO两个值.YES表⽰示表达式结果为真,NO表示表达式结果为假. 2.在C语言中,认为非0即为真. 3.分⽀支语句中,经常 ...

  7. python元组类型的变量以及字符串类型的变量作为参数进行传值

    今天做selenium元素对象剥离时(我把元素对象都放到了元组类型的变量中,格式:user = (“id”,“X-Auto-2”)),遇到一个元组变量,以及str字符串变量一起作为参数传值的问题,发现 ...

  8. Python:Base1(数据类型,print语句,变量,定义字符串,raw字符串与多行字符串,Unicode字符串,整数和浮点数运算,布尔类型运算)

    1.Python中数据类型: 计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值.但是,计算机能处理的远不止数值,还可以处理文本.图形.音频.视频.网页等各种各样的数据 ...

  9. ECMAScript1.1 js书写位置 | 声明变量 | 基本数据类型 | 数据类型转换 | 操作符 | 布尔类型的隐式转换

    js书写位置 由于在写css样式时使用的时双引号,所以我们在写js代码时建议使用单引号(‘’)! 行内式 <input type="button" value="点 ...

  10. Python 为什么要在 18 年前引入布尔类型?且与 C、C++ 和 Java 都不同?

    花下猫语:在上一篇<Python 为什么能支持任意的真值判断? >文章中,我们分析了 Python 在真值判断时的底层实现,可以看出 Python 在对待布尔值时,采用了比较宽泛的态度.官 ...

随机推荐

  1. map标签是什么

    <map>标签用于在HTML中定义一个 图像映射(image map),它允许你将图像划分为多个可点击的区域(称为"热点"),每个区域可以链接到不同的URL或执行不同的 ...

  2. AspNetCore MVC 跨域

    通过XMLHttpRequest或者ajax去请求一个AspNetCore API接口服务时,Firefox提示我 已拦截跨源请求:同源策略禁止读取位于 http://localhost:33694/ ...

  3. .NET多线程编程之CountdownEvent使用

    简单来说,使用这个类可以让主线程等待子线程都完成任务之后才执行任务 1 static void Main(string[] args) 2 { 3 ///子任务的数量 4 CountdownEvent ...

  4. PandasAI:当数据分析遇上自然语言处理

    数据科学的新范式 在数据爆炸的时代,传统的数据分析工具正面临着前所未有的挑战.数据科学家们常常需要花费70%的时间在数据清洗和探索上,而真正的价值创造时间却被大幅压缩.PandasAI的出现,正在改变 ...

  5. 理解PostgreSQL和SQL Server中的文本数据类型

    理解PostgreSQL和SQL Server中的文本数据类型 在使用PostgreSQL时,理解其文本数据类型至关重要,尤其对有SQL Server背景的用户而言.尽管两个数据库系统都支持文本存储, ...

  6. ESP32系列,IDF官方实例——外设:通用GPIO

    示例位于 \examples\peripherals\gpio\generic_gpio 文件夹内 GPIO示例逻辑简单,直接看代码理解. /* GPIO示例 此示例代码位于公共域(或CC0许可,由您 ...

  7. 使用Python解析求解拉普拉斯方程

    引言 大家好!今天我们将探讨一个经典的偏微分方程-拉普拉斯方程,并使用 Python 进行求解.拉普拉斯方程广泛应用于物理学中,尤其是在电磁学.流体力学和热传导等领域.通过这篇文章,你将了解什么是拉普 ...

  8. 从 MySQL 获取数据,是从磁盘读取的吗?(buffer pool)

    从 MySQL 获取数据,是从磁盘读取的吗?(Buffer Pool) 在 MySQL 中,数据是否从磁盘读取取决于数据是否已经被加载到内存中.MySQL 使用 InnoDB 存储引擎 中的 Buff ...

  9. cglib 代理类 自己equals自己 返回false

    简单的cglib代理示例 普通的 Java 类 package cglib; public class UserService { public void saveUser(String userna ...

  10. GeoIP库商业版调研-支持IPV6

    背景 因需要支持ipv6网络,目前所使用的GeoIP库无法解析或者很少量的能解析出IPV6的IP地址位置信息,所以需要更新最新的GeoIP库文件.目的配置在Nginx或者服务直接调用使用,从而获取城市 ...