请求springboot接口的路径不存在,如何自定义status code和返回的JSON格式

请求springboot接口的路径不存在,如何自定义status code和返回的JSON格式

一、背景

如果你有个springboot项目,如果访问它不存在的endpoint,会得到404状态码,并且如下的错误信息

{"timestamp":"2022-10-04T08:40:27.808+00:00","status":404,"error":"Not Found","path":"/testEndpointNotExist"}

**我能否自定义这个信息呢?**用我自己的 JSON 对象以及状态码可以吗?比如我要改成200,改成如下字段

public class ResultBean {
    private String code;
    private String msg;
    private Object data;
    private Object debugInfo;
    private Date time;
}

能想到的就是拦截器之类的方式。如果需要快速找答案,请看标题跳转

二、复习一下javax.servlet.Filter,spring的Interceptor,以及AOP的拦截的顺序

代码的写法详见附录,注意到都没有设置Order的优先级别(我觉得设置后也是一样的,毕竟Filter/Interceptor/AOP是不同种类的东西,要是生效也仅仅是同一种类里面生效,不可能越级别的)

  • 没发生异常时
Filter begin,/test
springinterceptor: preHandle,/test
----- AOP aspect ---- begin
----- test ------
----- AOP aspect ---- end
springinterceptor: postHandle,/test
springinterceptor: afterCompletion,/test
Filter end,/test
  • 发生异常时,考虑@RestControllerAdvice的拦截是在哪个位置?如下
Filter begin,/testEx
springinterceptor: preHandle,/testEx
----- AOP aspect ---- begin
----- testEx ------
----- AOP aspect ---- end
----- exception occurs,log in @RestControllerAdvice ----
springinterceptor: afterCompletion,/testEx
Filter end,/testEx

在这里插入图片描述

三、如果请求的endpoint不存在,谁能拦截得了?

随便请求一个不存在的endpoint,比如 /testNotExistEnpoint

Filter begin,/testNotExistEnpoint
springinterceptor: preHandle,/testNotExistEnpoint
springinterceptor: postHandle,/testNotExistEnpoint
springinterceptor: afterCompletion,/testNotExistEnpoint
Filter end,/testNotExistEnpoint
springinterceptor: preHandle,/error
----- AOP aspect ---- begin
----- AOP aspect ---- end
springinterceptor: postHandle,/error
springinterceptor: afterCompletion,/error

可以看到其实也是进入了拦截器的,这给我们一点希望,我能否通过自己的拦截器判断如果是/error就认定为请求的endpoint不存在?

实际测试是不行的!,原因如下:

  • 如果你使用Filter

    根本就不进入catch(并且只拦截了/testNotExistEnpoint没有拦截/error,你根本没法判断这个endpoint是否真的不存在,当然也应该有办法获得所有的endpoint)

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      HttpServletRequest httpReq = (HttpServletRequest) request;
      System.out.println("Filter begin," + httpReq.getServletPath());
      try {
        chain.doFilter(request, response);
      } catch (Throwable t) {
        System.err.println("Filter ex occur");
        throw t;
      }
      System.out.println("Filter end," + httpReq.getServletPath());
    }
    
  • 如果你使用Interceptor

    也不行,会进入两次,一次是/testNotExistEnpoint另外是/error,由于方法是void,只能用response来写,但是实际上会报错,因为response已经写出去了

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
      throws Exception {
      System.out.println("springinterceptor: afterCompletion," + request.getServletPath());
      // 访问的URL不存在
      if (request.getServletPath().equals("/error")) {
        String errMsg = ENDPOINT_NOT_EXIST + request.getRequestURI();
        ResultBean fail = ResultBean.fail(BizCode.FAIL, ex == null ? errMsg : errMsg + System.lineSeparator() + StackTraceGetter.getStackTrace(ex));
        String jsonStr = new ObjectMapper().writeValueAsString(fail);
        response.getWriter().write(jsonStr);
      }
    }
    

    报错如下(是getWritter()这步报错,而不是write() )

    java.lang.IllegalStateException: getOutputStream() has already been called for this response
    	at org.apache.catalina.connector.Response.getWriter(Response.java:584) ~[tomcat-embed-core-9.0.65.jar:9.0.65]
    
  • 如果你使用AOP

    根本不进入catch,并且也只拦截了/error不拦截/testNotExistEnpoint

    @Around(value = "pointCutControllerMethod()")
    public Object aroundRestApi(ProceedingJoinPoint joinPoint) throws Throwable {
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
      System.out.println("----- AOP aspect ---- begin," + request.getServletPath());
      try {
        return joinPoint.proceed();
      } catch (Throwable t) {
        System.err.println("---- error log in AOP -----," + request.getServletPath());
        return ResultBean.fail(BizCode.FAIL, StackTraceGetter.getStackTrace(t));
      } finally {
        System.out.println("----- AOP aspect ---- end," + request.getServletPath());
      }
    }
    
  • 突发奇想,是否可以将 @RestControllerAdvice或@RestControllerAdvice的拦截的顺序改前面一些?

    通过 @org.springframework.core.annotation.Order(Integer.MIN_VALUE)。结果也不行,压根都还没进入

    @Order(Integer.MIN_VALUE)
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(Throwable.class)
        public ResultBean handleException(Throwable t, HttpServletResponse response) throws Throwable {
            System.err.println("----- exception occurs,log in @RestControllerAdvice ----");
            return ResultBean.fail(BizCode.FAIL, StackTraceGetter.getStackTrace(t));
        }
    }
    

