您的位置:首页 > 编程语言 > Java开发

【原创】遨游springmvc之HandlerExceptionResolver

2016-08-16 14:50 579 查看

1.前言

在我们的程序中,很多时候会碰到对异常的处理,我们也许会定义一些自己特殊业务的异常,在发生错误的时候会抛出异常,在springmvc的实际应用中,我们经常需要返回异常的信息以及错误代码,并且对异常进行一些处理然后返回再返回视图。这就要涉及到我们这一篇主要讲的HandlerExceptionResolver

2.原理

其实springmvc已经默认给我们注入了3个异常处理的解器:

AnnotationMethodHandlerExceptionResolver(针对@ExceptionHandler,3.2已废除,转而使用ExceptionHandlerExceptionResolver)
ResponseStatusExceptionResolver(针对加了@ResponseStatus的exception)
DefaultHandlerExceptionResolver(默认异常处理器)

2.1 依赖

2.1.1 解析器依赖



图小可以放大!😳

2.1.2 springmvc内部处理的一些标准异常



2.2 接口说明

public interface HandlerExceptionResolver {

/**
* Try to resolve the given exception that got thrown during handler execution,
* returning a {@link ModelAndView} that represents a specific error page if appropriate.
* <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
* to indicate that the exception has been resolved successfully but that no view
* should be rendered, for instance by setting a status code.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or {@code null} if none chosen at the
* time of the exception (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding {@code ModelAndView} to forward to, or {@code null}
* for default processing
*/
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

}

HandlerExceptionResolver只有一个核心方法,就是resolveException,方法体中包含处理的方法,异常已经请求和响应参数。

在我们自己去实现自定义异常解析器的时候,我们一般是去继承AbstractHandlerExceptionResolver

AbstractHandlerExceptionResolver实现了HandlerExceptionResolver和Ordered

那么针对异常的处理具体是在哪里执行的呢?

答案是springmvc核心类DispatcherServlet

在DispatcherServlet的doDispatch()方法最后会执行

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

它将异常给统一处理了!

我们先来看下DispatcherServlet类中的两个方法:

源码2.2.1
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {

// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
exMv.setViewName(getDefaultViewName(request));
}
if (logger.isDebugEnabled()) {
logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}

throw ex;
}

在以上源码可知:

1)异常处理器只有当返回的ModelAndView不是空的时候才会返回最终的异常视图,当异常处理返回的ModelAndView如果是空,那么它将继续去下一个异常解析器。

2)异常解析器是有执行顺序的,我们在合适的场景可以定义自己的order来绝对哪个异常解析器先执行,order越小,越先执行

源码2.2.2
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

boolean errorView = false;

if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}

if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

当异常返回的视图ModelAndView不是空的时候,DispatcherServlet最终会重定向到执行View。

3.实例

我们接下来要实现2种自定义异常处理器

实现rest下的异常处理返回json信息,附加validate验证

自定义页面异常

通过ControllerAdvice

先上一个rest的response的一个标准实体

/**
* <p class="detail">
* 功能:REST接口标准容器
* </p>
* @param <T> the type parameter
*
* @author Kings
* @ClassName Rest response.
* @Version V1.0.
* @date 2016.08.16 09:28:55
*/
@Setter
@Getter
public class RestResponse<T> {
/**
* The constant VOID_REST_RESPONSE.
*/
public static final RestResponse<Void> VOID_REST_RESPONSE = new RestResponse<>(null);

@ApiModelProperty(value = "状态码", required = true)
private int code;

@ApiModelProperty(value = "服务端消息", required = true)
private String message;

@ApiModelProperty (value = "数据")
private T data = null;

/**
* Instantiates a new Rest response.
* @param code    the code
* @param message the message
* @param data    the data
*/
public RestResponse(int code, String message, T data) {
this.code = code;
this.message = message;
if (data != null && "class com.github.pagehelper.PageInfo".equals(data.getClass().toString())) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("pageInfo", data);
this.data = (T) map;
} else {
this.data = data;
}

}

/**
* Instantiates a new Rest response.
* @param status the status
*/
public RestResponse(HttpStatus status, T data) {
this(status.value(), status.getReasonPhrase(), data);
}

/**
* Instantiates a new Rest response.
* @param data the data
*/
public RestResponse(T data) {
this(HttpStatus.OK.value(), "OK", data);
}

@Override
public String toString() {
return "{\"code\":" + code + ",\"message\":\"" + message + "\",\"data\":" + data + "}";
}
}


3.1 Rest异常解析器

