一、统一异常处理

1.1 常见后端报错

  • 访问不存在的页面:Whitelabel Error Page

  • 访问不存在的接口:Whitelabel Error Page

  • 访问存在的页面,控制器抛出异常:Whitelabel Error Page

  • 访问存在的接口,控制器抛出异常:Whitelabel Error Page

但用户并看不懂这些错误信息,常见的网站处理,都是跳转到对应的错误提示页面。

如:

  • 访问不存在的页面 ----- 返回 404 错误页面;

  • 访问不存在的接口 ----- 返回 404 错误页面;

  • 访问存在的页面,控制器抛出异常

    • 没有权限 ----- 返回 403 错误页面;
    • 其他异常 ----- 返回 500 错误页面;
  • 访问存在的接口,控制器抛出异常

    • 没有权限 ---- 返回错误 result bean,调用放得到 json 数据;
    • 其他异常 ---- 返回错误 result bean,调用放得到 json 数据;

1.2 控制层异常统一处理

实现分析:

  • 创建403,404,500错误信息页面
  • 将错误信息保存到数据库
  • 控制层统一处理,判断请求类型(页面请求还是接口请求),返回错误页面或是JSON数据
  • 非控制层的异常统一处理,判断调用的方式(浏览器访问 or postman 访问),返回错误页面 or json 数据
页面跳转

template/common/下,并使用404,403,500命名。

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
* @Description:
* @ClassName:spring_boot_j220601
* @Author:。。。。
* @CreateDate:2022-09-18 22:18:29
**/
@RequestMapping("common")
@Controller
public class CommonController {
/**
* @Author: zgx
* @Description: errorPageFor403
* 127.0.0.1:82/common/403 ---- get
* @Date: 2022/9/18
* @return: java.lang.String
**/

@GetMapping("/403")
public String errorPageFor403() {
return "common/403";
}

/**
* @Author: zgx
* @Description: errorPageFor404
* 127.0.0.1:82/common/404 ---- get
* @Date: 2022/9/18
* @return: java.lang.String
**/

@GetMapping("/404")
public String errorPageFor404() {
return "common/404";
}

/**
* @Author: zgx
* @Description: errorPageFor500
* 127.0.0.1:82/common/500 ---- get
* @Date: 2022/9/18
* @return: java.lang.String
**/

@GetMapping("/500")
public String errorPageFor500() {
return "common/500";
}
}
异常信息保存到数据库
  • entity层----ExceptionLog.java

    import lombok.Data;
    
    import javax.persistence.Entity;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "common_exception_log")
    @Data
    public class ExceptionLog extends AbstractEntity {
        private String ip;
        private String path;
        private String className;
        private String methodName;
        private String exceptionType;
        private String exceptionMessage;
    }
    
  • dao层----ExceptionLogDao.java

    import com.zgx.springBoot.modules.common.entity.ExceptionLog;
    import org.apache.ibatis.annotations.*;
    import org.springframework.stereotype.Repository;
    
    /**
     * @Description:
     * @ClassName:spring_boot_j220601
     * @Author:。。。。
     * @CreateDate:2022-09-18 22:23:08
     **/
    @Mapper
    @Repository
    public interface ExceptionLogDao {
        @Insert("insert into common_exception_log (ip, path, class_name, method_name, " +
                "exception_type, exception_message) " +
                "values (#{ip}, #{path}, #{className}, #{methodName}, " +
                "#{exceptionType}, #{exceptionMessage})")
        @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
        void insertExceptionLog(ExceptionLog exceptionLog);
    
        @Select("select * from common_exception_log where path = #{path} and " +
                "method_name = #{methodName} and exception_type = #{exceptionType} limit 1")
        ExceptionLog getExceptionLogByParam(
                @Param("path") String path,
                @Param("methodName") String methodName,
                @Param("exceptionType") String exceptionType) ;
    }
    
  • service层----ExceptionLogServiceImpl.java

    /**
     * @Description:
     * @ClassName:spring_boot_j220601
     * @Author:。。。。
     * @CreateDate:2022-09-18 22:24:54
     **/
    @Service
    public class ExceptionLogServiceImpl implements ExceptionLogService {
        @Autowired
        private ExceptionLogDao exceptionLogDao;
    
        @Override
        @Transactional
        public Result<ExceptionLog> insertExceptionLog(ExceptionLog exceptionLog) {
            // 先查询数据库中是否有该条错误日志
            ExceptionLog temp = exceptionLogDao.getExceptionLogByParam(
                    exceptionLog.getPath(),
                    exceptionLog.getMethodName(),
                    exceptionLog.getExceptionType()
            );
            if (temp != null) {
                return new Result<>(Result.ResultStatus.SUCCESS.code, "异常已经记录.");
            }
    
            exceptionLog.setCreateDate(LocalDateTime.now());
            exceptionLog.setUpdateDate(LocalDateTime.now());
    
            // 插入数据
            exceptionLogDao.insertExceptionLog(exceptionLog);
    
            return new Result<>(Result.ResultStatus.SUCCESS.code, "插入成功", exceptionLog);
        }
    }
    
  • controller层---- ExceptionLogDaoController.java

    /**
     * @Description:
     * @ClassName:spring_boot_j220601
     * @Author:。。。。
     * @CreateDate:2022-09-18 22:22:36
     **/
    @RestController
    @RequestMapping("/api")
    public class ExceptionLogDaoController {
        @Autowired
        private ExceptionLogService exceptionLogService;
    
        /**
         * @Author: zgx
         * @Description: insertExceptionLog
         * 
         * 127.0.0.1:82/api/exceptionLog ---- post
         * {"ip":"127.0.0.1","path":"/api/city","className":"CityController",
         * "methodName":"getCityById","exceptionType":"NullPointException",
         * "exceptionMessage":"*******"}
         * 
         * @Date: 2022/9/18
         * @Param: exceptionLog
         * @return: com.zgx.springBoot.modules.common.vo.Result<com.zgx.springBoot.modules.common.entity.ExceptionLog>
         **/
    
        @PostMapping(value = "/exceptionLog", consumes = MediaType.APPLICATION_JSON_VALUE)
        public Result<ExceptionLog> insertExceptionLog(@RequestBody ExceptionLog exceptionLog) {
            return exceptionLogService.insertExceptionLog(exceptionLog);
        }
    }
    
