SpringBoot 拦截器与捕获/处理异常

处理方式

1. RestControllerAdvice 注解创建拦截器

使用 @RestControllerAdvice 注解的方式进行拦截 Controller 中的异常。

只用如下一个类,加个带有 @ExceptionHandler 注解的进行处理的方法即可

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Object handler(Exception e, HttpServletRequest request, Object result) {
        final Map<String, Object> data = new HashMap<>();
        data.put("code", 500);
        data.put("data", result);
        data.put("msg", "执行时出现错误:uri=" + request.getRequestURI() + ", message=" + e.getMessage());
        return data;
    }

}

但是上面信息比较单一,如果我们想自定义设置返回的内容,可以加上自定义注解,设置自定义内容。

先创建一个自定义注解 HandleException,使用 @AliasFor 指定 value() 注解方法的别名设置为 desc 方法

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author zhangxuetu
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HandleException {

    @AliasFor("desc")
    String value() default "error";

    /***描述内容*/
    @AliasFor("value")
    String desc() default "error";

    /***状态码*/
    int code() default 500;

}

写个测试类用于测试

import com.example.global_exception_interceptor.common.interceptor.HandleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/test")
public class TestController {

    @HandleException(desc = "出现空值", code = 10001)
    @GetMapping("/object")
    public Object getObject() {
        throw new NullPointerException();
    }

}

Controller 类异常拦截器里,增加 HandlerMethod 类型参数获取出现异常的方法,并获取这个类的注解内容

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.HandlerMethod;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zhangxuetu
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Object handler(Exception e, HttpServletRequest request, HandlerMethod handlerMethod, Object result) {
        // 获取注解对象
        final HandleException annotation = handlerMethod.getMethodAnnotation(HandleException.class);
        if (annotation != null) {
            System.out.println(handlerMethod.getBean());
            final Map<String, Object> data = new HashMap<>();
            data.put("data", null);
            // 返回注解内容
            data.put("code", annotation.code());
            data.put("desc", annotation.value());
            return data;
        } else {
            // 没有注解返回默认内容
            final Map<String, Object> data = new HashMap<>();
            data.put("code", 500);
            data.put("data", result);
            data.put("msg", "执行时出现错误:uri=" + request.getRequestURI() + ", message=" + e.getMessage());
            return data;
        }
    }

}

2. 创建其他类的异常捕获

上面的只能捕获 Controller 类里的异常,功能不够强。如果要拦截其他内容,比如捕获一个定时任务类抛出的异常,可以使用如下方式进行拦截捕获异常:

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ExecuteTask {

    @Scheduled(cron = "0/5 * * * * ?")
    public void test() throws Exception {
        log.info("每5秒执行一次任务");
        throw new Exception("测试语句,抛出异常");
    }

}

注意还要在你的 项目名Application 类里加上 @EnableScheduling 注解启动定时任务,比如我的是:

@EnableScheduling
@SpringBootApplication
public class GlobalExceptionInterceptorApplication {

    public static void main(String[] args) {
        SpringApplication.run(GlobalExceptionInterceptorApplication.class, args);
    }

}

定时任务类必须有 Spring bean 的注解,比如加上 @Component 注解,否则定时任务不生效

pom.xml 文件中添加 AOP 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建一个拦截这个包下的所有类执行的方法的拦截器

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author zhangxuetu
 */
@Aspect
@Component
@Slf4j
public class TaskExceptionHandler {

    /**
     * 拦截 com.example.global_exception_interceptor.task 包下的所有执行的方法
     * @param point
     * @return
     */
    @Around("execution(* com.example.global_exception_interceptor.task.*.*(..))")
    public Object handler(ProceedingJoinPoint point) {

        try {
            // 执行拦截的方法
            return point.proceed();
        } catch (Throwable e) {
            // 处理异常
            final MethodSignature signature = (MethodSignature) point.getSignature();
            final Method method = signature.getMethod();
            final String name = method.getName();
            log.info("执行 {} 方法时出现错误", name);
            log.info("错误信息:{}", e.getMessage());
        }
        return null;
    }

}

使用 AOP 切面的方式可以非常方便的在调用方法之前或之后进行处理各种功能,比如计算方法执行的时间,获取参数内容等等,非常强大。


几种拦截方法:

  • Filter/OncePerRequestFilter
  • Interceptor
  • @ControllerAdvice/@RestControllerAdvice注解下的RequestBodyAdvice和ResponseBodyAdvice
  • aspect

请求被拦截的顺序从上到下:

  • Filter/OncePerRequestFilter:可以拿到原始的HTTP请求和响应信息,拿不到处理请求的方法值信息
  • interceptor:既可以拿到HTTP请求和响应信息,也可以拿到请求的方法信息,拿不到方法调用的参数值信息
  • RequestBodyAdvice和ResponseBodyAdvice:前者拦截不到无@RequestBody的方法,后者拦截不到无@ResponseBody的方法
  • aspect:可以拿到请求方法的传入参数值,拿不到原始的HTTP请求和响应的对象

推荐阅读

发表评论