先上springmvc validate切面实现错误信息绑定,validate是通过切面来实现,省去控制器层一大堆对BindingResult处理代码。

3.1.1 ErrorMessage

public class ErrorMessage {

/** 字段名 */
private String fieldName;
/** 错误提示. */
private String message;

/**
* Instantiates a new Error message.
* @param fieldName the field name
* @param message   the message
*/
public ErrorMessage(String fieldName, String message) {
this.fieldName = fieldName;
this.message = message;
}

/**
* Gets field name.
* @return the field name
*/
public String getFieldName() {
return fieldName;
}

/**
* Gets message.
* @return the message
*/
public String getMessage() {
return message;
}

@Override
public String toString() {
return "{\"fieldName\":\""+fieldName+"\",\"message\":\""+message+"\"}";
}
}

validate错误信息实体

3.1.2 @ValidMethod

/**
* <p class="detail">
* 功能:验证注解,aop扫描次注解进行错误信息的绑定输出
* </p>
* @author Kings
* @ClassName Validate method.
* @Version V1.0.
* @date 2016.08.03 17:51:01
*/
@Retention (RetentionPolicy.RUNTIME)
@Target (ElementType.METHOD)
public @interface ValidateMethod {

}


3.1.3 ValidateException

/**
* <p class="detail">
* 功能:验证异常
* </p>
* @author Kings
* @ClassName Validate exception.
* @Version V1.0.
* @date 2016.08.09 14:45:58
*/
@ResponseStatus (value = HttpStatus.BAD_REQUEST, code = HttpStatus.BAD_REQUEST)
public class ValidateException extends RuntimeException {

/**
* Instantiates a new Validate exception.
* @param message the message
*/
public ValidateException(String message) {
super(message);
}
}

状态码定义是400

3.1.4 ErrorHelper

public class ErrorHelper {
private static Logger logger = LoggerFactory.getLogger(ErrorHelper.class);

public RestResponse converBindError2AjaxError(BindingResult result, boolean validAllPropeerty) {

try {
RestResponse res = new RestResponse(HttpStatus.BAD_REQUEST,"validate error!");

List<ErrorMessage> errorMesages = new ArrayList<>();
List<ObjectError> objectErrors = result.getAllErrors();
for (ObjectError objError : objectErrors) {
if (objError instanceof FieldError) {
FieldError objectError = (FieldError) objError;
errorMesages.add(new ErrorMessage(objectError.getField(), objError.getDefaultMessage()));
} else {
errorMesages.add(new ErrorMessage(objError.getCode(), objError.getDefaultMessage()));
}
if(!validAllPropeerty){
//noinspection BreakStatement
break;//just one error object
}
}
res.setData(errorMesages);
return res;
} catch (Exception e) {
logger.error("com.gttown.common.support.web.validate.ErrorHelper error",e);
}
return null;
}
}


3.1.5 ValidHandlerAspect

/**
* <p class="detail">
* 功能:验证切面
* </p>
* @author Kings
* @ClassName Validate handel aspect.
* @Version V1.0.
* @date 2016.08.03 17:51:42
*/
@Aspect
public class ValidateHandelAspect {
/**judge is all property error need to be export*/
private boolean outputAllPropError = false;

/**
* <p class="detail">
* 功能:验证输出结果
* </p>
* @param pjp :
*
* @return object
* @throws Throwable the throwable
* @author Kings
* @date 2016.08.03 17:51:42
*/
@Around ("validatePointcut()")
public Object validateAround(ProceedingJoinPoint pjp) throws Throwable  {
Object[] args =  pjp.getArgs();
BindingResult bindingResult = null;
if (args != null) {
for (Object obj : args) {
if (obj instanceof BindingResult) {
bindingResult = (BindingResult) obj;
//noinspection BreakStatement
break;
}
}
}

if ( bindingResult != null && bindingResult.hasErrors() ){//异常输出
ErrorHelper errorHelper = new ErrorHelper();
throw new ValidateException(errorHelper.converBindError2AjaxError(bindingResult,outputAllPropError).toString());
//return errorHelper.converBindError2AjaxError(bindingResult,outputAllPropError);
} else {//正常输出
return pjp.proceed(args);
}
}

/**
* <p class="detail">
* 功能:切点
* </p>
* @author Kings
* @date 2016.08.03 17:51:42
*/
@Pointcut ("@annotation(com.kings.common.validate.ValidateMethod)")
public void validatePointcut() {

}

public void setOutputAllPropError(boolean outputAllPropError) {
this.outputAllPropError = outputAllPropError;
}
}

