一、响应体包装
- 全局接口响应体包装,返回json数据
- 支持对部分接口或者类放行
# mvc配置
mvc:
body-exclude-paths:
- /test/**
body-exclude-classes:
- com.qiangesoft.rdp.starter.XXX
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* mvc接口包装配置
*
* @author qiangesoft
* @date 2023-09-18
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "mvc")
public class MvcProperties {
/**
* 忽略包装的接口
*/
private List<String> bodyExcludePaths = new ArrayList<>();
/**
* 忽略包装的类
*/
private List<Class> bodyExcludeClasses = new ArrayList<>();
}
import java.lang.annotation.*;
/**
* 返回结果包装忽略注解
*
* @author qiangesoft
* @date 2023-09-18
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseBodyIgnore {
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiangesoft.rdp.starter.mvc.config.MvcProperties;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.annotation.Annotation;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 返回结果包装
*
* @author qiangesoft
* @date 2023-09-18
*/
@Slf4j
@RestControllerAdvice
public class RdpResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MvcProperties mvcProperties;
/**
* 接口忽略包装的注解
*/
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseBodyIgnore.class;
/**
* 判断类或者方法是否使用了@ResponseBodyIgnore
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) && !returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
/**
* 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 配置接口忽略包装
boolean ignorePath = this.isIgnore(request);
if (ignorePath) {
return body;
}
// 配置类忽略包装
boolean ignoreClazz = this.isIgnore(returnType);
if (ignoreClazz) {
return body;
}
// 未拦截到的错误信息返回json
Class<?> returnClass = returnType.getMethod().getReturnType();
if (Objects.equals(returnClass, ResponseEntity.class) && body instanceof LinkedHashMap) {
Map map = (LinkedHashMap) body;
Integer status = (Integer) map.get("status");
String error = (String) map.get("error");
return ResponseInfo.fail(status, error);
}
// 如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
if (body instanceof String) {
return objectMapper.writeValueAsString(ResponseInfo.success(body));
}
// 防止重复包裹的问题出现
if (body instanceof ResponseInfo) {
return body;
}
return ResponseInfo.success(body);
}
/**
* 是否忽略包装
*
* @param request
* @return
*/
private boolean isIgnore(ServerHttpRequest request) {
String path = request.getURI().getPath();
AntPathMatcher antPathMatcher = new AntPathMatcher();
List<String> bodyExcludePaths = mvcProperties.getBodyExcludePaths();
for (String excludePath : bodyExcludePaths) {
boolean match = antPathMatcher.match(excludePath, path);
if (match) {
return true;
}
}
return false;
}
/**
* 是否忽略包装
*
* @param returnType
* @return
*/
private boolean isIgnore(MethodParameter returnType) {
Class clazz = returnType.getContainingClass();
List<Class> bodyExcludeClasses = mvcProperties.getBodyExcludeClasses();
for (Class excludeClazz : bodyExcludeClasses) {
if (excludeClazz.equals(clazz)) {
return true;
}
}
return false;
}
}
二、全局异常处理
- 全局异常拦截
- 支持异常扩展
import com.qiangesoft.rdp.starter.mvc.exception.result.ExceptionResultHandler;
import com.qiangesoft.rdp.starter.mvc.exception.result.ResultMessageEnum;
/**
* 异常基类
*
* @author qiangesoft
* @date 2023-09-18
*/
public class BaseException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
public int code;
/**
* 错误提示
*/
public String message;
/**
* 空构造方法,避免反序列化问题
*/
public BaseException() {
}
public BaseException(String message) {
this.code = ResultMessageEnum.INTERNAL_SERVER_ERROR.getCode();
this.message = message;
}
public BaseException(int code, String message) {
this.code = code;
this.message = message;
}
public BaseException(ExceptionResultHandler exceptionAdvice) {
this.code = exceptionAdvice.getCode();
this.message = exceptionAdvice.getMessage();
}
public int getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
}
/**
* 异常信息接口
*
* @author qiangesoft
* @date 2023-09-18
*/
public interface ExceptionResultHandler {
/**
* 编码
*
* @return
*/
int getCode();
/**
* 详细信息
*
* @return
*/
String getMessage();
}
/**
* 业务异常枚举
*
* @author qiangesoft
* @date 2023-09-18
*/
public enum ResultMessageEnum implements ExceptionResultHandler {
/***********基础响应码***********/
SUCCESS(200, "请求成功"),
FAIL(500, "请求失败"),
/***********常规响应码***********/
PARAM_ERROR(4001, "请求参数错误"),
PARAM_TYPE_ERROR(4002, "参数类型错误"),
MESSAGE_NOT_READABLE(4003, "参数不可读"),
BODY_MEDIA_TYPE_NOT_SUPPORT(4004, "请求体MediaType不支持"),
UNAUTHORIZED(4011, "用户未登录"),
FORBIDDEN(4031, "权限不足"),
NOT_FOUND(4041, "请求资源不存在"),
METHOD_NOT_ALLOWED(4051, "请求方式不正确"),
INTERNAL_SERVER_ERROR(5001, "服务器内部错误,请稍后再试");
private int code;
private String message;
ResultMessageEnum(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
}
import com.qiangesoft.rdp.starter.mvc.exception.BaseException;
import com.qiangesoft.rdp.starter.mvc.exception.NotLoginException;
import com.qiangesoft.rdp.starter.mvc.exception.result.ResultMessageEnum;
import com.qiangesoft.rdp.starter.mvc.response.ResponseInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
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.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.nio.file.AccessDeniedException;
import java.util.List;
import java.util.Set;
/**
* 全局异常处理
* * @author qiangesoft
* @date 2023-09-18
*/
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {
/**
* 认证异常
*
* @param exception
* @return
*/
@ExceptionHandler(NotLoginException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseInfo handleNotLoginException(NotLoginException exception) {
return ResponseInfo.fail(ResultMessageEnum.UNAUTHORIZED);
}
/**
* 权限异常
*
* @param exception
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseInfo handleAccessDeniedException(AccessDeniedException exception) {
return ResponseInfo.fail(ResultMessageEnum.FORBIDDEN);
}
/**
* 请求方式异常
*
* @param exception
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ResponseInfo handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException exception, HttpServletRequest request) {
String method = request.getMethod();
return ResponseInfo.fail(ResultMessageEnum.METHOD_NOT_ALLOWED.getCode(), String.format("请求方式%s不支持", method));
}
/**
* 参数合法性校验异常
* {@link org.springframework.validation.BindException}(以form-data形式传参)
* {@link org.springframework.web.bind.MethodArgumentNotValidException}(常以body传参)
*
* @param exception
* @return
*/
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleBindException(BindException exception) {
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
StringBuilder message = new StringBuilder();
for (FieldError fieldError : fieldErrors) {
message.append(fieldError.getField()).append(fieldError.getDefaultMessage()).append(";");
}
return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR.getCode(), message.toString());
}
/**
* 参数合法性校验异常(通常是query或者form-data传参时的异常)
*
* @param exception
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleConstraintViolationException(ConstraintViolationException exception) {
Set<ConstraintViolation<?>> constraintViolations = exception.getConstraintViolations();
StringBuilder message = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
message.append(constraintViolation.getPropertyPath()).append(constraintViolation.getMessage()).append(";");
}
return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR.getCode(), message.toString());
}
/**
* ValidationException异常
*
* @param exception
* @return
*/
@ExceptionHandler(value = ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleValidationException(ValidationException exception) {
return ResponseInfo.fail(ResultMessageEnum.PARAM_ERROR);
}
/**
* 参数校验异常(以@RequestParam的传参的校验)
* 例如:接口上设置了@RequestParam("xx")参数,结果并未传递xx参数
*
* @param exception
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
return ResponseInfo.fail(exception.getMessage());
}
/**
* 参数类型异常(通常为表单传参错误导致参数无法类型转换)
* 例如:接口上设置了@RequestParam("xx")参数为Integer,结果传递xx参数类型为String
*
* @param exception
* @return
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleMissingServletRequestParameterException(MethodArgumentTypeMismatchException exception) {
return ResponseInfo.fail(ResultMessageEnum.PARAM_TYPE_ERROR.getCode(), exception.getName() + ResultMessageEnum.PARAM_TYPE_ERROR.getMessage());
}
/**
* 参数不可读异常(通常为传参错误导致参数无法解析映射实体属性,body传参时参数类型错误也会进来)
*
* @param exception
* @return
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseInfo handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) {
return ResponseInfo.fail(ResultMessageEnum.MESSAGE_NOT_READABLE.getCode(), exception.getMessage());
}
/**
* MediaType不支持异常(通常为body传参时contentType错误)
*
* @param exception
* @return
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public ResponseInfo handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException exception) {
return ResponseInfo.fail(ResultMessageEnum.BODY_MEDIA_TYPE_NOT_SUPPORT.getCode(), exception.getMessage());
}
/**
* 【业务校验】过程中的非法参数异常
* 该异常基本由{@link org.springframework.util.Assert}抛出
*
* @param exception
* @return
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.OK)
public ResponseInfo handleIllegalArgumentException(IllegalArgumentException exception) {
return ResponseInfo.fail(ResultMessageEnum.INTERNAL_SERVER_ERROR.getCode(), exception.getMessage());
}
/**
* 业务异常
* 该异常基本由{@link com.qiangesoft.rdp.starter.mvc.exception.BaseException}及其子类抛出
*
* @param exception
* @return
*/
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.OK)
public ResponseInfo handleBaseException(BaseException exception) {
return ResponseInfo.fail(exception.getCode(), exception.getMessage());
}
/**
* 未知异常和错误(兜底处理)
*
* @param throwable
* @return
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.OK)
public ResponseInfo handleThrowable(Throwable throwable) {
return ResponseInfo.fail(ResultMessageEnum.INTERNAL_SERVER_ERROR);
}
}
三、参数校验
1.使用hibernate-validator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.枚举值处理
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 枚举值校验注解
*
* @author qiangesoft
* @date 2023-09-18
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {
String message() default "参数必须为指定的值!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends IEnum> clazz();
}
import lombok.SneakyThrows;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Objects;
/**
* 枚举值校验器
*
* @author qiangesoft
* @date 2023-09-18
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private IEnum[] enums;
@SneakyThrows
@Override
public void initialize(EnumValue constraintAnnotation) {
enums = constraintAnnotation.clazz().getEnumConstants();
}
@Override
public boolean isValid(String code, ConstraintValidatorContext context) {
if (Objects.isNull(code)) {
return false;
}
for (IEnum iEnum : enums) {
if (iEnum.getCode().equals(code)) {
return true;
}
}
return false;
}
}
3.业务枚举类需要实现此接口并重写getCode(),getDesc()
/**
* 枚举定义
*
* @author qiangesoft
* @date 2023-09-18
*/
public interface IEnum {
/**
* 获取枚举code
*/
String getCode();
/**
* 获取枚举描述
*/
String getDesc();
}
源码地址
https://gitee.com/qiangesoft/rdp-starter/tree/master/rdp-starter-mvc