优化你的代码返回值:不要再返回 null 了

今天 review 同事的代码,发现了如下代码片段

// 这是一段伪代码
public SomeObject func() {
	...
	if (发生异常) {
		log.error(...);
		return null;
	}
	...
	return 正常值;
}

类似的代码我见过很多,自己曾经也写过这样的代码。
但是如此的写法实际并不安全,因为调用方很容易取到一个 null 值,进而诱发空指针问题。

为了重构这个代码,有如下几种处理策略

☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️
☀️ 1. 返回 Optional 对象                        ☀️
☀️ 2. 返回包装类                                          ☀️
☀️ 3. 异常时返回空对象或默认值                 ☀️
☀️ 4. 上抛异常                                              ☀️
☀️ 5. 必须要返回 null 时的小技巧              ☀️
☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️ ☀️ ☀️

在文章的最后,我会给出另外一种 常见但是不推荐 的解决方案:通过异常的返回值标记内部的状态

几种方案的比较

方案额外代码量灵活度调用者额外判定错误细节感知沉默的错误
返回 Optional 对象一般不可以可以避免
返回包装类有,但全局可用灵活可以可以避免
异常时返回空对象或默认值一般不可以容易发生
上抛异常有,但不多灵活有,但解耦在 catch 中处理可以可以避免
必须要返回 null 时的小技巧不可以可以避免

表格中维度的解释:

  • 额外代码量:使用这种模式额外的代码量
  • 灵活度:调用方是否可以灵活使用
  • 调用者额外判定:调用者是否需要判空代码来避免异常
  • 错误细节感知:调用者是否能感知到方法内部异常的原因,并进行针对的处理
  • 是否容易出现 “沉默的错误”:是否容易造成无法感知的错误,进而产生难以排查的 bug

1. 返回 Optional 对象

Optional 对象是 java 特有的,但其他语言也有类似的实现。这种方法的伪代码如下

// 函数改造
public Optional<SomeObject> func() {
	...
	if (发生异常) {
		log.error(...);
		return Optional.empty();
	}
	...
	return Optional.of(正产值);
}

这样调用方就知道,函数返回值是可能为空的,可以通过函数式编程的方式进行接下来的处理。

// 调用方代码
Optional<SomeObject> result = func();
result.ifPresent(obj -> {...});

2. 返回包装类

这种方式和 Optional 类似,只不过包装类是自己写的。优点是大大增加了灵活性

  • 在实际使用 Optional 对象的时候发现提供的 api 不一定能满足需求
  • 对于需要感知内部错误原因的场合,Optional 就显得无能为力了(虽然这违反了范式,但有时候真的很便捷)

例如可以使用一个全局的函数返回包装类

// 包装类
public class FunctionResult<T> {
	private boolean success;
	private String msg;
	private ErrorCodeEnum code;
	private T data;

	// 这里可以提供丰富灵活的 api
	// 例如 void ifSuccess(Consumer<T> consumer);
	// 还可以 FunctionResult<T> ifSuccess(Consumer<T> consumer) 用来提供链式调用
	// 其他的也可以大胆地进行设计
	// 当然最简单的,就是让调用方判定 success 的值
}
// 函数改造
public FunctionResult<SomeObject> func() {
	...
	if (发生异常) {
		log.error(...);
		return FunctionResult.failure("错误信息");  // 可以提供丰富的构造函数
	}
	...
	return FunctionResult.success(正产值);
}

这样调用方就可以知道返回值有可能是失败的,及时做出检测。

// 调用方代码
FunctionResult<SomeObject> result = func();
result.ifSuccess(obj -> {...});

3. 异常时返回空对象或默认值

这也是一种常见的设计模式,所谓 “空对象” 也被称作 “null 对象”,例如

// 空对象示例
public class Circle {
	
	/**
 	 * 半径
 	 */
	private int r;  

	public Circle(int r) {
		this.r = r;
	}

	public int area() {
		return pi * r * r;
	}
}

// 一个 null 圆,半径为 0
public class NullCircle extends Circle {
	public NullCircle() {
		super(0);
	}
}

这样当发生异常时返回 “空对象” 调用方完全不需要担心空指针的问题。

另外一种常见的思路,是返回一个默认值,很多的包也会采用类似的设计,例如返回字符串的函数,当发生异常时返回空字符串("")作为默认值而不是返回 null;或者在上面圆形的例子中,如果是特定的业务领域,有可能可以返回一个 “标准圆” 作为默认的返回值。

4. 上抛异常

可以在发生异常时抛出合理的异常或者是自定义的异常,伪代码如下

