【Spring Boot】错误处理及流程解析

一、错误处理

默认情况下,Spring Boot提供/error处理所有错误的映射

对于机器客户端(例如PostMan),它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息(如果设置了拦截器,需要在请求头中塞入Cookie相关参数)
在这里插入图片描述

对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
在这里插入图片描述

另外,templates下面error文件夹中的4xx,5xx页面会被自动解析

二、底层相关组件

那么Spring Boot是怎么实现上述的错误页相关功能的呢?
我们又要来找一下相关源码进行分析了

首先我们先了解一个概念:@Bean配置的类的默认id是方法的名称,但是我们可以通过value或者name给这个bean取别名,两者不可同时使用

我们进入ErrorMvcAutoConfiguration,看这个类名应该是和错误处理的自动配置有关,我们看下这个类做了什么

  • 向容器中注册类型为DefaultErrorAttributes,id为errorAttributes的bean(管理错误信息,如果要自定义错误页面打印的字段,就自定义它),这个类实现了ErrorAttributes, HandlerExceptionResolver(异常处理解析器接口), Ordered三个接口
@Bean
@ConditionalOnMissingBean(
    value = {ErrorAttributes.class},
    search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
}

点进去后发现,这个类是和我们响应页面中的message、error等字段有关
在这里插入图片描述

  • 向容器中注册一个id为basicErrorController的控制器bean(管理错误相应逻辑,不想返回json或者错误视图,就自定义它)
@Bean
@ConditionalOnMissingBean(
    value = {ErrorController.class},
    search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

这个控制器就和前面我们返回json或者错误视图有关
在这里插入图片描述

  • 声明类型为DefaultErrorViewResolver,id为conventionErrorViewResolver的bean(管理错误视图跳转路径,如果要改变跳转路径,就自定义它)
@Configuration(
   proxyBeanMethods = false
)
@EnableConfigurationProperties({WebProperties.class, WebMvcProperties.class})
static class DefaultErrorViewResolverConfiguration {
   private final ApplicationContext applicationContext;
   private final Resources resources;

   DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
       this.applicationContext = applicationContext;
       this.resources = webProperties.getResources();
   }

   @Bean
   @ConditionalOnBean({DispatcherServlet.class})
   @ConditionalOnMissingBean({ErrorViewResolver.class})
   DefaultErrorViewResolver conventionErrorViewResolver() {
       return new DefaultErrorViewResolver(this.applicationContext, this.resources);
   }
}

这个类中,解释了为什么前面会根据不同的状态码转向不同的错误页
在这里插入图片描述

  • 声明一个静态内部类WhitelabelErrorViewConfiguration,它与错误视图配置相关,这个类中声明了一个id为error的视图对象提供给basicErrorController中使用,还定义了视图解析器BeanNameViewResolver ,它会根据返回的视图名作为组件的id去容器中找View对象
@Configuration(
   proxyBeanMethods = false
)
@ConditionalOnProperty(
   prefix = "server.error.whitelabel",
   name = {"enabled"},
   matchIfMissing = true
)
@Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class})
protected static class WhitelabelErrorViewConfiguration {
   private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView();

   protected WhitelabelErrorViewConfiguration() {
   }

   @Bean(
       name = {"error"}
   )
   @ConditionalOnMissingBean(
       name = {"error"}
   )
   public View defaultErrorView() {
       return this.defaultErrorView;
   }

   @Bean
   @ConditionalOnMissingBean
   public BeanNameViewResolver beanNameViewResolver() {
       BeanNameViewResolver resolver = new BeanNameViewResolver();
       resolver.setOrder(2147483637);
       return resolver;
   }
}
  • 另外还声明了一个静态内部类StaticView,这里面涉及错误视图的渲染等相关操作
private static class StaticView implements View {
   private static final MediaType TEXT_HTML_UTF8;
   private static final Log logger;

   private StaticView() {
   }

