IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 请求springboot接口的路径不存在,如何自定义status code和返回的JSON格式 -> 正文阅读

[Java知识库]请求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>
    
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 20:27:04  更:2022-10-08 20:31:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 7:41:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码