SprignBoot中的一些小知识点(二)

目录

1、SpringBoot 集成SpringMVC 底层,表单提交REST风格请求,后端GET/POST/PUT/DELETE 四种方式处理细节

2、请求映射原理(只看RequestMappingHandlerMapping)

3、@MatrixVariable 接收请求的矩阵变量(不是传统意义的请求参数)


1、SpringBoot 集成SpringMVC 底层,表单提交REST风格请求,后端GET/POST/PUT/DELETE 四种方式处理细节

首先,前端表单只能发起 GET 和 POST 请求,但后端四种请求均可处理。那么前端如何发起DELETE 和 PUT 请求呢?

如下图所示,四种请求,在后端不作任何配置的情况下,同样是 /user 路径,get 和 post 能找到对应的后端请求,但是 delete 和 put 却都跑到了 后端的 get 请求里面

我们看一下SpringBoot 底层如何处理的请求

我们找到 WebMvcAutoConfiguration  下的 Http方法过滤器

(注释:SpringBoot 底层关于SpringMVC 的配置基本在 WebMvcAutoConfiguration 这个类中可以找到)


    @Bean
    @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
    @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"},
        matchIfMissing = false
    )
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

我们发现,它已经默认给返回了一个过滤器,直接点进去 ,发现他继承了 HiddenHttpMethodFilter ,在 HiddenHttpMethodFilter 中我们发现真正过滤http方法的实现代码如下:

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        // 当请求为POST 时
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            // 续上,我才会将你请求中的一个key为 _method 的value拿出来
            String paramValue = request.getParameter(this.methodParam);
            // 如果传的方法名不为空
            if (StringUtils.hasLength(paramValue)) {
                // 那么我帮你转成大写
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                // 校验一下,是否在我的承受范围内,这里的 ALLOWED_METHODS ,见注释代码 2
                if (ALLOWED_METHODS.contains(method)) {
                    // 最后返回一个包装的request(详见注释代码3),意思就是帮你转成你想要的那个 DELETE 或PUT
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }




注释代码:
1、
    private String methodParam = "_method";

2、    
static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

3、 
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }
 HttpMethodRequestWrapper 继承了HttpServletRequest ,只是重写了他的构造方法,将你传进来的方法名称放进去而已

那么,依照上面的规则,我们只需要在表单请求中添加key 为 _method 的参数,既可以让表单请求到想要的后端方法了

修改过的表单代码如下:

再次请求delete 和 put ,发现还是不对,反而两个都跳转 post 里面了

先别着急,我们回过头再看下一开始 hiddenHttpMethodFilter 这个过滤器头上的注解

    //我是一个需要被IOC容器管理的组件
    @Bean
    //按需加载:如果用户没配我,我就加载我,反之不加载
    @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
    //按需加载:看你有没有配置 spring.mvc.hiddenmethod.filter 这个属性,你配置true 我才加载它,你不配置,我就默认false,于是你后面的一切过滤条件都不会生效
    @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"},
        matchIfMissing = false
    )

那好吧,我们在yaml中配置一下这个属性为true,再试

发现,这次可以了✅

2、请求映射原理(只看RequestMappingHandlerMapping)

我们再深入一下,看SpringBoot底层处理请求,如何准确找到要访问的路径

我们知道,所有请求来到后端都会经过DispacherServlet

先找到DispacherServlet :/org/springframework/spring-webmvc/5.3.6/spring-webmvc-5.3.6.jar!/org/springframework/web/servlet/DispatcherServlet.class

发现 doPost doGet 的实现,我们在 DispacherServlet 继承的 FrameworkServlet 中 找到了 doPost 的实现,而doPost又调用  processRequest()

    protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

