我的个人网站:http://riun.xyz
1、依赖JDK1.8,SpringBoot2.6.0
2、统一返回体类4.0.0 org.springframework.boot spring-boot-starter-parent 2.6.0 com.example demo 0.0.1-SNAPSHOT demo Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
package com.example.demo.entity.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import javax.servlet.http.HttpServletResponse; @Data @AllArgsConstructor @Builder public class baseResponse{ 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; } }
工具类:
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;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class JacksonUtil {
private final static ObjectMapper objectMapper;
static {
objectMapper = initWrapperObjectMapper(new ObjectMapper());
}
public static String toJson(Object object) {
if (isCharSequence(object)) {
return (String) object;
}
try {
return getObjectMapper().writevalueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static ObjectMapper getObjectMapper() {
return objectMapper;
}
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;
}
public static Boolean isCharSequence(Object object) {
return !Objects.isNull(object) && isCharSequence(object.getClass());
}
public static boolean isCharSequence(Class> clazz) {
return clazz != null && CharSequence.class.isAssignableFrom(clazz);
}
public static Object parse(String json) {
Object object = null;
try {
object = getObjectMapper().readValue(json, Object.class);
} catch (Exception ignored) {
}
return object;
}
public static T parseObject(String json, Class clazz) {
T t = null;
try {
t = getObjectMapper().readValue(json, clazz);
} catch (Exception ignored) {
}
return t;
}
}
3、自定义注解
自定义注解@ResultUnite,该注解标注的类或方法,都能被包装成统一的返回。
wrapper包/annotation包下新建此类
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;
@Target({ ElementType.TYPE, ElementType.METHOD }) //作用范围
@Retention(RetentionPolicy.RUNTIME)
@documented
@Inherited
public @interface ResultUnite {
}
然后使用@ControllerAdvice注解,来拦截所有 @RequestMapping 等注解标注的方法。这些方法返回时,不立刻返回,而是先经过 supports方法判断是否需要处理,返回true时,则执行beforeBodyWrite方法。
wrapper包/advice包下新建此类
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; @ControllerAdvice public class baseResponseBodyAdvice implements ResponseBodyAdvice
在supports方法中,我们的处理是:通过反射获取到执行方法/类上是否有@ResultUnite注解,有就返回true;无则返回false。
在beforeBodyWrite方法中,我们将方法的返回作为data传入baseResponse构造方法中,重新new了一个baseResponse对象,所以就达到了将返回数据包装成baseResponse的目的。
以上两个类就给我们提供了一个具有能够包装返回数据的注解@ResultUnite。
3.1、阶段测试新建controller类测试一下
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;
@Slf4j
@RestController
public class TestController {
@ResultUnite
@RequestMapping("/test1")
public String test1() {
log.info("---------------test1---------------");
return "111";
}
}
上述test1方法被本应该返回“111”的,但是被@ResultUnite注解了,所以执行观察,返回包装后的结果:
{
"code": 200,
"msg": "success",
"data": "111"
}
再试一下:
@ResultUnite
@RequestMapping("/test2")
public List test2() {
log.info("---------------test2---------------");
List list = new ArrayList(){
{
add("123");
add("456");
}
};
return list;
}
执行返回:
{
"code": 200,
"msg": "success",
"data": [
"123",
"456"
]
}
这样我们就在任何成功的情况下,统一了对外部的返回(code,msg,data)。且实现非常清爽,在业务代码中只需要一个注解即可。
可是异常情况呢?异常情况因为不会再从controller中的return出去,所以就不受控制了。接下来处理异常情况。
4、统一异常处理 4.1、自定义异常类这个类看起来和baseResponse结构一样,那为什么不能使用baseResponse代替呢?
因为此类需要继承RuntimeException,将来在代码中可以catch住此异常信息,然后做处理。况且两个类分别是不同功能的,即使属性一样,也不能复用。
package com.example.demo.entity.vo;
import com.example.demo.entity.enums.ExceptionCodeEnum;
import lombok.Data;
@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;
}
}
枚举异常:
package com.example.demo.entity.enums;
import javax.servlet.http.HttpServletResponse;
public enum ExceptionCodeEnum {
//通用异常
Bad_Request(HttpServletResponse.SC_BAD_REQUEST, "请求信息错误,请检查参数"),
UnAuth(HttpServletResponse.SC_UNAUTHORIZED, "请先登陆"),
Forbidden(HttpServletResponse.SC_FORBIDDEN, "无权查看"),
Not_Fount(HttpServletResponse.SC_NOT_FOUND, "未找到该路径或资源"),
Method_Not_Allowed(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "请求方式不支持"),
Internal_Server_Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"),
Service_Unavailable(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "请求超时"),
//自定义扩展其他业务异常
Username_Or_PassWord_Error(HttpServletResponse.SC_BAD_REQUEST, "用户名密码错误"),
Username_Exist(HttpServletResponse.SC_BAD_REQUEST, "用户名已存在"),
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;
}
}
工具类:
package com.example.demo.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
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);
}
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;
}
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处理。
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;
@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandle {
@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());
}
@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处理。
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;
@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、阶段测试
①、我们故意抛出一个异常:
@ResultUnite
@RequestMapping("/test3")
public String test3() {
log.info("---------------test3---------------");
try {
int i = 5 / 0;
} catch (Exception e) {
throw e;
}
return "111";
}
执行测试:
{
"code": 500,
"msg": "服务器异常",
"data": null
}
这个throw e;就是被GlobalExceptionHandle中的handleLeftException拦截到了。
②、让程序内部(非代码)抛出一个异常
@ResultUnite
@RequestMapping("/test4")
public String test4() {
log.info("---------------test4---------------");
int i = 5 / 0;
return "111";
}
执行测试:
{
"code": 500,
"msg": "服务器异常",
"data": null
}
也能被GlobalExceptionHandle中的handleLeftException拦截到。
③、某些情况需要抛出自定义异常
@ResultUnite
@RequestMapping("/test5")
public String test5() {
log.info("---------------test5---------------");
//假装i是入参
int i = 1;
if (1 == i) {
throw new CustomerExceptionVO(ExceptionCodeEnum.Forbidden);
}
return "111";
}
执行测试:
{
"code": 403,
"msg": "无权查看",
"data": null
}
被GlobalExceptionHandle中的handleCustomerException拦截到。
5、异常结束工具在4.4的③中,这种情况应该是经常由的,我们需要对业务情况判断,然后众多不符合队则的case都异常返回了,这样每次都throw new CustomerExceptionVO是很麻烦的事情,所以可以搞一个工具来使用。
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;
@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);
}
}
然后就可以在业务代码中使用:
@ResultUnite
@RequestMapping("/test6")
public String test6() {
log.info("---------------test6---------------");
//假装i是入参
int i = 1;
if (1 == i) {
ApiAssert.badRequest("请求不合法");
}
return "111";
}
执行测试:
{
"code": 400,
"msg": "请求不合法",
"data": null
}
这样就可以在任何需要的地方使用ApiAssert来结束并返回。
6、总结上述统一返回体的处理只是一种处理方式,包含了正常结束,异常返回,业务代码自定义中断返回的处理。也许并不适合所有。但能包容大多数情况。
其实仍有许多改进的地方,比如抛出的异常错误码,完全可以自定义一个自己系统统一的错误码表,然后使用这个码表,就不用每次都使用HttpServletResponse的了。



