1. Spring MVC 统一异常处理
这里使用实现 HandlerExceptionResolver 接口的方式来处理非 rest 服务统一异常。
1.1 继承 HandlerExceptionResolver 接口编写异常处理类
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
/** 非 rest 应用,需要返回视图 */
public class MyExceptionHandler implements HandlerExceptionResolver {
private static final Logger logger = LogManager.getLogger(MyExceptionHandler.class);
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
Map<String, Object> model = new HashMap<String, Object>();
model.put("ex", ex);
// 将异常信息写入日志文件
logger.error("internal error", ex);
// 根据不同的错误转向不同的页面
ModelAndView modelAndView = null;
if(ex instanceof BusinessException) {
modelAndView = new ModelAndView("error-business", model);
} else if(ex instanceof ParameterException) {
modelAndView = new ModelAndView("error-parameter", model);
} else {
modelAndView = new ModelAndView("error", model);
}
return modelAndView;
}
}
这里根据不同类型的异常,转到不同的异常视图(传入异常相关信息,可以在视图上显示这些信息)。
1.2 添加spring配置
<bean id="exceptionHandler" class="com.johnfnash.study.exception.MyExceptionHandler"/>
<!-- 配置视图解析器 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsps/"/>
<property name="suffix" value=".jsp" />
</bean>
1.3 视图中显示错误信息
1.2 中传入了异常相关信息,这里我们就可以将异常信息显示到页面上了。
<%@ page contentType="text/html; charset=UTF-8"%>
<html>
<head><title>Exception!</title></head>
<body>
<% Exception e = (Exception)request.getAttribute("ex"); %>
<H2>业务错误: <%= e.getClass().getSimpleName()%></H2>
<hr />
<P>错误描述:</P>
<%= e.getMessage()%>
<P>错误信息:</P>
<% e.printStackTrace(new java.io.PrintWriter(out)); %>
</body>
</html>
1.4 示例
上面已经完成了 spring 统一异常配置,下面就可以在发生异常时,进行处理了。
@RequestMapping(value = "/controller.do", method = RequestMethod.GET)
public void controller(HttpServletResponse response, @QueryParam("id") Integer id) throws Exception {
switch (id) {
case 1:
throw new BusinessException("10", "controller10");
case 2:
throw new BusinessException("20", "controller20");
case 3:
throw new BusinessException("30", "controller30");
case 4:
throw new BusinessException("40", "controller40");
case 5:
throw new BusinessException("50", "controller50");
case 6:
throw new Exception("Internal error");
default:
throw new ParameterException("Controller Parameter Error");
}
}
页面上访问 http://localhost:8080/spring-global-exception/controller.do?id=3 , 就会触发 BusinessException 异常。上面配置的 HandlerExceptionResolver 解析到异常,转入异常页面。大致如下:
2. Rest 服务统一异常处理
2.1 定义 Rest 异常处理器
Rest 服务统一异常处理和 Spring MVC 的稍有不同,前者不用返回视图,后者需要。这里使用 ResponseEntityExceptionHandler 来处理 Resr 服务统一异常。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.johnfnash.study.model.Result;
/** Rest 异常处理器,不需要返回视图。该类会处理所有在执行标有@RequestMapping注解的方法时发生的异常 */
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LogManager.getLogger(RestExceptionHandler.class.getName());
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
HttpStatus status, WebRequest request) {
log.error("Internal error", ex);
//return super.handleExceptionInternal(ex, body, headers, status, request);
return new ResponseEntity<Object>(Result.getFailResult("Error,please contact administrator!"), HttpStatus.INTERNAL_SERVER_ERROR);
}
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
return super.handleMissingServletRequestParameter(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
return super.handleTypeMismatch(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
log.error("Method not allowed", ex);
//return super.handleHttpRequestMethodNotSupported(ex, headers, status, request);
return new ResponseEntity<Object>(Result.getFailResult("Request method not allowed!"), HttpStatus.METHOD_NOT_ALLOWED);
}
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ResponseEntity<Object> handleHttpBusinessException(BusinessException ex) {
log.error("Business Exception", ex);
return new ResponseEntity<Object>(Result.getFailResult(ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(ParameterException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ResponseEntity<Object> handleHttpParameterException(ParameterException ex) {
log.error("Parameter Exception", ex);
return new ResponseEntity<Object>(Result.getFailResult(ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ResponseEntity<Object> handleException(Exception ex) {
log.error("Exception", ex);
return new ResponseEntity<Object>(Result.getFailResult("Exception!"), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
在 spring 配置文件中添加 Handler 的定义
<bean id="restExHandler" class="com.johnfnash.study.exception.RestExceptionHandler"/>
注意:
ResponseEntityExceptionHandler 类中实现了一些常见的异常的处理,如 请求方法不对、确实 path param 等,可以根据实际情况进行重写
这里新增了几个用于处理自定义异常的方法 handleHttpBusinessException、handleHttpParameterException、handleException。这里使用了 spring 中的 @ExceptionHandler 注解用于指定当前方法要处理的异常类型,@ResponseStatus 用于设置 response status code。这几个方法返回的类型为 spring 中的 ResponseEntity,我们可以很方便地使用这个来返回接口的响应
2.2 RestExceptionHandler 使用
为了更好地说明 RestExceptionHandler 的用法,这里添加一个实体类,以及引入 spring validation。
2.2.1 添加 validation maven 依赖
<!-- validation -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.1.GA</version>
</dependency>
2.2.2 添加实体类 User
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class User {
@NotNull(message = "{username.null}")
@Size(min = 5, max = 16, message = "{username.size}")
private String username;
@NotNull(message = "{password.null}")
@Size(min = 5, max = 25, message = "{password.size}")
private String password;
@Size(min = 2, max = 30, message = "{firstName.size}")
private String firstName;
@Size(min = 2, max = 30, message = "{lastName.size}")
private String lastName;
@NotBlank
@Email(message = "{email.valid}")
private String email;
// getter, setter
}
注意:
- @NotNull 限制必须不为null,当传入的参数不包含该字段时,会触发校验规则;而如果设置为 @NotBlank,则当传入的参数不包含该字段时,不会触发校验规则
2.2.2 ValidationMessages.properties 添加错误信息配置
src/main/resources 下添加 ValidationMessages.properties 文件配置校验不通过时的提示信息
firstName.size=First name must be between {min} and {max} characters long.
lastName.size=Last name must be between {min} and {max} characters long.
username.null=Username is required.
username.size=Username must be between {min} and {max} characters long.
password.size=Password must be between {min} and {max} characters long.
password.null=Password is required.
email.valid=The email address must be valid.
注意:
- @NotNull 限制必须不为null,当传入的参数不包含该字段时,会触发校验规则;而如果设置为 @NotBlank,则当传入的参数不包含该字段时,不会触发校验规则
- 文件中配置的 key 对应实体类注解中 message 的设置,value 则对应具体的值
2.2.3 Controller 中添加 validation 校验
import java.util.List;
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.johnfnash.study.exception.ParameterException;
import com.johnfnash.study.model.Result;
import com.johnfnash.study.model.User;
@RestController
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Result addUser(@Valid @RequestBody User user, BindingResult errors) {
if(errors.hasFieldErrors()) {
StringBuilder sb = new StringBuilder();
List<ObjectError> errorList = errors.getAllErrors();
for (ObjectError err : errorList) {
sb.append(err.getDefaultMessage());
}
// 传入参数校验不通过时,抛出 ParameterException 异常。RestExceptionHandler 会捕获该异常
throw new ParameterException(sb.toString());
}
return Result.getSuccessResult(null);
}
}
注意:这里在 user 参数前加了 @Valid 注解,用于校验传入的参数;user 参数后面 必须紧跟着一个 BindingResult 类型的参数,校验结果放在了这个变量中
传入如下参数调用该接口
{"firstName":"123456"}
响应如下:
{
"success": false,
"result": "Username is required.Password is required."
}
如果不想每次都在controller方法里写if(result.hasErrors())怎么办呢,写个切面,把if写在切面里,如果有errors直接就返回,不再执行controller方法。详见 2.2.4。
2.2.4 使用 @Aspect 定义切面将校验错误处理逻辑分离出来
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import javax.validation.Valid;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
//import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import com.johnfnash.study.exception.ParameterException;
import com.johnfnash.study.model.Result;
@Aspect
//@Component
public class ValidAspect {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object processErrors(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
if(!Result.class.equals(method.getReturnType())) {
return pjp.proceed();
}
Object[] args = pjp.getArgs();
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < annotations.length; i++) {
if(!hasValidAnnotation(annotations[i])){
continue;
}
if(! ( i<annotations.length-1 && args[i+1] instanceof BindingResult ) ) {
//验证对象后面没有跟bindingResult,事实上如果没有应该到不了这一步
continue;
}
BindingResult bindingResult = (BindingResult) args[i+1];
if(bindingResult.hasErrors()) {
throw new ParameterException(parseError(bindingResult));
//return Result.getFailResult(parseError(bindingResult));
}
}
return pjp.proceed();
}
private boolean hasValidAnnotation(Annotation[] annotations) {
if(annotations == null){
return false;
}
for(Annotation annotation : annotations){
if(annotation instanceof Valid){
return true;
}
}
return false;
}
private String parseError(BindingResult result) {
StringBuilder sBuilder = null;
if(null != result && result.hasErrors()) {
sBuilder = new StringBuilder();
for (ObjectError error : result.getAllErrors()) {
sBuilder.append(error.getDefaultMessage()).append('\n');
}
}
return sBuilder==null ? null : sBuilder.toString();
}
}
注意:springmvc 中加入如下配置:
<!-- 参数 error 处理切面 -->
<bean id="errorAspect" class="com.johnfnash.study.validation.ValidAspect" />
<context:annotation-config />
<!-- 启用aop -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
配置切面之后,原来的 addUser 方法就可以改成下面这样了
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Result addUser(@Valid @RequestBody User user, BindingResult errors) {
return Result.getSuccessResult(null);
}
本文参考:
- 使用Spring MVC统一异常处理实战 http://cgs1999.iteye.com/blog/1547197#comments
- 使用 @RestController,@ExceptionHandler 和 @Valid,把检验和异常处理从主要业务逻辑里面抽离出来 http://blog.csdn.net/sanjay_f/article/details/47441631
- springboot 使用校验框架validation校验http://blog.csdn.net/u012373815/article/details/72049796
- springmvc使用JSR-303进行校验http://blog.csdn.net/tsingheng/article/details/42555307