关于validate的就涉及到以上几个类

下面上异常处理器

3.1.6 ResponseStatusAndBodyExceptionResolver

/**
* <p class="detail">
* 功能:针对ResponseStatus和ResponseBody的异常处理器,请在配置文件中将order设置为-1覆盖ResponseStatusExceptionResolver
* </p>
* @author Kings
* @ClassName Response status and body exception resolver.
* @Version V1.0.
* @date 2016.08.09 15:23:54
*/
public class ResponseStatusAndBodyExceptionResolver extends AbstractHandlerExceptionResolver {

/** Argument error. */
private boolean argumentError = false;

@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
try {
return resolveResponseStatus(responseStatus, request, response, handler, ex);
} catch (Exception resolveEx) {
logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);
}
} else if (ex.getCause() instanceof Exception) {
if (judgeInstance(ex)) {
argumentError = true;
}
ex = (Exception) ex.getCause();
return doResolveException(request, response, handler, ex);
}

//just Intercept the method @ResponseBody and @RestController or else skip
ResponseBody rexist = ((HandlerMethod) handler).getMethod().getAnnotation(ResponseBody.class);
RestController rcexist = ((HandlerMethod) handler).getBeanType().getAnnotation(RestController.class);

if (rexist != null || rcexist != null) {
try {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;//默认500
if (argumentError) {//参数错误400
status = HttpStatus.BAD_REQUEST;
}
response.setStatus(status.value());

Object data;
if (ex instanceof ValidateException) {//validateExcepption已经包含了错误的信息
data = JSONObject.fromObject(ex.getMessage());
} else {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("error", ex.toString());
data = errorMap;// for json
}
RestResponse res = new RestResponse(status, data);

Map<String, Object> map = new HashMap<>();//put error message
map.put("error", res);
return new ModelAndView("errorJsonView", map);
} catch (Exception e) {
logger.warn("error", e);
} finally {
argumentError = false;//release
}
}
return null;
}

/**
* <p class="detail">
* 功能:
* </p>
* @param responseStatus :ResponseStatus
* @param request        :请求
* @param response       :响应
* @param handler        :methodHandler
* @param ex             :异常
*
* @return model and view
* @throws Exception the exception
* @author Kings
* @date 2016.08.09 15:23:54
*/
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
int statusCode = responseStatus.code().value();

response.setStatus(statusCode);
Map<String, Object> map = new HashMap<>();
Object data;
if (ex instanceof ValidateException) {
data = JSONObject.fromObject(ex.getMessage());
} else {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("error", ex.toString());
data = errorMap;// for json
}
map.put("error", data);
return new ModelAndView("errorJsonView", map);//返回jsonView
}

private boolean judgeInstance(Exception ex) {
return ex instanceof PropertyAccessException || ex instanceof ServletRequestBindingException;
}

}

springmvc默认使用了ResponseStatusExceptionResolver来处理异常带有@ResponseStatus的异常类,并且返回对应code的视图。而rest在发生错误的时候,友好的形式是返回一个json视图,并且说明错误的信息,这样更加有利于在碰到异常的情况下进行错误的定位,提高解决bug的效率。

我们采用ResponseStatusAndBodyExceptionResolver,是对ResponseStatusExceptionResolver做了进一步处理,并作用在ResponseStatusExceptionResolver之前。ResponseStatusAndBodyExceptionResolver是针对加了@ResponseBody或者控制器加了@RestController的处理程序遇到异常的异常解析器,获得异常结果并且返回json(RestResponse)视图

ResponseStatusExceptionResolver需要我们在配置文件中加入配置

请看3.1.8中的配置

3.1.7 ErrorJsonView

/**
* <p class="detail">
* 功能:JsonView for error
* </p>
* @author Kings
* @ClassName Error json view.
* @Version V1.0.
* @date 2016.08.09 15:17:39
*/
public class ErrorJsonView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("text/json; charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
Gson jb = new Gson();
out.write(jb.toJson(model.get("error")));
out.flush();
} catch (IOException e) {
logger.error("com.gttown.common.support.web.view.ErrorJsonView", e);
}
}

}


3.1.8 配置

<mvc:annotation-driven validator="validator"/>

<!--验证bean-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties -->
<property name="validationMessageSource" ref="messageSource"/>
</bean>

<!-- 国际化的消息资源文件(本系统中主要用于显示/错误消息定制) -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<!-- 在web环境中一定要定位到classpath 否则默认到当前web应用下找  -->
<value>classpath:error</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="60"/>
</bean>

