完善 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

@ -1,5 +1,6 @@
{
"local": {
"baseUrl": "http://127.0.0.1:8080"
"baseUrl": "http://127.0.0.1:8080/api",
"token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w"
}
}

View File

@ -3,6 +3,7 @@ ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# VUE_APP_BASE_API = '/api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -34,7 +34,7 @@ export function logout() {
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
url: '/captcha/get-image',
method: 'get'
})
}

View File

@ -8,7 +8,7 @@ axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API,
baseURL: process.env.VUE_APP_BASE_API + '/api/', // 此处的 /api/ 地址,原因是后端的基础路径为 /api/
// 超时
timeout: 10000
})
@ -76,13 +76,13 @@ service.interceptors.response.use(res => {
})
return Promise.reject('error')
} else {
return res.data
return res.data.data // 第二层 data 才是后端返回的 CommonResult.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
if (message === "Network Error") {
message = "后端接口连接异常";
}
else if (message.includes("timeout")) {

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 的统一前缀
*
*
* 意义通过该前缀避免 SwaggerActuator 意外通过 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>

View File

@ -20,6 +20,8 @@ spring:
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
web:
api-prefix: /api
security:
token-header: Authorization
token-secret: abcdefghijklmnopqrstuvwxyz