异常控制层处理
  • 首先理解两个注解:

    • @ControllerAdvice给Controller控制器添加统一的操作或处理
    • @ExceptionHandler捕获异常

    以上两个注解加起来就可以实现全局异常处理。

  • ExceptionController.java — 处理全局异常得控制类

    /**
     * @Description:
     * @ClassName:spring_boot_j220601
     * @Author:。。。。
     * @CreateDate:2022-09-20 09:16:11
     **/
    @ControllerAdvice
    public class ExceptionController {
    
        private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class);
    
        @Autowired
        private ExceptionLogService exceptionLogService;
        
        @ExceptionHandler(value = NoHandlerFoundException.class)
        public ModelAndView notHandlerFoundExceptionHandler(HttpServletRequest request,Exception e){
            return new ModelAndView("redirect:/common/404");
        }
    
        /**
         * @Author: zgx
         * @Description: exceptionHandle
         * @Date: 2022/9/20
         * @Param: request
         * @Param e
         * @return: org.springframework.web.servlet.ModelAndView 
         **/
    
        @ExceptionHandler(value = Exception.class)
        public ModelAndView exceptionHandle(HttpServletRequest request,Exception e){
            // 输出异常日志
            e.printStackTrace();
            LOGGER.error(e.getMessage());
            // 构建返回数据
            int code = 200;
            String message = "";
            String data = "";
            // 没有权限时得异常,也可以监听 by zero 的异常来代替没有权限时异常
            if (e instanceof ArithmeticException) {
                code = 403;
                message = "没有权限";
                data = "/common/403";
            }else {
                code = 500;
                message = "服务器错误";
                data = "/common/500";
            }
            // 保存异常信息到数据库
            insertExceptionLog(request,e);
    
            // 判断是否为接口
            // 包装数据返回不同的 modelAndView
            if (isInterface(request)) {
                // 是接口时,返回数据
                ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
                modelAndView.addObject("code",code);
                modelAndView.addObject("message",message);
                modelAndView.addObject("data",data);
                return modelAndView;
            }else {
                // 不是接口时,直接放回页面
                return new ModelAndView("redirect:" + data);
            }
        }
    
        private boolean isInterface(HttpServletRequest request){
            // 通过debug调试可以看出request中含有该对象。
            HandlerMethod handlerMethod = (HandlerMethod)request.
                getAttribute("org.springframework.web.servlet.HandlerMapping.bestMatchingHandler");
            // getBeanType()拿到发送请求得类模板(Class),getDeclaredAnnotationsByType(指定注解类模板)通过指定得注解,得到一个数组。
            RestController[] annotations1 = handlerMethod.getBeanType().getDeclaredAnnotationsByType(RestController.class);
            ResponseBody[] annotations2 = handlerMethod.getBeanType().getDeclaredAnnotationsByType(ResponseBody.class);
            ResponseBody[] annotations3 = handlerMethod.getMethod().getAnnotationsByType(ResponseBody.class);
            // 判断当类上含有@RestController或是@ResponseBody或是方法上有@ResponseBody时,则表明该异常是一个接口请求发生的
            return annotations1.length > 0 || annotations2.length>0 || annotations3.length>0?true:false;
        }
        
        // 保存异常信息
        private void insertExceptionLog(HttpServletRequest request,Exception e){
            ExceptionLog exceptionLog = new ExceptionLog();
            // 将数据封装到ExceptionLog中
            
            // Ip
            exceptionLog.setIp(request.getRemoteAddr());
            
            String url1 = request.getRequestURI();
            StringBuffer url2 = request.getRequestURL();
            
            // url
            exceptionLog.setPath(request.getServletPath());
            
            // 同上
            HandlerMethod handlerMethod = (HandlerMethod)request.
                getAttribute("org.springframework.web.servlet.HandlerMapping.bestMatchingHandler");        
            // 类名
            exceptionLog.setClassName(handlerMethod.getBeanType().getName());
            // 方法名
            exceptionLog.setMethodName(handlerMethod.getMethod().getName());
            // 异常的类型
            exceptionLog.setExceptionType(e.getClass().getName());
            // 异常消息
            exceptionLog.setExceptionMessage(e.getMessage());
    
            // 将信息插入数据库
            exceptionLogService.insertExceptionLog(exceptionLog);
        }
    }
    
    
  • 通过debug可以发现org.springframework.web.servlet.HandlerMapping.bestMatchingHandler,这个key中包含处理当前请求的controller类。图上表示的TestController

    01