// 函数改造
public Optional<SomeObject> func() throws 自定义的异常 {
	...
	if (发生异常) {
		throw 自定义的异常;
	}
	...
	return Optional.of(正产值);
}
// 调用方代码
try {
	SomeObject result = func();
} catch(自定义的异常 e) {
	// 按需进行异常处理
}

警告:不要一味的抛出 Exception 类而应该自己去继承 Exception 类实现自定义的异常。否则会造成异常处理混乱,代码难以维护

5. 必须要返回 null 时的小技巧

如果觉得上述方案都太过麻烦了,另一种可能是工期过紧,非得想要返回 null 值,那么通过以下两个小技巧,也能在一定程度上缓解这一问题

函数命名

比如原来你的函数名字是 myFunc,简单的改成 myFuncNullable,调用方就会意识到返回值可能为 null,进而避免空指针。

注解

在你的函数上使用 @Nullable 注解,大多数的现代 IDE 都能检测到这个注解,在编写代码的时候,如果调用方使用其返回值不合理,就会给出 “标黄” 的提示,一眼识别问题。

在这里插入图片描述

😖 不推荐的解决方案:通过异常的返回值标记内部的状态

对于返回数字类型的函数,一种常见的写法如下

// 函数改造
pubic int func() {
	...
	if (发生异常) {
		log.error(...);
		return -1;
	}
	...
	return 正产值;
}

这种“错误码”的写法可以追溯到 C 语言时代,C 语言比较偏底层,而且早期内存很小,这种写法很正常。但是时至今日,使用高级语言的我们应该用表现力更加丰富的手段,原因如下:

  • -1 这种错误码可读性很差,有时候需要深入实现才能理解 -1 是什么含义,-2 又是什么
  • 如果未来 func 返回值含义发生扩展,从正数扩展到负数,改造起来会很麻烦
  • 从设计原则上来说,一个变量承担返回值和是否成功两个含义,也是更容易造成 bug 的一种设计