   public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
       if (response.isCommitted()) {
           String message = this.getMessage(model);
           logger.error(message);
       } else {
           response.setContentType(TEXT_HTML_UTF8.toString());
           StringBuilder builder = new StringBuilder();
           Object timestamp = model.get("timestamp");
           Object message = model.get("message");
           Object trace = model.get("trace");
           if (response.getContentType() == null) {
               response.setContentType(this.getContentType());
           }
           ...

三、异常处理流程

为了了解Spring Boot的异常处理流程,我们写一个demo进行debug

首先写一个会发生算术运算异常的接口/test_error

/**
 * 测试报错信息
 * @return 跳转错误页面
 */
@GetMapping(value = "/test_error")
public String testError() {
    int a = 1/0;
    return String.valueOf(a);
}

然后放置一个错误页面5xx.html于templates下的error文件夹中

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta name="description" content="">
  <meta name="author" content="ThemeBucket">
  <link rel="shortcut icon" href="#" type="image/png">

  <title>500 Page</title>

  <link href="css/style.css" rel="stylesheet">
  <link href="css/style-responsive.css" rel="stylesheet">

  <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
  <!--[if lt IE 9]>
  <script src="js/html5shiv.js"></script>
  <script src="js/respond.min.js"></script>
  <![endif]-->
</head>

<body class="error-page">
<section>
    <div class="container ">
        <section class="error-wrapper text-center">
            <h1><img alt="" src="images/500-error.png"></h1>
            <h2>OOOPS!!!</h2>
            <h3 th:text="${message}">Something went wrong.</h3>
            <p class="nrml-txt" th:text="${trace}">Why not try refreshing you page? Or you can <a href="#">contact our support</a> if the problem persists.</p>
            <a class="back-btn" href="index.html" th:text="${status}"> Back To Home</a>
        </section>
    </div>
</section>

<!-- Placed js at the end of the document so the pages load faster -->
<script src="js/jquery-1.10.2.min.js"></script>
<script src="js/jquery-migrate-1.2.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/modernizr.min.js"></script>

<!--common scripts for all pages-->
<!--<script src="js/scripts.js"></script>-->
</body>
</html>

然后我们开启debug模式,发送请求

首先,我们的断点还是来到DispatcherServlet类下的doDispatch()方法
经过mv = ha.handle(processedRequest, response, mappedHandler.getHandler());调用目标方法之后,他会返回相关错误信息,并将其塞入dispatchException这个对象

然后调用this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);处理调度结果
在这里插入图片描述

然后他会在processDispatchResult()中经过判断是否存在异常,异常不为空,调用processHandlerException()方法,这里它会遍历系统中所有的异常处理解析器,哪个解析器返回结果不为null,就结束循环

在调用DefaultErrorAttributes时,它会将错误中的信息放入request请求域中(我们后面模板引擎页面解析会用到)

遍历完所有解析器,我们发现他们都不能返回一个不为空的ModelAndView对象,于是它会继续抛出异常
在这里插入图片描述

当系统发现没有任何人能处理这个异常时,底层就会发送 /error 请求,它就会被我们上面介绍的BasicErrorController下的errorHtml()方法处理
在这里插入图片描述
在这里插入图片描述

这个方法会通过ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);去遍历系统中所有的错误视图解析器,如果调用解析器的resolveErrorView()方法返回结果不为空就结束循环
在这里插入图片描述
系统中只默认注册了一个错误视图解析器,也就是我们上面介绍的DefaultErrorViewResolver,跟随debug断点我们得知,这个解析器会把error+响应状态码作为错误页的地址,最终返回给我们的视图地址为error/5xx.html
在这里插入图片描述

四、定制错误处理逻辑

1、自定义错误页面

error下的4xx.html和5xx.html,根据我们上面了解的DefaultErrorViewResolver类可以,它的resolveErrorView()方法在进行错误页解析时,如果有精确的错误状态码页面就匹配精确,没有就找 4xx.html,如果都没有就转到系统默认的错误页

2、使用注解或者默认的异常处理
  • @ControllerAdvice+@ExceptionHandler处理全局异常,我们结合一个demo来了解一下用法
    首先我们创建一个类用来处理全局异常
package com.decade.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

   // 指定该方法处理某些指定异常,@ExceptionHandler的value可以是数组,这里我们指定该方法处理数学运算异常和空指针异常
   @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
   public String handleArithmeticException(Exception exception) {
       log.error("异常信息为:{}", exception);
       // 打印完错误信息后,返回登录页
       return "login";
   }
}

我们还是使用上面的会发生算术运算异常的接口/test_error进行测试
请求接口后发现,页面跳转到登录页了
在这里插入图片描述
为什么没有再走到5xx.html呢?
因为@ControllerAdvice+@ExceptionHandler的底层是ExceptionHandlerExceptionResolver来处理的

这样在进入DispatcherServlet类下的processHandlerException()方法时,就会调用ExceptionHandlerExceptionResolver这个异常处理解析器,从而跳转到我们自己创建的异常处理类进行异常处理,然后返回不为null的ModelAndView对象给它,终止遍历,不会再发送/error请求

  • @ResponseStatus+自定义异常
    首先我们自定义一个异常类
package com.decade.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

