Java&JAVA EE

SpringMVC控制器统一异常处理

2019-05-17  本文已影响0人  理查德成

摘要

介绍spring mvc控制器中统一处理异常的两种方式:HandlerExceptionResolver以及@ExceptionHandler;以及使用@ControllerAdvice@ExceptionHandler方法的影响扩大。

一、问题的提出

Spring MVC 项目的开发中,不管是底层的数据库操作过程,业务层的业务逻辑的处理,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常。

这些异常可以在每个单独的环节捕获,处理;但是大多数情况下,异常情况都会反馈到控制器(无论是通过抛出异常的方式,还是自定义特殊返回值,如null等的方式),然后由控制器结合具体异常情况,返回特定信息(通常是不同的返回码,错误信息)给http请求的调用方。

然而,每个环节都单独捕获处理异常,业务代码可读性不强,工作量大且不好统一,维护的工作量也很大。那么,能不能将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护?答案是肯定的。

二、统一异常处理

对于spring mvc来说,一次http请求在服务端处理涉及到的环节一般如下:

http处理环节

每个环节都有可能发生异常;问题的解决思路,恰恰是对于异常处理的自然过程: 能够处理异常就捕获处理,不能处理异常就将异常抛出(或者转换抛出)。

一般来说,服务层和持久层发生的异常,这两层都无能为力,因为这些异常情况会转换为相关的信息返回到http调用方。既然不能处理,何不直接抛出(转换抛出)到控制层?然后由http请求的入口处——控制层统一处理。

那么,可能的处理方法是这样的:

controller:
    @RequestMapping(...)
    public Object doController(){
        try {
            invokeService();
        } catch(CustomizedEx1 e) {
            // 返回码1
        } catch(CustomizedEx2 e) {
            // 返回码2
        } ...
        catch(Exception e) {
            // 系统异常 ?
        }
    }

这样可以做到在一次请求中,统一在入口控制器方法处处理异常。但是这样的话,对于每个请求,在控制器中处理将请求处理委托给服务层的代码外,不得不书写捕获各种异常的catch块,对于懒惰的程序员来说,无疑是灾难性的操作。

封装

考虑一下异常的种类,事实上业务异常的种类是有限的,不同的请求出现的异常情况无非就那么几种。这时可将catch处理封装起来,作为一个统一的方法,共各个controller方法调用。

superController:
    class SuperController {
        public Object uniformExHandle(Exception e) {
            if (e instanceof CustomizedEx1) {
                // 返回码1
            } else if (e instanceof CustomizedEx2) {
                // 返回码2
            }...
            else {
                // 系统异常 ?
            }
        }
    }

specificContoller:
    @Controller
    class HelloController extends SuperController {
        @RequestMapping(...)
        public Object doController(){
            try {
                invokeService();
            } 
            catch(Exception e) {
                uniformExHandle(e);
            }
        }
    }

封装异常处理,为了各个控制器能够方便调用,抽象一个控制器的父类,供各个具体控制器继承。

松耦合

上面的封装+控制器统一异常处理,似乎解决了开始提出的问题,事实上也解决了问题。但是也引入了新的问题:所有控制器不得不继承 SuperController 以获得统一处理异常的能力。

这是一种紧耦合的体现,彷佛回到了EJB时代,为了获取框架的功能,一个类必须实现一堆类,继承一堆接口。这也是良好的设计提倡 少用继承,多用组合 的原因。

诚然,使用组合的方式,将异常统一处理暴露出去供控制器方法调用,是一种松耦合的方法。但是既然在spring mvc的生态中,spring mvc也考虑到了这个问题,提供了两种方式实现控制器的异常处理:

  1. 使用实现HandlerExceptionResolver接口的类处理异常
  2. 使用@Exception注解的方法处理异常

这两种方法原理一样,区别只在使用方式而已。

三、HandlerExceptionResolver

参考HandlerExceptionResolver的jdk文档,就能轻松了解如何使用。

public interface HandlerExceptionResolver {
    @Nullable
    ModelAndView resolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

实现HandlerExceptionResolver接口的类能够解决在处理器映射或处理器方法执行过程中产生的异常,通常导向错误的view,实现类通常需要注册到应用spring上下文中才能生效;其resolveException方法试图解决在处理器执行期间抛出的异常,并在适当的情况下的返回代表特定错误页面的ModelAndView。返回的ModelAndView为空时标明异常已经被成功地解决,但是没有错误页面返回,例如,设置了错误码。

简单的使用方式:

@Component // 必须注册到spring容器中才有效
public class GlobalExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request,
                     HttpServletResponse response, Object handler, Exception ex) {
              String exMsg = "";
               if(null != ex) {
                     exMsg = ex.getMessage();
              }
              ModelAndView modelAndView = new ModelAndView();
              modelAndView.setViewName("exception");
              Map<String, String> map = new HashMap<String, String>();
              map.put( "key", "exception occured: " + exMsg);
              modelAndView.addAllObjects(map);
              return modelAndView;
       }
}

