一、响应体包装
- 全局接口响应体包装,返回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> {@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate MvcProperties mvcProperties;/*** 接口忽略包装的注解*/private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseBodyIgnore.class;/*** 判断类或者方法是否使用了@ResponseBodyIgnore*/@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return !AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) && !returnType.hasMethodAnnotation(ANNOTATION_TYPE);}/*** 当类或者方法使用了 @ResponseResultBody 就会调用这个方法*/@SneakyThrows@Overridepublic 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;}// 未拦截到的错误信息返回jsonClass<?> 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是直接返回的,此时需要手动转化为jsonif (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;}@Overridepublic int getCode() {return this.code;}@Overridepublic 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@Overridepublic void initialize(EnumValue constraintAnnotation) {enums = constraintAnnotation.clazz().getEnumConstants();}@Overridepublic 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