1. 程式人生 > >Spring Boot出現Request method 'POST' not supported,深入原始碼原因分析

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

handleInternal返回結果

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";//重定向
    }
}