SpringBoot异常的统一处理
springboot的统一异常处理
一、统一异常处理
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
类
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
更多推荐
所有评论(0)