實戰Spring Boot 2.0系列(二) - 全局異常處理和測試

前言

在平常 web 開發中發生了異常,每每須要經過一個統一的 異常處理,來保證客戶端可以收到友好的提示。本文將會介紹 Spring Boot 中的 全局統一異常處理java

本系列文章

  1. 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker鏡像
  2. 實戰Spring Boot 2.0系列(二) - 全局異常處理和測試
  3. 實戰Spring Boot 2.0系列(三) - 使用@Async進行異步調用詳解
  4. 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理異步任務
  5. 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
  6. 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

正文

1. 建立項目

利用 Spring Initializer 建立一個 gradle 項目 spring-boot-global-exception-handle,建立時添加相關依賴。獲得的初始 build.gradle 以下:web

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.projectlombok:lombok')
    compile('org.apache.commons:commons-lang3:3.1')
    compile('com.google.guava:guava:19.0')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
複製代碼

2. 配置入口類

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
複製代碼

3. 配置實體類

首先安裝 Intellij Idealombok 插件,這裏不作詳細的介紹。切記,須要在設置中將 Enable annotation processing 勾選上,不然 測試代碼 在 編譯時 會沒法對 lombok 插件配置的 註解 進行處理。spring

使用 lombok 工具提供的 註解 配置一個實體類apache

import lombok.Data;

@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String accountName;
}
複製代碼

4. 配置異常響應實體

ErrorMessage 實體用於記錄具體的 異常信息,並響應 客戶端編程

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

@NoArgsConstructor
@Setter
@Getter
@ToString
public class ErrorMessage<T> {
    public static final Integer OK = 0;
    public static final Integer ERROR = 100;

    private Integer code;
    private String message;
    private String url;
    private T data;
}
複製代碼

5. 配置相關異常類

SessionNotFoundException.java後端

public class SessionNotFoundException extends Exception {
    @Getter
    @Setter
    protected String message;

    public SessionNotFoundException() {
        setMessage("Session is not found!");
    }

    public SessionNotFoundException(String message) {
        this.message = message;
    }
}
複製代碼

NullOrEmptyException.java緩存

public class NullOrEmptyException extends Exception {
    @Getter
    @Setter
    protected String message;

    public NullOrEmptyException() {
        setMessage("Parameter is null or empty!");
    }

    public NullOrEmptyException(String message) {
        this.message = message;
    }
}
複製代碼

IllegalPropertiesException.javaspringboot

public class IllegalPropertiesException extends Exception {
    @Getter
    @Setter
    protected String message;

    public IllegalPropertiesException() {
        setMessage("Prop is illegal!");
    }

    public IllegalPropertiesException(String message) {
        this.message = message;
        setMessage(String.format("Prop: %s is illegal!", message));
    }
}
複製代碼

6. 配置全局異常通知

spring 3.2 開始,新增了 @ControllerAdvice 註解,能夠用於定義 @ExceptionHandler,並應用到配置了 @RequestMapping 的控制器中。session

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(SessionNotFoundException.class)
    @ResponseBody
    public ErrorMessage<String> sessionNotFoundExceptionHandler(HttpServletRequest request, SessionNotFoundException exception) throws Exception {
        return handleErrorInfo(request, exception.getMessage(), exception);
    }

    @ExceptionHandler(NullOrEmptyException.class)
    @ResponseBody
    public ErrorMessage<String> nullOrEmptyExceptionHandler(HttpServletRequest request, NullOrEmptyException exception) throws Exception {
        return handleErrorInfo(request, exception.getMessage(), exception);
    }

    @ExceptionHandler(IllegalPropertiesException.class)
    @ResponseBody
    public ErrorMessage<String> illegalPropExceptionHandler(HttpServletRequest request, IllegalPropertiesException exception) throws Exception {
        return handleErrorInfo(request, exception.getMessage(), exception);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorMessage<String> exceptionHandler(HttpServletRequest request, Exception exception) throws Exception {
        return handleErrorInfo(request, exception.getMessage(), exception);
    }

    private ErrorMessage<String> handleErrorInfo(HttpServletRequest request, String message, Exception exception) {
        ErrorMessage<String> errorMessage = new ErrorMessage<>();
        errorMessage.setMessage(message);
        errorMessage.setCode(ErrorMessage.ERROR);
        errorMessage.setData(message);
        errorMessage.setUrl(request.getRequestURL().toString());
        return errorMessage;
    }
}
複製代碼

上述代碼指定了 3特定 的異常處理器和 1默認 的異常處理器。當請求處理出現異常時,會根據 異常處理器配置順序 依次嘗試 異常匹配處理多線程

當異常不在 SessionNotFoundException、NullOrEmptyException、IllegalPropertiesException 中時,Spring 會委託 默認exceptionHandler 進行處理。

7. 配置控制器

根據請求數據的差別,控制器能覆蓋以上 3 種異常處理路徑。

@RestController
public class UserController {
    @PostMapping("user")
    public ResponseEntity<?> save(HttpServletRequest request, HttpSession session) throws Exception {
        String sessionId = (String) session.getAttribute("sessionId");
        if (StringUtils.isBlank(sessionId)) {
            throw new SessionNotFoundException();
        }

        String userPlainText = request.getParameter("user");
        if (StringUtils.isBlank(userPlainText) || StringUtils.equalsIgnoreCase("{}", userPlainText)) {
            throw new NullOrEmptyException();
        }

        ObjectMapper objectMapper = new ObjectMapper();
        User user = objectMapper.readValue(userPlainText, User.class);

        if (StringUtils.isBlank(user.getUsername())) {
            throw new IllegalPropertiesException("username");
        }

        if (StringUtils.isBlank(user.getAccountName())) {
            throw new IllegalPropertiesException("accountName");
        }
        return ResponseEntity.ok("Successful");
    }
}
複製代碼

8. 配置Mock測試類

Spring Mock 的相關配置這裏就不詳細介紹了,如下測試類覆蓋了 UserController 的全部執行路徑。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootApplication
@WebAppConfiguration
@Slf4j(topic = "UserControllerTester")
public class ApplicationTests {
    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;
    private MockHttpSession session;

    @Autowired
    private UserController userController;

    private ImmutableMap<Long, Pair<String, String>> map = new ImmutableMap.Builder<Long, Pair<String, String>>()
            .put(0x00001L, Pair.of("user", ""))
            .put(0x00002L, Pair.of("user", "{}"))
            .put(0x00003L, Pair.of("user", "{\"username\": \"\", \"accountName\": \"\"}"))
            .put(0x00004L, Pair.of("user", "{\"username\": \"Harrison\", \"accountName\": \"\"}"))
            .put(0x00005L, Pair.of("user", "{\"username\": \"Harrison\", \"accountName\": \"ostenant\"}"))
            .build();


    @Before
    public void setUp() throws Exception {
        boolean singleRunner = false;
        if (singleRunner) {
            this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
        } else {
            this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
        }
        session = new MockHttpSession();
        session.setAttribute("sessionId", StringUtils.replace(UUID.randomUUID().toString(), "-", ""));
        log.debug("sessionId: {}", session.getAttribute("sessionId"));
    }

    /** * 測試SessionNotFoundException * @throws Exception */
    @Test
    public void testSessionNotFoundException() throws Exception {
        session.clearAttributes();
        // 模擬發送請求
        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00005L).getKey(), map.get(0x00005L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
    }

    /** * 測試NullOrEmptyException * @throws Exception */
    @Test
    public void testNullOrEmptyException() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00001L).getKey(), map.get(0x00001L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();

        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00002L).getKey(), map.get(0x00002L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();

    }

    /** * 測試IllegalPropException * @throws Exception */
    @Test
    public void testIllegalPropException() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00003L).getKey(), map.get(0x00003L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();

        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00004L).getKey(), map.get(0x00004L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
    }

    /** * 測試正常運行的狀況 * @throws Exception */
    @Test
    public void testNormal() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/user")
                        .param(map.get(0x00005L).getKey(), map.get(0x00005L).getValue())
                        .session(session))
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                .andExpect(MockMvcResultMatchers.handler().methodName(("save")))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
    }
}
複製代碼

9. 測試結果

批量運行測試,測試結果以下,全部的測試用例所有經過。

小結

使用 @ControllerAdvice 處理異常也有必定的 侷限性。只有進入 Controller 層的錯誤,纔會由 @ControllerAdvice 處理。攔截器 拋出的錯誤,以及 訪問錯誤地址 的狀況 @ControllerAdvice 處理不了,由 Spring Boot 默認的 異常處理機制 處理。


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索