SpringBoot异常处理机制-BasicErrorController与@ControllerAdvice

一、SpringBoot异常处理机制

在SpringBoot中,常用的异常处理有两种:一种是 BasicErrorController,另一种是 @ControllerAdviceBasicErrorController 用于处理非Controller抛出的异常,而@ControllerAdvice 用于处理Controller抛出的异常,对于非Controller抛出的异常它是不会管的。但是,如果是Controller层调用Service层,从Service层抛出,依然会抛到Controller,所以还是会调用@ControllerAdvice进行处理。


二、BasicErrorController 方式

在浏览器上随便访问一个不存在的端点, 会看到如下提示:

如果返回格式是json,或用postman访问一个不存在的端点,响应消息如下:

{
    "timestamp": "2020-05-25T12:29:35.418+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/"
}

原因:如果程序抛出了未能被 @ControllerAdvice 处理的异常,SpringBoot 会把请求转向 /error 这个端点(转发),SpringBoot 有一个默认的 Controller 用于处理这类请求,它就是 BasicErrorController

BasicErrorController 中有两个标注了 @RequestMapping 的方法,当请求头的 Accept 中包含 text/html 时,会调用 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)方法,其他情况则会调用 public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)方法。通过断点调试可以看到,这个 request 对象中封装了很多有用的信息,我们可以利用这些信息来定义自己的信息体。

2.1、BasicErrorController 子类创建及配置

BasicErrorController 默认返回的信息如下图:

这个方法是属于类 DefaultErrorAttributes,返回一个map,里面包含了异常发生的时间、异常状态、异常详细信息、发生异常的请求路径。如下图是一个异常发生的时候所获取到的异常信息:

那么,我们该如何抛弃掉或者覆盖掉Spring Boot默认的异常处理呢,Spring Boot开发指南上提供了以下四种方法:

1、自定义一个bean,实现 ErrorController 接口,那么默认的错误处理机制将不再生效。
2、自定义一个bean,继承 BasicErrorController 类,使用一部分现成的功能,自己也可以添加新的public方法,使用 @RequestMapping 及其produces属性指定新的地址映射。
3、自定义一个 ErrorAttribute 类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
4、定义一个bean,继承 AbstractErrorController

下面使用第2种方式。

1、创建类并继承 BasicErrorController
public class MyErrorController extends BasicErrorController {
    public MyErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
    }
    
    @SneakyThrows
    @Override
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
        Map<String, Object> attributes = super.getErrorAttributes(request, includeStackTrace);
        attributes.remove("timestamp");
        attributes.remove("status");
        attributes.remove("error");
        attributes.remove("exception");
        attributes.remove("path");
        String messageCode = (String) attributes.get("message");
        ErrorEnum errorEnum = null;
        try {
            errorEnum = ErrorEnum.getEnumByCode(messageCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (errorEnum == null){
            attributes.put("type","0");
            attributes.put("message","枚举类异常");
            return attributes;
        }
        attributes.put("errorControllerType","basicErrorController");
        attributes.put("type",errorEnum.getCode());
        attributes.put("message",errorEnum.getMessage());
        return attributes;
    }
}
2、配置MyErrorController

SpringBoot关于错误处理的配置类 ErrorMvcAutoConfiguration,其中对 BasicErrorController 进行了配置,因此子类继承BasicErrorController后也应当对其进行配置。

ErrorMvcAutoConfiguration中的 BasicErrorController 配置如下:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}

参考这个配置,我们自定义的MyErrorController进行配置:

@Configuration
public class ErrorConfiguration {
	@Bean
    public MyErrorController basicErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
                                                  ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
        return new MyErrorController(errorAttributes, serverProperties.getError(),
                errorViewResolversProvider.getIfAvailable());
    }
}

2.2、Spring Boot错误处理原理

Spring Boot的错误处理自动配置类:org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration(在spring-boot-autoconfigure.jar下),这个类注册了很多关于错误处理的组件,主要的有四个 DefaultErrorAttributesBasicErrorControllerErrorPageCustomizerDefaultErrorViewResolver

