[Java SE/JDK/Servlet/Tomcat] 核心源码精讲:javax.servlet.http.HttpServletRequest

0 引言

  • 参见本文: CASE: HttpServletRequest#getHeader(headerNameWithIgnoreCase):获取header时将不区分大小写 章节

1 概述 : HttpSerletRequest //TODO

2 核心API

  • request.getSession().getId()

服务端创建,一个浏览器独占一个session对象(默认情况下)
在多实例部署的时候,用户第一次登录的时候,我们可以将request.getSession().getId()作为key,然后将用户的信息作为value,存储到redis中,以方便下次请求需要权限验证的接口的时候的验证。

request.getSession().getId() E9BC57198DC995860E3DA208642E5372 (32位)
  • request.getRequestedSessionId()

客户端的,也就是浏览器里面的。
比如在用户第一次登录的时候,这里是null,当用户登录了之后,服务端的request.getSession().getId()就会同步到这里。这里就会有了。
该值和服务端的request.getSession().getId()是保持一致的。

request.getRequestedSessionId() null

2.X 使用示例

ApplicationProjectName:MedicineMs
login.jsp[action:login|method:get] to loginServlet

Output:
request.getAuthType() null
request.getCharacterEncoding() UTF-8
request.getContentLength() -1
request.getContextPath() /MedicineMS
request.getLocalAddr() 0:0:0:0:0:0:0:1
request.getLocalName() 0:0:0:0:0:0:0:1
request.getLocalPort() 8080
request.getMethod() GET
request.getPathInfo() null
request.getProtocol() HTTP/1.1
request.getQueryString() accountNo=staff002&password=12345678
request.getRealPath() D:\Tomcat\MY_WEBAPPS\MedicineMS
request.getRequestURI() /MedicineMS/login
request.getRequestedSessionId() 3210E9844068DDD59A7B3DAB0E195393 (32位)
request.getScheme() http
request.getServletPath() /login
request.getSession() org.apache.catalina.session.StandardSessionFacade@33d55ddc
request.getUserPrincipal() null

3 源码分析

CASE: HttpServletRequest#getHeader(headerNameWithIgnoreCase):获取header时将不区分大小写

[1] 环境/版本

  • springboot: 2.3.12.RELEASE
  • org.springframework:spring-webmvc: 5.2.15.RELEASE

即 : springmvc

  • tomcat-embed: 9.0.46 (springboot内嵌的tomcat)
  • javax.servlet:javax.servlet-api:4.0.1

javax.servlet.http.HttpServletRequest 的 所属jar包

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>javax.servlet-api</artifactId>
</dependency>
  • 调试工具: IDEA

[2] 故事背景: HttpServletRequest#getHeader(headerNameWithIgnoreCase):获取header时不区分大小写的需求

  • 最近项目上有个业务需求,翻译成技术需求,即:将request.headers中的几个header入参转换成request.body(pageRequest)中的内置参数。

为便于灵活配置,header 参数名称是动态可配置的(存放于nacos配置中心),比如:sysCodeAccept-Language

  • 技术实现,主要就 springmvcorg.springframework.web.bind.WebDataBinder ,并结合 javax.servlet.http.HttpServletRequest,实现将header中的指定参数转发至request.body(pageRequest).params

核心代码如下:

[2-1] com.xxx.biz.common.util.WebDataBinderUtils

package com.xxx.biz.common.util;

import com.xxx.common.dto.page.PageRequest;
import com.xxx.common.dto.serviceconfig.ForwardHeaderToParamConfig;
import com.xxx.common.dto.serviceconfig.ForwardedHeaders;
import com.xxx.common.dto.serviceconfig.ServiceConfig;
import com.xxx.common.exception.CommonErrEnum;
import com.xxx.common.utils.CollectionUtil;
import com.alibaba.fastjson2.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.WebDataBinder;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
 * @author xxx
 * @version v1.0
 * @create-time 2025/8/6 14:44
 * @update-time 2025/8/6 14:44
 * @description ...
 * @refrence-doc
 * @gpt-promt
 */
public class WebDataBinderUtils {
    private final static Logger log = LoggerFactory.getLogger( WebDataBinderUtils.class );