原理

DispatcherServlet是SpringMVC的核心,当然他也负责了这个“全局异常的处理”。

1)分发请求中捕获异常:

doDispatch()是DispatcherServlet分发请求的入口,方法中捕获请求执行可能的异常,并交给processDispatchResult()处理

DispatcherServlet#doDispatch():
    try {
        ...
        // Actually invoke the handler.
        v = ha.handle(processedRequest, response, mappedHandler.getHandler());
        ...
    } catch (Exception ex) {
        dispatchException = ex;
    }
    // 处理结果以及异常
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

2)processDispatchResult核心

DispatcherServlet#processDispatchResult():
    if (exception != null) { // 处理结果,存在异常
        if (exception instanceof ModelAndViewDefiningException) {
            ...
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            // 调用异常处理获得 ModelAndView
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }
    
DispatcherServlet#processHandlerException():
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
            exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }

可见最终遍历了DispatcherServlet的handlerExceptionResolvers,依次调用配置的exception resolver来处理异常,直到异常处理器返回的ModelAndView不为空。

3)handlerExceptionResolvers初始化

在初始化阶段,会初始化异常处理器,将spring容器中注册的HandlerExceptionResolver加入到DispatcherServlet的handlerExceptionResolvers列表中:

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
    ...
    initHandlerExceptionResolvers(context);
    ...
}
private void initHandlerExceptionResolvers(ApplicationContext context) {
    if (this.detectAllHandlerExceptionResolvers) {
        // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
                .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
            // We keep HandlerExceptionResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
}

四、ExceptionHandler

上面的HandlerExceptionResolver方式也需要实现这个接口;另一种注解方式是使用@ExceptionHandler,只需在指定的控制器中简单使用即可:

@Controller
class ExampleController {
    @ExceptionHandler(Exception.class)
    @ReponseBody
    public Object exceptionHandler(Exception e) {
        ...
        return new Object();
    }
    
    @RequestMapping(...)
    public Object doController() {
    }

!!!需要注意的是,注解@ExceptionHandler修饰的方法,只能处理所在控制器的@RequestMapping方法的未捕获异常,超出该控制器,或者没有使用@RequestMapping修饰的方法调用,发生的未捕获异常都不会被处理。

@ExceptionHandler

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {

    /**
     * Exceptions handled by the annotated method. If empty, will default to any
     * exceptions listed in the method argument list.
     */
    Class<? extends Throwable>[] value() default {};

}

简要说来,使用这个注解标注的方法能处理方法所在Controller的处理器中未捕获的异常,处理异常的方法可以有多种方式的签名,参数可以有

  1. 异常类型的参数;Exception或者特定类型的异常类,要与value中指定的异常匹配
  2. request和response对象;javax.servlet.ServletRequest/javax.servlet.ServletResponse,javax.servlet.http.HttpServletRequest/javax.servlet.http.HttpServletRequest
  3. Session对象
  4. 等等

返回值可以是:

  1. ModelAndView, model object, Map, View
  2. 表示视图名的String
  3. @Response修饰,设置响应内容;使用配置的message converts将返回值转换为响应流
  4. HttpEntity / ResponseEntity,同样使用message converts转换
  5. void,如果方法自己处理http response输出

可以看出@ExceptionHandler方式灵活得多,而且其原理与HandlerExceptionResolver是一样的。

全局配置

由于@ExceptionHandler方法只能处理同一个控制器内的方法,这样每一个控制器都要声明@ExceptionHandler方法?

很自然的可以想到在所有控制器的一个父类中声明一个@ExceptionHandler方法,即可全局处理。更优雅的方式是使用@ControllerAdvice

正如其名字一样,注解修饰的类是“协助”其他控制器,是@Component的具化注解,通过类路径扫描(component scan)修饰的类可以被自动检测(注册到spring容器)。
典型的用法是用来定义 @ExceptionHandler, @InitBinder, 和 @ModelAttribute方法,这些方法可运用于所有的@RequestMapping方法。默认情况下@ControllerAdvice的修饰类会“协助”所有已知的控制器。

以下为使用demo:

@ControllerAdvice
public class UniformControllerExHandler {
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public Object exHandler(Throwable e) {
        AgentBaseResponse resp = new AgentBaseResponse();
        resp.setRetMsg(e.getMessage());
        log.error("控制器异常(Throwable), 返回: " + JSON.toJSONString(resp), e);
        return resp;
    }
}

五、总结

spring mvc中业务方法的异常,可以在控制层统一处理。
通过实现spring提供的HandlerExceptionResolver接口,并把实现类注入到spring容器,可统一处理控制器方法未捕获的异常。
另一种方法是使用@ExceptionHandler,借助@ControllerAdvice可将影响扩大到每一个控制器。

上一篇下一篇

猜你喜欢

热点阅读