Spring 统一异常处理

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"/>

注意:

  1. ResponseEntityExceptionHandler 类中实现了一些常见的异常的处理,如 请求方法不对、确实 path param 等,可以根据实际情况进行重写

  2. 这里新增了几个用于处理自定义异常的方法 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

}

注意:

  1. @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.

注意:

  1. @NotNull 限制必须不为null,当传入的参数不包含该字段时,会触发校验规则;而如果设置为 @NotBlank,则当传入的参数不包含该字段时,不会触发校验规则
  2. 文件中配置的 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);
}

本文参考:

  1. 使用Spring MVC统一异常处理实战 http://cgs1999.iteye.com/blog/1547197#comments
  2. 使用 @RestController,@ExceptionHandler 和 @Valid,把检验和异常处理从主要业务逻辑里面抽离出来 http://blog.csdn.net/sanjay_f/article/details/47441631
  3. springboot 使用校验框架validation校验http://blog.csdn.net/u012373815/article/details/72049796
  4. springmvc使用JSR-303进行校验http://blog.csdn.net/tsingheng/article/details/42555307

猜你喜欢

转载自blog.csdn.net/xxc1605629895/article/details/79307497