目录
二、使用@ControllerAdvice来统一处理全局异常
在使用Spring开发WEB应用时,一般需要定义全局异常处理器,对应用中抛出的异常统一进行处理。但此时面临的一个问题就是,生成的异常信息需要是客户端能够处理的格式:比如:
- 客户端只能处理json,那异常信息就应该是json格式。
- 客户端需要一个错误页面,那异常信息就应该生成在一个错误页面上,再返回给客户端。
一、如何知道客户端可以处理什么格式的数据
HTTP中定义了Accept请求头,客户端就是通过传递Accept的值,来告诉Web服务器它可以处理什么格式的数据的。
下面准备一个测试页面,此处使用freemarker作为模板引擎,测试页面为login.ftl
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="${request.contextPath}/static/bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body style="background: #d2d6de; text-align: center;">
<div class="container" style="max-width: 450px; margin: 5% auto;">
<div class="row">
<div class="col-md-12">
<h1 style="margin: 20px;">登录测试系统</h1>
</div>
</div>
<form id="submitForm" action="${request.contextPath}/sys/login" method="post" style="background: #fff; padding: 20px;">
<div class="row">
<div class="col-md-12">
<p>登录您的账号</p>
<div class="form-group has-feedback">
<input type="text" name="username" class="form-control" placeholder="账户"> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" name="password" class="form-control" placeholder="密码"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-5 col-md-5">
<button id="submitBtn" type="button" class="btn btn-primary btn-block btn-flat">登录</button>
</div>
</div>
</form>
</div>
<script src="${request.contextPath}/static/jquery/3.4.1/jquery-3.4.1.min.js"></script>
<script src="${request.contextPath}/static/bootstrap/3.4.1/js/bootstrap.min.js"
integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
</body>
<script type="text/javascript">
$("#submitBtn").click(function(){
$.ajax({
url: "${request.contextPath}/sys/login",
type: "POST",
dataType: "json",
data: $("#submitForm").serialize(),
processData: false,
success: function (data) {
alert(JSON.stringify(data));
// alert((new XMLSerializer()).serializeToString(data));
// alert(data);
},
error: function () {
alert("error");
}
});
});
</script>
</html>
可以通过修改dataType的值,告诉WEB服务器应该返回什么格式的数据,常用值:json、text、html、xml、script等。以下是使用chrome发起请求时ACCEPT的值:
Type | Accept |
xml | application/xml, text/xml, */*; q=0.01 |
json | application/json, text/javascript, */*; q=0.01 |
text | text/plain, */*; q=0.01 |
html | text/html, */*; q=0.01 |
script | text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01 |
以上页面是通过JQuery做异常提交,也可以改成form直接提交:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="${request.contextPath}/static/bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body style="background: #d2d6de; text-align: center;">
<div class="container" style="max-width: 450px; margin: 5% auto;">
<div class="row">
<div class="col-md-12">
<h1 style="margin: 20px;">登录测试系统</h1>
</div>
</div>
<form id="submitForm" action="${request.contextPath}/sys/login" method="post" style="background: #fff; padding: 20px;">
<div class="row">
<div class="col-md-12">
<p>登录您的账号</p>
<div class="form-group has-feedback">
<input type="text" name="username" class="form-control" placeholder="账户"> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" name="password" class="form-control" placeholder="密码"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-5 col-md-5">
<button id="submitBtn" type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
</div>
</div>
</form>
</div>
<script src="${request.contextPath}/static/jquery/3.4.1/jquery-3.4.1.min.js"></script>
<script src="${request.contextPath}/static/bootstrap/3.4.1/js/bootstrap.min.js"
integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
</body>
</html>
使用chrome发起form提交时ACCEPT的值:
Type | Accept |
form | text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 |
如何针对不同的ACCEPT值做不同的处理,下面举例说明如何解决这个问题。
二、使用@ControllerAdvice来统一处理全局异常
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class GlobalControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler
public String errorController(HttpServletRequest request, Throwable ex) {
request.setAttribute("exception", ex);
return "forward:/error";
}
}
@ExceptionHandler默认会处理所有Throwable异常,在方法errorController中,将捕获的异常存放到HttpServletRequest中,然后使用forward:/error将请求转到可以处理/error的Controller中,forward表示将HttpServletRequest中的所有信息一起带入可以处理/error的Controller中。
三、定义处理/error的Controller
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@RequestMapping("/")
@Controller
public class BaseController {
private static final Logger LOG = LoggerFactory.getLogger(BaseController.class);
@GetMapping("index.html")
public String doIndex() {
return "login";
}
@RequestMapping(value = "error", produces = { "text/html", "text/plain" })
public String handleErrorPage(@RequestAttribute("exception") Throwable ex) {
LOG.error("handlePage", ex);
return "index";
}
@RequestMapping(value = "error", produces = { "application/json" })
@ResponseBody
public Map<String, Object> handleErrorJson(@RequestAttribute("exception") Throwable ex) {
LOG.error("handleJson", ex);
Map<String, Object> retMap = new HashMap<>();
retMap.put("code", 200);
retMap.put("msg", ex.getMessage());
return retMap;
}
@RequestMapping(value = "error", produces = { "application/xml", "text/xml" })
public HttpEntity<Map<String, Object>> handleErrorXml(@RequestAttribute("exception") Throwable ex) {
LOG.error("handleErrorXml", ex);
Map<String, Object> retMap = new HashMap<>();
retMap.put("code", 200);
retMap.put("msg", ex.getMessage());
return ResponseEntity.status(HttpStatus.OK).body(retMap);
}
}
其中,@RequestMapping中的produces表示客户端请求报文头中Accept的值,只有满足条件的才能由对应的异常处理方法处理。@RequestAttribute("exception")表示从HttpServletRequest中取出异常。
- handleErrorPage可以针对text、html两种类型的请求,通过index.ftl,生成返回页面。
- handleErrorJson可以针对json类型的请求,生成json返回
- handleErrorXml可以针对xml类型的请求,生成xml返回
以上代码即可以针对form提交进行处理,也可以针对JQuery发起的异步请求做处理。如果需要生成xml或其它类型的数据,则可以添加相应的处理方法。
页面index.ftl的内容如下:
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="${request.contextPath}/bootstrap/4.3.1/css/bootstrap.min.css">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<script src="${request.contextPath}/jquery/3.4.1/jquery-3.4.1.min.js"></script>
<script src="${request.contextPath}/bootstrap/4.3.1/js/bootstrap.bundle.min.js"></script>
<script src="${request.contextPath}/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>
App.java内容如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
@EnableAutoConfiguration(exclude = { ErrorMvcAutoConfiguration.class })
public class App {
private static final Logger LOG = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);
ctx.registerShutdownHook();
LOG.info("=========app started!=========");
}
}
参考文档
Spring Web MVC - 1.1.7. Exceptions
Spring Web MVC - 1.3.6. Exceptions
Spring Web MVC - 1.3.7. Controller Advice
Spring Boot Reference Guide - 29.2.5 Error Handling
jQuery ajax - ajax() 方法
Spring MVC - @ExceptionHandler based on Accept header