在日常 web 开发中发生了异常,往往需要通过一个统一的 异常处理,来保证客户端能够收到友好的提示。本文将会介绍 Spring Boot 中的 全局统一异常处理。
利用 Spring Initializer 创建一个 gradle 项目 spring-boot-global-exception-handle,创建时添加相关依赖。得到的初始 build.gradle 如下:
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 插件配置的 注解 进行处理。
使用 lombok 工具提供的 注解 配置一个实体类
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.java
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 的控制器中。
@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 默认的 异常处理机制 处理。
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。
Original url: Access
Created at: 2019-05-29 15:26:46
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论