springboot项目中简单的统一异常处理
刚工作时,接触的第一个项目是一个新开发的运维系统,该项目是springboot 框架,也按照控制层(controller),业务层(service),数据层(dao)的结构进行的开发。而进入到开发中,难免会遇到许多业务异常,和运行时异常需要处理。那时候,项目没有统一的返回实体包装数据,返回的数据结构很随意,甚至很多异常都不会去处理,任由错误经框架抛出,很是粗暴。
后续开发过程中,意识到任由异常抛出,这样不妥,且前端需要"优雅的"展示异常信息。于是开始采用 try catch 捕获异常后,获取异常信息,赋给异常字段后返回给前端。甚至图方便直接 controller 里面直接获取。@Controller public class AutoController { @Autowired private UserInfoService userInfoService; @RequestMapping("/test") @ResponseBody public ResultDto test(){ try{ String name = userInfoService.getName(); ResultDto resultDto = new ResultDto(); resultDto.setData(name); return resultDto; }catch (Exception e){ ResultDto resultDto = new ResultDto(); resultDto.setErrorInfo(e.getMessage()); return resultDto; } }
这种方式简直折磨人,每一个controller 都搞一堆 try catch 。而且,业务层的代码,遇到问题也是new 一个对象处理,后面当我接触老的项目的时候,我发现很多异常都是这么处理的,着实让人崩溃。ResultDto resultDto = new ResultDto(); resultDto.setErrorInfo("参数错误");
后面的工作中认识到,其实项目中的统一异常处理真的很简单创建一个统一返回包装类package com.chinamobile.cmss.dmp.deployer.common.response; import com.chinamobile.cmss.dmp.deployer.common.response.HttpStatusJsonSerializer; import com.chinamobile.cmss.dmp.deployer.exception.BusinessException; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; /** * @description: * @date: 2020/11/12/16:05 */ @AllArgsConstructor @NoArgsConstructor @Builder @Data public class ApiResponse { /** * http状态码 */ @JsonSerialize(using = HttpStatusJsonSerializer.class) private HttpStatus status; /** * 系统错误码 */ private String code; /** * 提示信息,展示给用户 */ private String message; /** * 调试信息 */ private Object debugInfo; /** * 响应主体 */ private Object content; }
2.创建一个包装返回实体的工具类,定义一些成功返回的方法。package com.chinamobile.cmss.dmp.deployer.common.response; import org.springframework.http.HttpStatus; /** * 封装统一返回类 * * @author */ public class Response { /** * 具有返回数据的封装 * * @param content 数据内容 * @return ApiResponse */ public static ApiResponse invokeSuccess(Object content){ ApiResponse apiResponse = ApiResponse.builder() .status(HttpStatus.OK) .code(SystemCode.OK) .message("请求成功") .content(content) .build(); return apiResponse; } /** * 没有返回数据的封装 * * @return ApiResponse */ public static ApiResponse invokeSuccess(){ ApiResponse apiResponse = ApiResponse.builder() .status(HttpStatus.OK) .code(SystemCode.OK) .message("请求成功") .content(null) .build(); return apiResponse; } /** * @param content 返回数据 * @param message 消息 * @return ApiResponse */ public static ApiResponse invokeSuccess(Object content,String message){ ApiResponse apiResponse = ApiResponse.builder() .status(HttpStatus.OK) .code(SystemCode.OK) .message(message) .content(content) .build(); return apiResponse; } /** * * @param status 请求状态 * @param code 状态码 * @param content 返回内容 * @param message 消息 * @return ApiResponse */ public static ApiResponse invoke(HttpStatus status, String code, Object content, String message){ ApiResponse apiResponse = ApiResponse.builder() .status(status) .code(code) .message(message) .content(content) .build(); return apiResponse; } /** * * @param status 请求状态 * @param code 状态码 * @param message 消息 * @return ApiResponse */ public static ApiResponse invoke(HttpStatus status, String code, String message){ ApiResponse apiResponse = ApiResponse.builder() .status(status) .code(code) .message(message) .build(); return apiResponse; } }
3.创建一个定义错误信息的枚举,定义业务错误信息package com.li.core.hellomeeting.common.Response; /** * 返回码和返回消息 * */ public enum ResponseCodeEnum { SUCCESS(200,"请求成功"), INTERNAL_SERVER_ERROR(500,"服务器内部错误"), /** 参数错误 **/ PARAM_INVALID_ERROR(1001,"参数校验错误"), USER_NOTE_EXIST(2004,"用户不存在"), FORBIDDEN(403,"禁止访问"), UNAUTHORIZED(401,"未登录"); private Integer code; private String message; ResponseCodeEnum(Integer code, String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
3.创建一个业务异常类,继承 RuntimeException 类package com.li.core.hellomeeting.exception; import com.li.core.hellomeeting.common.Response.ResponseResult; /** * 统一业务异常 * */ public class BusinessException extends RuntimeException{ private ResponseResult responseResult; public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } public BusinessException(Throwable cause) { super(cause); } public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public ResponseResult getResponseResult() { return responseResult; } public void setResponseResult(ResponseResult responseResult) { this.responseResult = responseResult; } }
4.创建一个异常捕获处理类,使用 @RestControllerAdvice 注解,拦截抛出的异常。package com.li.core.hellomeeting.exception; import com.li.core.hellomeeting.common.Response.ErrorInfo; import com.li.core.hellomeeting.common.Response.ResponseResult; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MultipartException; import java.util.List; import java.util.stream.Collectors; import static com.li.core.hellomeeting.common.Response.StatusCode.*; import static com.li.core.hellomeeting.common.Response.StatusCode.PERMISSION_NO_ACCESS; /** * 全局异常拦截 * * @author */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 参数校验错误 * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = BindException.class) public ResponseResult handleBindException(BindException e){ log.error("参数校验错误", e); ErrorInfo errorInfo = new ErrorInfo(); List fieldErrorInfos = e.getFieldErrors().stream() .map(fieldError -> new ErrorInfo.FieldErrorInfo(fieldError.getField(),fieldError.getRejectedValue(),fieldError.getDefaultMessage())) .collect(Collectors.toList()); errorInfo.setFieldErrorInfos(fieldErrorInfos); return ResponseResult.builder() .code(PARAM_IS_INVALID.code()) .message(PARAM_IS_INVALID.message()) .flag(PARAM_IS_INVALID.status()) .build(); } /** *处理参数错误信息 * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = MethodArgumentNotValidException.class) public ResponseResult handleBindException(MethodArgumentNotValidException e) { log.error("参数错误{}", e); return ResponseResult.builder() .code(PARAM_IS_INVALID.code()) .message(PARAM_IS_INVALID.message()) .flag(PARAM_IS_INVALID.status()) .build(); } /** *处理非法的请求方法错误 * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class) public ResponseResult handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { String msg = String.format("不支持 [%s] 请求", e.getMethod()); log.error(msg+"{}", e); return ResponseResult.builder() .code(METHOD_NOT_ALLOWED.code()) .message(METHOD_NOT_ALLOWED.message()) .flag(METHOD_NOT_ALLOWED.status()) .build(); } /** * 处理参数缺失错误信息 * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = MissingServletRequestParameterException.class) public ResponseResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { String msg = String.format("缺少 %s 参数", e.getParameterName()); log.error(msg+"{}", e); return ResponseResult.builder() .code(PARAM_NOT_COMPLETE.code()) .message(PARAM_NOT_COMPLETE.message()) .flag(PARAM_NOT_COMPLETE.status()) .build(); } /** * 处理上传异常信息 * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = MultipartException.class) public ResponseResult handleMultipartException(MultipartException e) { log.error("上传异常{}", e); return ResponseResult.builder() .code(EXCEED_MAX_SIZE.code()) .message(EXCEED_MAX_SIZE.message()) .flag(EXCEED_MAX_SIZE.status()) .build(); } /** * 处理访问权限错误 * * @param e 错误信息 * @return ResponseResult */ @ResponseBody @ExceptionHandler(AccessDeniedException.class) public ResponseResult handleAccessDeniedException(AccessDeniedException e) { return ResponseResult.builder() .code(PERMISSION_NO_ACCESS.code()) .message(PERMISSION_NO_ACCESS.message()) .flag(PERMISSION_NO_ACCESS.status()) .build(); } /** * 处理全局异常 * 通常处理统一的未知异常 * * @param e 异常信息 * @return ResponseResult */ @ExceptionHandler(value = Exception.class) public ResponseResult handleException(Exception e) { log.error("服务器内部错误", e); return ResponseResult.builder() .code(INTERNAL_SERVER_ERROR.code()) .message(INTERNAL_SERVER_ERROR.message()) .flag(INTERNAL_SERVER_ERROR.status()) .build(); } /** * 处理业务异常 * 如果携带了一个 ResponseResult,则提取出来进行返回 * 否则返回 null * * @param e 错误信息 * @return ResponseResult */ @ExceptionHandler(value = BusinessException.class) public ResponseResult handleBusinessException(BusinessException e) { log.error(e.getMessage(), e); ResponseResult responseResult; if(e.getResponseResult() != null){ responseResult = e.getResponseResult(); }else{ log.error("服务器内部错误{}", e.getCause()); responseResult = ResponseResult.builder() .code(INTERNAL_SERVER_ERROR.code()) .message(INTERNAL_SERVER_ERROR.message()) .flag(INTERNAL_SERVER_ERROR.status()) .build(); } return responseResult; } }
5.封装一些异常错误,配合定义好的错误枚举类使用。package com.chinamobile.cmss.dmp.deployer.exception; import com.chinamobile.cmss.dmp.deployer.common.response.Response; import com.chinamobile.cmss.dmp.deployer.common.response.SystemCode; import org.springframework.http.HttpStatus; /** * 异常统一抛出封装 * */ public class MyException { /** *抛出错误信息 * * @param status 状态 * @param code 状态码 * @param content 内容 * @param message 错误信息 * @return BusinessException */ public BusinessException throwException(HttpStatus status, String code, Object content, String message){ BusinessException businessException = new BusinessException(message); businessException.setApiResponse(Response.invoke(status,code,content,message)); return businessException; } /** * 参数错误异常 * @param message 异常信息 * @return BusinessException */ public BusinessException badRequest(String message){ BusinessException businessException = new BusinessException(message); businessException.setApiResponse(Response.invoke(HttpStatus.BAD_REQUEST, SystemCode.BAD_REQUEST,null,message)); return businessException; } /** * 找不到目标值错误 * * @param message 错误信息 * @return BusinessException */ public BusinessException notFound(String message){ BusinessException businessException = new BusinessException(message); businessException.setApiResponse(Response.invoke(HttpStatus.NOT_FOUND, SystemCode.NOT_FOUND,null,message)); return businessException; } /** * 无权限错误 * * @param message 错误信息 * @return BusinessException */ public BusinessException forbideen(String message){ BusinessException businessException = new BusinessException(message); businessException.setApiResponse(Response.invoke(HttpStatus.FORBIDDEN,SystemCode.FORBIDDEN,null,message)); return businessException; } }
在业务中使用,截取个例子一部分代码@Override public void deployService(DeployParam deployParam){ log.info("------> 【自动化部署接口】参数:{}",deployParam); //校验一下 checkParam(deployParam); //获取环境配置信息 ServiceDeployInfoDto serviceDeployInfoDto = getServiceDeployInfoDto(deployParam); log.info("------> 整合环境配置信息:{}",serviceDeployInfoDto); //判断服务是否在部署中 boolean status = DeployUtils.isDeploying(deployParam.getEnv(),deployParam.getInstance()); log.info("------> 判断当前实例是否正在部署,status: {}",status); if(status){ log.error("{} 服务正在部署中,请稍后再部署",deployParam.getInstance()); throw new MyException().badRequest("该服务正在部署中,请稍后"); } //2.开始部署 log.info("------> 开始进行异步部署......"); if(deployParam.getInstance().equals("openapi2")){ serviceDeployInfoDto.setInstanceName("openapi"); } deployManager.deployService(serviceDeployInfoDto); }
在controller 中使用@PostMapping(value = "/deploy") public ResponseEntity