使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler註解實現全局處理Controller層的異常

一、前言

   在進行項目開發的過程中,我們不可避免地都要對代碼中可能存在的異常進行處理和捕獲,而我們通常的做法有如下兩種:

 

方式一:

1、對於在Service層的異常,使用try-catch進行捕獲處理,其Service層捕獲異常代碼如下所示:

@Service
public class UserServiceImpl implements UserService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private UserMapper userMapper;

    @Override
    public User selectUserById(Integer id) {

        User user = null;

        try {
             user = userMapper.selectUserById(id);
        } catch (Exception e) {
            logger.error("user信息有誤,需要回滾");
            e.printStackTrace();
        }
        
        logger.info("代碼繼續往下執行");

        return user;
    }
}

 

   對於如上在Service層使用try-catch對異常進行捕獲並處理的代碼,其存在如下缺陷:

      1、由於Service層是與Dao層進行交互的,我們通常都會把事務配置在Service層,當操作數據庫失敗時,會拋出異常並由Spring事務管理器幫我們進行回滾,而當我們使用try-catch對異常進行捕獲處理時,這時Spring事務管理器並不會幫我們進行回滾,而代碼也會繼續往下執行,這時我們就有可能讀取到髒數據。

      2、由於我們在Service層對異常進行了捕獲處理,在Controller層調用該Service層的相關方法時,並不能把其相關異常信息拋給Controller層,也就無法把異常信息展示給前端頁面,而當用戶進行相關操作失敗時,也就無法得知其操作失敗的緣由。

 

方式二:

1、在Service層將異常拋出,並在Controller層對Service層拋出的異常使用try-catch進行捕獲處理,其Controller層捕獲異常代碼如下所示:

@RestController
public class UserController {

    @Autowired
    private UserService userService;
    
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @GetMapping(value = "/User/{id}")
    public User selectUserById(@PathVariable(value = "id") Integer id) {

        User user = null;

        try {
            user = userService.selectUserById(id);
            
        } catch (UserNotExistException e) {
            logger.error("用戶不存在");
            e.printStackTrace();

        } catch (Exception e) {
            logger.error(e.getMessage());
            e.printStackTrace();
        }

        return user;
    }
}

 

 對於如上在Controller層使用try-catch對異常進行捕獲並處理的代碼,其存在如下缺陷:

      1、由於Controller層是與Service層進行交互的,這樣一來,在Controller層調用Service層拋出異常的方法時,我們都需要在Controller層的方法體中,寫一遍try-catch代碼來捕獲處理Service層拋出的異常,就會使得代碼很難看而且也很難維護。

      2、對於Service層拋出的不同異常,那麼Controller層的方法體中需要catch多個異常分別進行處理。

   這裏就會有人說了,我直接在Controller層的方法上使用 throws關鍵字 把Service層的異常繼續往上拋不就行了,還不需要使用try-catch來進行捕獲處理,確實是可以,但是這樣會把大量的異常信息帶到前端頁面,對用戶來說是非常不友好的,例如如下界面顯示:

 

二、簡介

  使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler註解能夠實現全局處理Controller層的異常,並能夠自定義其返回的信息(前提:Controller層不使用try-catch對異常進行捕獲處理)

優缺點:

    優點:將 Controller 層的異常和數據校驗的異常進行統一處理,減少模板代碼,減少編碼量,提升擴展性和可維護性。

    缺點:能處理 Controller 層的異常 (未使用try-catch進行捕獲) 和 @Validated 校驗器註解的異常,但是對於 Interceptor(攔截器)層的異常 和 Spring 框架層的異常,就無能爲力了。

 

三、使用

1、導入相關依賴jar包

<properties>
    <spring.version>5.1.5.RELEASE</spring.version>
</properties>

<dependencies>

    <!-- Lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.20</version>
      <scope>provided</scope>
    </dependency>

    <!-- javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <!-- Spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jms</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- slf4j的相關依賴包 -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.25</version>
    </dependency>

</dependencies>

 

2、編寫applicationContext-spring.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
	   http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context-4.2.xsd">

    <!-- 配置包掃描器,掃描註解的類 -->
    <context:component-scan base-package="com.exception"/>

</beans>

 

3、編寫web.xml配置文件

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- post亂碼過濾器 -->
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>utf-8</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>


  <!-- 前端控制器 -->
  <servlet>
    <servlet-name>ExceptionHandlerProject</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- contextConfigLocation不是必須的, 如果不配置contextConfigLocation, springmvc的配置文件默認在:WEB-INF/servlet的name+"-servlet.xml" -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring/applicationContext-spring.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>ExceptionHandlerProject</servlet-name>
    <!-- 攔截所有請求jsp除外 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

 

4、自定義異常信息的枚舉類

package com.exception.pojo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;


