Commit ee9a358b11cf38435d683dd6111e973d2faa7894

Authored by YunaiV
1 parent e85c3426

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

http-client.env.json
1 1 {
2 2 "local": {
3   - "baseUrl": "http://127.0.0.1:8080"
  3 + "baseUrl": "http://127.0.0.1:8080/api",
  4 + "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w"
4 5 }
5 6 }
... ...
ruoyi-ui/.env.development
... ... @@ -3,6 +3,7 @@ ENV = 'development'
3 3  
4 4 # 若依管理系统/开发环境
5 5 VUE_APP_BASE_API = '/dev-api'
  6 +# VUE_APP_BASE_API = '/api'
6 7  
7 8 # 路由懒加载
8 9 VUE_CLI_BABEL_TRANSPILE_MODULES = true
... ...
ruoyi-ui/src/api/login.js
... ... @@ -34,7 +34,7 @@ export function logout() {
34 34 // 获取验证码
35 35 export function getCodeImg() {
36 36 return request({
37   - url: '/captchaImage',
  37 + url: '/captcha/get-image',
38 38 method: 'get'
39 39 })
40   -}
41 40 \ No newline at end of file
  41 +}
... ...
ruoyi-ui/src/utils/request.js
... ... @@ -8,7 +8,7 @@ axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
8 8 // 创建axios实例
9 9 const service = axios.create({
10 10 // axios中请求配置有baseURL选项,表示请求URL公共部分
11   - baseURL: process.env.VUE_APP_BASE_API,
  11 + baseURL: process.env.VUE_APP_BASE_API + '/api/', // 此处的 /api/ 地址,原因是后端的基础路径为 /api/
12 12 // 超时
13 13 timeout: 10000
14 14 })
... ... @@ -76,13 +76,13 @@ service.interceptors.response.use(res => {
76 76 })
77 77 return Promise.reject('error')
78 78 } else {
79   - return res.data
  79 + return res.data.data // 第二层 data 才是后端返回的 CommonResult.data
80 80 }
81 81 },
82 82 error => {
83 83 console.log('err' + error)
84 84 let { message } = error;
85   - if (message == "Network Error") {
  85 + if (message === "Network Error") {
86 86 message = "后端接口连接异常";
87 87 }
88 88 else if (message.includes("timeout")) {
... ...
src/main/java/cn/iocoder/dashboard/framework/swagger/config/SwaggerAutoConfiguration.java
... ... @@ -57,9 +57,7 @@ public class SwaggerAutoConfiguration {
57 57 .paths(PathSelectors.any())
58 58 .build()
59 59 .securitySchemes(securitySchemes())
60   - .securityContexts(securityContexts())
61   -// .pathMapping() TODO 芋艿:稍后解决,统一 api 前缀
62   - ;
  60 + .securityContexts(securityContexts());
63 61 }
64 62  
65 63 /**
... ...
src/main/java/cn/iocoder/dashboard/framework/web/config/WebConfiguration.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.web.config;
  2 +
  3 +import com.alibaba.fastjson.serializer.SerializerFeature;
  4 +import com.alibaba.fastjson.support.config.FastJsonConfig;
  5 +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
  6 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.Configuration;
  9 +import org.springframework.core.annotation.Order;
  10 +import org.springframework.http.MediaType;
  11 +import org.springframework.http.converter.HttpMessageConverter;
  12 +import org.springframework.web.bind.annotation.RestController;
  13 +import org.springframework.web.cors.CorsConfiguration;
  14 +import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
  15 +import org.springframework.web.filter.CorsFilter;
  16 +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
  17 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  18 +
  19 +import javax.annotation.Resource;
  20 +import java.nio.charset.Charset;
  21 +import java.util.Collections;
  22 +import java.util.List;
  23 +import java.util.function.Predicate;
  24 +
  25 +/**
  26 + * Web 配置类
  27 + */
  28 +@Configuration
  29 +@EnableConfigurationProperties(WebProperties.class)
  30 +public class WebConfiguration implements WebMvcConfigurer {
  31 +
  32 + @Resource
  33 + private WebProperties webProperties;
  34 +
  35 + @Override
  36 + public void configurePathMatch(PathMatchConfigurer configurer) {
  37 + configurer.addPathPrefix(webProperties.getApiPrefix(), clazz ->
  38 + clazz.isAnnotationPresent(RestController.class)
  39 + && clazz.getPackage().getName().contains("cn.iocoder.dashboard"));
  40 + }
  41 +
  42 + // ========== MessageConverter 相关 ==========
  43 +
  44 + @Override
  45 + public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  46 + // 创建 FastJsonHttpMessageConverter 对象
  47 + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
  48 + // 自定义 FastJson 配置
  49 + FastJsonConfig fastJsonConfig = new FastJsonConfig();
  50 + fastJsonConfig.setCharset(Charset.defaultCharset()); // 设置字符集
  51 + fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, // 剔除循环引用
  52 + SerializerFeature.WriteNonStringKeyAsString); // 解决 Integer 作为 Key 时,转换为 String 类型,避免浏览器报错
  53 + fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
  54 + // 设置支持的 MediaType
  55 + fastJsonHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
  56 + // 添加到 converters 中
  57 + converters.add(0, fastJsonHttpMessageConverter); // 注意,添加到最开头,放在 MappingJackson2XmlHttpMessageConverter 前面
  58 + }
  59 +
  60 + // ========== Filter 相关 ==========
  61 +
  62 + /**
  63 + * 创建 CorsFilter Bean,解决跨域问题
  64 + */
  65 + @Bean
  66 + @Order(Integer.MIN_VALUE)
  67 + public CorsFilter corsFilter() {
  68 + // 创建 CorsConfiguration 对象
  69 + CorsConfiguration config = new CorsConfiguration();
  70 + config.setAllowCredentials(true);
  71 + config.addAllowedOriginPattern("*"); // 设置访问源地址
  72 + config.addAllowedHeader("*"); // 设置访问源请求头
  73 + config.addAllowedMethod("*"); // 设置访问源请求方法
  74 + // 创建 UrlBasedCorsConfigurationSource 对象
  75 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  76 + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
  77 + return new CorsFilter(source);
  78 + }
  79 +
  80 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/web/config/WebProperties.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.web.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
  7 +
  8 +import javax.validation.constraints.NotNull;
  9 +
  10 +@ConfigurationProperties(prefix = "yudao.web")
  11 +@Validated
  12 +@Data
  13 +public class WebProperties {
  14 +
  15 + /**
  16 + * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
  17 + *
  18 + *
  19 + * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
  20 + * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
  21 + *
  22 + * @see WebConfiguration#configurePathMatch(PathMatchConfigurer)
  23 + */
  24 + @NotNull(message = "API 前缀不能为空")
  25 + private String apiPrefix;
  26 +
  27 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.web.core.handler;
  2 +
  3 +import cn.iocoder.dashboard.common.exception.GlobalException;
  4 +import cn.iocoder.dashboard.common.exception.ServiceException;
  5 +import cn.iocoder.dashboard.common.pojo.CommonResult;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.beans.factory.annotation.Value;
  8 +import org.springframework.validation.BindException;
  9 +import org.springframework.validation.FieldError;
  10 +import org.springframework.web.HttpRequestMethodNotSupportedException;
  11 +import org.springframework.web.bind.MethodArgumentNotValidException;
  12 +import org.springframework.web.bind.MissingServletRequestParameterException;
  13 +import org.springframework.web.bind.annotation.ExceptionHandler;
  14 +import org.springframework.web.bind.annotation.RestControllerAdvice;
  15 +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
  16 +import org.springframework.web.servlet.NoHandlerFoundException;
  17 +
  18 +import javax.servlet.http.HttpServletRequest;
  19 +import javax.validation.ConstraintViolation;
  20 +import javax.validation.ConstraintViolationException;
  21 +import javax.validation.ValidationException;
  22 +
  23 +import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.*;
  24 +
  25 +/**
  26 + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
  27 + */
  28 +@RestControllerAdvice
  29 +@Slf4j
  30 +public class GlobalExceptionHandler {
  31 +
  32 + /**
  33 + * 处理所有异常,主要是提供给 Filter 使用
  34 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
  35 + *
  36 + * @param request 请求
  37 + * @param ex 异常
  38 + * @return 通用返回
  39 + */
  40 + public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
  41 + if (ex instanceof MissingServletRequestParameterException) {
  42 + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
  43 + }
  44 + if (ex instanceof MethodArgumentTypeMismatchException) {
  45 + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
  46 + }
  47 + if (ex instanceof MethodArgumentNotValidException) {
  48 + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
  49 + }
  50 + if (ex instanceof BindException) {
  51 + return bindExceptionHandler((BindException) ex);
  52 + }
  53 + if (ex instanceof ConstraintViolationException) {
  54 + return constraintViolationExceptionHandler((ConstraintViolationException) ex);
  55 + }
  56 + if (ex instanceof ValidationException) {
  57 + return validationException((ValidationException) ex);
  58 + }
  59 + if (ex instanceof NoHandlerFoundException) {
  60 + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
  61 + }
  62 + if (ex instanceof HttpRequestMethodNotSupportedException) {
  63 + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
  64 + }
  65 + if (ex instanceof ServiceException) {
  66 + return serviceExceptionHandler((ServiceException) ex);
  67 + }
  68 + if (ex instanceof GlobalException) {
  69 + return globalExceptionHandler(request, (GlobalException) ex);
  70 + }
  71 + return defaultExceptionHandler(request, ex);
  72 + }
  73 +
  74 + /**
  75 + * 处理 SpringMVC 请求参数缺失
  76 + *
  77 + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
  78 + */
  79 + @ExceptionHandler(value = MissingServletRequestParameterException.class)
  80 + public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
  81 + log.warn("[missingServletRequestParameterExceptionHandler]", ex);
  82 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
  83 + }
  84 +
  85 + /**
  86 + * 处理 SpringMVC 请求参数类型错误
  87 + *
  88 + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
  89 + */
  90 + @ExceptionHandler(MethodArgumentTypeMismatchException.class)
  91 + public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
  92 + log.warn("[missingServletRequestParameterExceptionHandler]", ex);
  93 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
  94 + }
  95 +
  96 + /**
  97 + * 处理 SpringMVC 参数校验不正确
  98 + */
  99 + @ExceptionHandler(MethodArgumentNotValidException.class)
  100 + public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
  101 + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
  102 + FieldError fieldError = ex.getBindingResult().getFieldError();
  103 + assert fieldError != null; // 断言,避免告警
  104 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
  105 + }
  106 +
  107 + /**
  108 + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
  109 + */
  110 + @ExceptionHandler(BindException.class)
  111 + public CommonResult<?> bindExceptionHandler(BindException ex) {
  112 + log.warn("[handleBindException]", ex);
  113 + FieldError fieldError = ex.getFieldError();
  114 + assert fieldError != null; // 断言,避免告警
  115 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
  116 + }
  117 +
  118 + /**
  119 + * 处理 Validator 校验不通过产生的异常
  120 + */
  121 + @ExceptionHandler(value = ConstraintViolationException.class)
  122 + public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
  123 + log.warn("[constraintViolationExceptionHandler]", ex);
  124 + ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
  125 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
  126 + }
  127 +
  128 + /**
  129 + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
  130 + */
  131 + @ExceptionHandler(value = ValidationException.class)
  132 + public CommonResult<?> validationException(ValidationException ex) {
  133 + log.warn("[constraintViolationExceptionHandler]", ex);
  134 + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
  135 + return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确");
  136 + }
  137 +
  138 + /**
  139 + * 处理 SpringMVC 请求地址不存在
  140 + *
  141 + * 注意,它需要设置如下两个配置项:
  142 + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true
  143 + * 2. spring.mvc.static-path-pattern 为 /statics/**
  144 + */
  145 + @ExceptionHandler(NoHandlerFoundException.class)
  146 + public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
  147 + log.warn("[noHandlerFoundExceptionHandler]", ex);
  148 + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
  149 + }
  150 +
  151 + /**
  152 + * 处理 SpringMVC 请求方法不正确
  153 + *
  154 + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
  155 + */
  156 + @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  157 + public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
  158 + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
  159 + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
  160 + }
  161 +
  162 + /**
  163 + * 处理业务异常 ServiceException
  164 + *
  165 + * 例如说,商品库存不足,用户手机号已存在。
  166 + */
  167 + @ExceptionHandler(value = ServiceException.class)
  168 + public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
  169 + log.info("[serviceExceptionHandler]", ex);
  170 + return CommonResult.error(ex.getCode(), ex.getMessage());
  171 + }
  172 +
  173 + /**
  174 + * 处理全局异常 ServiceException
  175 + *
  176 + * 例如说,Dubbo 请求超时,调用的 Dubbo 服务系统异常
  177 + */
  178 + @ExceptionHandler(value = GlobalException.class)
  179 + public CommonResult<?> globalExceptionHandler(HttpServletRequest req, GlobalException ex) {
  180 + // 系统异常时,才打印异常日志
  181 + if (INTERNAL_SERVER_ERROR.getCode().equals(ex.getCode())) {
  182 + // 插入异常日志
  183 + this.createExceptionLog(req, ex);
  184 + // 普通全局异常,打印 info 日志即可
  185 + } else {
  186 + log.info("[globalExceptionHandler]", ex);
  187 + }
  188 + // 返回 ERROR CommonResult
  189 + return CommonResult.error(ex);
  190 + }
  191 +
  192 + /**
  193 + * 处理系统异常,兜底处理所有的一切
  194 + */
  195 + @ExceptionHandler(value = Exception.class)
  196 + public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
  197 + log.error("[defaultExceptionHandler]", ex);
  198 + // 插入异常日志
  199 + this.createExceptionLog(req, ex);
  200 + // 返回 ERROR CommonResult
  201 + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMessage());
  202 + }
  203 +
  204 + // TODO 芋艿:增加异常日志
  205 + public void createExceptionLog(HttpServletRequest req, Throwable e) {
  206 +// // 插入异常日志
  207 +// SystemExceptionLogCreateDTO exceptionLog = new SystemExceptionLogCreateDTO();
  208 +// try {
  209 +// // 增加异常计数 metrics TODO 暂时去掉
  210 +//// EXCEPTION_COUNTER.increment();
  211 +// // 初始化 exceptionLog
  212 +// initExceptionLog(exceptionLog, req, e);
  213 +// // 执行插入 exceptionLog
  214 +// createExceptionLog(exceptionLog);
  215 +// } catch (Throwable th) {
  216 +// log.error("[createExceptionLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
  217 +// }
  218 + }
  219 +
  220 +// // TODO 优化点:后续可以增加事件
  221 +// @Async
  222 +// public void createExceptionLog(SystemExceptionLogCreateDTO exceptionLog) {
  223 +// try {
  224 +// systemExceptionLogRpc.createSystemExceptionLog(exceptionLog);
  225 +// } catch (Throwable th) {
  226 +// log.error("[addAccessLog][插入异常日志({}) 发生异常({})", JSON.toJSONString(exceptionLog), ExceptionUtils.getRootCauseMessage(th));
  227 +// }
  228 +// }
  229 +//
  230 +// private void initExceptionLog(SystemExceptionLogCreateDTO exceptionLog, HttpServletRequest request, Throwable e) {
  231 +// // 设置账号编号
  232 +// exceptionLog.setUserId(CommonWebUtil.getUserId(request));
  233 +// exceptionLog.setUserType(CommonWebUtil.getUserType(request));
  234 +// // 设置异常字段
  235 +// exceptionLog.setExceptionName(e.getClass().getName());
  236 +// exceptionLog.setExceptionMessage(ExceptionUtil.getMessage(e));
  237 +// exceptionLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
  238 +// exceptionLog.setExceptionStackTrace(ExceptionUtil.getStackTrace(e));
  239 +// StackTraceElement[] stackTraceElements = e.getStackTrace();
  240 +// Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
  241 +// StackTraceElement stackTraceElement = stackTraceElements[0];
  242 +// exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
  243 +// exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
  244 +// exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
  245 +// exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
  246 +// // 设置其它字段
  247 +// exceptionLog.setTraceId(MallUtils.getTraceId())
  248 +// .setApplicationName(applicationName)
  249 +// .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
  250 +// .setQueryString(HttpUtil.buildQueryString(request))
  251 +// .setMethod(request.getMethod())
  252 +// .setUserAgent(HttpUtil.getUserAgent(request))
  253 +// .setIp(HttpUtil.getIp(request))
  254 +// .setExceptionTime(new Date());
  255 +// }
  256 +
  257 +}
... ...
src/main/java/cn/iocoder/dashboard/framework/web/core/package-info.java 0 → 100644
  1 +package cn.iocoder.dashboard.framework.web.core;
... ...
src/main/java/cn/iocoder/dashboard/framework/web/package-info.java 0 → 100644
  1 +/**
  2 + * 针对 SpringMVC 的基础封装
  3 + */
  4 +package cn.iocoder.dashboard.framework.web;
... ...
src/main/java/cn/iocoder/dashboard/framework/web/《芋道 Spring Boot SpringMVC 入门》.md 0 → 100644
  1 +<http://www.iocoder.cn/Spring-Boot/SpringMVC/?dashboard>
... ...
src/main/resources/application.yaml
... ... @@ -20,6 +20,8 @@ spring:
20 20  
21 21 # 芋道配置项,设置当前项目所有自定义的配置
22 22 yudao:
  23 + web:
  24 + api-prefix: /api
23 25 security:
24 26 token-header: Authorization
25 27 token-secret: abcdefghijklmnopqrstuvwxyz
... ...