注册

都说了布尔类型的变量不要加 is 前缀,非要加,这不是坑人了嘛

开心一刻


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

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

哥们:咋啦,心情不好?

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

哥们:然后呢?

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

哥们:所以难受了?

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

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

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


痛苦面具

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 isClearData) {
this.taskId = taskId;
this.isClearData = isClearData;
}

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));



这是哪里出了问题?


20240115000802

问题排查


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


640 (15)

当他启动 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是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方法的功能)


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的



能够查到如下重点信息


springmvc参数转换

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绑定参数

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


set反射设值

通过前端传过来的参数 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 前缀

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



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





作者:青石路
来源:juejin.cn/post/7521642915278422070

0 个评论

要回复文章请先登录注册