请求springboot接口的路径不存在,如何自定义status code和返回的JSON格式
Posted 石头wang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了请求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>
以上是关于请求springboot接口的路径不存在,如何自定义status code和返回的JSON格式的主要内容,如果未能解决你的问题,请参考以下文章
如果授权标头不存在,则无法在 Webfilter 中发送自定义正文
SpringBoot2 静态文件路径与接口路径冲突(相同)解决方案