请求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());
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;
@RestControllerAdvice
@RestController
@Slf4j
public class GlobalExceptionHandler {
@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));
}
@GetMapping("${server.error.path:${error.path:/error}}")
public ResultBean error(Throwable t, HttpServletRequest request, HttpServletResponse response) {
ResponseEntity<Map<String, Object>> error = basicErrorController.error(request);
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;
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");
}
}
}
下面是需要引入的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
|