1.3 非控制层统一异常处理

  • spring boot有一个非控制层异常处理类BasicErrorController

    它有个注解@RequestMapping("${server.error.path:${error.path:/error}}"),它会按照server.error.path没有就找error.path,error.path没有就找error资源,所以建一个名为error.html的页面,作为错误信息页面提示。

  • MyBasicErrorController.java — 按照BasicErrorController中的代码格式重写其中方法

    /**
     * @Description:
     * @ClassName:spring_boot_j220601
     * @Author:。。。。
     * @CreateDate:2022-09-20 16:12:03
     **/
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class MyBasicErrorController implements ErrorController {
    
        // 页面请求异常
        @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
        public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            HttpStatus status = getStatus(request);
            if (status.value() == 404) {
                return new ModelAndView("redirect:/common/404");
            } else {
                return new ModelAndView("redirect:/common/500");
            }
        }
        
        // 接口请求异常
        @RequestMapping
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            Map<String, Object> map = new HashMap<>();
            HttpStatus status = getStatus(request);
            map.put("status", status.value());
            map.put("message", status.getReasonPhrase());
            if (status == HttpStatus.NOT_FOUND) {
                map.put("data", "/common/404");
            } else {
                map.put("data", "/common/500");
            }
            return new ResponseEntity<Map<String, Object>>(map, status);
        }
    
        protected HttpStatus getStatus(HttpServletRequest request) {
            // 获取状态值
            Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
            if (statusCode == null) {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            } else {
                try {
                    return HttpStatus.valueOf(statusCode);
                } catch (Exception var4) {
                    return HttpStatus.INTERNAL_SERVER_ERROR;
                }
            }
        }
    }
    
    
    

1.4 将非控制层异常也交给控制层处理

  • 只有找不到对应该请求的处理器时,才会进入下面的noHandler方法去抛出NoHandlerFoundException异常。

    但是springboot的WebMvcAutoConfiguration会默认配置如下资源映射

    /映射到 /static(或/public、/resources、/META-INF/resources) 
    /webjars/ 映射到classpath:/META-INF/resources/webjars/ 
    /**/favicon.ico 映射favicon.ico文件.
    

    即使你的地址错误,仍然会匹配到/**这个静态资源映射地址,就不会进入noHandlerFound方法,自然不会抛出NoHandlerFoundException了。

    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null || mappedHandler.getHandler() == null) {
    	noHandlerFound(processedRequest, response);
    	return;
    }
    

    因此,可以使用spring.web.resources.add-mappings=false禁用静态资源

    或是如下配置:

  • application.properties

    spring.mvc.throw-exception-if-no-handler-found=true
    spring.mvc.static-path-pattern=/statics/**
    
  • 并监听NoHandlerFoundException异常

    @ExceptionHandler(value = NoHandlerFoundException.class)
    public ModelAndView notHandlerFoundExceptionHandler(HttpServletRequest request,Exception e){
        return new ModelAndView("redirect:/common/404");
    }
    
  • 做了以上配置后,虽然能够访问了,但是其他静态资源就失效了。

常见异常类

    /**
     * 404异常处理
     */
    @ExceptionHandler(value = NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)

    /**
     * 405异常处理
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)

    /**
     * 415异常处理
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)

    /**
     * 500异常处理
     */
    @ExceptionHandler(value = Exception.class)

	/**
     * 403异常处理
     * shiro---没有权限异常
     */
    @ExceptionHandler(value = AuthorizationException.class)


    /**
     * 业务异常处理
     */
    @ExceptionHandler(value = BasicException.class)

    /**
     * 表单验证异常处理
     * 在controller上使用@valid注解,实体类的熟悉上使用@NotBlank("xxxx不能为空") 
     * 如果参数校验不通过,就会报这个错误
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