<!--validate 切面-->
<aop:aspectj-autoproxy />
<bean class="com.kings.common.validate.ValidateHandelAspect">
<!--outputAllPropError默认是false,将只输出一个错误字段的信息,如果需要全部字段异常错误信息,那么outputAllPropError设置为true-->
<property name="outputAllPropError" value="true"/>
</bean>

<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="-1" /><!--这边的order必须要大于我们jsp等视图模板的order-->
</bean>
<!--错误JsonView-->
<bean id="errorJsonView" class="com.kings.template.mvc.view.ErrorJsonView"/>

<!--responseStatus和responseBody异常处理器-->
<bean id="responseStatusAndBodyExceptionResolver" class="com.kings.template.mvc.ResponseStatusAndBodyExceptionResolver">
<property name="order" value="-1"/><!--负1用来覆盖springmvc自带的ResponseStatusExceptionResolver-->
</bean>


3.1.9 控制器

@ValidateMethod
@RequestMapping (value = "/errorhandler/2", method = RequestMethod.POST)
public Person demo1(@Valid Person p, BindingResult bindingResult) {//BindingResult必须得写,而且是紧跟在验证实体之后,验证的不多说了,就是得在方法体上加注解@ValidateMethod
return p;
}

@RequestMapping (value = "/errorhandler/{id}", method = RequestMethod.GET)
public String demo1(@PathVariable Long id) {
return id.toString();
}


3.1.10 效果

1.验证



2.普通400



3.2 自定义页面异常解析器

3.2.1 CustomerSimpleMappingExceptionResolver

/**
* <p class="detail">
* 功能:自定义异常处理类
* </p>
* @author Kings
* @ClassName Custom simple mapping exception resolver.
* @Version V1.0.
* @date 2016.07.18 14:40:16
*/
public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {
/** Logger. */
private Logger logger = Logger.getLogger(CustomSimpleMappingExceptionResolver.class);

@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
super.doResolveException(request, response, handler, ex);
logger.error(ex.getMessage(), ex);
String viewName = determineViewName(ex, request);
if (viewName != null) {// JSP格式返回
if (! (request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")))) {
// 如果不是异步请求
// Apply HTTP status code for error views, if specified.
// Only apply it if we're processing a top-level request.
Integer statusCode = determineStatusCode(request, viewName);
if (statusCode != null) {
applyStatusCodeIfPossible(request, response, statusCode);
}

return getModelAndView(viewName, ex, request);
} else {
return null;
}
} else {
return null;
}
}
}


3.2.2 配置

<!-- 统一异常处理 具有集成简单、有良好的扩展性、对已有代码没有入侵性 -->
<bean id="exceptionResolver" class="com.kings.common.resolver.CustomSimpleMappingExceptionResolver">
<property name="defaultErrorView" value="/error/500"/>
<property name="exceptionAttribute" value="ex"/>
<property name="exceptionMappings">
<props>
<!-- 自定义业务异常 -->
<prop key="com.gttown.common.support.exception.BizException">/error/biz</prop>
<!-- 可再添加 -->
</props>
</property>
<!-- 默认HTTP错误状态码 -->
<property name="defaultStatusCode" value="500"/>
<!-- 将路径映射为错误码,供前端获取。 -->
<property name="statusCodes">
<props>
<prop key="/error/500">500</prop>
</props>
</property>
</bean>

statusCodes需要web.xml error-code码结合使用指向指定页面

<error-page>
<error-code>500</error-code>
<location>/WEB-INF/pages/error/500.jsp</location>
</error-page>


3.3 ControllerAdvice

3.3.1 CustomerControllerAdvice

@ControllerAdvice
public class CustomerControllerAdvice {
@ExceptionHandler (Exception.class)
@ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public RestResponse handleBadRequestException(Exception ex) {
Map<String,Object> map = new HashMap<String,Object>();
map.put("error",ex.toString());
RestResponse response = new RestResponse(map);
response.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setMessage("error");
return response;
}

}

通过ExceptionHandler指定哪些类型的错误执行具体某个返回错误方法

并且可以使用@ResponseStatus执行错误代码

注意在配置ControllerAdvice的时候,必须跟controller一样在springmvc.xml配置扫描初始化

4.总结

在springmvc中我们可以有各种类型的异常解析器来统一处理异常,方便了我们对异常的处理,通过在配置中加入异常处理的解析器,节约了控制器层的代码,并且使得前端呈现出不同的响应code。

发现一个机制的导航😳

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息