package cn.stylefeng.rest.core.error; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.stylefeng.roses.kernel.auth.api.exception.AuthException; import cn.stylefeng.roses.kernel.auth.api.exception.enums.AuthExceptionEnum; import cn.stylefeng.roses.kernel.demo.exception.DemoException; import cn.stylefeng.roses.kernel.demo.exception.enums.DemoExceptionEnum; import cn.stylefeng.roses.kernel.rule.constants.SymbolConstant; import cn.stylefeng.roses.kernel.rule.exception.AbstractExceptionEnum; import cn.stylefeng.roses.kernel.rule.exception.base.ServiceException; import cn.stylefeng.roses.kernel.rule.exception.enums.defaults.DefaultBusinessExceptionEnum; import cn.stylefeng.roses.kernel.rule.pojo.response.ErrorResponseData; import cn.stylefeng.roses.kernel.rule.util.ExceptionUtil; import cn.stylefeng.roses.kernel.rule.util.HttpServletUtil; import cn.stylefeng.roses.kernel.rule.util.ProjectUtil; import cn.stylefeng.roses.kernel.rule.util.ResponseRenderUtil; import cn.stylefeng.roses.kernel.validator.api.exception.ParamValidateException; import cn.stylefeng.roses.kernel.validator.api.exception.enums.ValidatorExceptionEnum; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.exceptions.PersistenceException; import org.mybatis.spring.MyBatisSystemException; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.ui.Model; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; 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.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.NoHandlerFoundException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.ValidationException; import java.util.List; import static cn.stylefeng.rest.core.consts.ProjectConstants.ROOT_PACKAGE_NAME; /** * 全局异常处理器,拦截控制器层的异常 * * @author fengshuonan * @since 2020/12/16 14:20 */ @ControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 请求参数缺失异常 * * @author fengshuonan * @since 2020/12/16 14:20 */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData missingParam(MissingServletRequestParameterException missingServletRequestParameterException) { String parameterName = missingServletRequestParameterException.getParameterName(); String parameterType = missingServletRequestParameterException.getParameterType(); return renderJson(ValidatorExceptionEnum.MISSING_SERVLET_REQUEST_PARAMETER_EXCEPTION, parameterName, parameterType); } /** * HttpMessageConverter转化异常,一般为json解析异常 * * @author fengshuonan * @since 2020/12/16 14:21 */ @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData httpMessageNotReadable(HttpMessageNotReadableException httpMessageNotReadableException) { log.error("参数格式传递异常,具体信息为:{}", httpMessageNotReadableException.getMessage()); return renderJson(ValidatorExceptionEnum.HTTP_MESSAGE_CONVERTER_ERROR); } /** * 拦截不支持媒体类型异常 * * @author fengshuonan * @since 2020/12/16 14:26 */ @ExceptionHandler(HttpMediaTypeNotSupportedException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData httpMediaTypeNotSupport(HttpMediaTypeNotSupportedException httpMediaTypeNotSupportedException) { log.error("参数格式传递异常,具体信息为:{}", httpMediaTypeNotSupportedException.getMessage()); return renderJson(ValidatorExceptionEnum.HTTP_MEDIA_TYPE_NOT_SUPPORT); } /** * 不受支持的http method * * @author fengshuonan * @since 2020/12/16 14:56 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData methodNotSupport(HttpServletRequest request) { String httpMethod = request.getMethod().toUpperCase(); return renderJson(ValidatorExceptionEnum.HTTP_METHOD_NOT_SUPPORT, httpMethod); } /** * 404找不到资源 * * @author fengshuonan * @since 2020/12/16 14:58 */ @ExceptionHandler(NoHandlerFoundException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData notFound(NoHandlerFoundException e) { return renderJson(ValidatorExceptionEnum.NOT_FOUND); } /** * 请求参数校验失败,拦截 @Valid 校验失败的情况 * * @author fengshuonan * @since 2020/12/16 14:59 */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData methodArgumentNotValidException(MethodArgumentNotValidException e) { String bindingResult = getArgNotValidMessage(e.getBindingResult()); return renderJson(ValidatorExceptionEnum.VALIDATED_RESULT_ERROR, bindingResult); } /** * 请求参数校验失败,拦截 @Validated 校验失败的情况 *

* 两个注解 @Valid 和 @Validated 区别是后者可以加分组校验,前者没有分组校验 * * @author fengshuonan * @since 2020/12/16 15:08 */ @ExceptionHandler(BindException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData bindException(BindException e) { String bindingResult = getArgNotValidMessage(e.getBindingResult()); return renderJson(ValidatorExceptionEnum.VALIDATED_RESULT_ERROR, bindingResult); } /** * 拦截 @TableUniqueValue 里抛出的异常 * * @author fengshuonan * @since 2020/12/26 14:05 */ @ExceptionHandler(ValidationException.class) @ResponseBody @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponseData bindException(ValidationException e) { if (e.getCause() instanceof ParamValidateException) { ParamValidateException paramValidateException = (ParamValidateException) e.getCause(); return renderJson(paramValidateException.getErrorCode(), paramValidateException.getUserTip()); } return renderJson(e); } /** * 拦截全校校验一类的异常 *

* 这里重点做一类特殊处理,也就是对过期登录用用户 *

* 如果用户登录过期,并且为ajax请求,则response的header增加session-timeout的标识 *

* 如果用户登录过期,不是ajax请求,则直接跳转到登录页面,并提示会话超时 * * @author fengshuonan * @since 2020/12/16 15:11 */ @ExceptionHandler(AuthException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String authError(AuthException authException, HttpServletRequest request, HttpServletResponse response, Model model) { String errorCode = authException.getErrorCode(); // 如果是会话过期或超时 if (AuthExceptionEnum.AUTH_EXPIRED_ERROR.getErrorCode().equals(errorCode)) { // 如果是普通请求 if (HttpServletUtil.getNormalRequestFlag(request)) { // 根据是否是前后端分离项目,进行结果响应 return this.renderLoginResult(response, authException, model); } else { // 其他请求或者是ajax请求 response.setHeader("Guns-Session-Timeout", "true"); ErrorResponseData errorResponseData = renderJson(authException.getErrorCode(), authException.getUserTip(), authException); ResponseRenderUtil.renderJsonResponse(response, errorResponseData); return null; } } // 如果是没带token访问页面,则返回到登录界面 if (AuthExceptionEnum.TOKEN_GET_ERROR.getErrorCode().equals(errorCode)) { if (HttpServletUtil.getNormalRequestFlag(request)) { // 根据是否是前后端分离项目,进行结果响应 return this.renderLoginResult(response, authException, model); } } // 默认响应前端json ErrorResponseData errorResponseData = renderJson(authException.getErrorCode(), authException.getUserTip(), authException); ResponseRenderUtil.renderJsonResponse(response, errorResponseData); return null; } /** * 拦截业务代码抛出的异常 * * @author fengshuonan * @since 2020/12/16 15:11 */ @ExceptionHandler(ServiceException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData businessError(ServiceException e) { log.error("业务异常。", e); return renderJson(e.getErrorCode(), e.getUserTip(), e); } /** * 拦截业务代码抛出的异常 * * @author fengshuonan * @since 2020/12/16 15:11 */ @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData businessError(RuntimeException e) { log.error("业务异常。", e); return renderJson("500", e.getMessage(), e); } /** * 拦截mybatis数据库操作的异常 *

* 用在demo模式,拦截DemoException * * @author stylefeng * @since 2020/5/5 15:19 */ @ExceptionHandler(MyBatisSystemException.class) @ResponseBody public ErrorResponseData persistenceException(MyBatisSystemException e) { log.error(">>> mybatis操作出现异常,", e); Throwable cause = e.getCause(); if (cause instanceof PersistenceException) { Throwable secondCause = cause.getCause(); if (secondCause instanceof DemoException) { return renderJson(DemoExceptionEnum.DEMO_OPERATE); } } return renderJson(e); } /** * 拦截未知的运行时异常 * * @author fengshuonan * @since 2020/12/16 15:12 */ @ExceptionHandler(Throwable.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody public ErrorResponseData serverError(Throwable e) { log.error("服务器运行异常", e); return renderJson(e); } /** * 渲染异常json * * @author fengshuonan * @since 2020/5/5 16:22 */ private ErrorResponseData renderJson(String code, String message) { return renderJson(code, message, null); } /** * 渲染异常json * * @author fengshuonan * @since 2020/5/5 16:22 */ private ErrorResponseData renderJson(AbstractExceptionEnum exception, Object... params) { return renderJson(exception.getErrorCode(), StrUtil.format(exception.getUserTip(), params), null); } /** * 渲染异常json * * @author fengshuonan * @since 2020/5/5 16:22 */ private ErrorResponseData renderJson(AbstractExceptionEnum abstractExceptionEnum) { return renderJson(abstractExceptionEnum.getErrorCode(), abstractExceptionEnum.getUserTip(), null); } /** * 渲染异常json * * @author fengshuonan * @since 2020/5/5 16:22 */ private ErrorResponseData renderJson(Throwable throwable) { return renderJson(DefaultBusinessExceptionEnum.SYSTEM_RUNTIME_ERROR.getErrorCode(), DefaultBusinessExceptionEnum.SYSTEM_RUNTIME_ERROR.getUserTip(), throwable); } /** * 渲染异常json *

* 根据异常枚举和Throwable异常响应,异常信息响应堆栈第一行 * * @author stylefeng * @since 2020/5/5 16:22 */ private ErrorResponseData renderJson(String code, String message, Throwable throwable) { if (ObjectUtil.isNotNull(throwable)) { ErrorResponseData errorResponseData = new ErrorResponseData<>(code, message); ExceptionUtil.fillErrorResponseData(errorResponseData, throwable, ROOT_PACKAGE_NAME); return errorResponseData; } else { return new ErrorResponseData<>(code, message); } } /** * 获取请求参数不正确的提示信息 *

* 多个信息,拼接成用逗号分隔的形式 * * @author stylefeng * @since 2020/5/5 16:50 */ private String getArgNotValidMessage(BindingResult bindingResult) { if (bindingResult == null) { return ""; } StringBuilder stringBuilder = new StringBuilder(); //多个错误用逗号分隔 List allErrorInfos = bindingResult.getAllErrors(); for (ObjectError error : allErrorInfos) { stringBuilder.append(SymbolConstant.COMMA).append(error.getDefaultMessage()); } //最终把首部的逗号去掉 return StrUtil.removePrefix(stringBuilder.toString(), SymbolConstant.COMMA); } /** * 渲染登录界面,分两种情况 *

* 第一种,如果是前后端不分离,则渲染登录界面即可 *

* 第二种,如果是前后端分离项目,则渲染Json结果 * * @author fengshuonan * @since 2021/5/18 10:48 */ private String renderLoginResult(HttpServletResponse response, AuthException authException, Model model) { if (ProjectUtil.getSeparationFlag()) { response.setHeader("Guns-Session-Timeout", "true"); ErrorResponseData errorResponseData = renderJson(authException.getErrorCode(), authException.getUserTip(), authException); ResponseRenderUtil.renderJsonResponse(response, errorResponseData); return null; } model.addAttribute("tips", authException.getUserTip()); return "/login.html"; } }