总结:spring使用一个servlet来接受所有的请求并分发,这个应该是一个总入口,比用户能接触到的早期多了,如果一个endpoint是乱写的不存在的则在早期就

四、终于找到了方法,其实很简单

1、方法一

只要写一个 /error 的endpoint即可,访问的endpoint如果不存在则会调用该endpoint进行处理,当然,如果要更加灵活,可以写成@GetMapping("${server.error.path:${error.path:/error}}"),大多数情况下都不会有人去改这个的路径的,所以写死 /error 也问题不大。

另外我将 /error 的处理方法写在了@RestControllerAdvice类上,单独出来也是可以的,我只是不想再写一个

package com.wyf.test.testrestcontrolleradvice.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

/**
 * 全局处理HTTP请求异常
 * 能处理:
 * 1、endpoint不存在的异常(通过/error)
 * 2、controller里某个endpoint内部发生的异常(请求已经打到了controller方法里)
 * 3、未进入endpoint如参数校验失败的异常(请求还未达到controller方法里)
 * 3.1、请求的Method错误:如GET/POST...
 * 3.2、请求时未传必填参数 @RequestParam(required=true)
 * 3.3、请求的参数转换错误:如字串无法转为整型、布尔类型、日期类型
 * 3.4、请求的Content-Type错误
 * 3.5、请求参数jsr303错误:即hibernate validator校验出来的@NotNull、@NotBlank、@NotEmpty、@Min、@Max、@Size、@Pattern...
 * <p>
 * 不能捕获的异常:
 * 1、域名、IP写错或端口写错都不会得到任何status code
 * 2、域名、IP和端口写正确,但endpoint路径写错,返回 404 的status code
 */
@RestControllerAdvice
@RestController
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 是spring中基础的用于处理失败的(IDEA里显示无法注入的红线,实际可注入)
     */
    @Autowired
    private BasicErrorController basicErrorController;

    @ExceptionHandler(Throwable.class)
    public ResultBean handleException(Throwable t, HttpServletResponse response) throws Throwable {
        log.error("----- exception occurs,log in @RestControllerAdvice ----", t);
        return ResultBean.fail(BizCode.FAIL, StackTraceGetter.getStackTrace(t));
    }

    /**
     * 处理异常,一般是请求的endpoint不存在就会进入这里
     *
     * @param t
     * @param request
     * @param response
     * @return
     */
    @GetMapping("${server.error.path:${error.path:/error}}")
    public ResultBean error(/*Exception e*/Throwable t, HttpServletRequest request, HttpServletResponse response) {
        ResponseEntity<Map<String, Object>> error = basicErrorController.error(request);
        // four field in map: timestamp/status/exception/path
        Map<String, Object> body;
        String notExistingPath = error == null ? null : ((body = error.getBody()) == null ? null : String.valueOf(body.get("path")));
        return ResultBean.fail(BizCode.ENDPOINT_NOT_EXIST, "path:" + notExistingPath);
    }
}

2、方法二

重写 BasicErrorController,具体的详细参考网上的教程。

附录

  • filter

    package com.wyf.test.testrestcontrolleradvice.config;
    
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @Component
    public class MyFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpReq = (HttpServletRequest) request;
            System.out.println("Filter begin," + httpReq.getServletPath());
            chain.doFilter(request, response);
            System.out.println("Filter end," + httpReq.getServletPath());
        }
    }
    
  • interceptor

    package com.wyf.test.testrestcontrolleradvice.config;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 校验接口调用是否可信<br>
     *
     * @author Stone
     */
    public class SpringInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
            System.out.println("springinterceptor: preHandle," + request.getServletPath());
            return true;
        }
    
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                               ModelAndView modelAndView) throws Exception {
            System.out.println("springinterceptor: postHandle," + request.getServletPath());
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                throws Exception {
            System.out.println("springinterceptor: afterCompletion," + request.getServletPath());
    
        }
    }
    
    

    下面是配置类

    package com.wyf.test.testrestcontrolleradvice.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    @Configuration
    public class SpringInterceptorConfig extends WebMvcConfigurerAdapter {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new SpringInterceptor()).addPathPatterns("/**");
        }
    
    }
    
  • AOP

    
    package com.wyf.test.testrestcontrolleradvice.config;
    
    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.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    @Slf4j
    public class ControllerAspect {
        @Pointcut(
                "((@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)) " +
                        "&& (@annotation(org.springframework.web.bind.annotation.GetMapping) " +
                        "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
                        "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
                        "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
                        "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)))")
        public void pointCutControllerMethod() {
        }
    
        @Around(value = "pointCutControllerMethod()")
        public Object aroundRestApi(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("----- AOP aspect ---- begin");
            try {
                return joinPoint.proceed();
            } catch (Throwable e) {
                throw e;
            } finally {
                System.out.println("----- AOP aspect ---- end");
            }
        }
    
    
    }
    

    下面是需要引入的依赖

    <!-- AOP,Springboot默认未引入,需要自行引入-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值