news 2026/10/20 14:15:49

HttpServletRequest 实战指南:参数读取、请求体缓存与文件上传全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HttpServletRequest 实战指南:参数读取、请求体缓存与文件上传全攻略

1. 为什么在注解满天飞的年代还要直接用 HttpServletRequest

先说说我自己的经历。前几年接手过一个老项目,用的是早期版本的 Spring MVC,Controller 里到处是HttpServletRequest request作为方法入参,配合request.getParameter("xxx")手动取值。当时觉得这写法又土又啰嗦,后来在新项目里用惯了@RequestParam、@PathVariable、@RequestBody,就更不想碰它了。直到有一次对接一个第三方回调接口,对方把参数放在了一个自定义的 HTTP 头里面,同时还要求记录完整的原始请求体用于签名校验,我才发现那些注解在这时候帮不上什么忙,最后还是得老老实实把HttpServletRequest拿出来。

这个接口的场景很典型:URL 上带着查询参数,请求体是一段 JSON,但真正用于验签的数据是 Header 里的一个加密串加原始 body 的拼接结果。用@RequestBody接收 JSON 没问题,但拿不到原始字符串,因为框架已经把流里的数据反序列化成对象了,再想读一次输入流就会报错。这时候直接操作HttpServletRequest才是唯一靠谱的路径。也是从那次之后,我开始系统地把HttpServletRequest的常见用法整理了一遍,发现它在实际项目中能做的事情远比想象中多。

这篇文章就把我整理的这些用法分享出来。内容围绕 Java Web 开发中最常用的几个场景:从请求行和 Header 里取参数、从请求体里读 JSON 或表单、处理文件上传、拿会话和客户端信息,以及我在真实项目中踩过的坑。无论你是刚接触 Servlet 的新手,还是天天写接口但很少直接碰HttpServletRequest的老手,这都应该对你有用。

2. 请求行和 Header 里的数据:从 URL 参数到自定义头部取值

HttpServletRequest最基础的职责,就是把客户端发来的 HTTP 请求包了一层,让你能方便地取出请求行和 Header 上的信息。我先说请求行,也就是 URL 那一行。一个典型的请求长这样:

GET /api/order/1001?source=app&debug=true HTTP/1.1 Host: api.example.com User-Agent: Mozilla/5.0 X-Request-Id: 7f3a2b91-c4d8-4e11-9a3f-001

请求行里包含方法、路径和查询字符串三部分。HttpServletRequest分别对应getMethod()、getRequestURI()、getQueryString()。但实际开发中,最常用的是通过getParameter()系列方法获取查询参数的值。

2.1 getParameter 和 getParameterValues 的区别

getParameter("source")返回的是 String 类型,拿到 URL 上的source=app就是app。但有一个场景很容易被忽略:当同一个参数名出现多次时,比如?tag=java&tag=http,getParameter("tag")只返回第一个值java,要拿到全部值得用getParameterValues("tag"),它返回String[]。

这个差异在处理复选框、多选下拉框时非常关键。我见过不少初学者在做批量删除接口时,前端传了id=1&id=2&id=3,后端却只拿到了1,排错排了半天,最后才发现是用了getParameter而不是getParameterValues。如果你不确定参数可能传几个值,直接写String[] ids = request.getParameterValues("id")是最稳妥的。

2.2 参数不存在与空字符串的区分

getParameter还有一个容易让人困惑的点:如果参数不存在,它返回null;如果参数存在但没赋值,比如?name=,它返回空字符串""。这两个状态在接口逻辑里往往代表不同的含义。举个例子,一个搜索接口?keyword=表示用户明确搜了空字符串,可能应该返回空结果或提示输入关键词;而?page=2里根本没有keyword这个参数,可能就应该走默认逻辑。如果只写String keyword = request.getParameter("keyword")然后拼 SQL,这两种情况都会被if (keyword == null || keyword.isEmpty())拦截下来,但实际的业务意图可能完全不同。

我的习惯是先用getParameterMap()看一眼参数是否真的存在。getParameterMap()返回Map<String, String[]>,可以一次性拿到全部参数。判断某个参数是否存在,最准确的方式不是getParameter("x") != null,而是request.getParameterMap().containsKey("x")。因为参数存在但值缺失时,getParameter返回空字符串也是非 null 的,用 containsKey 才能区分"没传"和"传了空值"。

2.3 Header 参数的读取

Header 的读取方式也很直接。getHeader("X-Request-Id")返回单个字符串值,getHeaders("X-Forwarded-For")返回Enumeration<String>,适合同一个头名出现多次的情况,比如代理服务器会叠加X-Forwarded-For链。

说说我实际项目里最常用的几个 Header 场景:

  • 自定义请求 ID:前后端联调时,前端把一次请求的唯一 ID 放在X-Request-Id里,后端从 Header 取出后写入日志和响应头,排查问题的时候直接按 ID 串起整个调用链路,省了老大的劲。
  • 鉴权信息:Token 一般放在Authorization头里,格式通常是Bearer eyJhbGci...,取出后需要去掉前缀再解析,参考代码:
String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // 校验 token }
  • 客户端类型识别:有个需求要根据请求来源返回不同格式的数据,比如 App 端返回精简字段,Web 端返回完整字段。这时通过User-Agent或自定义的X-Client-Type头来判断,比在 URL 上挂?client=app优雅得多。因为 Header 不会出现在浏览器历史、代理日志的 URL 部分,相对更隐蔽,也便于客户端统一维护。

Header 的一个坑是:某些头会被代理覆盖或剥离。比如X-Forwarded-For,如果你们前面有三层 Nginx,第一层没配置透传,后端拿到的就可能是内网 IP。这类问题最好在联调环境就确认清楚,别等上线后才发现 IP 全是 127.0.0.1。

3. 请求体里的秘密:表单参数、JSON 原始串和处理顺序

请求体是 POST、PUT 接口的主要数据来源。很多人以为HttpServletRequest只能读表单参数,其实不是。请求体本质上就是一个输入流,无论里面是 JSON、XML 还是纯文本,你都可以通过getInputStream()按字节读出来,关键在于请求的Content-Type决定了容器怎么解析它。

3.1 application/x-www-form-urlencoded 表单

当浏览器或客户端提交 HTML 表单,或者用axios的默认方式发 POST 时,Content-Type通常是application/x-www-form-urlencoded,body 的内容是name=zhangsan&age=25这种键值对拼接。Servlet 容器在收到这种请求后,会帮你把 body 解析好,所以你依然可以用request.getParameter("name")直接取值。

这里有一个很容易踩的坑:一旦以表单方式提交,getParameter 能读到 body 里的参数,但如果你改用getInputStream()去读,会发现流已经读过了(容器解析表单时会消耗掉 body 内容),或者读到的是空或已关闭的流。所以对表单请求,老老实实用getParameter就行,别想着再读原始 body。

3.2 application/json 请求体:getInputStream 的正确姿势

对 JSON 请求,getParameter是永远拿不到 body 里的数据的,因为容器不解析 JSON 为参数表。必须从输入流中读取。标准操作是:

BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); char[] buf = new char[1024]; int len; while ((len = reader.read(buf)) != -1) { sb.append(buf, 0, len); } String jsonBody = sb.toString();

拿到jsonBody后再用 Jackson 或 Gson 反序列化成对象。这段代码在绝大多数场景下都能正常工作,但有一个重要的细节:请求体只能读一次。输入流一旦读完,再用request.getReader()读取,会拿到空串,因为流已经到达末尾且容器不会重新创建它。这在使用 Spring MVC 的@RequestBody时没问题,因为框架帮你读了;但如果你自己读了一遍,又在后面的拦截器里想用它做参数校验,就会遇到读不到的问题。

我建议封装一个工具方法,把读取 body 的逻辑放在最前面统一处理,后续需要的模块都从缓存里拿,别重复读流。下面是我项目里用的方案:

public class RequestBodyCache { public static String getBody(HttpServletRequest request) throws IOException { // 先从 request attribute 里查缓存 Object cached = request.getAttribute("CACHED_BODY"); if (cached != null) { return cached.toString(); } String body = readBody(request); request.setAttribute("CACHED_BODY", body); return body; } private static String readBody(HttpServletRequest request) throws IOException { BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); char[] buf = new char[1024]; int len; while ((len = reader.read(buf)) != -1) { sb.append(buf, 0, len); } return sb.toString(); } }

把这个工具放在过滤器或拦截器入口调用,后面再有人要读 body,直接从 attribute 里拿。这个方法我用了很久,稳定性很高。

3.3 读取顺序:先读流再做其他判断

还有一个顺序问题值得强调。如果你打算在过滤器里对请求体做统一的日志记录或签名校验,一定要让读取流的操作发生在 Controller 处理之前,并且读完后把数据缓存。因为 Controller 一旦通过@RequestBody把流消费掉了,过滤器里再想读就晚了。反过来说,如果过滤器先读了流,但没有缓存,Controller 里的@RequestBody就会因为流已经耗尽而解析失败,报java.io.IOException: Stream closed或拿到 null。

正确做法是:要么用ContentCachingRequestWrapper(Spring 提供的包装类)包装一下 request,让流的读取可以被重复进行;要么就用上面说的 attribute 缓存方案。下面是一个用包装类的过滤器示例:

@Component public class CachingRequestBodyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); chain.doFilter(wrappedRequest, response); // 获取已读内容 byte[] body = wrappedRequest.getContentAsByteArray(); String content = new String(body, request.getCharacterEncoding()); // 日志记录 log.info("request body: {}", content); } }

注意ContentCachingRequestWrapper也有一个限制:它缓存的是getInputStream读取过的内容,如果后续代码根本没有读取流,那缓存可能为空。所以拿到包装类之后,最好从getContentAsByteArray()读取时确认是否还需要主动触发一次流读取。我在实际使用中,倾向把wrappedRequest.getInputStream().readAllBytes()先调用一遍,让缓存区有机会填充。

4. 文件上传:getPart 和多文件处理的完整链路

文件上传是HttpServletRequest里比较特别的一块。它依赖multipart/form-data这种 Content-Type,请求的 body 结构不再是单纯键值对,而是由一段段 boundary 分隔开的分段数据,每一段可以是一个普通表单字段,也可以是一个文件。

在 Servlet 3.0 以前,处理文件上传要引入第三方库(比如 commons-fileupload)手动解析multipart请求,比较麻烦。Servlet 3.0 起,标准 API 提供了request.getPart("file")和request.getParts(),配合容器的配置就能直接拿到文件部分。

4.1 单文件上传的取值流程

假设前端用的是<input type="file" name="file">,后端代码大致是这样:

Part filePart = request.getPart("file"); String fileName = filePart.getSubmittedFileName(); long size = filePart.getSize(); InputStream in = filePart.getInputStream(); // 保存或处理文件

getSubmittedFileName()返回客户端提交的原始文件名。这里有个细节:不同浏览器、不同前端库对文件名处理方式不同,有的会带上路径前缀(比如 IE 老版本会传C:\fakepath\test.txt),有的则只传文件名。我习惯在拿到的 fileName 里做一层清理,截取出真正的文件名部分:

String rawFileName = filePart.getSubmittedFileName(); String cleanName = rawFileName != null ? rawFileName.substring(rawFileName.lastIndexOf("\\") + 1) : "unnamed";

4.2 普通表单字段和文件混在一起时

multipart/form-data请求里,文件上传往往还伴随着其他业务字段,比如上传图片时附带一个description描述字段。取出这些普通字段的方式有两种:一是表单字段在 multipart 结构里也是以 Part 形式存在,可以遍历getParts()判断getContentType() == null(普通字段没有 Content-Type)来区分;二是直接在容器解析后,用request.getParameter("description")来拿。

这里建议优先使用request.getParameter()拿普通字段,因为容器已经帮你解析好了,代码简洁。但有一个小坑:如果你已经通过getParts()遍历过一次请求,部分容器实现会消耗掉输入流,再调getParameter可能拿不到值。稳妥的顺序是先取普通字段参数,再取文件 Part;或者反过来,在拿到所有需要的值之后再做进一步处理,避免交叉读取。

4.3 多文件上传的迭代

多文件上传就是前端把多个文件都用同一个字段名files提交,后端遍历:

Collection<Part> parts = request.getParts(); for (Part part : parts) { if (part.getContentType() != null) { // 这是一个文件 String name = part.getName(); String fileName = part.getSubmittedFileName(); long size = part.getSize(); // 处理 } }

part.getName()返回的是表单字段名,part.getSubmittedFileName()返回的是文件名。这两者经常被混淆,我之前就差点把name当文件名存进数据库,存出来的全是files,排查半天才意识到。

4.4 上传大小限制的坑

文件上传遇到最多的错误是MaxUploadSizeExceededException或请求直接被容器拒绝。如果你用的是 Spring Boot,默认单文件大小限制是 1MB,总请求大小限制是 10MB,可通过spring.servlet.multipart.max-file-size和spring.servlet.multipart.max-request-size调整。但如果你是传统 Servlet 项目,在标准 Tomcat 下,限制由maxSwallowSize和连接器的maxPostSize控制,超过限制时请求可能会被直接丢弃或抛出异常,而不像 Spring Boot 那样能捕获到异常并返回友好提示。

我遇到过的情况是:上传一个 12MB 的压缩包,接口返回 500,Tomcat 日志里有一句"the request or response is too large"。当时没经验,以为是程序 bug,查了半天才发现是 Tomcat 的maxPostSize默认只有 2MB,超出后根本没走到 Controller 就断了。改成 -1(表示不限制)或适当调大即可解决。

5. 请求的"周边信息":路径、IP、会话与 Cookie

除了参数和文件,HttpServletRequest还承载了大量和请求相关的上下文信息。这些信息在日志追踪、安全校验、业务逻辑里都很有用,但很多人没有系统整理过,我在这里列一下实际项目中最常用的几个。

5.1 请求路径成分的区分

初学者最容易搞混的是这几个方法:

方法示例说明
getRequestURI()/app/api/order/1001不包括协议和域名,包含上下文路径
getRequestURL()http://api.example.com/app/api/order/1001完整 URL,包含协议、域名、端口和路径
getContextPath()/app应用上下文路径,常用来拼静态资源路径
getServletPath()/api/order/1001去掉上下文路径后剩余的路径
getPathInfo()null(取决于映射方式)在 Servlet 匹配中额外附加的路径信息

实际业务中,解析路径时我一般这样用:想看"当前访问的是哪个接口",用getRequestURI()去掉getContextPath()的部分;想生成绝对链接(比如回调地址)时,用getRequestURL()拼上参数;想判断请求属于哪个模块时,可以用getServletPath()更干净。

5.2 获取客户端 IP 的完整姿势

取客户端 IP 看起来简单,request.getRemoteAddr()一行就搞定了。但在真实网络环境中,客户端和你的服务之间往往隔了 Nginx、F5、SLB 等代理,getRemoteAddr()拿到的可能是最后一跳代理的 IP,不是真实客户端 IP。这种情况下要从X-Forwarded-For或X-Real-IP头里取。

X-Forwarded-For的格式是:client, proxy1, proxy2,从左到右依次为客户端 IP 和各级代理 IP。比较稳妥的取法是取第一个逗号前的部分,但要注意:这个头是可伪造的。如果应用直接暴露在公网,攻击者可以随便伪造这个头。所以要取 IP 做安全校验(比如 IP 白名单)时,不能完全信任X-Forwarded-For,得结合代理配置来过滤可信来源。

我项目里的取 IP 工具方法大致长这样:

public static String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } // 多级代理时取第一个 IP if (ip != null && ip.contains(",")) { ip = ip.split(",")[0].trim(); } return ip; }

这个逻辑不算最优,但在大多数中小项目里够用了。更严谨的做法是配置一个代理信任列表,只从可信代理的转发头中取值。

5.3 会话、Cookie 和请求方法

getSession()是另一个高频用法。需要特别注意的是:getSession()会创建新会话(如果当前没有),而getSession(false)仅在已有会话时返回,不会新建。在需要鉴权的接口里,我通常用getSession(false),避免恶意请求被强制创建大量无效会话,浪费服务端内存。

Cookie 的读取相对简单:

Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("SESSION_ID".equals(cookie.getName())) { // 处理 } } }

getMethod()用于区分 GET/POST/PUT/DELETE,在做统一拦截校验时很常用。比如某些接口只允许 POST,可以直接在过滤器里判断。getProtocol()可以拿到 HTTP 版本,偶尔排查性能问题时会用到,比如发现线上请求全是HTTP/1.0,那可能是代理或客户端配置有问题,而不是服务端慢。

6. 我踩过的那些深坑:读流顺序、编码、空指针

这一节我想集中说几个真实工作中的教训。它们都不是什么难懂的大道理,但每个都让我至少排查了几个小时。

6.1 getInputStream 和 getParameter 不能混用的真正原因

Servlet 规范里,如果请求的 Content-Type 是application/x-www-form-urlencoded,并且请求方法是 POST,那么容器会解析 body 生成参数表,getParameter可从其中取值。但与此同时,getInputStream的流可能已经被消耗掉。这意味着:

  • 对表单请求,先调getParameter后调getInputStream读 body,有可能读到空流。
  • 对 JSON 请求,先调getInputStream读 body 后调getParameter,getParameter返回的值不受影响,因为浏览器/容器不会从 JSON 里生成参数表,它内部一定是一个空参数表,流读取不影响空表。

所以,接口里到底用哪种参数读取方式,要在代码设计阶段就确定下来。同一个接口既想用@RequestBody拿 JSON 体,又想用@RequestParam拿 URL 参数,这是允许的,因为 URL 参数和 body 是两套数据源。但同一个 body 数据源,要么走表单解析,要么走流读取,不能在业务代码里同时操作。

6.2 字符编码问题

getParameter在解析 URL 参数和表单 body 时,默认使用容器的编码设置。Tomcat 8 及以上默认 URI 编码是 UTF-8,但表单 body 的默认编码取决于request.setCharacterEncoding()的调用时机和容器配置。

最容易出问题的场景是:客户端用application/json; charset=utf-8发送中文,服务端读取流时用默认ISO-8859-1做new String(bytes),结果中文全变成乱码。解决办法很直接,读流之前先明确编码:

String encoding = request.getCharacterEncoding(); if (encoding == null) { encoding = "UTF-8"; } byte[] bodyBytes = request.getInputStream().readAllBytes(); String body = new String(bodyBytes, encoding);

还有一种情况是 Header 里的中文乱码。HTTP Header 本身不支持直接的中文,通常需要 URL 编码后传输,服务端做 URLDecoder 解码。这个细节容易被人忽略,我试过一次直接在 Header 里传中文值,部分容器或代理会直接报错或者被截断。

6.3 参数类型转换和空值判断

getParameter返回的永远是字符串,在转成数字、布尔值等类型时要注意:参数传了非法值(比如age=abc),Integer.parseInt会直接抛NumberFormatException;参数传了空字符串"",Integer.parseInt("")也会抛异常。我的处理办法是封装一个安全转换的方法:

public static Integer getIntParameter(HttpServletRequest request, String name) { String value = request.getParameter(name); if (value == null || value.isEmpty()) { return null; } try { return Integer.parseInt(value); } catch (NumberFormatException e) { return null; } }

接口对外尽可能宽容处理。参数格式不对时,返回一个明确的错误提示比直接抛 500 好太多,尤其是对外提供的 API,调用方五花八门,什么参数都可能给你传过来。

6.4 重复读取 body 的后果与缓存方案

再强调一次,请求体只能读一次这个事实。我在做接口签名校验时栽过一个大跟头。当时设计了一个拦截器,要在业务 Controller 执行前读取请求体,和签名串做比对。第一版代码直接在拦截器里调了request.getReader(),读完后签名校验通过,但进入 Controller 后@RequestBody怎么都拿不到对象,一直报 400。排查了整整一个下午,从 Controller 写错了查到参数类型不匹配,最后才意识到是拦截器把流读光了。

这个问题的本质是:Servlet 的ServletRequest.getInputStream()返回的流是一次性的,底层就是 socket 的输入流,读完了就没了。容器虽然能多次调用getInputStream(),但每次都返回同一个流对象,流位置已经在末尾。

解决方案也不复杂,就是我前面提到的两种:用ContentCachingRequestWrapper,或者自己把 body 缓存到 attribute 里。如果项目用了 Spring,第一种更省事;如果不用 Spring,第二种更直观。核心思路都是让 body 内容在整个请求生命周期内可以被多次读取。

7. 写在最后:几个可以直接落地的经验

做 Web 开发这几年,HttpServletRequest给我的感觉是:它像一个万能的"脏盒子",你可以从里面拿出所有和 HTTP 请求有关的东西,但里面的每一样东西都有使用前提,不了解这些前提,就容易踩坑。

如果让我给读者一个落地建议清单,大概是这几条:

  • 能分清"表单参数""URL 参数""JSON body"三个数据来源,不要混用读取方式。
  • 凡是需要请求体内容的场景,在架构上提前设计缓存方案,别等用到时再四处救火。
  • 文件上传时注意文件大小限制的配置层级,Spring Boot 的配置只管 Spring 自己的上传解析,传统容器的限制你得单独确认。
  • 取客户端 IP 不要盲目信X-Forwarded-For,要在了解自己网络链路的前提下决定信任策略。
  • 所有从请求里拿到的字符串,默认按"不可信"处理,该做类型转换就做安全转换,该判空就判空。

最后分享一个小技巧。排查请求相关问题时,最快的定位方式是在过滤器里把请求的getMethod()、getRequestURI()、getContentType()、所有 Header 名,以及 body 内容打印一条日志。这样一个请求的完整面貌就出来了,参数解析不到、中文乱码、流被提前读取这类问题,往往一眼就能看出来。我现在接手任何新项目,第一件事就是先把这条请求日志加上,它能帮你在不熟悉代码的时候快速建立全局认知。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 21:07:33

Spring Boot + Vue前后端分离实战:网络书店系统设计与踩坑记录

前阵子帮朋友做了一个网络书店管理系统&#xff0c;后端用Spring Boot&#xff0c;前端用Vue&#xff0c;前后端完全分离。做完整体盘点了一下&#xff0c;这套技术组合在中小型业务系统里确实能打&#xff1a;开发效率高、部署省心&#xff0c;而且把一个电商类项目会遇到的典…

作者头像 李华
网站建设 2026/10/18 14:33:45

免费办公批处理工具:图片压缩、批量重命名与智能归类实操指南

每天打开工作电脑的第一件事&#xff0c;往往不是点开邮箱&#xff0c;而是先面对一团乱麻&#xff1a;上个月活动拍的几百张照片还在存储卡里睡着&#xff0c;原图一张动辄十几兆&#xff0c;想发到聊天工具里直接被提示“文件过大”&#xff1b;供应商发来的扫描件&#xff0…

作者头像 李华
网站建设 2026/10/19 2:05:45

Cocos Creator 2.x休闲小游戏源码模板:方块鸟中的状态机与对象池实战

方块鸟冒险这个名字听起来像是Flappy Bird的换皮&#xff0c;但真要在Cocos Creator 2.x里把一个“点按跳跃 障碍碰撞 分数累积”的小游戏做成一套能直接复用的源码模板&#xff0c;里面牵扯的东西比大多数人想的多得多。这个模板项目解决的正是这样一个问题&#xff1a;怎么…

作者头像 李华
网站建设 2026/10/19 2:34:41

55chat登录加密算法分析:从抓包到Python复现

最近接了个移动端App的兼容性测试任务&#xff0c;其中一个叫55chat的聊天应用在登录流程里做了不少手脚。用户名密码不是明文提交&#xff0c;请求体里还多了一个sign字段。折腾了一天时间&#xff0c;从抓包、反编译、定位加密代码&#xff0c;到最后写脚本复现登录&#xff…

作者头像 李华
网站建设 2026/10/19 7:21:28

Windows句柄机制与泄漏排查:从内核对象到RAII管理实践

1. 一次句柄数飙升的线上事故&#xff0c;让我决定把它写透1.1 任务管理器里那个只涨不跌的“句柄数”大概两年前&#xff0c;我接手过一个很诡异的线上问题&#xff1a;某个数据采集服务每次运行到第三天左右就开始变卡&#xff0c;响应越来越慢&#xff0c;重启之后又恢复正常…

作者头像 李华
网站建设 2026/10/20 10:38:04

2017县级行政区shp加载与1100万点空间归属实战指南

简介&#xff1a;这套一千一百万基础地理数据库收录了二〇一七年全国县级行政区的矢量边界&#xff0c;面向GIS数据分析师、城乡规划与科研工作者&#xff0c;用于区域统计、空间制图和辅助决策。数据采用一比一百万比例尺&#xff0c;配合中国大地坐标系统二〇〇〇与WGS一九八…

作者头像 李华