springboot中优雅实现异常拦截和返回统一结构数据
2023-12-13 07:41:08
做前后端分离的项目,为了方便前端处理数据,都会将返回的数据封装到统一的结构下,这样前端拿到数据可以根据指定的字段做不同的业务逻辑处理。
1、异常信息统一拦截
项目开发中,难免会发生异常,如果不做拦截,当项目发生异常时会把异常的堆栈信息返回给前端,这样不仅对前端没有意义,而且会把服务器的相关信息暴露给外部用户造成信息的泄露,在springboot中,可以通过 @RestControllerAdvice 注解实现对异常信息的拦截处理,拦截到异常后给前端返回友好的提示,实现逻辑如下:
- 定义一个异常信息拦截类,接口调用中抛出的异常都会被拦截到,拦截到的异常会进入不同的处理方法,将处理后的数据返回给请求端:
import org.example.pojo.ApiResult;
import org.example.pojo.StatusCode;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.RestClientException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.List;
import java.util.StringJoiner;
/**
* 统一异常处理
*
* @Author xingo
* @Date 2023/12/7
*/
@RestControllerAdvice
@ConditionalOnWebApplication
public class ApiExceptionHandler {
/**
* 自定义异常处理
*/
@ExceptionHandler(BusinessException.class)
public ApiResult businessExceptionHandler(BusinessException e) {
return ApiResult.fail(e.getCode(), e.getMessage());
}
/**
* 参数错误异常处理
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ApiResult illegalArgumentExceptionHandler(IllegalArgumentException e) {
e.printStackTrace();
return ApiResult.fail(StatusCode.C_10004);
}
/**
* 参数校验异常处理
*/
@ExceptionHandler(BindException.class)
public ApiResult bindExceptionHandler(BindException e) {
List<FieldError> errors = e.getBindingResult().getFieldErrors();
if(errors != null && !errors.isEmpty()) {
return ApiResult.fail(StatusCode.C_10004).setMessage(errors.get(0).getDefaultMessage());
}
return ApiResult.fail(StatusCode.C_10004);
}
/**
* 参数校验异常处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
List<FieldError> errors = e.getBindingResult().getFieldErrors();
StringBuilder builder = new StringBuilder();
if(null != errors) {
for(FieldError error : errors) {
builder.append(",").append(error.getField()).append(":").append(error.getDefaultMessage());
}
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "参数校验失败-[" + builder.substring(1) + "]");
}
return ApiResult.fail(StatusCode.C_10004);
}
/**
* 不支持的请求方法
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResult<Object> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), "不支持的请求方法");
}
/**
* 请求类型错误
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ApiResult<Object> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), "请求类型错误");
}
/**
* 请求超时
*/
@ExceptionHandler(AsyncRequestTimeoutException.class)
public ApiResult<Object> asyncRequestTimeoutExceptionHandler(AsyncRequestTimeoutException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.REQUEST_TIMEOUT.value(), "请求超时");
}
/**
* 操作数据库出现异常
*/
@ExceptionHandler(DataAccessException.class)
public ApiResult<Object> handleSqlException(DataAccessException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "操作数据库出现异常");
}
/**
* 关联接口请求失败
*/
@ExceptionHandler(RestClientException.class)
public ApiResult<Object> restClientExceptionHandler(RestClientException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "关联接口请求失败");
}
/**
* 请求参数缺失
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ApiResult<Object> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数" + e.getParameterName() + "缺失,数据类型:" + e.getParameterType());
}
/**
* 请求参数类型错误
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ApiResult<Object> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数类型错误");
}
/**
* 请求地址不存在
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ApiResult<Object> handleNoFoundException(NoHandlerFoundException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.NOT_FOUND.value(), "请求地址不存在");
}
/**
* 必须的请求参数不能为空
*/
@ExceptionHandler(MissingServletRequestPartException.class)
public ApiResult<Object> missingServletRequestPartExceptionHandler(MissingServletRequestPartException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "必须的请求参数" + e.getRequestPartName() + "不能为空");
}
/**
* 请求参数解析异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResult<Object> httpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) {
e.printStackTrace();
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数解析异常");
}
/**
* 请求参数不满足
*/
@ExceptionHandler(UnsatisfiedServletRequestParameterException.class)
public ApiResult<Object> unsatisfiedServletRequestParameterExceptionHandler(UnsatisfiedServletRequestParameterException e) {
String conditions = StringUtils.arrayToDelimitedString(e.getParamConditions(), ",");
StringJoiner params = new StringJoiner(",");
e.getActualParams().forEach((k, v) -> params.add(k.concat("=").concat(ObjectUtils.nullSafeToString(v))));
e.printStackTrace();
return ApiResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数异常:" + conditions + "|" + params);
}
/**
* 未捕获的异常
* @param e
* @return
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ApiResult exceptionHandler(Exception e) {
e.printStackTrace();
return ApiResult.fail(StatusCode.C_ERROR);
}
}
- 自定义了一个异常类,当业务逻辑处理时不符合要求可以抛出下面的异常:
import org.example.pojo.StatusCode;
/**
* 自定义业务异常
*
* @Author xingo
* @Date 2023/12/7
*/
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public static BusinessException fail(int code, String message) {
return new BusinessException(code, message);
}
public static BusinessException fail(StatusCode statusCode) {
return new BusinessException(statusCode.getCode(), statusCode.getMessage());
}
public int getCode() {
return code;
}
}
- 接口统一返回的数据实体类,它的定义如下:
import java.io.Serializable;
/**
* @Author xingo
* @Date 2023/10/27
*/
public class ApiResult<T> implements Serializable {
/**
* 响应状态码:200-成功;其他-失败
*/
private int code;
/**
* 响应数据
*/
private T data;
/**
* 响应结果描述
*/
private String message = "";
/**
* 响应耗时:毫秒
*/
private long time;
public ApiResult() {
}
public ApiResult(T data) {
this.data = data;
}
public ApiResult(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public int getCode() {
return code;
}
public ApiResult setCode(int code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public ApiResult setMessage(String message) {
this.message = message;
return this;
}
public T getData() {
return data;
}
public ApiResult setData(T data) {
this.data = data;
return this;
}
public long getTime() {
return this.time;
}
public ApiResult setTime(long time) {
this.time = time;
return this;
}
/**
* 成功
*
* @return
*/
public static ApiResult success() {
ApiResult result = new ApiResult(StatusCode.C_200.getCode(), null, StatusCode.C_200.getMessage());
return result;
}
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> ApiResult success(T data) {
ApiResult result = new ApiResult(StatusCode.C_200.getCode(), data, StatusCode.C_200.getMessage());
return result;
}
/**
* 失败
*
* @param statusCode
* @return
*/
public static ApiResult fail(StatusCode statusCode) {
return new ApiResult().setCode(statusCode.getCode()).setMessage(statusCode.getMessage());
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static ApiResult fail(int code, String message) {
return new ApiResult().setCode(code).setMessage(message);
}
/**
* 异常
*
* @return
*/
public static ApiResult error() {
return new ApiResult().setCode(StatusCode.C_ERROR.getCode()).setMessage(StatusCode.C_ERROR.getMessage());
}
/**
* 判断响应是否为成功响应
*
* @return
*/
public boolean isSuccess() {
if (this.code != 200) {
return false;
}
return true;
}
public static ApiResult copyCodeAndMessage(ApiResult result) {
return new ApiResult().setCode(result.getCode()).setMessage(result.getMessage());
}
}
/**
* 状态枚举
*/
public enum StatusCode {
/**
* 正常
*/
C_200(200, "success"),
/**
* 系统繁忙
*/
C_ERROR(-1, "系统繁忙"),
/**
* 特殊错误信息
*/
C_10000(10000, "特殊错误信息"),
/**
* 用户未登录
*/
C_10001(10001, "用户未登录"),
/**
* 用户无访问权限
*/
C_10002(10002, "用户无访问权限"),
/**
* 用户身份验证失败
*/
C_10003(10003, "用户身份验证失败"),
/**
* 请求参数错误
*/
C_10004(10004, "请求参数错误"),
/**
* 请求信息不存在
*/
C_10005(10005, "请求信息不存在"),
/**
* 更新数据失败
*/
C_10006(10006, "更新数据失败"),
;
private Integer code;
private String message;
StatusCode(int code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
public static StatusCode getByCode(int code) {
StatusCode[] values = StatusCode.values();
for (StatusCode value : values) {
if (code == value.code) {
return value;
}
}
return StatusCode.C_ERROR;
}
}
- 写一个测试接口,在方法中抛出异常,会发现返回的信息不是异常的堆栈内容,而是一个友好的提示内容:
import org.example.handler.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* @Author xingo
* @Date 2023/12/7
*/
@RestController
public class DemoController {
@GetMapping("/demo1")
public Object demo1() {
int i = 1, j = 0;
return i / j;
}
@GetMapping("/demo2")
public Object demo2() {
if(System.currentTimeMillis() > 1) {
throw BusinessException.fail(88888, "业务数据不合法");
}
return System.currentTimeMillis();
}
@GetMapping("/demo3")
public Map<String, Object> demo3() {
Map<String, Object> map = new HashMap<>();
map.put("key1", "Hello,world!");
map.put("key2", new Date());
return map;
}
@GetMapping("/demo4")
public List<Object> demo4() {
List<Object> list = new ArrayList<>();
list.add(new Date());
list.add("Hello,world!");
list.add(Long.MAX_VALUE);
list.add(Integer.MAX_VALUE);
return list;
}
}
2、返回统一数据结构
如果接口返回的数据格式不统一,接口请求端处理数据就会非常麻烦,在接口交互中,可以定义好一个数据结构,接口服务端和请求端根据这个结构封装数据,这样处理数据就会变得容易,假设现在统一的数据结构是ApiResult这个结构,规定code=200时表示成功,其他的code都是失败并且定义好code的值表示的失败含义。要达到统一数据结构返回,需要借助springboot中的ResponseBodyAdvice接口,它的主要作用是:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。只要借助这个接口里面的两个方法:supports()和beforeBodyWrite()就可以实现我们想要的功能:
import org.example.pojo.ApiResult;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.annotation.Annotation;
import java.util.Objects;
/**
* @Author xingo
* @Date 2023/12/7
*/
@RestControllerAdvice
public class ApiResultResponseBodyAdvice implements ResponseBodyAdvice<Object>, Ordered {
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseBody.class;
/**
* 判断类或者方法是否使用了 @ResponseBody 注解
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !ApiResult.class.isAssignableFrom(returnType.getParameterType())
&& (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
|| returnType.hasMethodAnnotation(ANNOTATION_TYPE));
}
/**
* 当写入body之前调用这个方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// String类型的返回值要单独处理否则会报错:将数据写入data字段然后再序列化为json字符串
Class<?> returnClass = returnType.getMethod().getReturnType();
if(body instanceof String || Objects.equals(returnClass, String.class)) {
return JacksonUtils.toJSONString(ApiResult.success(body));
}
// 已经是目标数据类型不处理
if(body instanceof ApiResult) {
return body;
}
// 封装统一对象
return ApiResult.success(body);
}
@Override
public int getOrder() {
return Integer.MIN_VALUE + 1;
}
}
这里面用到了json序列化,使用jackson封装了一个json处理类对实体类进行序列化操作:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
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 java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* @Author xingo
* @Date 2023/12/7
*/
public class JacksonUtils {
private static final ObjectMapper mapper = new ObjectMapper();
static {
JavaTimeModule module = new JavaTimeModule();
// 序列化时对Long类型进行处理,避免前端js处理数据时精度缺失
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
// java8日期处理
module.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
module.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module.addSerializer(LocalTime.class,
new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
module.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
module.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
module.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
mapper.registerModules(module);
// 反序列化的时候如果多了其他属性,不抛出异常
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 如果是空对象的时候,不抛异常
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 空对象不序列化
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 日期格式化
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 设置时区
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
// 驼峰转下划线
// mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// 语言
mapper.setLocale(Locale.SIMPLIFIED_CHINESE);
}
/**
* 反序列化
* @param json json字符串
* @param clazz 发序列化类型
* @return
* @param <T>
*/
public static <T> T parseObject(String json, Class<T> clazz) {
try {
return mapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化列表
* @param json
* @return
* @param <T>
*/
public static <T> List<T> parseArray(String json) {
try {
TypeReference<List<T>> type = new TypeReference<List<T>>(){};
return mapper.readValue(json, type);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 写为json串
* @param obj 对象
* @return
*/
public static String toJSONString(Object obj) {
try {
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
通过上面的封装处理,只要接口返回的数据不是ApiResult类型,都会封装成该类型返回,达到统一数据类型的目的。
文章来源:https://blog.csdn.net/dream_xin2013/article/details/134862577
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!