resultDemo,接口统一返回对象
如果接口返回的内容格式不规范,会对前端开发人员造成困扰,现在让我们来对接口做一些优化。
实现内容
- 创建统一返回的对象Result
- 使用注解让实现方式变得更加优雅
- 结合全局异常捕获
创建统一返回的对象Result
package com.cc.api;
import java.io.Serializable;
/**
* 统一返回对象
* @author cc
* @date 2021-07-12 10:10
*/
public class Result<T> implements Serializable {
// 自定义状态码
private Integer code;
// 提示内容,如果接口出错,则存放异常信息
private String msg;
// 返回数据体
private T data;
// 接口成功检测。拓展字段,前台可用该接口判断接口是否正常,或者通过code状态码
private boolean success;
private static final long serialVersionUID = 1L;
public Result() {}
public Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
/**
* 请求成功返回
* public和返回值间的<T>指定的这是一个泛型方法,这样才可以在方法内使用T类型的变量
* @author cc
* @date 2021-07-12 10:11
*/
public static <T> Result<T> success() {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
}
/**
* 请求失败返回
* @param msg:
* @author cc
* @date 2021-07-12 10:11
*/
public static <T> Result<T> failed(String msg) {
return new Result<>(ResultCode.FAILED.getCode(), msg, null);
}
public static <T> Result<T> failed(String msg, T data) {
return new Result<>(ResultCode.FAILED.getCode(), msg, data);
}
public static <T> Result<T> failed(ResultCode errorCode) {
return new Result<>(errorCode.getCode(), errorCode.getMsg(), null);
}
public static <T> Result<T> failed(ResultCode errorCode, T data) {
return new Result<>(errorCode.getCode(), errorCode.getMsg(), data);
}
...
public boolean isSuccess() {
return this.code == ResultCode.SUCCESS.getCode();
}
public void setSuccess(boolean success) {
this.success = success;
}
@Override
public String toString() {
return "Result{" + "code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
", success=" + success +
'}';
}
}
自定义状态码表
package com.cc.api;
/**
* APi返回的状态码表
* @author cc
* @date 2021-07-12 8:58
*/
public enum ResultCode {
/**
* api状态码管理
* @author cc
* @date 2021-07-12 10:00
*/
SUCCESS(10000, "请求成功"),
FAILED(10001, "操作失败"),
TOKEN_FAILED(10002, "token失效"),
NONE(99999, "无");
private int code;
private String msg;
private ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
...
}
用到我们的接口上:
package com.cc.controller;
import com.cc.api.Result;
import com.cc.api.ResultCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/a")
public class TestController {
@GetMapping("/t1")
public Result t1() {
return Result.success("请求成功,返回数据");
}
@GetMapping("/t2")
public Result t2() {
return Result.failed("请求失败,这里是错误信息");
}
// 返回自定义的错误
@GetMapping("/t3")
public Result t3() {
return Result.failed(ResultCode.TOKEN_FAILED);
}
}
返回内容:
{"code":10000,"msg":"请求成功","data":"请求成功,返回数据","success":true}
使用注解让实现方式变得更加优雅
虽然我们已经初步实现功能,但是现在需要我们将每一个接口的返回对象改成Result,这有两个问题,一是如果我们的接口已经有很多,那么改动量比较大,二是我们很难分清这个接口本来应该返回什么类型的值,这样反而造成了维护的不清晰,不能忍受。
所以要保持原来接口的代码,然后通过给接口类添加注解,来实现返回内容的统一。
我们要创建三个类:
- ResponseResult,注解类
- ResponseResultAdvice,返回结果处理类
- ResponseResultInterceptor,拦截器
ResponseResult:
package com.cc.response;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 需要统一对返回值进行封装的注解
* @author cc
* @date 2021-07-12 10:50
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseResult {
}
ResponseResultAdvice:
package com.cc.response;
import com.cc.api.Result;
import com.cc.api.ResultCode;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 统一封装返回结果
* @author cc
* @date 2021-07-12 10:53
*/
@ControllerAdvice
public class ResponseResultAdvice implements ResponseBodyAdvice<Object> {
/**
* 标记名称
* @author cc
* @date 2021-07-12 10:53
*/
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
/**
* 判断请求,是否有包装标记,没有就直接返回,不需要重写返回体
* @author cc
* @date 2021-07-12 10:53
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
ResponseResult responseResult = (ResponseResult)request.getAttribute(RESPONSE_RESULT_ANN);
// 是否要执行beforeBodyWrite方法
return responseResult != null;
}
/**
* 对response处理的执行方法
* @author cc
* @date 2021-07-12 10:53
*/
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
/**
* 不能直接包装String因为当返回值为String的时候,beforeBodyWrite会使用StringHttpMessageConverter转换器,
* 所以在返回Result的时候会报cannot be cast to java.lang.String错误,解决方式有三种:
* 1. 在接口层将String打包成Result,这个需要跟诸位开发者协商一致,比较麻烦
* 2. 将Result对象转换成字符串返回,这样在前端显示的也是字符串,不妥(实现代码:ObjectMapper om = new ObjectMapper(); return om.writeValueAsString(Result.success(o));)
* 3. 在实现了WebMvcConfigurer的配置类上,修改转换器的执行顺序,让Json比String更早被处理(具体实现在WebMvcConfig类中)
*/
// 如果已经是Result类,就不再进行封装
if (o instanceof Result) {
return o;
}
// 如果是列表类
if (o instanceof List) {
// do something
}
// 如果是整型
if (o instanceof Integer) {
// do something
}
return Result.success(o);
}
}
ResponseResultInterceptor:
package com.cc.response;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* 返回结果拦截器
* @author cc
* @date 2021-07-12 10:55
*/
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
/**
* 标记名称
* @author cc
* @date 2021-07-12 10:55
*/
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod)handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断类上是否有该注解
if (clazz.isAnnotationPresent(ResponseResult.class)) {
// 设置此请求体,添加标记,向下传递,在ResponseResultAdvice进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
// System.out.println("类上有ResponseResult注解");
} else if(method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
// System.out.println("方法上有ResponseResult注解");
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
为了使其生效,我们把它加到配置类中:
WebMvcConfig:
package com.cc.config;
import com.cc.response.ResponseResultInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
/**
* web mvc的配置类
* @author cc
* @date 2021-07-12 10:29
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
private final ResponseResultInterceptor responseResultInterceptor;
public WebMvcConfig(ResponseResultInterceptor responseResultInterceptor) {
this.responseResultInterceptor = responseResultInterceptor;
}
/**
* 统一返回格式 Result
* @author cc
* @date 2021-07-12 10:30
*/
// 结合接口版本管理后,添加过滤器的代码要放到requestMappingHandlerMapping
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(responseResultInterceptor).addPathPatterns("/**");
super.addInterceptors(registry);
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 将json处理器的执行顺序提前,避免Result因为返回String的时候,被string处理器执行导致报错
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
然后在需要的地方加上这个注解即可,不需要改动原有接口代码:
package com.cc.controller;
import com.cc.response.ResponseResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ResponseResult
@RestController
@RequestMapping("/b")
public class Test2Controller {
@GetMapping("/t1")
public String t1() {
return "hello java";
}
@GetMapping("/t2")
public Integer t2() {
return 1;
}
}
返回结果:
{"code":10000,"msg":"请求成功","data":"hello java","success":true}
结合全局异常捕获
接口正常执行的时候没有问题,但是当出现异常时Result就不生效了,所以结合全局异常捕获,可以让系统在程序出错的时候仍然返回规范的结果给前端:
GlobalExceptionHandler:
package com.cc.exception;
import com.cc.api.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常捕获,出现异常时以Result的形式返回
* @author cc
* @date 2021/06/16 15:10
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result<Object> catchException(Exception e) {
todoErrorLogger(e, null);
e.printStackTrace();
return Result.failed(e.getMessage());
}
private void todoErrorLogger(Exception e, String customErrorMsg) {
StringBuilder sb = new StringBuilder();
sb.append(e.getClass().getName()).append(":");
if (customErrorMsg != null) {
sb.append(customErrorMsg);
} else {
sb.append(e.getMessage());
}
StackTraceElement[] elements = e.getStackTrace();
if(elements.length > 0){
StackTraceElement element = elements[0];
sb.append("##function:").append(element.getClassName()).append("-").append(element.getMethodName()).append("-").append(element.getLineNumber());
}
e.printStackTrace();
}
}