JDK1.8,SpringBoot2.6.0
1、依赖
<?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、统一返回体类
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;
}
}
工具类:
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包下新建此类
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包下新建此类
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类测试一下
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注解了,所以执行观察,返回包装后的结果:
{
"code": 200,
"msg": "success",
"data": "111"
}
再试一下:
@ResultUnite
@RequestMapping("/test2")
public List<String> test2() {
log.info("---------------test2---------------");
List<String> list = new ArrayList<String>(){
{
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;
/**
* @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;
}
}
枚举异常:
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;
}
}
工具类:
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处理。
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处理。
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、阶段测试
①、我们故意抛出一个异常:
@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;
/**
* @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);
}
}
然后就可以在业务代码中使用:
@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的了。
评论