环境:SpringBoot3.2.5
1. 获取请求/响应对象
在编写Controller中,我们通常可以通过如下的2种方式直接获取Request和Response对象,如下示例:
@RestController
public class UserController {
private final HttpServletRequest request ;
private final HttpServletResponse response ;
public ContextFilterController(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
}
直接在Controller中注入对象,你也可以在方法参数中获取:
@GetMapping
public ResponseEntity<Object> query(HttpServletRequest request,
HttpServletResponse response)
// ...
}
如果你需要在其它组件中获取该对象,如果通过方法参数传递那就太麻烦了,这时候我们就可以使用如下方式:
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() ;
HttpServletRequest request = attributes.getRequest() ;
HttpServletResponse response = attributes.getResponse() ;
直接通过ThreadLocal获取,而这个数据的存入则是由DispatcherServlet完成,不过默认还有一个RequestContextFilter也会做这个事,但是会被该Servlet覆盖。
ThreadLocal绑定当前线程,如果遇到子线程怎么办呢?
如果希望在子线程中也能获取当前上下文,那么你需要进行如下的配置才可:
@Bean(name = "dispatcherServletRegistration")
DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
// 设置从父线程中继承
dispatcherServlet.setThreadContextInheritable(true) ;
// ...
return registration;
}
测试接口
@GetMapping
public ResponseEntity<Object> context() throws Exception {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() ;
HttpServletRequest request = attributes.getRequest() ;
HttpServletResponse response = attributes.getResponse() ;
System.err.printf("%s - %s, %s%n", Thread.currentThread().getName(), request, response) ;
Thread t = new Thread(() -> {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() ;
HttpServletRequest req = attr.getRequest() ;
HttpServletResponse resp = attr.getResponse() ;
System.err.printf("%s - %s, %s%n", Thread.currentThread().getName(), req, resp) ;
}, "T1") ;
t.start() ;
return ResponseEntity.ok("success") ;
}
控制台输出如下
图片
成功获取。
警告!如下方式操作,你将收获一个错误
Thread t = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1) ;
} catch (Exception e) {
e.printStackTrace();
}
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() ;
HttpServletRequest req = attr.getRequest() ;
System.err.println(req.getParameter("token")) ;
}, "T1") ;
如上代码,休眠1s后,在从request中获取数据,将抛出如下错误
图片
这是因为主线程已经将Request,Response对象回收了。
总结:不建议开启子线程共享。
2. 异步拦截器
Spring提供一个专门处理异步请求的拦截器AsyncWebRequestInterceptor,该接口包含一个在处理异步请求期间被调用的回调方法。
当处理器开始处理异步请求时,DispatcherServlet会像平常一样退出,而不调用postHandle和afterCompletion方法,因为请求处理的结果(例如ModelAndView)在当前线程中不可用,且处理尚未完成。在这种情况下,会调用afterConcurrentHandlingStarted(WebRequest)方法,允许实现执行诸如清理线程绑定属性之类的任务。
当异步处理完成时,请求会被分发到容器中进行进一步处理。在这个阶段,DispatcherServlet会像平常一样调用preHandle、postHandle和afterCompletion方法。
public class LogInterceptor implements AsyncWebRequestInterceptor {
// 请求一开始会执行一次
@Override
public void preHandle(WebRequest request) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s, %s, 开始处理%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
}
// 当异步请求结束时执行
@Override
public void postHandle(WebRequest request, ModelMap model) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s, postHandle%n", Thread.currentThread().getName()) ;
}
// 当异步请求结束时执行
@Override
public void afterCompletion(WebRequest request, Exception ex) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s afterCompletion%n", Thread.currentThread().getName()) ;
}
// 异步请求开始时执行
@Override
public void afterConcurrentHandlingStarted(WebRequest request) {
System.err.printf("AsyncWebRequestInterceptor >>> %s, %s, 异步处理%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
}
}
注册异步拦截器:
@Component
public class WebInterceptorConfig implements WebMvcConfigurer{
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(new LogInterceptor()).addPathPatterns("/admin/**") ;
}
}
下面通过如下异步接口进行测试
@GetMapping("/async")
public Callable<String> async() {
System.err.println("async interface...") ;
return new Callable<String>() {
public String call() throws Exception {
System.err.printf("%s, %s - 执行任务%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
TimeUnit.SECONDS.sleep(3) ;
return "异步数据" ;
}
};
}
输出结果
图片
等待异步处理完成以后再执行preHandle、postHandle和afterCompletion方法。
实际整个异步请求从开始到结束,preHandle是执行了两回。
3. 获取当前请求相关信息
Spring MVC在处理一个请求时,会为我们做很多的事,其中会往Request对象设置一些非常有用的数据,如下所示:
获取当前的请求路径
String key = ServletRequestPathUtils.PATH ;
String requestPath = request.getAttribute(key) ;
获取当前请求最佳匹配的路径(Controller中定义的路径)
String key = HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;
String pathPattern = request.getAttribute(key) ;
返回:/params/{type}/{id}
获取当前请求中的路径参数值
String key = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE ;
String pathPattern = request.getAttribute(key) ;
返回:{id=666, type=s0}
4. 类型转换器注册方式
Spring 本身提供了非常多的类型转换器,绝大多数情况下我们是不需要再自定义类型转换器的。如果确实需要自定义类型转换器,那么我们通常会通过如下的方法进行注册自定义的转换器:
@Component
public class TypeConvertConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new EnumConverter()) ;
}
}
感觉是不是有点麻烦。其实我们可以直接定义类型转换器为Bean对象即可,并且支持:GenericConverter,Converter,Printer,Parser类型的转换器。
@Component
public class EnumConverter implements Converter<Sex, Integer> {
public Integer convert(Sex source) {
if (source == null) {
return 0 ;
}
return source.getCode() ;
}
}
注意:这里的自定义转换器并不支持有关属性配置的类型转换。
5. 接口不存在时特殊处理
当我们访问的接口不存在时,默认输出如下:
图片
或者我们也可以在如下位置提供对应的404.html或4xx.html页面
图片
如上位置添加页面后,当出现404错误,将会调转到该页面。
其实,我们还可以通过如下全局异常的方式处理404错误,默认如果出现404错误会抛出NoHandlerFoundException异常。
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Object> noHandlerFount(NoHandlerFoundException e) {
return ResponseEntity.ok(Map.of("code", -1, "message", "接口不存在")) ;
}
}
当发生404后,页面展示:
图片