一、前言
Spring Boot对异常的处理有一套默认的机制:当应用中产生异常时,Spring Boot根据发送请求头中的accept是否包含text/html来分别返回不同的响应信息。当从浏览器地址栏中访问应用接口时,请求头中的accept便会包含text/html信息,产生异常时,Spring Boot通过org.springframework.web.servlet.ModelAndView对象来装载异常信息,并以HTML的格式返回;而当从客户端访问应用接口产生异常时(客户端访问时,请求头中的accept不包含text/html),Spring Boot则以JSON的格式返回异常信息。下面来验证一下。
二、默认异常处理机制
假设应用中有如下一个Controller:
@RestController@RequestMapping("calculate")publicclassCalculateController{@GetMapping("/{divisor:\\d+}/{dividend:\\d+}")publicvoidcalculate(@PathVariableLongdivisor,@PathVariableLongdividend){thrownewRuntimeException("calculate is error");}}在代码中我们主动的抛出了一个RuntimeException,使用浏览器访问http://localhost:8080/calculate/1/0:
可看到页面返回了一些异常描述,并且请求头的accpet包含了text/html片段。
接着使用模拟发送REST请求,使用IDEA插件HTTP Client发送http://localhost:8080/calculate/1/0:
可以看到请求头的accept值为*/*,并且返回一段JSON格式的信息。
查看Spring Boot的BasicErrorController类便可看到这一默认机制的具体实现:
可看到errorHtml和error方法的请求地址和方法是一样的,唯一的区别就是errorHtml通过produces = {“text/html”}判断请求头的accpet属性中是否包含text/html,如果包含,便走该方法。
三、自定义html异常页面
我们可以通过在src/main/resources/resources/error路径下定义友好的异常页面,比如定义一个500.html页面:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>500</title></head><body>系统内部异常</body></html>使用浏览器再次访问http://localhost:8080/calculate/1/0:
同样的,我们也可以定义404.html等常见的HTTP状态码对应的异常页面。
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>404</title></head><body>链接走丢了</body></html>使用浏览器访问http://localhost:8080/calculater/1/0:
通过自定义html异常页面并不会影响客户端发送请求异常返回的结果。
四、自定义异常处理
除了可以通过自定义html异常页面来改变浏览器访问接口时产生的异常信息,我们也可以自定义异常处理来改表默认的客户端访问接口产生的异常信息。
我们手动定义一个CalculateErrorException,继承RuntimeException。
publicclassCalculateErrorExceptionextendsRuntimeException{privatestaticfinallongserialVersionUID=-1L;privateStringid;publicCalculateErrorException(Stringid){super("calculate is error");this.id=id;}publicStringgetId(){returnid;}publicvoidsetId(Stringid){this.id=id;}}然后定义一个Controller异常处理类ControllerExceptionHandler:
@ControllerAdvicepublicclassControllerExceptionHandler{@ExceptionHandler(CalculateErrorException.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)publicMap<String,Object>handleUserNotExistsException(CalculateErrorExceptione){Map<String,Object>map=newHashMap<>();map.put("id",e.getId());map.put("message",e.getMessage());returnmap;}}其中注解@ExceptionHandler指定了要处理的异常类型,注解@ResponseStatus指定异常处理方法返回的HTTP状态码为HttpStatus.INTERNAL_SERVER_ERROR,即500。HttpStatus是一个spring自带的枚举类型,封装了常见的HTTP状态码及描述:
publicenumHttpStatus{CONTINUE(100,"Continue"),SWITCHING_PROTOCOLS(101,"Switching Protocols"),PROCESSING(102,"Processing"),CHECKPOINT(103,"Checkpoint"),OK(200,"OK"),CREATED(201,"Created"),ACCEPTED(202,"Accepted"),NON_AUTHORITATIVE_INFORMATION(203,"Non-Authoritative Information"),NO_CONTENT(204,"No Content"),RESET_CONTENT(205,"Reset Content"),PARTIAL_CONTENT(206,"Partial Content"),MULTI_STATUS(207,"Multi-Status"),ALREADY_REPORTED(208,"Already Reported"),IM_USED(226,"IM Used"),MULTIPLE_CHOICES(300,"Multiple Choices"),MOVED_PERMANENTLY(301,"Moved Permanently"),FOUND(302,"Found"),/** @deprecated */@DeprecatedMOVED_TEMPORARILY(302,"Moved Temporarily"),SEE_OTHER(303,"See Other"),NOT_MODIFIED(304,"Not Modified"),/** @deprecated */@DeprecatedUSE_PROXY(305,"Use Proxy"),TEMPORARY_REDIRECT(307,"Temporary Redirect"),PERMANENT_REDIRECT(308,"Permanent Redirect"),BAD_REQUEST(400,"Bad Request"),UNAUTHORIZED(401,"Unauthorized"),PAYMENT_REQUIRED(402,"Payment Required"),FORBIDDEN(403,"Forbidden"),NOT_FOUND(404,"Not Found"),METHOD_NOT_ALLOWED(405,"Method Not Allowed"),NOT_ACCEPTABLE(406,"Not Acceptable"),PROXY_AUTHENTICATION_REQUIRED(407,"Proxy Authentication Required"),REQUEST_TIMEOUT(408,"Request Timeout"),CONFLICT(409,"Conflict"),GONE(410,"Gone"),LENGTH_REQUIRED(411,"Length Required"),PRECONDITION_FAILED(412,"Precondition Failed"),PAYLOAD_TOO_LARGE(413,"Payload Too Large"),/** @deprecated */@DeprecatedREQUEST_ENTITY_TOO_LARGE(413,"Request Entity Too Large"),URI_TOO_LONG(414,"URI Too Long"),/** @deprecated */@DeprecatedREQUEST_URI_TOO_LONG(414,"Request-URI Too Long"),UNSUPPORTED_MEDIA_TYPE(415,"Unsupported Media Type"),REQUESTED_RANGE_NOT_SATISFIABLE(416,"Requested range not satisfiable"),EXPECTATION_FAILED(417,"Expectation Failed"),I_AM_A_TEAPOT(418,"I'm a teapot"),/** @deprecated */@DeprecatedINSUFFICIENT_SPACE_ON_RESOURCE(419,"Insufficient Space On Resource"),/** @deprecated */@DeprecatedMETHOD_FAILURE(420,"Method Failure"),/** @deprecated */@DeprecatedDESTINATION_LOCKED(421,"Destination Locked"),UNPROCESSABLE_ENTITY(422,"Unprocessable Entity"),LOCKED(423,"Locked"),FAILED_DEPENDENCY(424,"Failed Dependency"),UPGRADE_REQUIRED(426,"Upgrade Required"),PRECONDITION_REQUIRED(428,"Precondition Required"),TOO_MANY_REQUESTS(429,"Too Many Requests"),REQUEST_HEADER_FIELDS_TOO_LARGE(431,"Request Header Fields Too Large"),UNAVAILABLE_FOR_LEGAL_REASONS(451,"Unavailable For Legal Reasons"),INTERNAL_SERVER_ERROR(500,"Internal Server Error"),NOT_IMPLEMENTED(501,"Not Implemented"),BAD_GATEWAY(502,"Bad Gateway"),SERVICE_UNAVAILABLE(503,"Service Unavailable"),GATEWAY_TIMEOUT(504,"Gateway Timeout"),HTTP_VERSION_NOT_SUPPORTED(505,"HTTP Version not supported"),VARIANT_ALSO_NEGOTIATES(506,"Variant Also Negotiates"),INSUFFICIENT_STORAGE(507,"Insufficient Storage"),LOOP_DETECTED(508,"Loop Detected"),BANDWIDTH_LIMIT_EXCEEDED(509,"Bandwidth Limit Exceeded"),NOT_EXTENDED(510,"Not Extended"),NETWORK_AUTHENTICATION_REQUIRED(511,"Network Authentication Required");...}编写完自定义异常处理逻辑后,我们将CalculateController中的方法抛出的异常改为CalculateErrorException:
@GetMapping("/{divisor:\\d+}/{dividend:\\d+}")publicvoidcalculate(@PathVariableLongdivisor,@PathVariableLongdividend){thrownewCalculateErrorException("500");}重启项目,再次访问http://localhost:8080/calculater/1/0,响应如下: