接口返回真实的业务数据对象,而且还要有统一的返回数据格式。例如,接口是这样的:
@GetMapping("/user/{id}")
public User user(@PathVariable("id") Integer id){
return userService.getUser(id);
}
但是返回的数据格式是这样的
{
"code": 1,
"msg": "成功",
"data": {
"id": 1,
"username": "user",
"password": "e10adc3949ba59abbe56e057f20f883e",
"email": null,
"authorities": null,
"enabled": true,
"accountNonLocked": true,
"accountNonExpired": true,
"credentialsNonExpired": true
}
}
好处是我们在编写接口的时候,不用每次都把数据封装到统一的数据返回类型中。直接以最真实的业务对象返回,更加清晰明了。
@ResponseResult
用于在方法或者类上面标注,标识这个接口需要包装数据@ResponseResult
注解标注ResponseBodyAdvice
,然后需要用@ControllerAdvice
注解激活,用于判断请求是否需要包装,如果需要,就把Controller中的返回值进行重写@Data
public class Result implements Serializable {
// 返回状态码
private Integer code;
// 返回消息
private String msg;
// 返回数据
private Object data;
public void setResultCode(ResultCode code){
this.code = code.code();
this.msg = code.msg();
}
public void setResultCode(ResultCode code,String msg){
this.code = code.code();
this.msg = msg;
}
/* 调用成功 */
public static Result success(){
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
/* 调用成功 */
public static Result success(Object data){
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
/* 调用失败 */
public static Result error(ResultCode code){
Result result = new Result();
result.setResultCode(code);
return result;
}
/* 调用失败 */
public static Result error(ResultCode code,Object data){
Result result = new Result();
result.setResultCode(code);
result.setData(data);
return result;
}
public enum ResultCode {
/* 成功状态码 */
SUCCESS(1,"成功"),
/* 参数错误,1001-1999 */
PARAM_IS_INVALID(1001,"参数无效"),
PARAM_IS_BACK(1002,"参数为空"),
PARAM_TYPE_BIND_ERROR(1003,"参数类型错误"),
PARAM_NOT_COMPLETE(1004,"参数缺失"),
/* 用户错误: 2001-2999 */
USER_NOT_LOGIN(2001,"用户未登录,访问的路径需要验证,请登录"),
USER_LOGIN_ERROR(2002,"账号不存在或密码错误"),
USER_ACCOUNT_FORBIDDEN(2003,"账号已被禁用"),
USER_NOT_EXISTS(2004,"用户不存在"),
USER_HAS_EXISTS(2005,"用户已存在"),
/* 系统异常异常错误 4001-4999 */
SYSTEM_THROW_ERROR(4001,"系统内部异常");
private Integer code;
private String msg;
public Integer code(){
return this.code;
}
public String msg(){
return this.msg;
}
ResultCode(Integer code,String msg){
this.code = code;
this.msg = msg;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE , ElementType.METHOD})
@Documented
public @interface ResponseResult {
}
此注解仅做标注作用,所以不需要其他属性。
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
// 标记名称
private final static String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* 【 拦截标注了@ResponseResult注解的请求 】
* @Author zwx
* @Date 2020/7/28 09:28
**/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// handle是拦截的当前请求的方法的全类名+参数+抛出的异常
// 请求的方法
if (handler instanceof HandlerMethod){
final HandlerMethod handlerMethod = (HandlerMethod) handler;
// 所在类
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断类上是否加了@ResponseResult注解
if (clazz.isAnnotationPresent(ResponseResult.class)){
//设置次请求返回体,需要包装,向下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN,clazz.getAnnotation(ResponseResult.class));
}
// 如果类上没有,则再判断方法上
else if (method.isAnnotationPresent(ResponseResult.class)){
//设置次请求返回体,需要包装,向下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN,method.getAnnotation(ResponseResult.class));
}
}
return true;
}
// intercept 的其他两个接口不涉及逻辑,此处省略
... ...
}
主要就是利用反射API来获取此请求接口的全方法名,以及类名。然后判断如果此方法标注在类上,那么类中的所有接口都应该包装返回值。如果类上没有标注注解,再判断方法上是否标注了注解。如果标注了注解的话,就在请求中设置attribute
,仅用作标记用,具体的还需要在ResponseBodyAdvice
类中进行处理。
因为我们到时候注册拦截器的时候是拦截的所有的请求,然后再根据请求的类型来进行判断,handler instanceof HandlerMethod
,如果是一个web接口方法,则走进if判断中。如果不是,直接放行请求。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求
registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");
}
}
此处拦截所有请求,或者可以根据自己的业务需要来具体安排。
此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。重写返回体
@ControllerAdvice
public class ResponseResultAdvice implements ResponseBodyAdvice<Object> {
// 标记名称
private final static String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// 判断请求是否有包装标记
ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResultAnn != null;
}
}
这是实现ResponseBodyAdvice
接口必须要重写的两个方法之一,用来判断是否需要包装返回值。如果需要就包装返回值,然后方法返回值true,就会执行包装的方法beforeBodyWrite()
,如果不需要包装就直接返回false,不用重写beforeBodyWrite()
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
System.out.println("============开始包装返回类型");
System.out.println(body);
// 判断body是否是异常类型
if ( body instanceof Result){
return body;
}
return Result.success(body);
}
包装返回值的核心代码,直接将原本的返回值封装进Result
就可以。
这里的判断body instanceof Result
的作用是,如果需要包装返回值的接口发生了异常,我们需要对异常进行处理。
这里我采用了全局异常处理@ControllerAdvice+@ExceptionHandler
来处理发生的异常,然后将异常信息包装进Result
对象。
之所以这么判断,是因为当接口出现异常之后,会先调用被@ExceptionHandler
注解标注的方法ProcessException()
,来捕获指定的异常,这样的话异常信息就会被封装进Result
了。那之后才会调用包装返回值方法beforeBodyWrite()
,这里就需要判断body
的类型了,因为如果出现异常,那么此时body
的类型是在ProcessException()
中封装之后返回的Result
对象,那么此时并不需要再次封装,直接返回就好了。而如果没有出现异常,那么此时body
就是实际的业务对象了,此时就需要来封装返回值。
@ExceptionHandler( value = Exception.class)
@ResponseBody
public Object ProcessException(Exception e){
return Result.error(ResultCode.SYSTEM_THROW_ERROR,e.getMessage());
}
参考博客:
Java生鲜电商平台-统一格式返回的API架构设计与实战