public class ErrorMvcAutoConfiguration {
    ...
    //注入DefaultErrorAttributes组件
    @Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(
				this.serverProperties.getError().isIncludeException());
	}
	
    //注入BasicErrorController 组件
    //@Bean放在方法上,如果参数类型所对应的实例在spring容器中只有一个,则默认选择这个实例。
	//如果有多个,则需要根据参数名称来选择(参数名称就相当于是spring的配置文件中的bean的id)。
	//errorAttributes从容器中注入的其实就是DefaultErrorAttributes(实现ErrorAttributes )。
	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				this.errorViewResolvers);
	}
	
    //注入ErrorPageCustomizer 组件
	@Bean
	public ErrorPageCustomizer errorPageCustomizer() {
		return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
	}
	
    @Configuration
	static class DefaultErrorViewResolverConfiguration {
		private final ApplicationContext applicationContext;
		private final ResourceProperties resourceProperties;
		
		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				ResourceProperties resourceProperties) {
			this.applicationContext = applicationContext;
			this.resourceProperties = resourceProperties;
		}
		
        //注入DefaultErrorViewResolver 组件
		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean
		public DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext,
					this.resourceProperties);
		}
	}
    ...
}

Spring boot错误处理步骤:
1、当程序发生异常或者错误,ErrorPageCustomizer组件就会生效,它的作用是定制错误的响应规则。

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
    ...
    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        //this.properties.getError().getPath())的值是"/error"
		ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
					.getRelativePath(this.properties.getError().getPath()));
		errorPageRegistry.addErrorPages(errorPage);
	}
    ...
}

这个方法说明当系统出现错误后就去到 /error 进行处理。
2、/error 请求就会被 BasicErrorController 处理,处理完后返回响应页面。

@Controller
//如果server.error.path获取不到就用${error.path:/error},error.path获取不到就用/error,说明这个controller的地址是/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    ...
    //MediaType.TEXT_HTML_VALUE="text/html"
    //如果是浏览器的请求就走这个方法
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		//构建错误数据,保存到model中
		Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		//构建ModelAndView即处理完返回的视图,也就是响应页面
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		//如果找不到返回error视图
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}
	
    //如果是客户端的请求就走这个方法
	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
	    //构建错误数据
		Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<>(body, status);
	}
	...
}

3、响应页面的规则,实际上是通过 DefaultErrorViewResolver 组件来处理的。上面代码中的 resolveErrorView 方法就是返回响应页面的,它是 BasicErrorController 的父类 AbstractErrorController 中的方法。

public abstract class AbstractErrorController implements ErrorController {
    private final ErrorAttributes errorAttributes;
    private final List<ErrorViewResolver> errorViewResolvers;
    ...
    protected ModelAndView resolveErrorView(HttpServletRequest request,
			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
		//获取所有的ErrorViewResolver,包括DefaultErrorViewResolver,所以我们可以自己定制自己的ErrorViewResolver组件
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}
}

DefaultErrorViewResolver 类源码:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
	private static final Map<Series, String> SERIES_VIEWS;
	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}
	
    //构建响应页面规则,HttpStatus 是错误的状态码:404、500等。
    @Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
	   //调用resolve方法创建错误视图
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		//如果没有这个视图并且静态资源中也没有错误页面(具体状态码.html例如:404.html),而且状态码是4开头或者5开头。
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		   //创建error/4xx或者error/5xx视图。
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}
	
	private ModelAndView resolve(String viewName, Map<String, Object> model) {
	    //错误视图例:error/404
		String errorViewName = "error/" + viewName;
		//使用模板引擎找到这个视图
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		//如果能找到视图就创建视图对象
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		//没有找到则调用此方法,这个方法是去默认的静态资源文件夹下找
		return resolveResource(errorViewName, model);
	}
	
	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
	    // this.resourceProperties.getStaticLocations()获取静态资源文件夹:public、static这些。
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
				//错误页面,例:error/400.html
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
				   //如果存在这个html页面就用这个,否则返回null
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			} catch (Exception ex) {
			}
		}
		return null;
	}
}
由上可知,视图名是状态码的值.html,状态码是在 BasicErrorController 中通过 getStatus 方法获取的。
HttpStatus status = getStatus(request);

