web应用中一种统一返回体的设计 2021-11-25 11:33 > JDK1.8,SpringBoot2.6.0 ### 1、依赖 ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> ``` ### 2、统一返回体类 ```java package com.example.demo.entity.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import javax.servlet.http.HttpServletResponse; /** * @author: HanXu * on 2021/11/25 * Class description: controller对外统一返回体 */ @Data @AllArgsConstructor @Builder public class BaseResponse<T> { private int code; private String msg; private T data; public BaseResponse(T data) { code = HttpServletResponse.SC_OK; msg = "success"; this.data = data; } public BaseResponse(int code, String msg) { this.code = code; this.msg = msg; } } ``` 工具类: ```java package com.example.demo.utils; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Objects; /** * @author: HanXu * on 2021/11/25 * Class description: */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public abstract class JacksonUtil { private final static ObjectMapper objectMapper; static { objectMapper = initWrapperObjectMapper(new ObjectMapper()); } /** * 转换Json * * @param object * @return */ public static String toJson(Object object) { if (isCharSequence(object)) { return (String) object; } try { return getObjectMapper().writeValueAsString(object); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } /** * 获取ObjectMapper * * @return */ public static ObjectMapper getObjectMapper() { return objectMapper; } /** * 初始化 Wrapper ObjectMapper * * @param objectMapper * @return */ public static ObjectMapper initWrapperObjectMapper(ObjectMapper objectMapper) { if (Objects.isNull(objectMapper)) { objectMapper = new ObjectMapper(); } objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); //不显示为null的字段 objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // 忽略不能转移的字符 objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); // 过滤对象的null属性. objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //忽略transient objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true); SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); simpleModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); simpleModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); // simpleModule.addSerializer(Long.class, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); return objectMapper; } /** * <p> * 是否为CharSequence类型 * </p> * * @param object * @return */ public static Boolean isCharSequence(Object object) { return !Objects.isNull(object) && isCharSequence(object.getClass()); } /** * <p> * 是否为CharSequence类型 * </p> * * @param clazz class * @return true 为是 CharSequence 类型 */ public static boolean isCharSequence(Class<?> clazz) { return clazz != null && CharSequence.class.isAssignableFrom(clazz); } /** * Json转换为对象 转换失败返回null * * @param json * @return */ public static Object parse(String json) { Object object = null; try { object = getObjectMapper().readValue(json, Object.class); } catch (Exception ignored) { } return object; } /** * Json转换为对象 转换失败返回null * * @param json * @param clazz * @param <T> * @return */ public static <T> T parseObject(String json, Class<T> clazz) { T t = null; try { t = getObjectMapper().readValue(json, clazz); } catch (Exception ignored) { } return t; } } ``` ### 3、自定义注解 自定义注解@ResultUnite,该注解标注的类或方法,都能被包装成统一的返回。 wrapper包/annotation包下新建此类 ```java package com.example.demo.wrapper.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author: HanXu * on 2021/11/25 * Class description: 统一返回结果注解 */ @Target({ ElementType.TYPE, ElementType.METHOD }) //作用范围 @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface ResultUnite { } ``` 然后使用@ControllerAdvice注解,来拦截所有 @RequestMapping 等注解标注的方法。这些方法返回时,不立刻返回,而是先经过 supports方法判断是否需要处理,返回true时,则执行beforeBodyWrite方法。 wrapper包/advice包下新建此类 ```java package com.example.demo.wrapper.advice; import com.example.demo.entity.vo.BaseResponse; import com.example.demo.utils.JacksonUtil; import com.example.demo.wrapper.annotation.ResultUnite; 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.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.lang.reflect.Method; /** * @author: HanXu * on 2021/11/25 * Class description: 统一返回结果 */ @ControllerAdvice public class BaseResponseBodyAdvice implements ResponseBodyAdvice<Object> { private static final String CONVERT_NAME = "org.springframework.http.converter.StringHttpMessageConverter"; private boolean isResultUnite(MethodParameter methodParameter, Class aClass) { Method method = methodParameter.getMethod(); return aClass.isAnnotationPresent(ResultUnite.class) ||method.isAnnotationPresent(ResultUnite.class); } @Override public boolean supports(MethodParameter methodParameter, Class aClass) { return isResultUnite(methodParameter, aClass); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { //处理String类型的返回值 if (CONVERT_NAME.equalsIgnoreCase(selectedConverterType.getName())) { return JacksonUtil.toJson(new BaseResponse(body)); } return new BaseResponse(body); } } ``` 在supports方法中,我们的处理是:通过反射获取到执行方法/类上是否有@ResultUnite注解,有就返回true;无则返回false。 在beforeBodyWrite方法中,我们将方法的返回作为data传入BaseResponse构造方法中,重新new了一个BaseResponse对象,所以就达到了将返回数据包装成BaseResponse的目的。 以上两个类就给我们提供了一个具有能够包装返回数据的注解@ResultUnite。 #### 3.1、阶段测试 新建controller类测试一下 ```java package com.example.demo.controller; import com.example.demo.wrapper.annotation.ResultUnite; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author: HanXu * on 2021/11/25 * Class description: */ @Slf4j @RestController public class TestController { @ResultUnite @RequestMapping("/test1") public String test1() { log.info("---------------test1---------------"); return "111"; } } ``` 上述test1方法被本应该返回“111”的,但是被@ResultUnite注解了,所以执行观察,返回包装后的结果: ```json { "code": 200, "msg": "success", "data": "111" } ``` 再试一下: ```java @ResultUnite @RequestMapping("/test2") public List<String> test2() { log.info("---------------test2---------------"); List<String> list = new ArrayList<String>(){ { add("123"); add("456"); } }; return list; } ``` 执行返回: ```json { "code": 200, "msg": "success", "data": [ "123", "456" ] } ``` 这样我们就在任何成功的情况下,统一了对外部的返回(code,msg,data)。且实现非常清爽,在业务代码中只需要一个注解即可。 可是异常情况呢?异常情况因为不会再从controller中的return出去,所以就不受控制了。接下来处理异常情况。 ### 4、统一异常处理 #### 4.1、自定义异常类 这个类看起来和BaseResponse结构一样,那为什么不能使用BaseResponse代替呢? 因为此类需要继承RuntimeException,将来在代码中可以catch住此异常信息,然后做处理。况且两个类分别是不同功能的,即使属性一样,也不能复用。 ```java package com.example.demo.entity.vo; import com.example.demo.entity.enums.ExceptionCodeEnum; import lombok.Data; /** * @author: HanXu * on 2021/11/25 * Class description: 自定义异常:方便进行异常信息扩展,且与RuntimeException区分开 */ @Data public class CustomerExceptionVO extends RuntimeException { private int code; private String msg; private Object data; public CustomerExceptionVO(ExceptionCodeEnum exceptionCodeEnum) { code = exceptionCodeEnum.getCode(); msg = exceptionCodeEnum.getMsg(); } public CustomerExceptionVO(int code, String msg) { super(msg); this.code = code; this.msg = msg; } public CustomerExceptionVO(int code, String msg, Object data) { super(msg); this.code = code; this.msg = msg; this.data = data; } } ``` 枚举异常: ```java package com.example.demo.entity.enums; import javax.servlet.http.HttpServletResponse; /** * @author: HanXu * on 2021/11/25 * Class description: 通用异常枚举,供 CustomerException 使用 */ public enum ExceptionCodeEnum { //通用异常 /** * 400 */ Bad_Request(HttpServletResponse.SC_BAD_REQUEST, "请求信息错误,请检查参数"), /** * 401 */ UnAuth(HttpServletResponse.SC_UNAUTHORIZED, "请先登陆"), /** * 403 */ Forbidden(HttpServletResponse.SC_FORBIDDEN, "无权查看"), /** * 404 */ Not_Found(HttpServletResponse.SC_NOT_FOUND, "未找到该路径或资源"), /** * 405 */ Method_Not_Allowed(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "请求方式不支持"), /** * 500 */ Internal_Server_Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"), /** * 503 */ Service_Unavailable(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "请求超时"), //自定义扩展其他业务异常 /** * 400 */ Username_Or_PassWord_Error(HttpServletResponse.SC_BAD_REQUEST, "用户名密码错误"), /** * 400 */ Username_Exist(HttpServletResponse.SC_BAD_REQUEST, "用户名已存在"), /** * 400 */ ValidCode_Error(HttpServletResponse.SC_BAD_REQUEST, "验证码不正确"), ; private int code; private String msg; ExceptionCodeEnum(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } public static ExceptionCodeEnum getByCode(int code) { ExceptionCodeEnum[] enums = ExceptionCodeEnum.values(); for (ExceptionCodeEnum exceptionCodeEnum : enums) { if (exceptionCodeEnum.code == code) { return exceptionCodeEnum; } } return null; } } ``` 工具类: ```java package com.example.demo.utils; import java.text.SimpleDateFormat; import java.util.Date; /** * @author: HanXu * on 2021/11/25 * Class description: 通用工具类 */ public class CommonUtil { public static Object getNotNull(Object original, Object backup) { if (backup == null) { throw new RuntimeException("备用参数不能为空"); } return original != null ? original : backup; } public static Object getOrDefault(Object original, Object defaultValue) { return original != null ? original : defaultValue; } public static String getNonce() { return getNonce(8); } /** * 获取随机数 eg:0E35E147 * @param n * @return */ public static String getNonce(int n) { String nonce = ""; //一个十六进制的值的数组 String[] array = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}; //得到6个随机数 for (int i = 0; i < n; i++) { int num = (int) (Math.random() * 16); nonce += array[num]; } return nonce; } /** * 格式化获取当前时间 * @return */ public static String getNowDateFomart(){ SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = new Date(); return format.format(date); } } ``` #### 4.2、统一异常处理 使用@ControllerAdvice加@ExceptionHandler注解,拦截代码中抛出的异常。拦截到属于CustomerExceptionVO的异常就优先handleCustomerException方法处理;否则就放行,看是否属于Exception异常(显然剩下的都是),然后由handleLeftException处理。 ```java package com.example.demo.exception; import com.example.demo.entity.vo.BaseResponse; import com.example.demo.entity.vo.CustomerExceptionVO; import com.example.demo.utils.CommonUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author: HanXu * on 2021/11/25 * Class description: 统一异常处理 */ @Slf4j @ControllerAdvice @ResponseBody public class GlobalExceptionHandle { /** * 跟方法的上下顺序无关,系统优先给到捕获最小异常的方法,然后是大异常的方法 * 只能捕获代码中手动抛出的 CustomerException 或 RuntimeException * @param e * @return */ @ExceptionHandler(CustomerExceptionVO.class) public BaseResponse handleCustomerException(CustomerExceptionVO e, HttpServletRequest request) { log.error("异常request uri: {}", request.getRequestURI()); log.error(e.getMessage(), e); return new BaseResponse((Integer) CommonUtil.getNotNull(e.getCode(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR), (String) CommonUtil.getOrDefault(e.getMsg(), e.getMessage()), e.getData()); } /** * 捕获剩余所有异常 * @param e * @param request * @return */ @ExceptionHandler(Exception.class) public BaseResponse handleLeftException(Exception e, HttpServletRequest request) { log.error("异常request uri: {}", request.getRequestURI()); log.error(e.getMessage(), e); return new BaseResponse(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"); } } ``` #### 4.3、意料之外错误的处理 当不是我们的代码抛出的异常时,GlobalExceptionHandle拦截不到,需要使用@RequestMapping("error")加ErrorController处理。 ```java package com.example.demo.exception; import com.example.demo.entity.enums.ExceptionCodeEnum; import com.example.demo.entity.vo.BaseResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletResponse; /** * @author: HanXu * on 2021/11/25 * Class description: 拦截错误:GlobalExceptionHandle拦不到的异常(非代码中手动抛出的异常,上层SpringBoot中抛出的异常),当出错的时候,这里处理 */ @Slf4j @Controller @RequestMapping("error") public class GlobalErrorController implements ErrorController { @RequestMapping @ResponseBody public BaseResponse error(HttpServletResponse response) { int statusCode = response.getStatus(); ExceptionCodeEnum exceptionCodeEnum = ExceptionCodeEnum.getByCode(statusCode); if (exceptionCodeEnum == null) { exceptionCodeEnum = ExceptionCodeEnum.Internal_Server_Error; } log.error("error: {}", exceptionCodeEnum.getMsg()); BaseResponse baseResult = new BaseResponse(exceptionCodeEnum.getCode(), exceptionCodeEnum.getMsg());; return baseResult; } } ``` #### 4.4、阶段测试 ①、我们故意抛出一个异常: ```java @ResultUnite @RequestMapping("/test3") public String test3() { log.info("---------------test3---------------"); try { int i = 5 / 0; } catch (Exception e) { throw e; } return "111"; } ``` 执行测试: ```json { "code": 500, "msg": "服务器异常", "data": null } ``` 这个throw e;就是被GlobalExceptionHandle中的handleLeftException拦截到了。 ②、让程序内部(非代码)抛出一个异常 ```java @ResultUnite @RequestMapping("/test4") public String test4() { log.info("---------------test4---------------"); int i = 5 / 0; return "111"; } ``` 执行测试: ```json { "code": 500, "msg": "服务器异常", "data": null } ``` 也能被GlobalExceptionHandle中的handleLeftException拦截到。 ③、某些情况需要抛出自定义异常 ```java @ResultUnite @RequestMapping("/test5") public String test5() { log.info("---------------test5---------------"); //假装i是入参 int i = 1; if (1 == i) { throw new CustomerExceptionVO(ExceptionCodeEnum.Forbidden); } return "111"; } ``` 执行测试: ```json { "code": 403, "msg": "无权查看", "data": null } ``` 被GlobalExceptionHandle中的handleCustomerException拦截到。 ### 5、异常结束工具 在4.4的③中,这种情况应该是经常有的,我们需要对业务情况判断,然后众多不符合规则的case都异常返回了,这样每次都`throw new CustomerExceptionVO`是很麻烦的事情,所以可以搞一个工具来使用。 ```java package com.example.demo.exception; import com.example.demo.entity.enums.ExceptionCodeEnum; import com.example.demo.entity.vo.CustomerExceptionVO; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletResponse; /** * @author: HanXu * on 2021/11/25 * Class description: 代码工具类:判断代码逻辑,不符合则抛出异常 */ @Component public class ApiAssert { public static void notNull(Object a) { if (a == null) { failure(ExceptionCodeEnum.Bad_Request); } if (a instanceof String) { if (((String) a).length() == 0) { failure(ExceptionCodeEnum.Bad_Request); } } } public static void notNull(Object a, String msg) { if (a == null) { badRequest(msg); } if (a instanceof String) { if (((String) a).length() == 0) { badRequest(msg); } } } //-------------------------400----------------------- public static void badRequest(String msg) { throw new CustomerExceptionVO(HttpServletResponse.SC_BAD_REQUEST, msg); } //--------------------------500----------------------- public static void failure(ExceptionCodeEnum exceptionCodeEnum) { throw new CustomerExceptionVO(exceptionCodeEnum); } public static void failure(String msg, Object data) { throw new CustomerExceptionVO(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg, data); } public static void failure(int code, String msg, Object data) { throw new CustomerExceptionVO(code, msg, data); } public static void failure(Object data) { throw new CustomerExceptionVO(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "error", data); } //--------------------------业务工具---------------------- public static void forbidden() { throw new CustomerExceptionVO(ExceptionCodeEnum.Forbidden); } public static void internal() { throw new CustomerExceptionVO(ExceptionCodeEnum.Internal_Server_Error); } } ``` 然后就可以在业务代码中使用: ```java @ResultUnite @RequestMapping("/test6") public String test6() { log.info("---------------test6---------------"); //假装i是入参 int i = 1; if (1 == i) { ApiAssert.badRequest("请求不合法"); } return "111"; } ``` 执行测试: ```json { "code": 400, "msg": "请求不合法", "data": null } ``` 这样就可以在任何需要的地方使用ApiAssert来结束并返回。 ### 6、总结 上述统一返回体的处理只是一种处理方式,包含了正常结束,异常返回,业务代码自定义中断返回的处理。也许并不适合所有。但能包容大多数情况。 其实仍有许多改进的地方,比如抛出的异常错误码,完全可以自定义一个自己系统统一的错误码表,然后使用这个码表,就不用每次都使用HttpServletResponse的了。 --END--
发表评论