// code对应错误码,reason对应message
@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED, reason = "自定义异常")
public class CustomException extends RuntimeException {

   public CustomException() {
   }

   public CustomException(String message) {
       super(message);
   }
}

然后写一个接口去抛出自定义异常

/**
* 测试报错信息
* @return 跳转错误页面
*/
@GetMapping(value = "/test_responseStatus")
public String testResponseStatus(@RequestParam("param") String param) {
   if ("test_responseStatus".equals(param)) {
       throw new CustomException();
   }
   return "main";
}

最后我们调用接口,可以得到,跳转到了4xx.html,但是状态码和message都和我们自己定义的匹配
在这里插入图片描述
那么原理是什么呢?我们还是从DispatcherServlet类下的processHandlerException()方法开始看

当我们抛出自定义异常时,由于前面@ControllerAdvice+@ExceptionHandler修饰的类没有指定处理这个异常,所以循环走到下一个异常处理解析器ResponseStatusExceptionResolver

我们分析一下这里的代码

@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    try {
        if (ex instanceof ResponseStatusException) {
            return this.resolveResponseStatusException((ResponseStatusException)ex, request, response, handler);
        }

		// 由于我们自定义异常类使用了@ResponseStatus注解修饰,所以我们这里获取到的status信息不为空
        ResponseStatus status = (ResponseStatus)AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
        if (status != null) {
            return this.resolveResponseStatus(status, request, response, handler, ex);
        }
		...

protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
    // 获取@ResponseStatus注解的code和reason作为状态码和message
	int statusCode = responseStatus.code().value();
    String reason = responseStatus.reason();
    return this.applyStatusAndReason(statusCode, reason, response);
}

protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response) throws IOException {
    if (!StringUtils.hasLength(reason)) {
        response.sendError(statusCode);
    } else {
        String resolvedReason = this.messageSource != null ? this.messageSource.getMessage(reason, (Object[])null, reason, LocaleContextHolder.getLocale()) : reason;
		// 发送/error请求,入参为@ResponseStatus注解的code和reason
        response.sendError(statusCode, resolvedReason);
    }

	// 返回一个modelAndView
    return new ModelAndView();
}

经过debug我们知道,ResponseStatusExceptionResolver这个异常处理解析器返回了一个空的ModelAndView对象给我们,而且还通过response.sendError(statusCode, resolvedReason);发送了/error请求

这样就又走到了上面的第三节处理/error请求的流程中,从而带着我们@ResponseStatus注解的code和reason跳转到了4xx.html页面,这样就能解释为什么4xx.html页面中的状态码和message都是我们自定义的了

  • 如果没有使用上述2种方法处理指定异常或处理我们自己自定义的异常,那么系统就会按照Spring底层的异常进行处理,如 请求方法不支持异常等,都是使用DefaultHandlerExceptionResolver这个异常处理解析器进行处理的
    我们分析这个类的doResolveException()方法得知,它最后也会发送/error请求,从而转到4xx.html或者5xx.html页面
    在这里插入图片描述
    在这里插入图片描述
3、自定义异常处理解析器

使用@Component注解,并实现HandlerExceptionResolver接口来自定义一个异常处理解析器

package com.decade.exception;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 将优先级提到第一位,Order越小,优先级越高,所以我们这里设置int的最小值
@Order(Integer.MIN_VALUE)
@Component
public class CustomExceptionHandler implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            response.sendError(500, "自己定义的异常");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

当我们把优先级提到最高时,前面的那些异常处理解析器都会失效,这时我们的自定义异常处理解析器可以作为默认的全局异常处理规则
在这里插入图片描述

值得注意的是,当代码走到response.sendError时,就会触发/error请求,当你的异常没有人能处理时,也会走tomcat底层触发response.sendError,发送/error请求

如有错误,欢迎指正!!!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 基于Spring Boot的用户权限管理系统的设计与实现中,可能会用到以下一些函数: 1. @GetMapping:用于处理HTTP GET请求,常用于查询数据。 2. @PostMapping:用于处理HTTP POST请求,常用于新增数据。 3. @PutMapping:用于处理HTTP PUT请求,常用于更新数据。 4. @DeleteMapping:用于处理HTTP DELETE请求,常用于删除数据。 5. @PathVariable:用于从URL中获取参数值,常用于查询、更新和删除操作。 6. @RequestBody:用于将请求参数绑定到方法参数上,常用于新增和更新操作。 7. @Autowired:用于自动装配依赖对象,常用于服务层和数据访问层的注入。 8. @Repository:用于标识数据访问层的实现类,常用于注入数据访问层的对象。 9. @Service:用于标识服务层的实现类,常用于注入服务层的对象。 10. @Controller:用于标识控制层的实现类,常用于处理HTTP请求。 11. @RestController:用于标识控制层的实现类,常用于处理HTTP请求,并返回JSON格式的数据。 12. @Transactional:用于标识事务方法,常用于保证数据一致性。 13. @Valid:用于数据校验,常用于新增和更新操作。 14. ResponseEntity:用于封装HTTP响应,常用于返回JSON格式的数据。 15. JpaRepository:Spring Data JPA提供的接口,用于访问数据库。 16. Pageable:Spring Data JPA提供的接口,用于分页查询。 17. BCryptPasswordEncoder:Spring Security提供的加密解密工具,常用于密码加密。 以上是在基于Spring Boot的用户权限管理系统中可能会用到的一些函数和类。 ### 回答2: 基于Spring Boot的用户权限管理系统的设计与实现中常用的函数包括: 1. 用户注册和登录函数:实现用户注册和登录功能。可以使用Spring Boot提供的相关函数,如`@RequestMapping`注解来定义接口路径,使用`@PostMapping`、`@GetMapping`等注解定义用户注册和登录的方法。 2. 用户权限验证函数:用于验证用户是否有权限进行某个操作。可以使用Spring Security框架提供的函数来实现用户权限验证,如`@Secured`注解或`@PreAuthorize`注解。 3. 用户管理函数:实现用户的增删改查操作。可以创建相应的Controller和Service类,使用Spring MVC的相关注解定义接口路径和请求方法,通过Service类调用对应的数据访问层函数来完成相应的用户管理操作。 4. 角色管理函数:实现对角色的增删改查等操作。同样可以创建相应的Controller和Service类,定义角色管理的接口路径和请求方法,并调用对应的数据访问层函数进行角色管理操作。 5. 权限管理函数:实现对权限的增删改查等操作。可以采用类似的方式来完成权限管理的函数实现。 6. 数据访问层函数:用于操作数据库,实现对用户、角色和权限等数据表的增删改查。通常使用Spring Data JPA或MyBatis等框架提供的函数来进行数据访问操作。 7. 单元测试函数:用于对上述函数进行单元测试,保证代码的正确性和稳定性。可以使用JUnit等测试框架,编写相应的测试函数。 总之,基于Spring Boot的用户权限管理系统的设计与实现中,需要使用各种函数来实现用户注册和登录、权限验证、用户、角色、权限管理等各项功能。这些函数可以通过Spring Boot和相关的框架提供的注解和API来实现。同时,在实现过程中也需要编写单元测试函数,确保代码的质量。 ### 回答3: 基于Spring Boot的用户权限管理系统的设计与实现中,可以使用以下函数: 1. @GetMapping、@PostMapping、@PutMapping、@DeleteMapping:用于处理HTTP请求,获取、添加、修改、删除用户、角色、权限等信息。 2. @RequestMapping:用于处理请求映射,将HTTP请求映射到相应的控制器方法。 3. @RestController:用于创建RESTful风格的控制器,可以处理请求并返回JSON或XML格式的数据。 4. @RequestBody:用于将HTTP请求的JSON格式的数据绑定到Java对象。 5. @ResponseBody:用于将Java对象转换为JSON或XML格式的数据并返回给客户端。 6. @RequestParam:用于获取URL中的参数值或请求参数的值。 7. @PathVariable:用于获取URL路径中的参数值。 8. @Valid、@Validated:用于在参数绑定过程中进行数据校验。 9. @ExceptionHandler:用于处理全局异常,返回自定义的错误信息。 10. @Service:用于标识服务层的组件。 11. @Repository:用于标识数据访问层的组件。 12. @Autowired:用于自动装配依赖,实现依赖注入。 13. @Entity、@Table:用于定义实体类和数据库表之间的映射关系。 14. @Column、@Id、@GeneratedValue:用于定义实体类属性与数据库表字段之间的映射关系。 15. JPA的CRUD方法:包括save、findById、findAll、delete等方法,用于进行数据库的增删改查操作。 16. QueryDSL查询:用于复杂的查询需求,支持动态组合查询条件。 17. Spring Security:用于实现用户认证和授权功能,包括安全配置、自定义登录界面、角色权限控制等。 18. JWT Token:用于实现无状态的用户认证,生成、解析和验证Token。 以上是基于Spring Boot的用户权限管理系统设计与实现中常用到的函数,通过使用这些函数,可以实现系统各个模块的功能,并提供良好的用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值