getStatus 是 BasicErrorController 的父类 AbstractErrorController 中的方法:

protected HttpStatus getStatus(HttpServletRequest request) {
	Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
	if (statusCode == null) {
		return HttpStatus.INTERNAL_SERVER_ERROR;
	}
	try {
		return HttpStatus.valueOf(statusCode);
	} catch (Exception ex) {
		return HttpStatus.INTERNAL_SERVER_ERROR;
	}
}

所以设置状态码时设置名为"javax.servlet.error.status_code"

request.setAttribute("javax.servlet.error.status_code", 500);

DefaultErrorViewResolver 先去模板引擎(以thymeleaf为例)文件夹下(templates)找”error/状态码(404、500等).html"文件,没有,则去Spring Boot默认静态文件夹下找,没有,再去找”error/4xx(5xx).html"文件,找到了则创建对应的视图,没找到返回null。而在 BasicErrorController 的 resolveErrorView 方法的最后:

return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

表示如果找不到视图,则创建默认视图 "error"。而在ErrorMvcAutoConfiguration 中引入了error视图:

private final StaticView defaultErrorView = new StaticView();

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

error视图的类型是 StaticView,这个类的作用就是生成Spring Boot默认的错误页面。

4、错误数据
通过 @Bean 注解,把 DefaultErrorAttributes 注入到了 BasicErrorController 中,而在 BasicErrorController 的 errorHtml 方法中,调用 getErrorAttributes 方法,此方法再调用 DefaultErrorAttributesgetErrorAttributes 方法获取了封装好的错误数据。

2.3、自定义错误页面

由上可知,当有模板引擎时,将错误页面命名为"错误状态码.html"放在模板引擎文件夹里的error文件夹下,发生此状态码的错误就会来到对应的页面;找不到会去Spring Boot的静态资源文件夹下找,也可以使用 4xx.html/5xx.html 作为错误页面的文件名来匹配4类型和5类型的所有错误,但是精确优先(优先寻找精确的状态码.html)。没有模板引擎时直接去Spring Boot默认的静态资源文件夹下找。如果都没有找到,就生成默认的错误页面。

2.4、另一种自定义错误数据

上面说过,Spring Boot的错误数据处理是通过 DefaultErrorAttributes 类来实现的,我们可以通过自定义一个bean继承 DefaultErrorAttributes 来扩展自己的错误数据处理,这时 DefaultErrorAttributes 会失效(@ConditionalOnMissingBean)。

@Component//将MyErrorAttributes加到容器中
public class MyErrorAttributes extends DefaultErrorAttributes {
	@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		// 在DefaultErrorAttributes的基础上做扩展。
		Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
		map.put("company", "comDMF"); //添加自己的错误数据
		return map;
	}
}

三、@ControllerAdvice

@ControllerAdvice 是 Spring 提供的 Controller 级的异常拦截注解,通过它可以实现全局异常拦截。@RestControllerAdvice@ControllerAdvice + @ResponseBody 的语法糖。

这种统一异常处理,只需要在类上标注 @ControllerAdvice 注解,然后在类的方法上标注 @ExceptionHandler 注解并指出它要处理的异常类型。如下图:

@ControllerAdvice
public class ExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result exceptionHandler(Exception e){
        e.printStackTrace();
        return new Result(StatusCode.ERROR, e.getMessage());
    }
}

在单个 Controller 中也可以定义被 @ExceptionHandler 标注的方法作为本 Controller 的异常处理方法,例如 BasicErrorControllermediaTypeNotAcceptable 方法。
优先级是,Controller 中的异常处理方法 > 全局的异常处理方法。定义的处理异常类型越详细优先级越高。比如:如果出现 RuntimeException,那么它会被处理 RuntimeException 异常的方法拦截,不会被处理 Exception 异常的方法拦截。但是如果处理 Exception 异常的方法定义在它自己的 Controller 中,那么它只会被本 Controller 中的异常拦截方法拦截。

统一异常处理的使用:大多数情况下,不要去继承重写 BasicErrorController,使用 @ControllerAdvice 已经够用,因为你的所有请求都会经过Controller。

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值