實(shí)戰(zhàn)技巧!Spring Boot 非常有用的五個(gè)開發(fā)技巧,請(qǐng)收藏
環(huán)境:SpringBoot3.2.5
1. 獲取請(qǐng)求/響應(yīng)對(duì)象
在編寫Controller中,我們通常可以通過如下的2種方式直接獲取Request和Response對(duì)象,如下示例:
@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中注入對(duì)象,你也可以在方法參數(shù)中獲?。?/span>
@GetMapping
public ResponseEntity<Object> query(HttpServletRequest request,
HttpServletResponse response)
// ...
}
如果你需要在其它組件中獲取該對(duì)象,如果通過方法參數(shù)傳遞那就太麻煩了,這時(shí)候我們就可以使用如下方式:
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() ;
HttpServletRequest request = attributes.getRequest() ;
HttpServletResponse response = attributes.getResponse() ;
直接通過ThreadLocal獲取,而這個(gè)數(shù)據(jù)的存入則是由DispatcherServlet完成,不過默認(rèn)還有一個(gè)RequestContextFilter也會(huì)做這個(gè)事,但是會(huì)被該Servlet覆蓋。
ThreadLocal綁定當(dāng)前線程,如果遇到子線程怎么辦呢?
如果希望在子線程中也能獲取當(dāng)前上下文,那么你需要進(jìn)行如下的配置才可:
@Bean(name = "dispatcherServletRegistration")
DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
// 設(shè)置從父線程中繼承
dispatcherServlet.setThreadContextInheritable(true) ;
// ...
return registration;
}
測(cè)試接口
@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") ;
}
控制臺(tái)輸出如下
圖片
成功獲取。
警告!如下方式操作,你將收獲一個(gè)錯(cuò)誤
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中獲取數(shù)據(jù),將拋出如下錯(cuò)誤
圖片
這是因?yàn)橹骶€程已經(jīng)將Request,Response對(duì)象回收了。
總結(jié):不建議開啟子線程共享。
2. 異步攔截器
Spring提供一個(gè)專門處理異步請(qǐng)求的攔截器AsyncWebRequestInterceptor,該接口包含一個(gè)在處理異步請(qǐng)求期間被調(diào)用的回調(diào)方法。
當(dāng)處理器開始處理異步請(qǐng)求時(shí),DispatcherServlet會(huì)像平常一樣退出,而不調(diào)用postHandle和afterCompletion方法,因?yàn)檎?qǐng)求處理的結(jié)果(例如ModelAndView)在當(dāng)前線程中不可用,且處理尚未完成。在這種情況下,會(huì)調(diào)用afterConcurrentHandlingStarted(WebRequest)方法,允許實(shí)現(xiàn)執(zhí)行諸如清理線程綁定屬性之類的任務(wù)。
當(dāng)異步處理完成時(shí),請(qǐng)求會(huì)被分發(fā)到容器中進(jìn)行進(jìn)一步處理。在這個(gè)階段,DispatcherServlet會(huì)像平常一樣調(diào)用preHandle、postHandle和afterCompletion方法。
public class LogInterceptor implements AsyncWebRequestInterceptor {
// 請(qǐng)求一開始會(huì)執(zhí)行一次
@Override
public void preHandle(WebRequest request) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s, %s, 開始處理%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
}
// 當(dāng)異步請(qǐng)求結(jié)束時(shí)執(zhí)行
@Override
public void postHandle(WebRequest request, ModelMap model) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s, postHandle%n", Thread.currentThread().getName()) ;
}
// 當(dāng)異步請(qǐng)求結(jié)束時(shí)執(zhí)行
@Override
public void afterCompletion(WebRequest request, Exception ex) throws Exception {
System.err.printf("AsyncWebRequestInterceptor >>> %s afterCompletion%n", Thread.currentThread().getName()) ;
}
// 異步請(qǐng)求開始時(shí)執(zhí)行
@Override
public void afterConcurrentHandlingStarted(WebRequest request) {
System.err.printf("AsyncWebRequestInterceptor >>> %s, %s, 異步處理%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
}
}
注冊(cè)異步攔截器:
@Component
public class WebInterceptorConfig implements WebMvcConfigurer{
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(new LogInterceptor()).addPathPatterns("/admin/**") ;
}
}
下面通過如下異步接口進(jìn)行測(cè)試
@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 - 執(zhí)行任務(wù)%n", System.currentTimeMillis(), Thread.currentThread().getName()) ;
TimeUnit.SECONDS.sleep(3) ;
return "異步數(shù)據(jù)" ;
}
};
}
輸出結(jié)果
圖片
等待異步處理完成以后再執(zhí)行preHandle、postHandle和afterCompletion方法。
實(shí)際整個(gè)異步請(qǐng)求從開始到結(jié)束,preHandle是執(zhí)行了兩回。
3. 獲取當(dāng)前請(qǐng)求相關(guān)信息
Spring MVC在處理一個(gè)請(qǐng)求時(shí),會(huì)為我們做很多的事,其中會(huì)往Request對(duì)象設(shè)置一些非常有用的數(shù)據(jù),如下所示:
獲取當(dāng)前的請(qǐng)求路徑
String key = ServletRequestPathUtils.PATH ;
String requestPath = request.getAttribute(key) ;
獲取當(dāng)前請(qǐng)求最佳匹配的路徑(Controller中定義的路徑)
String key = HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;
String pathPattern = request.getAttribute(key) ;
返回:/params/{type}/{id}
獲取當(dāng)前請(qǐng)求中的路徑參數(shù)值
String key = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE ;
String pathPattern = request.getAttribute(key) ;
返回:{id=666, type=s0}
4. 類型轉(zhuǎn)換器注冊(cè)方式
Spring 本身提供了非常多的類型轉(zhuǎn)換器,絕大多數(shù)情況下我們是不需要再自定義類型轉(zhuǎn)換器的。如果確實(shí)需要自定義類型轉(zhuǎn)換器,那么我們通常會(huì)通過如下的方法進(jìn)行注冊(cè)自定義的轉(zhuǎn)換器:
@Component
public class TypeConvertConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new EnumConverter()) ;
}
}
感覺是不是有點(diǎn)麻煩。其實(shí)我們可以直接定義類型轉(zhuǎn)換器為Bean對(duì)象即可,并且支持:GenericConverter,Converter,Printer,Parser類型的轉(zhuǎn)換器。
@Component
public class EnumConverter implements Converter<Sex, Integer> {
public Integer convert(Sex source) {
if (source == null) {
return 0 ;
}
return source.getCode() ;
}
}
注意:這里的自定義轉(zhuǎn)換器并不支持有關(guān)屬性配置的類型轉(zhuǎn)換。
5. 接口不存在時(shí)特殊處理
當(dāng)我們?cè)L問的接口不存在時(shí),默認(rèn)輸出如下:
圖片
或者我們也可以在如下位置提供對(duì)應(yīng)的404.html或4xx.html頁(yè)面
圖片
如上位置添加頁(yè)面后,當(dāng)出現(xiàn)404錯(cuò)誤,將會(huì)調(diào)轉(zhuǎn)到該頁(yè)面。
其實(shí),我們還可以通過如下全局異常的方式處理404錯(cuò)誤,默認(rèn)如果出現(xiàn)404錯(cuò)誤會(huì)拋出NoHandlerFoundException異常。
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Object> noHandlerFount(NoHandlerFoundException e) {
return ResponseEntity.ok(Map.of("code", -1, "message", "接口不存在")) ;
}
}
當(dāng)發(fā)生404后,頁(yè)面展示:
圖片