方式一
基于内置的标准状态码进行响应,不做任何新的错误码定义,异常、错误就直接响应对应的HttpStatus
;正常就返回200并在body中带上业务数据;
优点
基于标准的状态码;不用进行新的定义;
减少前后端对于状态码的沟通
缺点
标准的只是定义了一些最基本的,无法满足一些个性化的业务场景;不过这种场景,也可以基于响应数据的格式去做
方式二
不用内置的标准状态码,所有的接口请求不管是正常、异常、错误;全部返回200;然后在doby的数据中定义自己系统的状态码;客户端收到body的数据之后,根据前后端约定的状态码进行校验并友好提示;
优点
灵活性强;可以根据自己的业务场景,去定义个性化的规则,
可扩展性强;可以根据需要任意扩展;
缺点
规则约定带来的负担,
维护成本增加;可能因为定义不规范导致后续维护的困难;
上面说的方式,没有对错,只有合不合适,更多的是根据业务的需要,场景的需要,找更合适的方式;
下面采用了上面两种方式的二合一版本;去定义一套响应规范;
成功:
{"status": 200,"msg": "成功!","data": {"userName": "李四","email": "lisi@qq"}
}
失败:
{"status": 1000,"msg": "参数错误!"
}
当前请求的状态码;这里定义的是200为成功;200之外的为异常情况;
状态码对应的描述
响应的数据;该属性是一个泛型值;其类型、值都是根据具体的业务场景需要进行匹配
我们可以延用系统自带的状态码;即org.springframework.http.HttpStatus枚举;但是这个往往只表述的一些通用的状态,不能够表达或说明一些详细的问题点;因此通常情况下我们会对错误码进行自定义;以更加详细的描述出现的问题;如下:
@Getter
@AllArgsConstructor
public enum BaseStatusCode {SUCCESS(200,"成功!"),ERR_1000(1000,"参数错误!"),ERR_9999(9999,"未知错误!");// 状态码private Integer status;// 状态码描述private String msg;
}
通常情况下就是这样去定义,使用也没有什么问题;但是这样写有一个比较大的问题;就是不够灵活、不易于扩展
;因为这样,意味着所有的错误码都得定义在这一个枚举里面(后面的异常对象需要通过这个枚举值实例化);比如说,用户模块、设备模块、电商模块、库存模块都有自己个性化的错误码;就意味着,所有的验证码都堆在这么一个注解里面;耦合性太强,不便于扩展;
为了解决上面不灵活的问题,那我们就采用一种面向接口的状态码定义;来提高状态码的可扩展性和灵活性;
调整起来也很简单,状态码同样也是以上面的枚举当时定义,但是增加一个接口出来;
第一步;定义接口
/*** 错误码的接口*/
public interface IStatusCode {/*** 获取状态码* @return 返回*/Integer getStatus();/*** 获取状态描述* @return 返回描述信息*/String getMsg();
}
第二步;定义状态码
此时;枚举中status和msg对应的get方法也就对应成了IStatusCode的实现
Getter
@AllArgsConstructor
public enum BaseStatusCode implements IStatusCode {SUCCESS(200,"成功!"),ERR_1000(1000,"参数错误!"),ERR_9999(9999,"未知错误!")// 状态码private Integer status;// 状态码描述private String msg;
}
第三步;扩展错误码
*** @title: UserStatusCode* @projectName springcloud-mbb* @description: TODO 用户相关的状态码*/
@Getter
@AllArgsConstructor
public enum UserStatusCode implements IStatusCode{ERR_2000(2000,"用户信息不存在"),ERR_2001(2001,"用户昵称格式错误");// 状态码private Integer status;// 状态码描述private String msg;
}
/*** @title: DeviceStatusCode* @projectName springcloud-mbb* @description: TODO 设备相关的状态码*/
@Getter
@AllArgsConstructor
public enum DeviceStatusCode implements IStatusCode{ERR_3000(3000,"设备id有误"),ERR_3001(3001,"设备名称格式错误"),ERR_3002(3002,"设备MAC地址无效");// 状态码private Integer status;// 状态码描述private String msg;
}
第四步;状态码的获取
IStatusCode baseStatusCode = BaseStatusCode.ERR_9999;
IStatusCode userStatusCode = UserStatusCode.ERR_2000;
IStatusCode deviceStatusCode = DeviceStatusCode.ERR_3002;
如此获取,优势就展现出来了,不管是以枚举的状态码还是对象的方式,只要是实现了IStatusCode
接口的类,都可以作为一个状态码对象;通过接口的getStatus()和getMsg()即可拿到状态码和状态描述;
这样,我们就可以只需要把所有模块公共的状态码定义在公共模块里面;其他模块个性化的状态码,定义在模块内部即可;
优点分析
定义解耦;不需要将所有的状态码定义到一起了;只要实现了IStatusCode接口即可
不限于枚举;因为是基于接口获取状态码和描述,因此不限于枚举,任何只要实现了IStatusCode都可以作为状态码
公共响应对象定义
有了规范好的响应对象的格式;有了状态码;那就可以定义一个基础的响应对象用来包装最后的返回结果;其中定义了4个构造方法,用于能够快速的实例化一个响应对象;
为了能更好的兼容;这里将HttpStatus状态码也封装了进来,这样就既可以使用默认状态码,可以使用自定义状态码,根据自己的需要灵活选择。
其中@JsonView的可以先不看,后面会介绍;只是为了后面不重复贴这一块的代码,先全部贴出来
/*** 基础的响应对象** @param <T> 响应数据*/
@Data
public class BaseResponceDto<T> {/*** 响应数据最外层的视图 也是所有响应视图的父类*/public interface ResponceBaseDtoView {}/*** 状态码*/@JsonView(ResponceBaseDtoView.class)private Integer status;/*** 状态描述*/@JsonView(ResponceBaseDtoView.class)private String msg;/*** 响应数据*/@JsonView(ResponceBaseDtoView.class)private T data;/*** 只有状态码的响应** @param statusCode*/public BaseResponceDto(IStatusCode statusCode) {if (null != statusCode) {this.status = Status();this.msg = Msg();}}/*** 有状态码且有参数的响应** @param statusCode* @param data*/public BaseResponceDto(IStatusCode statusCode, T data) {if (null != statusCode) {this.status = Status();this.msg = Msg();}if (null != data) {this.data = data;}}/*** 根据HttpStatus响应** @param httpStatus http请求状态码*/public BaseResponceDto(HttpStatus httpStatus) {if (null != httpStatus) {this.status = httpStatus.value();this.msg = ReasonPhrase();}}/*** 根据http状态码返回 并返回额外返回数据** @param httpStatus http状态码* @param data 数据*/public BaseResponceDto(HttpStatus httpStatus, T data) {if (null != httpStatus) {this.status = httpStatus.value();this.msg = ReasonPhrase();}if (null != data) {this.data = data;}}/*** 根据异常响应错误码** @param baseException 异常对象*/public BaseResponceDto(BaseException baseException) {if (null != baseException) {this.status = Error();this.msg = Msg();this.data = (T) Data();}}
}
自定义异常对象
在整个业务的请求到响应,异常并不是必定会出现的;但是,异常并不是必定会出现,但是又是不得不处理的;并且他贯穿了整个业务的始终,从请求到响应,都有可能牵扯到异常;所以一个好的异常处理机制,是整个代码健壮性必定要考虑的因素。
@Data
public class BaseException extends RuntimeException {/*** 错误码*/private Integer error;/*** 错误描述*/private String msg;/*** 错误后响应的信息*/private Object data;/*** 根据错误码实例化异常** @param statusCode 自定义错误码*/public BaseException(IStatusCode statusCode) {// 校验是否传递了异常码if (null == statusCode) {// 如果没有统一设置为未知错误setInfo(BaseStatusCode.ERR_9999);} else {setInfo(statusCode);}}/*** 根据http状态码抛出异常** @param httpStatus http状态码*/public BaseException(HttpStatus httpStatus) {if (null == httpStatus) {// 没有传递默认使用 未知异常setInfo(BaseStatusCode.ERR_9999);} else {setInfo(httpStatus);}}/*** 根据错误码实例化异常 并返回数据** @param statusCode 自定义错误码* @param data 数据*/public BaseException(IStatusCode statusCode, Object data) {// 校验是否传递了异常码if (null == statusCode) {// 如果没有统一设置为未知错误setInfo(BaseStatusCode.ERR_9999);} else {setInfo(statusCode);}// 校验数据是否为nullif (null != data) {this.data = data;}}/*** 根据http的状态码实例化异常 并返回数据** @param httpStatus http状态码* @param data 数据*/public BaseException(HttpStatus httpStatus, Object data) {// 校验是否传递了异常码if (null == httpStatus) {// 如果没有统一设置为未知错误setInfo(BaseStatusCode.ERR_9999);} else {setInfo(httpStatus);}// 校验数据是否为nullif (null != data) {this.data = data;}}/*** 设置状态码及描述信息* 内部使用的方法** @param statusCode*/private void setInfo(IStatusCode statusCode) {if (null != statusCode) { = Status();this.msg = Msg();}}/*** 根据HttpStatus设置属性* @param httpStatus*/private void setInfo(HttpStatus httpStatus) {if (null != httpStatus) { = httpStatus.value();this.msg = ReasonPhrase();}}
}
error:错误码
msg:错误描述
data:绑定的数据,异常也可能需要返回数据,因此可以在这里去指定
构造方法:基于IStatusCode和HttpStatus的构造方法;用于快速实例化异常对象
扩展异常:如果因业务需要,在特定场所需要一些一些特殊的异常;我们可以再建BaseException的子类去进一步细化。
抛异常:异常定义好之后,想抛一个异常,自然就是很简单的啦
throw new BaseException(HttpStatus.ACCEPTED, "123456");
响应数据初始化工具
上面是提供了各种方式的构造方法,可以根据实际的需要进行实例化;为了能够更加方便的使用,所以这里写了一个静态工具类;用于将实例化响应对象的动作进一步封装,让响应数据对象的实例化更加简单、便捷;
/** 响应帮助类*/
public class ReturnUtils {/*** 响应成功** @return*/public static BaseResponceDto<Void> success() {return new BaseResponceDto(BaseStatusCode.SUCCESS);}/*** 根据Http状态码返回** @return 基础的响应对象*/public static BaseResponceDto<Void> successByHttpStatus() {return new BaseResponceDto(HttpStatus.OK);}/*** 根据自定义的状态码返回* 有响应数据的成功** @param data 响应的数据* @param <T> 响应的数据类型* @return 基础的响应对象*/public static <T> BaseResponceDto success(T data) {return new BaseResponceDto<T>(BaseStatusCode.SUCCESS, data);}/*** 根据http状态码返回** @param data 响应的数据* @param <T> 响应的数据类型* @return 基础的响应对象*/public static <T> BaseResponceDto successByHttpStatus(T data) {return new BaseResponceDto<T>(HttpStatus.OK, data);}/*** 没有响应数据的失败** @param statusCode 状态码* @return*/public static BaseResponceDto<Void> error(BaseStatusCode statusCode) {return new BaseResponceDto(statusCode);}/*** 有响应数据的失败** @param statusCode 状态码* @param data 数据* @return*/public static <T> BaseResponceDto error(BaseStatusCode statusCode, T data) {return new BaseResponceDto<T>(statusCode, data);}/*** 异常后的响应** @param baseException 异常* @return*/public static BaseResponceDto error(BaseException baseException) {return new BaseResponceDto(baseException);}
}
使用示例
成功响应
// 不带数据
return ReturnUtils.success();
// 带数据
return ReturnUtils.success("123456");
失败响应
// 不带数据
(BaseStatusCode.ERR_9999);
// 带数据
(BaseStatusCode.ERR_9999,"123456");
这个放在响应的最后说,是因为他并不属于响应结构的东西,但是他又属于响应的一部分,而且很重要;一个系统,权限是不可缺少的一部分,所谓的权限,简单的说,也就是不同的人,不同的接口,看到的数据不一样;同样是用户查询,用户列表只需要返回用户名即可,而用户详情就需要返回更多的数据;那么这种情况我们需要怎么去响应呢?定义多个响应DTO,当然这是最简单的方式;同样,我们也可以和validator中的分组一样;使用JsonView对响应的结果进行分组,使得同一个对象,在不同接口中返回不同的属性;
JsonView说明
JsonView的定义和validator中的group是类似的概念;也是基于接口,使用也和
validator
类似;
使用
定义顶级接口
此接口为所有JsonView接口的父类;其作用于响应的基础属性上;如下:
@Data
public class BaseResponceDto<T> {/*** 响应数据最外层的视图 也是所有响应视图的父类*/public interface ResponceBaseDtoView {}/*** 状态码*/@JsonView(ResponceBaseDtoView.class)private Integer status;//....
}
业务接口定义
如下所示:
所有视图都直接或者间接继承自ResponceBaseDtoView基础视图;否则会导致响应的BaseResponceDto对象为空json {}
简单视图只返回用户名和手机号码
详情视图,返回所有的属性
/*** 用户响应请求*/@Datapublic class UserResponceDto {// 简单视图,只返回最基数的属性public interface UserResponceSimpleDtoView extends BaseResponceDto.ResponceBaseDtoView {};// 详情视图,返回详细的属性参数public interface UserResponceDetailDtoView extends UserResponceSimpleDtoView {};/*** 用户名*/@JsonView(UserResponceSimpleDtoView.class)public String userName;/*** 年龄*/@JsonView(UserResponceDetailDtoView.class)private Integer age;/*** 性别*/@JsonView(UserResponceDetailDtoView.class)private Integer gender;/*** 邮箱*/@JsonView(UserResponceDetailDtoView.class)private String email;/*** 电话号码*/@JsonView(UserResponceSimpleDtoView.class)private String phoneNum;/*** 修改人*/@JsonView(UserResponceDetailDtoView.class)private String optUser;
}
Controller指定视图
@GetMapping("getSimple")// 指定JsonView的简单视图@JsonView(UserResponceDto.UserResponceSimpleDtoView.class)public BaseResponceDto getSimple() {UserResponceDto userResponceDto = new UserResponceDto();userResponceDto.setUserName("张三");userResponceDto.setAge(10);userResponceDto.setEmail("zhangsan@qq");userResponceDto.setGender(0);userResponceDto.setPhoneNum("13888888888");userResponceDto.setOptUser("admin");return ReturnUtils.success(userResponceDto);}@GetMapping("getDetail")// 指定详细视图@JsonView(UserResponceDto.UserResponceDetailDtoView.class)public BaseResponceDto getDetail() {UserResponceDto userResponceDto = new UserResponceDto();// 内容和上面一样return ReturnUtils.success(userResponceDto);}
即可看到,两个接口,根据我们的指定,返回了不同的属性值。
需求
上面定义的代码;为了保证数据的响应格式是BaseResponceDto格式的;因此Controller所有的方法都是返回了这个对象;目的也是为了保证响应格式的一致性;但是,我们不返回这个对象可以吗?完全是可以的,而且也不会有任何报错;但是,这样却打破了我们定义的规则,导致响应的结构不一致了。
能够在一个统一的地方去配置返回;保证响应的都是BaseResponceDto
;而controller只需要返回数据即可;如下:
@GetMapping("getSimple")
@JsonView(UserResponceDto.UserResponceSimpleDtoView.class)
public UserResponceDto getSimple() {UserResponceDto userResponceDto = new UserResponceDto();// ....return userResponceDto;
}
响应自动包装外层结构
{"status": 200,"msg": "成功!","data": {"userName": "张三","phoneNum": "13888888888"}
}
restControllerAdvice拦截并重构响应
supports
判断是否指定了特定的注解
beforeBodyWrite
在写入body之前,会调用这个方法;因此,就可以在这里将响应对象给改掉
if (body instanceof BaseResponceDto) {return body;
}
// 否则的话就直接返回
return ReturnUtils.success(body);
@RestControllerAdvice(basePackages = "com.zhubayi")
如果以jar的方式加入;这里务必要指明一下当前类所处的路径;否则可能因为没有扫描到导致加载失败。
创建继承自@ResponseBody
的注解,用来添加到方法或者类上;当响应写入body之间拦截结果
/*** @title: ResponseDataBody* @projectName springcloud-mbb* @description: TODO 规范响应数据的注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseDataBody {
}
定义ResponseDataBodyAdvice拦截添加了@ResponseDataBody注解的响应
@RestControllerAdvice(basePackages = "com.lupf")
@Slf4j
public class ResponseDataBodyAdvice implements ResponseBodyAdvice<Object> {/*** 得到自定义的注解*/private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseDataBody.class;/*** 判断类或者方法是否使用了 @ResponseDataBody* 这里将注解添加在BaseController上面;以为着只要继承了BaseController的Controller都使用了该注解*/@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return AnnotatedElementUtils.ContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);}/*** 当类或者方法使用了 @ResponseDataBody 也就是上面的方法返回的true 就会调用这个方法*/@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 防止重复包裹的问题出现 如果已经是要返回的基础对象了 就直接返回if (body instanceof BaseResponceDto) {return body;}// 否则的话就直接返回return ReturnUtils.success(body);}
}
封装的优点
规范响应:
避免因为代码错误或者响应错误导致报文格式异常;这样写,可以包装返回的对象必定是BaseResponceDto
减少冗余代码
Controller中直接返回数据对象;封装统一去进行。
问题点
当我们的业务逻辑中出现了异常;比如要修改某个用户,请求的数据也没有问题;结果在修改直接去查找用户的时候,发现已经没有这个用户了;那么一般就抛出一个用户不存在的异常,如果不对异常进行处理的话,前端就只会收到一个400的错误;而我们希望的是这样:
{"status": 2001,"msg": "用户不存在!"
}
通过ExceptionHandler捕获全局异常
使用RestControllerAdvice注解
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(HttpMessageNotReadableException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)@ResponseBodypublic Object HttpMessageNotReadableExceptionHandler(HttpMessageNotReadableException httpMessageNotReadableException){("捕获请求参数读取异常....",httpMessageNotReadableException);// 前端未传递参数 导致读取参数异常(BaseStatusCode.ERR_1000);}@ExceptionHandler(BindException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)@ResponseBodypublic Object bindExceptionHandler(BindException bindException){("捕获请求参数校验异常....",bindException);// 获取到所有的校验失败的属性List<FieldError> fieldErrors = FieldErrors();// 实例化一个用于装参数错误的listList<ParamErrDto> paramErrDtos = new ArrayList<>();for (FieldError fieldError : fieldErrors) {// 那段字段名String field = Field();// 拿到异常的描述String defaultMessage = DefaultMessage();log.info("field:{} msg:{}", field, defaultMessage);// 添加到list中去paramErrDtos.add(new ParamErrDto(field, defaultMessage));}// 返回前端参数错误 并告诉前端那些字段不对 具体描述是什么(BaseStatusCode.ERR_1000, paramErrDtos);}@ExceptionHandler(BaseException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ResponseBodypublic Object baseExceptionHandler(BaseException baseException){("捕获到业务异常!",baseException);// 基础的业务异常(baseException);}/*** 通过ExceptionHandler 捕获controller未捕获到的异常,给用户一个友好的返回** @param ex 异常信息* @return*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ResponseBodypublic Object exceptionHandler(Exception ex) {(");// 所有的 自定义的、已知的异常全部都没有匹配上// 直接响应响应一个未知错误的提醒(BaseStatusCode.ERR_9999);}
}
@ExceptionHandler(HttpMessageNotReadableException.class)
当body没有传参数时,会触发这个异常,并返回参数错误的状态码
@ExceptionHandler(BindException.class)
当validator校验失败之后,会触发这个异常;因此这里将所有不符合规范的传参整理成列表返回。
@ExceptionHandler(BaseException.class)
自定义业务异常;直接将异常对象转换为响应对象;返回给前端
@ExceptionHandler(Exception.class)
用来处理那些没有特定处理的异常;然后由这里拦截之后,统一返回未知错误;
本文发布于:2024-01-29 08:15:43,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170648734513922.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |