Spring Boot出現Request method 'POST' not supported,深入原始碼原因分析
工程
- 專案靜態資源目錄結構
testConverter.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="testConverter" method="**POST**"> <input type="text" name="test"> <input type="submit" value="submit"> </form> </body> </html>
- 專案說明
在不使用themleaf的情況下,通過前端以POST方式提交from表單到controller,controller處理後使用InternalResourceViewResolver進行檢視解析,轉發到靜態資原始檔夾static下的目標頁面
擴充套件SpringMVC
@Configuration public class MyWebMvcConfiguration implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { MyConverter myConverter = new MyConverter(); registry.addConverter(myConverter);//新增自定義Converter } @Override public void configureViewResolvers(ViewResolverRegistry registry) { //新增自定義 InternalResourceViewResolv InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver(); internalResourceViewResolver.setPrefix("/");// 給contrlloer返回值設定前後綴 internalResourceViewResolver.setSuffix(".html"); registry.viewResolver(internalResourceViewResolver); }
自定義Converter
@Order(1) public class MyConverter implements Converter<String, Student> { @Override public Student convert(String s) { System.out.println("myconverter");//將前端提交的String轉為Student物件 String[] values = s.split("-"); String lastName= values[0]; int age = Integer.parseInt(values[1]); int departmentId = Integer.parseInt(values[2]); String departmentName =values[3]; Department department = new Department(departmentId,departmentName,null); return new Student(null, lastName,age,department); } }
Controller
@Controller
public class MyController {
//@ResponseBody
@RequestMapping("/testConverter")
public String testConverter(@RequestParam("test") Student student){
System.out.println(student);
return "testConverter2";//轉發到static資料夾下的testConverter2.html頁面
}
}
**
問題
**
當前端表單以POST方式提交請求時,返回405錯誤頁面,而以GET方式則可以到目標頁面
分析
從上面步驟看,控制檯成功列印Student資訊,證明自定義Converter有效,併成功將前端傳過來的String轉換為了Student,而且進一步說明Controller在前面程式碼執行沒有問題,那麼問題只能發生在return "testConverter2";
,那麼是什麼原因呢?
-
配置的InternalResourceViewResolver解析檢視有誤?
難道InternalResourceViewResolver未起作用?沒有將testConverter2解析為/testConverter2.html?但是如果將請求改為GET,則是可以到目標頁面的,通過debug的方式發現InternalResourceViewResolver是可以成功解析檢視的 -
上面的異常為不支援POST請求,那麼問題出在那呢?
通過debug,進入DispatchSeverlet,執行doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//public class **RequestMappingHandlerAdapter** extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean
//請求將由RequestMappingHandlerAdapter處理
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) { //**判斷請求是否是GET或HEAD**
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//RequestMappingHandlerAdapter(ha)處理請求,返回ModelAndView
//debug進去後,會發現問題就是在這一步發生的,具體後面詳細介紹
//執行父類AbstractHandlerMethodAdapter **handle方法**
// @Nullable
//public final ModelAndView handle(HttpServletRequest request, HttpServletResponse //response, Object handler) throws Exception {
//return **this.handleInternal(request, response, (HandlerMethod)handler);**具體見下
// }
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
執行handle方法,handle方法呼叫handleInternal方法(handle方法與hanleInternal方法均為RequestMappingHandlerAdapter父類AbstractHandlerMethodAdapter定義的方法)
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
this.checkRequest(request);//該方法會檢查請求的型別,this為RequestMappingHandlerAdapter型別
//protected final void checkRequest(HttpServletRequest request) throws ServletException {
//String method = request.getMethod();
// if (this.supportedMethods != null && !this.**supportedMethods**.contains(method)) {
// throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
//} else if (this.requireSession && request.getSession(false) == null) {
// throw new HttpSessionRequiredException("Pre-existing session required but none found");
// }
// }
.....//省略
}
執行hanleInternal方法中的checkRequest方法
執行結果
該方法沒有丟擲異常,所以handleInternal方法順序執行,返回ModelAndView
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndView var15;
try {
WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);//資料繫結工廠
//WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);獲得資料繫結器
ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);
.....//省略
//進行引數處理,進行轉換,即使用MyConverter
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
......//省略
}
回到DispatcherSeverlet的doDispatch方法
當前瀏覽器網頁狀況
當前後臺列印資訊
注意當前的mappedhandler
解析檢視
將解析得到的檢視放入候選檢視集合中
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
List<View> candidateViews = new ArrayList();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
Iterator var5 = this.viewResolvers.iterator();
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);//新增候選檢視
}
返回最佳檢視物件
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
if (requestedMediaTypes != null) {
List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;//從candidateViews獲取最佳檢視物件並返回
}
}
執行結果
返回到DispatcherSeverlet,執行doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
執行該方法
執行handleRequest(HttpServletRequest request, HttpServletResponse response)
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Resource resource = this.getResource(request);//獲取資源
if (resource == null) {
logger.debug("Resource not found");
response.sendError(404);
} else if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", this.getAllowHeader());
} else {
this.checkRequest(request);
if ((new ServletWebRequest(request, response)).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
} else {
this.prepareResponse(response);
MediaType mediaType = this.getMediaType(request, resource);
if ("HEAD".equals(request.getMethod())) {
找到目標資源
- 報錯原因(重點)
protected final void checkRequest(HttpServletRequest request) throws ServletException {
String method = request.getMethod();
if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
} else if (this.requireSession && request.getSession(false) == null) {
throw new HttpSessionRequiredException("Pre-existing session required but none found");
}
}
從上面程式碼看,該方法就是判斷是否滿足條件,然後決定是否丟擲異常
執行結果
該異常構造方法
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) {
//得到丟擲異常的資訊
this(method, supportedMethods, "Request method '" + method + "' not supported");
}
至此原因以找到,即不支援POST的方式獲取靜態資源
解決辦法
1)使用GET方式,即表單以GET方式提交
2)進行重定向
@Controller
public class MyController {
//@ResponseBody
@RequestMapping("/testConverter")
public String testConverter(@RequestParam("test") Student student){
System.out.println(student);
return "redirect:testConverter2.html";//重定向
}
}