在平常 web
開發中發生了異常,每每須要經過一個統一的 異常處理,來保證客戶端可以收到友好的提示。本文將會介紹 Spring Boot
中的 全局統一異常處理。java
利用 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')
}
複製代碼
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
首先安裝 Intellij Idea
的 lombok
插件,這裏不作詳細的介紹。切記,須要在設置中將 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;
}
複製代碼
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;
}
複製代碼
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));
}
}
複製代碼
從 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
進行處理。
根據請求數據的差別,控制器能覆蓋以上 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");
}
}
複製代碼
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();
}
}
複製代碼
批量運行測試,測試結果以下,全部的測試用例所有經過。
使用 @ControllerAdvice
處理異常也有必定的 侷限性。只有進入 Controller
層的錯誤,纔會由 @ControllerAdvice
處理。攔截器 拋出的錯誤,以及 訪問錯誤地址 的狀況 @ControllerAdvice
處理不了,由 Spring Boot
默認的 異常處理機制 處理。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。