<think>我们正在处理用户的问题:如何在SpringBoot的Controller中,当查询结果为空时返回提示信息而是空对象,特别是在使用@GetMapping注解的方法中处理null返回值。根据引用内容,我们可以结合统一结果返回格式和异常处理的思想。引用[2]和[3]提到了统一结果返回类,引用[4]展示了一个返回ResponseEntity的示例。解决方案思路:1.定义一个统一的结果返回类,通常包含状态码、消息和数据。当数据为空时,我们可以设置相应的提示消息。2.在Controller方法中,判断查询结果是否为空,如果为空,则返回一个带有提示信息的结果;如果为空,则返回正常数据。具体步骤:步骤1:定义统一返回结果类(可参考引用[3]的ResultResponse和引用[4]的ResponseObj)步骤2:在Controller方法中,对查询结果进行判空,并构造相应的返回对象。引用[3]中的ResultResponse类是一个带泛型的返回类,我们可以扩展它或者使用类似的结构。示例代码:首先,我们定义一个基础响应类(包含状态码和消息)和一个带数据的响应类(继承基础响应类):基础响应类(BaseResponse):属性:code(状态码),message(消息)带数据的响应类(ResultResponse<T>):属性:data(T类型,表示返回的数据)但是,用户要求当查询结果为空时返回提示信息,而返回一个data为null的对象。因此,我们可以在构造ResultResponse时,如果数据为空,则设置一个代表“数据为空”的状态码和消息。另一种做法是:在Controller方法中,判断数据是否为空,然后选择返回同的ResultResponse(即设置同的消息和状态码)。我们可以预先定义一些状态码常量或枚举(如IEnum,如引用[3]中所示),例如:成功:200,"成功"数据为空:404,"未找到数据"下面我们根据上述思路编写代码:1.定义枚举类(IEnum),用于表示同的返回状态(可选,但推荐):```javapublicinterfaceIEnum{intgetCode();StringgetMessage();}publicenumResultEnumimplementsIEnum{SUCCESS(200,"成功"),DATA_NOT_FOUND(404,"未找到数据");privatefinalintcode;privatefinalStringmessage;ResultEnum(intcode,Stringmessage){this.code=code;this.message=message;}@OverridepublicintgetCode(){returncode;}@OverridepublicStringgetMessage(){returnmessage;}}```2.定义基础返回类(BaseResponse):```javapublicclassBaseResponse{privateintcode;privateStringmessage;publicBaseResponse(){//默认使用成功状态this(ResultEnum.SUCCESS);}publicBaseResponse(IEnumiEnum){this.code=iEnum.getCode();this.message=iEnum.getMessage();}//省略getter和setter}```3.定义带数据的返回类(ResultResponse<T>):```javapublicclassResultResponse<T>extendsBaseResponse{privateTdata;publicResultResponse(){super();}publicResultResponse(Tdata){super();this.data=data;}publicResultResponse(IEnumiEnum){super(iEnum);}publicResultResponse(IEnumiEnum,Tdata){super(iEnum);this.data=data;}//省略getter和setter}```4.在Controller中使用:```java@RestController@RequestMapping("/api")publicclassUserController{@AutowiredprivateUserServiceuserService;@GetMapping("/users/{id}")publicResultResponse<User>getUserById(@PathVariableLongid){Useruser=userService.getUserById(id);if(user==null){//返回数据存在的响应,此时data为null,但我们有明确的消息returnnewResultResponse<>(ResultEnum.DATA_NOT_FOUND);}//正常返回,使用成功状态码,并携带数据returnnewResultResponse<>(user);}}```但是,这样返回的结果中,当数据存在时,data字段为null,而消息是“未找到数据”。如果前端期望在数据为空时不要有data字段,或者我们返回null,那么可以调整ResultResponse的构造方式:在数据存在时,我们只返回状态码和消息,设置data字段(即data为null,但消息已经说明情况)。另外,我们也可以考虑另一种设计:在ResultResponse中,如果数据为空,我们就返回data字段(通过注解@JsonInclude(JsonInclude.Include.NON_NULL)来忽略null字段)。但是,我们的状态码和消息已经说明了情况,所以data为null是可以接受的。如果用户希望返回一个没有data字段的响应,那么我们可以设计两个同的返回类:一个用于携带数据,一个用于携带数据(仅状态信息)。但是,为了统一,通常使用一个类,并允许data为null。另一种做法:在Controller中返回ResponseEntity,这样我们可以控制HTTP状态码,同时使用统一的结果格式(如引用[4]所示)。例如:```java@GetMapping("/users/{id}")publicResponseEntity<ResultResponse<User>>getUserById(@PathVariableLongid){Useruser=userService.getUserById(id);if(user==null){ResultResponse<User>response=newResultResponse<>(ResultEnum.DATA_NOT_FOUND);returnnewResponseEntity<>(response,HttpStatus.NOT_FOUND);}ResultResponse<User>response=newResultResponse<>(user);returnnewResponseEntity<>(response,HttpStatus.OK);}```这样,我们返回了自定义的状态码(在ResultResponse中),还通过HTTP状态码(404NotFound)来表明资源未找到,同时响应体中的消息也是自定义的。总结:两种方式都可以,取决于设计。如果项目已经统一使用200作为HTTP状态码,而用自定义状态码表示业务状态,那么第一种方式即可。如果想利用HTTP状态码,则可以使用第二种方式。根据引用[4]中的示例,他们使用了ResponseEntity,所以我们可以参考这种方式。因此,我们给出两种实现方式,并说明其区别。方式一:只使用统一结果类(ResultResponse),HTTP状态码始终为200,业务状态由code字段表示。方式二:结合ResponseEntity,同时改变HTTP状态码。用户可以根据项目规范选择。下面我们给出方式二的完整示例(结合枚举和ResponseEntity):枚举类(同上):基础返回类(同上):带数据的返回类(同上):Controller:```java@GetMapping("/users/{id}")publicResponseEntity<ResultResponse<User>>getUserById(@PathVariableLongid){Useruser=userService.getUserById(id);if(user==null){//使用自定义的枚举状态,同时HTTP状态码设置为404ResultResponse<User>response=newResultResponse<>(ResultEnum.DATA_NOT_FOUND);returnnewResponseEntity<>(response,HttpStatus.NOT_FOUND);}ResultResponse<User>response=newResultResponse<>(user);returnnewResponseEntity<>(response,HttpStatus.OK);}```这样,当数据存在时,返回的HTTP状态码为404,同时响应体为:{"code":404,"message":"未找到数据","data":null}如果返回data字段,可以在ResultResponse类上添加注解:```java@JsonInclude(JsonInclude.Include.NON_NULL)publicclassResultResponse<T>extendsBaseResponse{privateTdata;//...构造方法和getter/setter}```这样,当data为null时,响应体中就会出现data字段。因此,最终响应为:{"code":404,"message":"未找到数据"}这样更符合用户的要求:返回提示信息而是空对象(即没有data字段,或者即使有也是null,但通过消息已经说明)。综上所述,我们推荐使用方式二,并配合@JsonInclude(JsonInclude.Include.NON_NULL)来避免返回null的data字段。注意:需要确保项目中引入了Jackson库(SpringBoot默认使用),因为@JsonInclude是Jackson的注解。最后,我们根据引用[1]中提到的,使用@GetMapping注解,并返回ResponseEntity。因此,完整的代码示例:1.枚举类(同上)2.基础返回类(同上)3.带数据的返回类(添加注解):```java@JsonInclude(JsonInclude.Include.NON_NULL)publicclassResultResponse<T>extendsBaseResponse{privateTdata;//构造方法(同上)//...getter和setter}```4.Controller方法:```java@RestController@RequestMapping("/api")publicclassUserController{@AutowiredprivateUserServiceuserService;@GetMapping("/users/{id}")publicResponseEntity<ResultResponse<User>>getUserById(@PathVariableLongid){Useruser=userService.getUserById(id);if(user==null){ResultResponse<User>response=newResultResponse<>(ResultEnum.DATA_NOT_FOUND);returnResponseEntity.status(HttpStatus.NOT_FOUND).body(response);}ResultResponse<User>response=newResultResponse<>(user);returnResponseEntity.ok(response);}}```这样,当查询结果为空时,返回的HTTP状态码为404,响应体中没有data字段,只有code和message,提示用户未找到数据。如果用户希望即使成功返回,当数据为空对象(例如空列表)时也进行提示,那么需要根据业务需求调整判断逻辑。但根据问题,我们只处理了单个对象为null的情况。对于集合类型(如List),如果查询结果为空列表,我们可能希望返回404(因为路径是存在的,只是数据为空),而是返回200和空列表,并附加一个消息提示(如“暂无数据”)。这需要根据业务场景调整。因此,我们可以为集合类型单独处理:例如:```java@GetMapping("/users")publicResponseEntity<ResultResponse<List<User>>>getAllUsers(){List<User>users=userService.getAllUsers();if(users.isEmpty()){//使用一个表示数据为空的枚举,但HTTP状态码还是200(因为请求是成功的,只是数据为空)ResultResponse<List<User>>response=newResultResponse<>(ResultEnum.DATA_EMPTY);response.setData(users);//这里可以设置data为空列表,也可以设置(因为空列表null,所以会保留data字段,但内容为空数组)//或者,如果我们返回data字段,就不要设置,但这里我们可能希望返回一个空数组returnResponseEntity.ok(response);}ResultResponse<List<User>>response=newResultResponse<>(users);returnResponseEntity.ok(response);}```这里,我们可能需要定义一个新的枚举:DATA_EMPTY(200,"暂无数据")。因此,根据具体需求,我们可以灵活使用。回到用户问题:处理null返回值(单个对象)。我们上面的方案已经解决。最后,我们生成相关问题。</think>### 在 Spring Boot Controller 中处理空返回值 在 Spring Boot 的 `@GetMapping` 方法中,当查询结果为空时返回提示信息而非空对象,可以通过以下两种方式实现: #### 方案一:使用统一响应体(推荐) 参考引用[3]的 `ResultResponse` 设计,创建带状态信息的响应类: ```java // 统一响应基类 public class BaseResponse { private int code; private String message; public BaseResponse(int code, String message) { this.code = code; this.message = message; } // getters/setters } // 带数据的响应类(泛型) @JsonInclude(JsonInclude.Include.NON_NULL) // 空值序列化 public class ResultResponse<T> extends BaseResponse { private T data; public ResultResponse(int code, String message, T data) { super(code, message); this.data = data; } // getters/setters } ``` 在 Controller 中使用: ```java @GetMapping("/users/{id}") public ResultResponse<User> getUserById(@PathVariable Long id) { User user = userService.findById(id); if (user == null) { // 返回404状态和提示信息 return new ResultResponse<>(404, "用户存在", null); } // 返回200状态和数据 return new ResultResponse<>(200, "成功", user); } ``` #### 方案二:使用 ResponseEntity 结合 HTTP 状态码直接控制响应(参考引用[4]): ```java @GetMapping("/users/{id}") public ResponseEntity<Object> getUserById(@PathVariable Long id) { User user = userService.findById(id); if (user == null) { // 返回404和提示信息 Map<String, String> response = new HashMap<>(); response.put("code", "404"); response.put("message", "用户存在"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } // 返回200和数据 return ResponseEntity.ok(user); } ``` #### 关键点说明: 1. **空值处理**:使用 `@JsonInclude(JsonInclude.Include.NON_NULL)` 避免返回空字段 2. **状态码语义**: - `404 Not Found`:资源存在 - `200 OK`:请求成功 3. **统一格式**:保持所有接口返回相同结构,方便前端处理[^3] 4. **错误消息**:提供明确的可读提示,而非技术性错误[^2] #### 效果对比: | 场景 | 原始返回 | 优化返回 | |------|----------|------------| | 存在数据 | `{"id":1,"name":"John"}` | `{"code":200,"message":"成功","data":{"id":1,"name":"John"}}` | | 空数据 | `null` | `{"code":404,"message":"用户存在"}` | > 推荐使用方案一,保持接口响应格式统一,便于前端错误处理和日志监控[^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小郎碎碎念

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值