    /**
     * 将 指定的 headers 数据转发至 request.body.params 中
     * @note
     *  1. 将被应用到所有 @RequestMapping 注解方法,在其执行之前初始化数据绑定器
     *  2. 此方法不稳定/不可靠,建议弃用 (2025-10-14 408675)
     * @reference-doc
     *  [0] https://zhuanlan.zhihu.com/p/115902823 - CSDN - https://blog.csdn.net/airhhh/article/details/104905923 【推荐】
     *  [1] SpringMVC Databinding(数据绑定) - jianshu - https://www.jianshu.com/p/eda34afd9b8a
     *  [2] SpringBoot2教程29整合SpringMVC之@InitBinder处理请求参数的绑定(一) - Zhihu - https://zhuanlan.zhihu.com/p/115902823
     *  [3] SpringBoot教程30整合SpringMVC之WebDataBinder处理请求参数绑定(二) - Zhihu - https://zhuanlan.zhihu.com/p/117306638
     *  [4] SpringMVC常见组件之DataBinder数据绑定器分析 - CSDN - https://blog.csdn.net/J080624/article/details/121163715
     *  [5] SpringMVC-@InitBinder - segmentfault - https://segmentfault.com/a/1190000040157393
     *  [6] SpringMVC之WebDataBinder处理请求参数绑定(二) - CSDN - https://blog.csdn.net/qianfeng_dashuju/article/details/105706264
     * @param webDataBinder
     * @param request
     */
    @Deprecated
    public static void forwardRequestHeadersToParamsDataBinder(
        WebDataBinder webDataBinder, HttpServletRequest request, ServiceConfig serviceConfig
    ){
        String requestUrl = request.getRequestURI().toString(); //eg: "/bdp/public/api/V2/bdp-data-service/100031/v1.4", "/jqueryLearn/resources/request.jsp" , ...
        try {
            PageRequest<Map<String,Object>> pageRequest = (PageRequest<Map<String, Object>>) webDataBinder.getTarget();
            forwardRequestHeadersToParamsDataBinder(pageRequest, request, serviceConfig);
        } catch (Exception exception) {
            String errorMessage = String.format("Forward headers to params by DataBinder fail!errorCode:%s,requestUrl:%s", CommonErrEnum.FORWARD_REQUEST_HEADERS_TO_PARAMS.name(), requestUrl);
            log.error(errorMessage + ", exception:", exception);
            throw new RuntimeException(errorMessage, exception);
        }
    }

    /**
     * 数据绑定:转发指定的请求头到 pageRequest 中
     * @param pageRequest 接口请求对应的页面请求对象(追加的请求头将直接写入此页面请求对象中)
     * @param request 接口请求对应的 原始HTTP请求对象
     * @param serviceConfig 应用服务配置
     */
    public static void forwardRequestHeadersToParamsDataBinder(
        Object pageRequest, HttpServletRequest request, ServiceConfig serviceConfig
    ){
        String requestUrl = request.getRequestURI().toString(); //eg: "/sms/xxx/xx-list/v1.0", "/jqueryLearn/resources/request.jsp" , ...
        try {
            ForwardedHeaders forwardedHeaders = serviceConfig.getForwardedHeaders();//com.xxx.biz.dataservice.controller.v2.CommonSearchController#serviceConfig;
            if (ObjectUtils.isEmpty(forwardedHeaders) || forwardedHeaders.getEnable().equals(Boolean.FALSE)) {
                log.debug("Fail to forward headers to body's params because that `service-config.forwardedHeaders` be empty or not enabled!requestUrl:{}", requestUrl);
                return;
            }
            if (log.isDebugEnabled()) {
                log.debug("Start to forward request's headers to request body params with `service-config.forwardedHeaders`,config data as follows : {}!requestUrl:{},request.getPathInfo: {}", requestUrl, forwardedHeaders, request.getPathInfo());
            }

            //获取被绑定对象: PageRequest
            //PageRequest<Map<String,Object>> pageRequest = (PageRequest<Map<String, Object>>) webDataBinder.getTarget();
            Map<String, Object> dynamicParams = null;
            if(pageRequest instanceof PageRequest){
                PageRequest<Map<String,Object>> pageRequest2 = (PageRequest<Map<String, Object>>) pageRequest;
                dynamicParams = pageRequest2.getParams();
            } else if(pageRequest instanceof com.xxx.common.dto.page.PageRequest){
                com.xxx.common.dto.page.PageRequest<Map<String,Object>> pageRequest2 = (com.xxx.common.dto.page.PageRequest<Map<String, Object>>) pageRequest;
                dynamicParams = pageRequest2.getParams();
            } else {//不支持的 pageRequest 类型
                throw new RuntimeException("Not support the type of page request!PageRequest.class:" + pageRequest.getClass().getCanonicalName() );
            }
            if (ObjectUtils.isEmpty(pageRequest)) {
                log.debug("Fail to forward headers to body's params because that `request.body(pageRequest)` is empty!requestUrl:{},pageRequest:{}", requestUrl, JSON.toJSONString(pageRequest));
                return;
            } else {// pageRequest 非空
                if (dynamicParams == null) {
                    //排除掉( params != null && params.size() == 0 )的情况: pageRequest:{"currentPage":1,"needPaging":true,"pageSize":10,"params":{}}
                    log.info("Forward headers to body's params because that `request.body(pageRequest).params` is null!requestUrl:{},pageRequest:{}", requestUrl, JSON.toJSONString(pageRequest));
                }
            }

            List<ForwardHeaderToParamConfig> forwardHeaderToParamConfigList = forwardedHeaders.getHeaders();
            StringBuilder targetHeadersLog = new StringBuilder();
            Map<String, Object> finalDynamicParams = dynamicParams;
            forwardHeaderToParamConfigList.stream().forEach(forwardedHeaderConfig -> {
                String headerName = forwardedHeaderConfig.getHeader();
                String headerValue = request.getHeader(forwardedHeaderConfig.getHeader());
                String paramName = forwardedHeaderConfig.getParam();
                if (ObjectUtils.isEmpty(headerValue)) {
                    targetHeadersLog.append(String.format("| header(%s) is empty! |", headerName));
                } else {
                    targetHeadersLog.append(String.format("| headerName:%s, headerValue:%s, paramName:%s |", headerName, headerValue, paramName));
                    finalDynamicParams.put(paramName, headerValue);
                }
            });

            if (log.isDebugEnabled()) {
                log.debug(
                    " requestUrl: {}, targetHeaders: {}, request.headers: {}, request.body(pageRequest).params:{}"
                    , requestUrl, targetHeadersLog.toString(), CollectionUtil.enumerationToList(request.getHeaderNames()), JSON.toJSONString(finalDynamicParams)
                );
            }
        } catch (Exception exception) {
            String errorMessage = String.format("Forward headers to params fail!errorCode:%s,requestUrl:%s", CommonErrEnum.FORWARD_REQUEST_HEADERS_TO_PARAMS.name(), requestUrl);
            log.error(errorMessage + ", exception:", exception);
            throw new RuntimeException(errorMessage, exception);
        }
    }
}

[2-2] CommonSearchController

import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.WebDataBinder;
import javax.servlet.http.HttpServletRequest;

@RestController("com.controller.v2.CommonSearchController")
@Validated
@Api(tags = "DATA2API Controller # V2")
public class CommonSearchController implements DataServiceOpenApi {
    
    // ...

    @Autowired
    private ServiceConfig serviceConfig;

    // ...

    @InitBinder
    public void forwardedHeadersToParamsDataBinder(WebDataBinder binder, HttpServletRequest request) {
        WebDataBinderUtils.forwardRequestHeadersToParamsDataBinder(
            webDataBinder, request , serviceConfig
        );
    }

    // ...
}

image

  • 那么,我写这篇博客的目的是什么呢?
  • 你有没有这么一个疑惑:request.getHeader(headerName),这个基于Tomcat.catalina实现的方法,是否区分headerName的大小写?

如果不区分大小写,那还好办;如果区分大小写,就尴尬了————我将需要将Accept-Language的每一种字母大小写的可能性都要一一进行配置!
经过源码分析,答案是:request.getHeader(headerName)不区分大小写
感兴趣的朋友,可以进入第2章节,一起看看源码

[3] 源码分析

Step1 javax.servlet.http.HttpServletRequest : request.getHeader("Accept-Language")

import javax.servlet.http.HttpServletRequest;

//...
request.getHeader("Accept-Language")
//...

image

Step2 javax.servlet.http.HttpServletRequest#getHeader

javax.servlet.http.HttpServletRequest#getHeader

image

Step3 org.apache.catalina.connector.Request#getHeader

特别说明:org.apache.catalina.connector.Request#getHeader 其实现了接口:javax.servlet.http.HttpServletRequest#getHeader

org.apache.catalina.connector.Request#getHeader

image

Step4 org.apache.coyote.Request#getHeader

org.apache.coyote.Request#getHeader

image

Step5 org.apache.tomcat.util.http.MimeHeaders#getHeader

org.apache.tomcat.util.http.MimeHeaders#getHeader

image

Step6 org.apache.tomcat.util.http.MimeHeaders#getValue(java.lang.String)

org.apache.tomcat.util.http.MimeHeaders#getValue(java.lang.String)

image

X 参考文献

posted @ 2023-09-16 19:29  千千寰宇  阅读(1686)  评论(0)    收藏  举报