@Getter
@ToString
@AllArgsConstructor
public enum ReturnEnum {

    /**
     * 請求成功返回信息
     **/
    SERVICE_SUCCESS("200", "success", "請求成功"),

    /**
     * 請求失敗返回信息
     **/
    SERVICE_ERROR("500", "error", "請求失敗"),

    /**
     * 用戶不存在返回信息
     **/
    USER_NOT_EXIST("404", "user not exist", "用戶不存在");

    /**
     * 自定義異常狀態碼
     **/
    private String code;

    /**
     * 異常的英文信息,便於國際化切換使用
     **/
    private String detailsInEnglish;

    /**
     * 異常的中文信息
     **/
    private String detailsInChinese;

}

 

5、自定義封裝異常信息返回前端的JSON類

package com.exception.pojo;

import lombok.*;
import java.io.Serializable;


@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> implements Serializable {

    private String code;
    private String message;
    private T data;

    public JsonResult(ReturnEnum returnEnum, T data){
         this.code = returnEnum.getCode();
         this.message = returnEnum.getDetailsInChinese();
         this.data = data;
    }
}

 

6、自定義異常類

package com.exception;

import com.exception.pojo.ReturnEnum;
import lombok.Getter;
import lombok.Setter;


@Getter
@Setter
public class UserNotExistException extends Exception {

    /**
     * 異常信息的枚舉類
     **/
    private ReturnEnum returnEnum;

    public UserNotExistException(ReturnEnum returnEnum){
        super(returnEnum.getDetailsInChinese());
        this.returnEnum = returnEnum;
    }
}

 

7、創建User實體類

package com.exception.pojo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


@Getter
@Setter
@NoArgsConstructor
public class User {
    
    private String name;
    private String password;

}

 

8、編寫UserService接口 (這裏我就不給出Dao層了,直接在Service層模擬調用Dao層)

package com.exception.service;

import com.exception.UserNotExistException;
import com.exception.pojo.User;


public interface UserService {

    /**
     * 通過id查詢用戶信息
     *
     * @param id
     * @return com.exception.pojo.User
     * @throws UserNotExistException
     **/
    User selectUserById(Integer id) throws UserNotExistException;
}

 

9、編寫UserServiceImpl實現類

package com.exception.service.impl;

import com.exception.UserNotExistException;
import com.exception.pojo.ReturnEnum;
import com.exception.pojo.User;
import com.exception.service.UserService;
import org.springframework.stereotype.Service;


@Service
public class UserServiceImpl implements UserService {

    @Override
    public User selectUserById(Integer id) throws UserNotExistException {
        
        // id等於0,表示沒找到用戶
        if(0 == id){
            throw new UserNotExistException(ReturnEnum.USER_NOT_EXIST);
        }

        return new User();
    }
}

 

10、編寫UserController

package com.exception.controller;

import com.exception.UserNotExistException;
import com.exception.pojo.User;
import com.exception.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class UserController {

    @Autowired
    private UserService userService;
    
    @GetMapping(value = "/user/{id}")
    public User selectUserById(@PathVariable(value = "id") Integer id) throws UserNotExistException {
        
        //根據id查找用戶信息,並繼續拋出Service層的異常
        User user = userService.selectUserById(id);
        
        return user;
    }
}

 

11、編寫全局異常處理類,其中@ExceptionHandler(value = UserNotExistException.class)註解中的value用於指定需要捕獲的異常

package com.exception.handler;

import com.exception.UserNotExistException;
import com.exception.pojo.JsonResult;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;


@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 用於對UserNotExistException異常進行處理
     *
     * @param ex
     * @return com.exception.pojo.JsonResult
     * @throws
     **/
    @ExceptionHandler(value = UserNotExistException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public JsonResult handleUserNotExistException(UserNotExistException ex){
        
        //封裝異常信息
        JsonResult jsonResult = new JsonResult(ex.getReturnEnum(),null);
        return jsonResult;
    }

}

 

12、啓動項目,訪問 http://localhost:8080/user/0

 

        這時我們就會有疑惑了,爲什麼我對異常進行了處理,界面還是會把大量的異常信息帶到前端頁面呢?相信使用過Spring的開發人員都用過@RequestBody、@ResponseBody註解,可以直接將輸入解析成Json、將輸出解析成Json,但HTTP 請求和響應是基於文本的,意味着瀏覽器和服務器需要通過交換原始文本才能進行通信,而這裏其實就是通過HttpMessageConverter消息轉換器發揮着作用,所以我們需要配置HttpMessageConverter消息轉換器。

 

 

解決方式一:使用SpringMVC默認的HttpMessageConverter消息轉換器

1、在pom.xml文件中 添加 jackson-databind jar包

<!-- Jackson Json處理工具包 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>

 

