引言
在开发Spring Boot应用时,我们经常面临着不同的控制器方法需要处理各种不同类型的响应结果,以及在代码中分散处理异常可能导致项目难以维护的问题。你是否曾经遇到过在不同地方编写相似的返回格式,或者在处理异常时感到有些混乱?这些看似小问题的积累,实际上可能对项目产生深远的影响。统一结果返回和统一异常处理并非只是为了规范代码,更是为了提高团队的协作效率、降低项目维护的难度,并使代码更易于理解和扩展。
本文的目的是帮助你更好地理解和应用Spring Boot中的统一结果返回和统一异常处理。通过详细的讨论和实例演示,我们将为你提供一套清晰的指南,让你能够在自己的项目中轻松应用这些技术,提高代码质量,减轻开发压力。
统一结果返回
统一结果返回是一种通过定义通用的返回格式,使所有的响应结果都符合同一标准的方法。这有助于提高代码的一致性,减少重复代码的编写,以及使客户端更容易理解和处理API的响应。统一结果返回不仅规范了代码结构,还能提高团队协作效率,降低项目维护的难度。
接下来让我们一起看看在SpringBoot中如何实现统一结果返回。
1. 定义通用的响应对象
当实现统一结果返回时,需要创建一个通用的响应对象,定义成功和失败的返回情况,并确保在接口中使用这个通用返回对象。
@Setter
@Getter
public class ResultResponse<T> implements Serializable {
private static final long serialVersionUID = -1133637474601003587L;
/**
* 接口响应状态码
*/
private Integer code;
/**
* 接口响应信息
*/
private String msg;
/**
* 接口响应的数据
*/
private T data;
}
2. 定义接口响应状态码
统一结果返回的关键之一是规定一套通用的状态码。这有助于客户端更容易地理解和处理 API 的响应,同时也为开发者提供了一致的标准。通常,一些 HTTP 状态码已经被广泛接受,如:
-
200 OK
:表示成功处理请求。 -
201 Created
:表示成功创建资源。 -
204 No Content
:表示成功处理请求,但没有返回任何内容。
对于错误情况,也可以使用常见的 HTTP 状态码,如:
-
400 Bad Request
:客户端请求错误。 -
401 Unauthorized
:未授权访问。 -
404 Not Found
:请求资源不存在。 -
500 Internal Server Error
:服务器内部错误。
除了 HTTP 状态码外,你还可以定义自己的应用程序特定状态码,以表示更具体的情况。确保文档中清晰地说明了每个状态码所代表的含义,使开发者能够正确地解释和处理它们。
public enum StatusEnum {
SUCCESS(200 ,"请求处理成功"),
UNAUTHORIZED(401 ,"用户认证失败"),
FORBIDDEN(403 ,"权限不足"),
SERVICE_ERROR(500, "服务器去旅行了,请稍后重试"),
PARAM_INVALID(1000, "无效的参数"),
;
public final Integer code;
public final String message;
StatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
3. 定义统一的成功和失败的处理方法
定义统一的成功和失败的响应方法有助于保持代码一致性和规范性,简化控制器逻辑,提高代码复用性,降低维护成本,提高可读性,促进团队协作,以及更便于进行测试。
/**
* 封装成功响应的方法
* @param data 响应数据
* @return reponse
* @param <T> 响应数据类型
*/
public static <T> ResultResponse<T> success(T data) {
ResultResponse<T> response = new ResultResponse<>();
response.setData(data);
response.setCode(StatusEnum.SUCCESS.code);
return response;
}
/**
* 封装error的响应
* @param statusEnum error响应的状态值
* @return
* @param <T>
*/
public static <T> ResultResponse<T> error(StatusEnum statusEnum) {
return error(statusEnum, statusEnum.message);
}
/**
* 封装error的响应 可自定义错误信息
* @param statusEnum error响应的状态值
* @return
* @param <T>
*/
public static <T> ResultResponse<T> error(StatusEnum statusEnum, String errorMsg) {
ResultResponse<T> response = new ResultResponse<>();
response.setCode(statusEnum.code);
response.setMsg(errorMsg);
return response;
}
4. web层统一响应结果
在web层使用统一结果返回的目的是将业务逻辑的处理结果按照预定的通用格式进行封装,以提高代码的一致性和可读性。
@RestController
@RequestMapping("user")
@Validated
@Slf4j
public class UserController {
private IUserService userService;
/**
* 创建用户
* @param requestVO
* @return
*/
@PostMapping("create")
public ResultResponse<Void> createUser(@Validated @RequestBody UserCreateRequestVO requestVO){
return ResultResponse.success(null);
}
/**
* 根据用户ID获取用户信息
* @param userId 用户id
* @return 用户信息
*/
@GetMapping("info")
public ResultResponse<UserInfoResponseVO> getUser(@NotBlank(message = "请选择用户") String userId){
final UserInfoResponseVO responseVO = userService.getUserInfoById(userId);
return ResultResponse.success(responseVO);
}
@Autowired
public void setUserService(IUserService userService) {
this.userService = userService;
}
}
调用接口,响应的信息统一为:
{
"code": 200,
"msg": null,
"data": null
}
{
"code": 200,
"msg": null,
"data": {
"userId": "121",
"userName": "码农Academy"
}
}
统一结果返回通过定义通用的返回格式、成功和失败的返回情况,以及在控制器中使用这一模式,旨在提高代码的一致性、可读性和可维护性。采用统一的响应格式简化了业务逻辑处理流程,使得开发者更容易处理成功和失败的情况,同时客户端也更容易理解和处理 API 的响应。这一实践有助于降低维护成本、提高团队协作效率,并促进代码的规范化。
统一异常处理
统一异常处理的必要性体现在保持代码的一致性、提供更清晰的错误信息、以及更容易排查问题。通过定义统一的异常处理方式,确保在整个应用中对异常的处理保持一致,减少了重复编写相似异常处理逻辑的工作,同时提供友好的错误信息帮助开发者和维护人员更快地定位和解决问题,最终提高了应用的可维护性和可读性。
1.定义统一的异常类
我们需要定义服务中可能抛出的自定义异常类。这些异常类可以继承自RuntimeException
,并携带有关异常的相关信息。即可理解为局部异常,用于特定的业务处理中异常。手动埋点抛出。
@Getter
public class ServiceException extends RuntimeException{
private static final long serialVersionUID = -3303518302920463234L;
private final StatusEnum status;
public ServiceException(StatusEnum status, String message) {
super(message);
this.status = status;
}
public ServiceException(StatusEnum status) {
this(status, status.message);
}
}
2.异常处理器
创建一个全局的异常处理器,使用@ControllerAdvice
或者 @RestControllerAdvice
注解和@ExceptionHandler
注解来捕获不同类型的异常,并定义处理逻辑。
2.1 @ControllerAdvice注解
用于声明一个全局控制器建言(Advice),相当于把@ExceptionHandler
、@InitBinder
和@ModelAttribute
注解的方法集中到一个地方。常放在一个特定的类上,这个类被认为是全局异常处理器,可以跨足多个控制器。
当时用
@ControllerAdvice
时,我们需要在异常处理方法上加上@ResponseBody
,同理我们的web接口。但是如果我们使用@RestControllerAdvice
就可以不用加,同理也是web定义的接口
2.2 @ExceptionHandler
注解
用于定义异常处理方法,处理特定类型的异常。放在全局异常处理器类中的具体方法上。
通过这两个注解的配合,可以实现全局的异常处理。当控制器中抛出异常时,Spring Boot会自动调用匹配的@ExceptionHandler
方法来处理异常,并返回定义的响应。
@Slf4j
@ControllerAdvice
public class ExceptionAdvice {
/**
* 处理ServiceException
* @param serviceException ServiceException
* @param request 请求参数
* @return 接口响应
*/
@ExceptionHandler(ServiceException.class)
@ResponseBody
public ResultResponse<Void> handleServiceException(ServiceException serviceException, HttpServletRequest request) {
log.warn("request {} throw ServiceException \n", request, serviceException);
return ResultResponse.error(serviceException.getStatus(), serviceException.getMessage());
}
/**
* 其他异常拦截
* @param ex 异常
* @param request 请求参数
* @return 接口响应
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResultResponse<Void> handleException(Exception ex, HttpServletRequest request) {
log.error("request {} throw unExpectException \n", request, ex);
return ResultResponse.error(StatusEnum.SERVICE_ERROR);
}
}
3.异常统一处理使用
在业务开发过程中,我们可以在service
层处理业务时,可以手动抛出业务异常。由全局异常处理器进行统一处理。
@Service
@Slf4j
public class UserServiceImpl implements IUserService {
private IUserManager userManager;
/**
* 创建用户
*
* @param requestVO 请求参数
*/
@Override
public void createUser(UserCreateRequestVO requestVO) {
final UserDO userDO = userManager.selectUserByName(requestVO.getUserName());
if (userDO != null){
throw new ServiceException(StatusEnum.PARAM_INVALID, "用户名已存在");
}
}
@Autowired
public void setUserManager(IUserManager userManager) {
this.userManager = userManager;
}
}
@RestController
@RequestMapping("user")
@Validated
@Slf4j
public class UserController {
private IUserService userService;
/**
* 创建用户
* @param requestVO
* @return
*/
@PostMapping("create")
public ResultResponse<Void> createUser(@Validated @RequestBody UserCreateRequestVO requestVO){
userService.createUser(requestVO);
return ResultResponse.success(null);
}
@Autowired
public void setUserService(IUserService userService) {
this.userService = userService;
}
}
当我们请求接口时,假如用户名称已存在,接口就会响应:
{
"code": 1000,
"msg": "用户名已存在",
"data": null
}
统一异常处理带来的好处包括提供一致的异常响应格式,简化异常处理逻辑,记录更好的错误日志,以及更容易排查和解决问题。通过统一处理异常,我们确保在整个应用中对异常的处理方式一致,减少了重复性代码的编写,提高了代码的规范性。简化的异常处理逻辑降低了开发者的工作负担,而更好的错误日志有助于更迅速地定位和解决问题,最终提高了应用的可维护性和稳定性。
其他类型的异常处理
在项目开发过程中,我们还有一些常见的特定异常类型,比如MethodArgumentNotValidException
和UnexpectedTypeException
等,并为它们定义相应的异常处理逻辑。这些特定异常可能由于请求参数校验失败或意外的数据类型问题而引起,因此有必要为它们单独处理,以提供更具体和友好的异常响应。
1.MethodArgumentNotValidException
由于请求参数校验失败引起的异常,通常涉及到使用@Valid
注解或者@Validated
进行请求参数校验。我们可以在异常处理器中编写@ExceptionHandler
方法,捕获并处理MethodArgumentNotValidException
,提取校验错误信息,并返回详细的错误响应。
/**
* 参数非法校验
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResultResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
try {
List<ObjectError> errors = ex.getBindingResult().getAllErrors();
String message = errors.stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));
log.error("param illegal: {}", message);
return ResultResponse.error(StatusEnum.PARAM_INVALID, message);
} catch (Exception e) {
return ResultResponse.error(StatusEnum.SERVICE_ERROR);
}
}
当我们使用@Valid
注解或者@Validated
进行请求参数校验不通过时,响应结果为:
{
"code": 1000,
"msg": "请输入地址信息,用户年龄必须小于60岁,请输入你的兴趣爱好",
"data": null
}
关于
@Valid
注解或者@Validated
进行参数校验的功能请参考:SpringBoot优雅校验参数[1]
2.UnexpectedTypeException
意外的数据类型异常,通常表示程序运行时发生了不符合预期的数据类型问题。一个常见的使用场景是在数据转换或类型处理的过程中。例如,在使用 Spring 表单绑定或数据绑定时,如果尝试将一个不符合预期类型的值转换为目标类型,就可能抛出 UnexpectedTypeException
。这通常会发生在将字符串转换为数字、日期等类型时,如果字符串的格式不符合目标类型的要求。
我们可以在异常处理器中编写@ExceptionHandler
方法,捕获并处理UnexpectedTypeException
,提供适当的处理方式,例如记录错误日志,并返回合适的错误响应。
@ExceptionHandler(UnexpectedTypeException.class)
@ResponseBody
public ResultResponse<Void> handleUnexpectedTypeException(UnexpectedTypeException ex,
HttpServletRequest request) {
log.error("catch UnexpectedTypeException, errorMessage: \n", ex);
return ResultResponse.error(StatusEnum.PARAM_INVALID, ex.getMessage());
}
当发生异常时,接口会响应:
{
"code": 500,
"msg": "服务器去旅行了,请稍后重试",
"data": null
}
3.ConstraintViolationException
javax.validation.ConstraintViolationException
是 Java Bean Validation(JSR 380)中的一种异常。它通常在使用 Bean Validation 进行数据校验时,如果校验失败就会抛出这个异常。即我们在使用自定义校验注解时,如果不满足校验规则,就会抛出这个错误。
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResultResponse<Void> handlerConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
log.error("request {} throw ConstraintViolationException \n", request, ex);
return ResultResponse.error(StatusEnum.PARAM_INVALID, ex.getMessage());
}
案例请参考:SpringBoot优雅校验参数[2],注册ConstraintValidator示例中的
@UniqueUser
校验。
4.HttpMessageNotReadableException
表示无法读取HTTP消息的异常,通常由于请求体不合法或不可解析。
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResultResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex,
HttpServletRequest request) {
log.error("request {} throw ucManagerException \n", request, ex);
return ResultResponse.error(StatusEnum.SERVICE_ERROR);
}
5.HttpRequestMethodNotSupportedException
Spring Framework 中的异常类,表示请求的 HTTP 方法不受支持。当客户端发送了一个使用不被服务器支持的 HTTP 方法(如 GET、POST、PUT、DELETE等)的请求时,可能会抛出这个异常。
@ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeException.class})
@ResponseBody
public ResultResponse<Void> handleMethodNotSupportedException(Exception ex) {
log.error("HttpRequestMethodNotSupportedException \n", ex);
return ResultResponse.error(StatusEnum.HTTP_METHOD_NOT_SUPPORT);
}
全局异常处理与局部异常处理在Spring Boot应用开发中扮演不同角色。全局异常处理通过统一的异常处理器确保了整个应用对异常的处理一致性,减少了冗余代码,提高了代码的整洁度。然而,这种方式可能在灵活性上略显不足,无法满足每个具体控制器或业务场景的个性化需求。
相比之下,局部异常处理能够为每个控制器或业务场景提供更具体、灵活的异常处理逻辑,允许定制化的异常响应。这使得在复杂的项目中更容易处理特定的异常情况,同时提供更详细的错误信息。然而,局部异常处理可能带来代码冗余和维护难度的问题,特别是在大型项目中。
在实际应用中,选择全局异常处理还是局部异常处理应根据项目规模和需求进行权衡。对于小型项目或简单场景,全局异常处理可能是一种更简单、合适的选择。而对于大型项目或需要个性化异常处理的复杂业务逻辑,局部异常处理则提供了更为灵活的方案。最佳实践是在项目中根据具体情况灵活使用这两种方式,以平衡一致性和个性化需求。
最佳实践与注意事项
1. 最佳实践
-
统一响应格式: 在异常处理中,使用统一的响应格式有助于客户端更容易理解和处理错误。通常,返回一个包含错误码、错误信息和可能的详细信息的响应对象。
-
详细错误日志: 在异常处理中记录详细的错误日志,包括异常类型、发生时间、请求信息等。这有助于快速定位和解决问题。
-
使用HTTP状态码: 根据异常的性质,选择适当的HTTP状态码。例如,使用
HttpStatus.NOT_FOUND
表示资源未找到,HttpStatus.BAD_REQUEST
表示客户端请求错误等。 -
异常分类: 根据异常的种类,合理分类处理。可以定义不同的异常类来表示不同的异常情况,然后在异常处理中使用
@ExceptionHandler
分别处理。 -
全局异常处理: 使用全局异常处理机制来捕获未被特定控制器处理的异常,以确保应用在整体上的健壮性。
2 注意事项
-
不滥用异常: 异常应该用于表示真正的异常情况,而不是用作控制流程。滥用异常可能导致性能问题和代码可读性降低。
-
不忽略异常: 避免在异常处理中忽略异常或仅仅打印日志而不进行适当的处理。这可能导致潜在的问题被掩盖,难以追踪和修复。
-
避免空的catch块: 不要在
catch
块中什么都不做,这样会使得异常难以被发现。至少在catch
块中记录日志,以便了解异常的发生。 -
适时抛出异常: 不要过于吝啬地抛出异常,但也不要无谓地滥用。在必要的时候使用异常,例如表示无法继续执行的错误情况。
-
测试异常场景: 编写单元测试时,确保覆盖异常场景,验证异常的正确抛出和处理。
总结
异常处理在应用开发中是至关重要的一环,它能够提高应用的健壮性、可读性和可维护性。全局异常处理和局部异常处理各有优劣,需要根据项目的规模和需求来灵活选择。通过采用统一的响应格式、详细的错误日志、适当的HTTP状态码等最佳实践,可以使异常处理更为有效和易于管理。同时,注意避免滥用异常、忽略异常、适时抛出异常等注意事项,有助于确保异常处理的质量。在开发过程中,持续关注和优化异常处理,将有助于提高应用的稳定性和用户体验。
作者:码农Academy
链接:https://juejin.cn/post/7322463748006248459
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
参考资料
[1]
https://www.coderacademy.online/article/springbootvalidation.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.coderacademy.online%2Farticle%2Fspringbootvalidation.html
[2]
https://www.coderacademy.online/article/springbootvalidation.html: https://link.juejin.cn?target=https%3A%2F%2Fwww.coderacademy.online%2Farticle%2Fspringbootvalidation.html