继续进去processRequest  查看具体的实现代码,在DispacherServlet 中 找到了 doService 的实现代码

    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.logRequest(request);
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap();
            Enumeration attrNames = request.getAttributeNames();

            label104:
            while(true) {
                String attrName;
                do {
                    if (!attrNames.hasMoreElements()) {
                        break label104;
                    }

                    attrName = (String)attrNames.nextElement();
                } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }

        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
        if (this.flashMapManager != null) {
            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
            if (inputFlashMap != null) {
                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
            }

            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
        }

        RequestPath previousRequestPath = null;
        if (this.parseRequestPath) {
            previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
            ServletRequestPathUtils.parseAndCache(request);
        }

        try {
            // 前面都是初始化的set方法
            this.doDispatch(request, response);
        } finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
                this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }

            ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
        }

    }

掠过一系列set初始化方法,继续进入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;
                    // 这里最关键,获取处理器(即那个controller 处理请求)
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    String method = request.getMethod();
                    boolean isGet = "GET".equals(method);
                    if (isGet || "HEAD".equals(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    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);
            }

        }
    }

只看关键的一部,进入 this.getHandler(processedRequest)

    @Nullable
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            Iterator var2 = this.handlerMappings.iterator();

            while(var2.hasNext()) {
                HandlerMapping mapping = (HandlerMapping)var2.next();
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }

        return null;
    }

实际发起一个/ user 的 GET 请求,第一行断点,我们看到handlerMappings 中有四个mapping

可以简单看到,第一个是关于controller 的mapping,第五个是欢迎页的mapping

这个getHandler方法逻辑就是,拿request 通过while循环,匹配到适合的那个handler,并且返回给你供你使用

3、@MatrixVariable 接收请求的矩阵变量(不是传统意义的请求参数)

场景举例:通常会有一些信息存储在Session中,sessionId通常会存储在Cookie中进行前后端传递,如果用户吧该网站的Cookie 设置为禁用,那么我们就无法获取Cookie 进而无法获取Session中的某些信息。这时就可以使用区别于表单传参的方式,使用矩阵变量传参方式进行前后端交互。

例如这样写

前端:

/car/sell;low=34;brand=byd.bmw

后端接收:

@GetMapping("cars/sell")
public Map carSell(@MatrixVariable("low") Integer low,
                   @MatrixVariable("brand") List<String> brand){
          ...
}

但是矩阵变量在SpringBoot自动配置文件中的 UrlPathHelp 中又是默认禁止的,我们需要手动开启矩阵变量功能

我们先看他是怎么禁用的,然后在去开启

找到WebMvcAutoConfiguration 中的 configurePathMatch 路径映射配置

        public void configurePathMatch(PathMatchConfigurer configurer) {
            if (this.mvcProperties.getPathmatch().getMatchingStrategy() == MatchingStrategy.PATH_PATTERN_PARSER) {
                configurer.setPatternParser(new PathPatternParser());
            }

            configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
            configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
            this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {
                String servletUrlMapping = dispatcherPath.getServletUrlMapping();
                if (servletUrlMapping.equals("/") && this.singleDispatcherServlet()) {
                    UrlPathHelper urlPathHelper = new UrlPathHelper();
                    urlPathHelper.setAlwaysUseFullPath(true);
                    configurer.setUrlPathHelper(urlPathHelper);
                }

            });
        }

倒数第三行代码中的UrlPathHelper 中有一个 removeSemicolonContent 类型为布尔的变量默认是 true,他的意思就是移除分号内容

这就说明但凡在一个后端采用SpringBoot框架构建的网站的健康的网址中加入分号的后缀,都会被默认过滤掉

正好我们矩阵变量的传递方式也是以分号间隔的方式坠在网址后面,同样会被 UrlPathHelper 默认过滤掉

解除默认过滤配置的封印需要自己新增配置类覆盖默认配置

两种方式:第一种:配置类实现 WebMvcConfigurer 中的 configurePathMatch方法,并设置 removeSemicolonContent 为 false

                  第二种:自己新增一个配置类,并在配置类中新增一个用@bean注解加持的方法,返回一个 removeSemicolonContent 为 false 的WebMvcConfigrer

两种方法如下图所示:

原文地址:https://www.cnblogs.com/dk1024/p/14801725.html