2、在applicationContext-spring.xml配置文件中 添加如下配置

 2.1、在<beans>標籤中添加 mvc命名空間

xmlns:mvc="http://www.springframework.org/schema/mvc"

http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd

 

2.2、開啓SpringMVC註解驅動

        關於更多<mvc:annotation-driven/>的默認配置,請點擊<mvc:annotation-driven/>到底幫我們做了什麼

        關於更多<mvc:message-converters/>的介紹,請點擊<mvc:message-converters/>的簡單介紹

    <!--
        <mvc:annotation-driven/>

        等價於

        <mvc:annotation-driven>
         // 消息轉換器
         <mvc:message-converters>
              // 支持返回值類型爲byte[],content-type爲application/octet-stream,*/*
              <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>

              // 支持的返回值類型爲String,content-type爲 text/plain;charset=ISO-8859-1,*/*
              <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>

              // 支持的返回值類型爲Resource,content-type爲 */*
              <bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>

              // 支持的返回值類型爲DomSource,SAXSource,Source,StreamSource,content-type爲application/xml,text/xml,application/*+xml
              <bean class="org.springframework.http.converter.SourceHttpMessageConverter"/>

              // 支持的返回值類型爲MultiValueMap,content-type爲application/x-www-form-urlencoded,multipart/form-data
              <bean class="org.springframework.http.converter.AllEncompassingFormHttpMessageConverter"/>

               // Json消息轉換器,如果發現jackson相關jar包,則會自動Json消息轉換器
              //注意: Spring3.x 的Json消息轉換器爲 MappingJacksonHttpMessageConverter
                     Spring4.x 的Json消息轉換器爲 MappingJackson2HttpMessageConverter
             <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
         </mvc:message-converters>
       </mvc:annotation-driven>
    -->

    <!-- 開啓SpringMVC註解驅動,該註解幫我們註冊了多個特性,包括JSR-303校驗支持、信息轉換以及對域格式化的支持 -->
    <mvc:annotation-driven/>

 

3、運行結果如下所示

 

4、可以看到頁面上返回了我們 自定義封裝異常信息返回前端的JSON類,但是可以看到data:null也返回到了前端頁面上,有時候我們需要把爲null的字段 不展示到前端頁面中,這時我們需要在 自定義封裝異常信息返回前端的JSON類上添加這麼一個註解@JsonInclude(JsonInclude.Include.NON_NULL) 即可,運行結果如下所示:

 

 

解決方式二:使用FastJson自定義HttpMessageConverter消息轉換器

1、在pom.xml文件中 添加 fastjson jar包

<!-- FastJson Json處理工具包 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>

 

2、在applicationContext-spring.xml配置文件中 添加如下配置

 2.1、在<beans>標籤中添加 mvc命名空間

xmlns:mvc="http://www.springframework.org/schema/mvc"

http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd

 

2.2、自定義HttpMessageConverter消息轉換器,注意:序列化特徵配置對泛型字段不起作用

        關於更多serializerFeatures 序列化特性配置 和日期轉換格式的介紹,請點擊常用FastJSON的SerializerFeature特性及日期轉換格式

<!-- 自定義HttpMessageConverter​​​​​​​消息轉換器 -->
    <mvc:annotation-driven>
        <!-- register-defaults="false" 表示設置不使用默認的消息轉換器 -->
        <mvc:message-converters register-defaults="false">
            <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <!-- 配置支持的消息類型 -->
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html</value>
                        <value>application/json;charset=UTF-8</value>
                    </list>
                </property>
                <property name="fastJsonConfig" ref="fastJsonConfig" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="fastJsonConfig" class="com.alibaba.fastjson.support.config.FastJsonConfig">
        <!-- 序列化特性配置 -->
        <property name="serializerFeatures">
            <list>
                <!-- 格式化輸出 -->
                <value>PrettyFormat</value>
                <!-- 是否輸出值爲null的字段,默認爲false -->
                <value>WriteMapNullValue</value>
                <!-- List字段如果爲null,輸出爲[],而非null-->
                <value>WriteNullListAsEmpty</value>
                <!-- 字符類型字段如果爲null,輸出爲"",而非null -->
                <value>WriteNullStringAsEmpty</value>
                <!-- Boolean字段如果爲null,輸出爲false,而非null -->
                <value>WriteNullBooleanAsFalse</value>
                <!-- 消除對同一對象循環引用的問題,默認爲false(如果不配置有可能會進入死循環)-->
                <value>DisableCircularReferenceDetect</value>
            </list>
        </property>
        <!-- 日期格式配置 -->
        <property name="dateFormat" value="yyyy-MM-dd HH:mm:ss"/>
    </bean>

 

 

                   如果有遇到不懂或者有問題時,可以掃描下方二維碼,歡迎進羣交流與分享,希望能夠跟大家交流學習!

                                                               

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章