完善 SpringMVC 组件,增加统一 /api/ 前缀的封装

This commit is contained in:
YunaiV
2021-01-03 03:21:35 +08:00
parent e85c342696
commit ee9a358b11
12 changed files with 381 additions and 9 deletions

View File

@ -57,9 +57,7 @@ public class SwaggerAutoConfiguration {
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
// .pathMapping() TODO 芋艿:稍后解决,统一 api 前缀
;
.securityContexts(securityContexts());
}
/**

View File

@ -0,0 +1,80 @@
package cn.iocoder.dashboard.framework.web.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
/**
* Web 配置类
*/
@Configuration
@EnableConfigurationProperties(WebProperties.class)
public class WebConfiguration implements WebMvcConfigurer {
@Resource
private WebProperties webProperties;
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
clazz.isAnnotationPresent(RestController.class)
&& clazz.getPackage().getName().contains("cn.iocoder.dashboard"));
}
// ========== MessageConverter 相关 ==========
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建 FastJsonHttpMessageConverter 对象
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
// 自定义 FastJson 配置
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.defaultCharset()); // 设置字符集
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, // 剔除循环引用
SerializerFeature.WriteNonStringKeyAsString); // 解决 Integer 作为 Key 时,转换为 String 类型,避免浏览器报错
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
// 设置支持的 MediaType
fastJsonHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
// 添加到 converters 中
converters.add(0, fastJsonHttpMessageConverter); // 注意,添加到最开头,放在 MappingJackson2XmlHttpMessageConverter 前面
}
// ========== Filter 相关 ==========
/**
* 创建 CorsFilter Bean解决跨域问题
*/
@Bean
@Order(Integer.MIN_VALUE)
public CorsFilter corsFilter() {
// 创建 CorsConfiguration 对象
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*"); // 设置访问源地址
config.addAllowedHeader("*"); // 设置访问源请求头
config.addAllowedMethod("*"); // 设置访问源请求方法
// 创建 UrlBasedCorsConfigurationSource 对象
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.framework.web.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "yudao.web")
@Validated
@Data
public class WebProperties {
/**
* API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
*
*
* 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
* 这样Nginx 只需要配置转发到 /api/* 的所有接口即可。
*
* @see WebConfiguration#configurePathMatch(PathMatchConfigurer)
*/
@NotNull(message = "API 前缀不能为空")
private String apiPrefix;
}

View File

@ -0,0 +1,257 @@
package cn.iocoder.dashboard.framework.web.core.handler;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
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.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*;
/**
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理所有异常,主要是提供给 Filter 使用
* 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
*
* @param request 请求
* @param ex 异常
* @return 通用返回
*/
public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
if (ex instanceof MissingServletRequestParameterException) {
return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
}
if (ex instanceof MethodArgumentTypeMismatchException) {
return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
}
if (ex instanceof MethodArgumentNotValidException) {
return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
}
if (ex instanceof BindException) {
return bindExceptionHandler((BindException) ex);
}
if (ex instanceof ConstraintViolationException) {
return constraintViolationExceptionHandler((ConstraintViolationException) ex);
}
if (ex instanceof ValidationException) {
return validationException((ValidationException) ex);
}
if (ex instanceof NoHandlerFoundException) {
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
}
if (ex instanceof HttpRequestMethodNotSupportedException) {
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
}
if (ex instanceof ServiceException) {
return serviceExceptionHandler((ServiceException) ex);
}
if (ex instanceof GlobalException) {
return globalExceptionHandler(request, (GlobalException) ex);
}
return defaultExceptionHandler(request, ex);
}
/**
* 处理 SpringMVC 请求参数缺失
*
* 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
*/
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
log.warn("[missingServletRequestParameterExceptionHandler]", ex);
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
}
/**
* 处理 SpringMVC 请求参数类型错误
*
* 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer结果传递 xx 参数类型为 String
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
log.warn("[missingServletRequestParameterExceptionHandler]", ex);
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
}
/**
* 处理 SpringMVC 参数校验不正确
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
FieldError fieldError = ex.getBindingResult().getFieldError();
assert fieldError != null; // 断言,避免告警
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
}
/**
* 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
*/
@ExceptionHandler(BindException.class)
public CommonResult<?> bindExceptionHandler(BindException ex) {
log.warn("[handleBindException]", ex);
FieldError fieldError = ex.getFieldError();
assert fieldError != null; // 断言,避免告警
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
}
/**
* 处理 Validator 校验不通过产生的异常
*/
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
log.warn("[constraintViolationExceptionHandler]", ex);
ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
}
/**
* 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
*/
@ExceptionHandler(value = ValidationException.class)
public CommonResult<?> validationException(ValidationException ex) {
log.warn("[constraintViolationExceptionHandler]", ex);
// 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确");
}
/**
* 处理 SpringMVC 请求地址不存在
*
* 注意,它需要设置如下两个配置项:
* 1. spring.mvc.throw-exception-if-no-handler-found 为 true
* 2. spring.mvc.static-path-pattern 为 /statics/**
*/
@ExceptionHandler(NoHandlerFoundException.class)
public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
log.warn("[noHandlerFoundExceptionHandler]", ex);
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
}
/**
* 处理 SpringMVC 请求方法不正确
*
* 例如说A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
}
/**
* 处理业务异常 ServiceException
*
* 例如说,商品库存不足,用户手机号已存在。
*/
@ExceptionHandler(value = ServiceException.class)
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
log.info("[serviceExceptionHandler]", ex);
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理全局异常 ServiceException
*
* 例如说Dubbo 请求超时,调用的 Dubbo 服务系统异常
*/
@ExceptionHandler(value = GlobalException.class)
public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {
// 系统异常时,才打印异常日志
if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {
// 插入异常日志
this.createExceptionLog(req, ex);
// 普通全局异常,打印 info 日志即可
} else {
log.info("[globalExceptionHandler]", ex);
}
// 返回 ERROR CommonResult
return CommonResult.error(ex);
}
/**
* 处理系统异常,兜底处理所有的一切
*/
@ExceptionHandler(value = Exception.class)
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
log.error("[defaultExceptionHandler]", ex);
// 插入异常日志
this.createExceptionLog(req, ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
}
// TODO 芋艿:增加异常日志
public void createExceptionLog(HttpServletRequest req, Throwable e) {
// // 插入异常日志
// SystemExceptionLogCreateDTO exceptionLog = new SystemExceptionLogCreateDTO();
// try {
// // 增加异常计数 metrics TODO 暂时去掉
//// EXCEPTION_COUNTER.increment();
// // 初始化 exceptionLog
// initExceptionLog(exceptionLog, req, e);
// // 执行插入 exceptionLog
// createExceptionLog(exceptionLog);
// } catch (Throwable th) {
// log.error("[createExceptionLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
// }
}
// // TODO 优化点:后续可以增加事件
// @Async
// public void createExceptionLog(SystemExceptionLogCreateDTO exceptionLog) {
// try {
// systemExceptionLogRpc.createSystemExceptionLog(exceptionLog);
// } catch (Throwable th) {
// log.error("[addAccessLog][插入异常日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
// }
// }
//
// private void initExceptionLog(SystemExceptionLogCreateDTO exceptionLog, HttpServletRequest request, Throwable e) {
// // 设置账号编号
// exceptionLog.setUserId(CommonWebUtil.getUserId(request));
// exceptionLog.setUserType(CommonWebUtil.getUserType(request));
// // 设置异常字段
// exceptionLog.setExceptionName(e.getClass().getName());
// exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e));
// exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
// exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e));
// StackTraceElement[] stackTraceElements = e.getStackTrace();
// Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
// StackTraceElement stackTraceElement = stackTraceElements[0];
// exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
// exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
// exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
// exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
// // 设置其它字段
// exceptionLog.setTraceId(MallUtils.getTraceId())
// .setApplicationName(applicationName)
// .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
// .setQueryString(HttpUtil.buildQueryString(request))
// .setMethod(request.getMethod())
// .setUserAgent(HttpUtil.getUserAgent(request))
// .setIp(HttpUtil.getIp(request))
// .setExceptionTime(new Date());
// }
}

View File

@ -0,0 +1 @@
package cn.iocoder.dashboard.framework.web.core;

View File

@ -0,0 +1,4 @@
/**
* 针对 SpringMVC 的基础封装
*/
package cn.iocoder.dashboard.framework.web;

View File

@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?dashboard>