spring mvc异常处理

spring mvc异常处理

异常在软件开发过程中随处可见,是所有程序员都必须要思考并解决的问题。有些异常是不可避免必须要处理的,譬如通过ObjectMapperjson字符串转换成对象就必须显示地处理异常;有些则是开发过程中可能注意不到的异常,比如调用JdbcTemplatequeryForObject方法查询对象的时候,如果数据库中没有满足条件的数据或者查询到记录多于1条的时候,就会抛EmptyResultDataAccessException或者IncorrectResultSizeDataAccessException异常。

在何时何处处理异常是一门高深的学问,往往需要积累大量的经验才能够很好地把握处理异常的时机,在这里采用前辈们的经验共勉:

仅当清楚地明白当前需要处理捕获到的异常时才处理该异常,否则直接或者重新封装后向上抛

总会有异常会从底层传递到接口返回处,这时候必须对异常进行处理,否则将会极大地影响接口的友好度,一般都会将异常转换成用户友好的提示信息返回给接口调用方。

在spring mvc中,在接口层面有三种方式处理异常:

  1. 直接在Controller调用Service的时候捕获并处理异常
  2. Controller中统一处理指定类型的异常
  3. Controller外统一处理异常

第一种方式虽然很直接明了,但是需要在所有接口中都要try-catch异常,会导致代码上的重复以及不必要的异常处理逻辑。将异常提取到接口外处理有助于保持接口的简洁性。

下面开始介绍spring mvc中的异常处理方式。

异常处理

如果在请求映射(request mapping)或者请求处理(request handler)的过程中发生了异常,那么DispatcherServlet加会委托HandlerExceptionResolver链去处理这些异常。

HandlerExceptionResolver有如下几种:

  • SimpleMappingExceptionResolver:将异常类的名字试图的名字进行关联映射,往往用于将异常对应于指定的错误页面。
  • DefaultHandlerExceptionResolver:将异常HTTP状态码进行关联映射。
  • ResponseStatusExceptionResolver:将异常@ResponseStatus注解进行关联映射,并将@ResponseStatus中的值映射成HTTP状态码
  • ExceptionHandlerExceptionResolver:将异常@ExceptionHandler进行关联映射。当异常与@ExceptionHandler注解中的值一致的或者是其子类的时候,则调用@ExceptionHandler标注的方法处理该异常。

通过给以上四种HandlerExceptionResolver设定不同的order值,可以构造成不同的异常处理链处理异常,order值越大,处理的时机就越晚。

从spring的@EnableWebMvc-->DelegatingWebMvcConfiguration --> WebMvcConfigurationSupport.addDefaultHandlerExceptionResolvers的源码中可以发现:
spring mvc默认设置的处理链为DefaultHandlerExceptionResolverResponseStatusExceptionResolverExceptionHandlerExceptionResolver三种异常处理器

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    ExceptionHandlerExceptionResolver exceptionHandlerResolver = this.createExceptionHandlerExceptionResolver();    exceptionHandlerResolver.setContentNegotiationManager(this.mvcContentNegotiationManager());        exceptionHandlerResolver.setMessageConverters(this.getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(this.getArgumentResolvers());      exceptionHandlerResolver.setCustomReturnValueHandlers(this.getReturnValueHandlers());
    if (jackson2Present){            
        exceptionHandlerResolver.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
    }
    if (this.applicationContext != null) {   
        exceptionHandlerResolver.setApplicationContext(this.applicationContext);
    }
    exceptionHandlerResolver.afterPropertiesSet();
    exceptionResolvers.add(exceptionHandlerResolver);
    ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
    responseStatusResolver.setMessageSource(this.applicationContext);
    exceptionResolvers.add(responseStatusResolver);
    exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}

可以通过覆盖WebMvcConfigurer接口中的configureHandlerExceptionResolvers方法指定自定义的异常处理链。

@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver();
    exceptionHandlerExceptionResolver.setOrder(2);
    resolvers.add(exceptionHandlerExceptionResolver);
}

如果这些异常处理类还没有处理好抛出的异常或者处理后HTTP状态码是4xx或者5xx,那么Servlet容器将会渲染默认的错误页面,可以通过在web.xml中自定义错误页。

<error-page> 
    <location>/error</location> 
</error-page>

然后由DispatcherServlet发出error请求,可以通过如下方式捕获:

@RestController 
public class ErrorController {
@RequestMapping(path = "/error") 
public Map<String, Object> handle(HttpServletRequest request) { 
    Map<String, Object> map = new HashMap<String, Object>();    
    map.put("status", request.getAttribute("javax.servlet.error.status_code")); 
    map.put("reason", request.getAttribute("javax.servlet.error.message")); return map; }
}

异常捕获

@Controller@ControllerAdvice中可以通过@ExceptionHandler标注的方法捕获异常进行处理,只不过@Controller只能捕获本Controller中接口抛出的异常,而@ControllerAdvice可以捕获所有Controller中接口抛出的异常。
因此在这里介绍spring mvc中全局统一异常捕获机制。

统一的返回

首先对于项目中的接口,定义统一的接口返回格式。

public final class OutPut {
    private final static String STATUS = "status";
    private final static String CODE = "code";
    private final static String MSG = "msg";
    private final static String DATA = "data";
    private OutPut() {
        throw new AssertionError();
    }

    public static Map<String, Object> success(String msg, Object data) {
        Objects.requireNonNull(msg);
        Objects.requireNonNull(data);
        return ImmutableMap.of(STATUS, ResponseStatus.SUCCESS, CODE, ResponseCode.SUCCESS, MSG, msg, DATA, data);
    }

    public static Map<String, Object> failure(int code, String msg) {
        Objects.requireNonNull(msg);
        return ImmutableMap.of(STATUS, ResponseStatus.FAILURE, CODE, code, MSG, msg);
    }
}

定义项目中需要的错误编码

public interface ResponseCode {
    int SUCCESS = 200;
    int FAILURE = 400;
    int INNER_ERROR = 500;
}

全局异常处理器

通过@RestControllerAdvice注解指定全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {IllegalArgumentException.class})
    public Map<String, Object> handleIllegalArgumentException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage());
    }
}

这里捕获了接口中常见的参数错误异常,读友们可以创建自定义的异常,通过类似的方式进行捕获处理。

当多个异常类型同事出现的时候,ExceptionDepthComparator会对所有的异常进行排序,会调用在异常继承链上该异常最近的父异常指定的处理方法。为了减少错误匹配的情况,建议方法参数给定指定的类型。

对于REST服务来说,可以让GlobalExceptionHandler继承ResponseEntityExceptionHandler,然后覆写父类的相关方法。

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = {RuntimeException.class})
    public Map<String, Object> handleRuntimeException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage() + "runtime");
    }

    @ExceptionHandler(value = {IllegalArgumentException.class})
    public Map<String, Object> handleIllegalArgumentException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage());
    }



    @ExceptionHandler(value = {Exception.class})
    public Map<String, Object> handleException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage() + "exception");
    }

     @Override
    protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

        return ResponseEntity.status(status).body(OutPut.failure(ResponseCode.TYPE_MIS_MATCH, ex.getValue() + "的类型不匹配,需要" + ex.getRequiredType()));
    }
}

在这里,覆写了父类的参数类型匹配错误异常(handleTypeMismatch),并且保持了与其他异常同样的错误格式,如下:

{
    "status": "failure",
    "code": 401,
    "msg": "abc的类型不匹配,需要int"
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容