写 Servlet 的时候,最绕不开的就是request.getParameter()。我见过不少新手,第一个能跑通的接口不是 "Hello World",而是把一个注册表单里的用户名、密码取出来再打印一遍。可是越基础的东西,坑往往越多:GET 和 POST 取参数的方式真的完全一样吗?为什么中文经常变成问号?同一个 name 的多个值为什么只能取到第一个?这些细节如果没弄透,后面排查问题会非常痛苦。
这篇文章我把HttpServletRequest获取参数这件事,从底层原理到实际操作完整梳理一遍。你不需要提前懂很多,只要能看懂 Java 基础语法就行。内容会覆盖 GET/POST 的参数获取差异、中文乱码的根治方案、多值参数的正确处理姿势,最后再给一个基于web.xml方式注册 Servlet 的完整可运行示例。你会知道每个方法背后的设计意图,以及踩坑之后该怎么定位问题。
1. HttpServletRequest 在 Servlet 里的角色:请求参数从哪里来
1.1 request 对象是怎么生成的
先把这个对象的身世搞清楚。浏览器发送一个 HTTP 请求到 Tomcat,Tomcat 作为 Servlet 容器,要做的事情不是直接调用你的 Servlet,而是先把网络上的原始字节流解析成一个符合 HTTP 协议规范的对象,也就是HttpServletRequest。这个对象封装了请求行、请求头、请求体里的所有信息,然后 Tomcat 会根据 URL 映射找到对应的 Servlet,调用它的service()方法,再把 request 和 response 传进去。
service()方法内部会根据 HTTP 方法分发:如果是 GET 请求,就调用doGet();如果是 POST 请求,就调用doPost()。所以你在doGet和doPost里拿到的 request,本质上是同一个对象,只是容器帮你把协议层的差异化工作做完了。你不需要关心 TCP 连接、HTTP 报文怎么拆包,只要面向这个封装好的对象编程。但这里有个隐藏前提:容器帮我们解析的数据,不一定符合业务预期,比如编码错了,或者参数被截断,这些都是实际开发里最常见的坑。
每个请求都会创建一个新的 request 对象,请求处理完就销毁。所以不能把用户数据放在 request 上跨请求传递,它是“一次性”的。这和我后面要说的参数获取方式有直接关系:容器解析 request 的时候,参数是以特定数据结构保存起来的,我们必须用规范提供的方法去读取,而不是自己造轮子去解析原始报文。
1.2 参数到底藏在请求的哪个位置
很多人分不清 GET 和 POST 参数藏在哪里。GET 请求的参数通常放在 URL 的查询字符串里,也就是问号后面的key=value&key=value部分。比如:
GET /param?username=zhangsan&age=20 HTTP/1.1 Host: localhost:8080这串username=zhangsan&age=20会在浏览器地址栏里原样显示,也正因为如此,GET 不适合传密码、身份证号这类敏感信息,会被记录在历史记录、日志里。
POST 请求的参数一般放在请求体(body)里。表单提交时,如果enctype是默认的application/x-www-form-urlencoded,body 内容长这样:
POST /param HTTP/1.1 Host: localhost:8080 Content-Type: application/x-www-form-urlencoded username=zhangsan&age=20重点来了:Servlet 规范把 URL 查询字符串和表单 body 统一抽象成“请求参数”。也就是说,不管参数来自 URL 还是 body,在doGet和doPost里,你都可以用相同的方法getParameter()去取。这给开发带来了极大的便利,但也模糊了一个事实:底层解码方式是不同的。GET 参数的解码依赖服务器对 URL 的编码配置,POST 参数的解码依赖请求体声明的字符集,乱码问题往往就出在这里。
如果客户端提交的是multipart/form-data(文件上传),或者application/json(前后端分离),那么getParameter()就不再适用。前者需要专门的文件上传解析组件,后者需要读取输入流再用 JSON 工具解析。这是另外两个独立话题,后文我会在问题排查里提一下。
2. 获取 GET 请求参数:从 URL 里把值抠出来
2.1 getParameter 与 getParameterValues 的区别
GET 参数最简单的情况是每个 key 只有一个 value。直接调用:
String username = request.getParameter("username"); System.out.println(username);这里要注意返回值:
- 如果参数存在且值为空字符串,返回
""。 - 如果参数不存在,返回
null。 - 如果参数存在且出现多次,返回第一个值。
第三个点特别容易踩坑。比如 URL 是/param?hobby=java&hobby=篮球,我用getParameter("hobby")只能拿到java,拿不到后面那个值。要拿所有值,必须用:
String[] hobbies = request.getParameterValues("hobby");这个方法会返回一个字符串数组。如果参数不存在,返回null,不是空数组。所以使用前最好判空,否则容易空指针。
实际开发里,getParameterValues最常见的场景是 checkbox。比如用户选了三个爱好,表单提交时浏览器会生成三个同名的hobby字段,服务端如果不习惯用数组接收,会出现“只保存了第一个选项”的诡异 bug。这个我后面单开一节详细讲。
还有一个小细节:getParameterValues返回的数组顺序,理论上和请求中出现顺序一致,但规范并没有强制要求,所以不要依赖顺序做业务逻辑,需要顺序时最好在参数里显式带上序号。
2.2 用 getParameterMap 一次性拿到所有参数
如果接口要接收的参数很多,或者你想写一个通用的参数处理逻辑,可以一次性拿到所有参数:
Map<String, String[]> parameterMap = request.getParameterMap(); for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) { String name = entry.getKey(); String[] values = entry.getValue(); System.out.println(name + " = " + Arrays.toString(values)); }注意这个 Map 只能读,不能 put 修改。它的每个 value 都是一个数组,因为一个参数名可能对应多个值。如果你只调用getParameter("name"),其实底层就是从某个参数的数组里取第一个元素。
getParameterMap在写统一入参日志时特别好用。我自己做接口调试时,会在 Servlet 的入口处把整个 Map 打出来,请求进来先看参数是否解析正确,比单点排查快得多。但也要注意,Map 里只会包含已经提交的参数;如果一个参数提交了空字符串,它的值是[""],数组长度为 1,而不是 0。这个细节在参数校验时容易让人迷惑。
2.3 手动解析查询字符串的补充方案
在某些场景下,容器解析出来的结果可能不符合预期。比如 URL 参数里包含了被转义的 JSON 字符串,或者你想拿到最原始的“没被解码”的查询串。此时可以用request.getQueryString(),它能拿到 URL 问号后面的原始字符串:
/param?info=%7B%22name%22%3A%22zhangsan%22%7D服务端:
String queryString = request.getQueryString(); System.out.println(queryString); // 输出:info=%7B%22name%22%3A%22zhangsan%22%7D如果需要手动解码,可以配合URLDecoder.decode():
String raw = request.getQueryString(); String decoded = URLDecoder.decode(raw, "UTF-8"); System.out.println(decoded); // 输出:info={"name":"zhangsan"}不过要提醒一句:如果你已经调用了request.getParameter(),容器可能已经按自己的规则解析了 URL 编码,再拿getQueryString()手动处理,很容易出现二次解码问题。所以“手动解析”最好在完全没有调用getParameter的情况下使用,或者只是用来做日志记录、参数复制,不要在同一个请求里混用。
手动解析的核心应用场景是 URL 中有重复且顺序敏感的参数,或者参数格式自定义且容器解析规则不满足业务需求。但对标准表单参数,我还是建议优先用 Servlet 规范提供的方法,毕竟容器做过兼容处理,比自己写正则解析要稳。
3. 获取 POST 请求参数:表单体里的乾坤
3.1 表单编码类型决定了 getParameter 是否有效
HTML 表单的enctype属性有三种常见取值:
| enctype 值 | 说明 | getParameter 是否能直接取到 |
|---|---|---|
application/x-www-form-urlencoded | 默认值,body 是key=value&key=value格式 | 能 |
multipart/form-data | 文件上传或带二进制内容的表单 | 不能,需要专门解析 |
text/plain | 极少使用,body 是普通文本 | 视容器实现而定,不推荐 |
浏览器在提交第一种表单时,会把表单字段逐个编码,然后拼成name=value的形式,字段与字段之间用&连接,空格变成+,特殊字符做百分号编码。Tomcat 看到Content-Type: application/x-www-form-urlencoded,就会主动解析 body,把结果放进参数表里。所以你在doPost里能用getParameter()。
multipart/form-data则会把请求体划分成多个 part,每个 part 有自己的头部和内容,不再是一个简单的 key=value 字符串。Servlet 规范没有要求容器默认解析 multipart,所以getParameter()拿不到普通字段。这时要么手动读输入流解析,要么用 Apache Commons FileUpload,或者用 Servlet 3.0 之后提供的@MultipartConfig注解配合request.getPart()来处理。这已经超出本文参数获取的范围,但你需要知道这个边界。
3.2 通过 getParameter 读取表单数据
POST 请求里通过getParameter读取表单字段,代码形式上跟 GET 一模一样:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置请求体解码字符集,必须放在第一次读取参数之前 request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); System.out.println("username = " + username); }注意我特意把setCharacterEncoding("UTF-8")放在最前面,而且强调“第一次读取参数之前”。原因很实在:Tomcat 解析 body 参数时会根据这个字符集解码。如果你先调用了getParameter(),容器已经把 body 按默认字符集(Tomcat 8/9 默认是 UTF-8,但很多旧项目改过)解析完了,这个时候再调用setCharacterEncoding(),对已经解析过的参数不会生效。
很多人觉得 POST 取参数就是“把 GET 换成了 doPost”,其实不是。GET 参数在 URL 上,URL 的编码规则由服务器配置决定;POST 参数在 body 里,body 的解码规则由服务端代码主动声明。这也是为什么 POST 乱码容易通过setCharacterEncoding解决,而 GET 乱码需要改服务器配置或额外转码。
3.3 直接读取请求体 InputStream 的场景
如果客户端提交的不是表单格式,比如把 JSON 放在 body 里,getParameter()就无能为力了。此时需要读取请求体:
BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } String jsonBody = sb.toString(); System.out.println(jsonBody);或者用request.getInputStream()拿字节流再转字符串。两者不能同时使用,因为一个请求体只能被读取一次。
这里有个非常经典的坑:如果你在代码里先调用了request.getParameter(),再调用getReader(),后者很可能拿不到完整 body,或者直接抛IllegalStateException。反过来也一样:先调用了getReader(),再调用getParameter(),参数表就已经是空的了。这是容器的设计约束,因为一旦 body 被读取,原来的字节流就不在了。
所以在设计接口时,你要先想清楚:这个接口是接收普通表单参数,还是接收原生请求体。两者不应该混用。如果真遇到一个接口既要 header 里的参数,又要 body 里的 JSON,至少要保证不要既读流又读参数表,否则顺序一乱就是灾难。
4. 中文乱码的根治方案:编码这件事必须一次做对
4.1 乱码是怎么产生的:编码和解码不一致
乱码的本质只有一句话:发送端编码用的字符集和接收端解码用的字符集不一致。
中文在 UTF-8 编码下,一个汉字通常占 3 个字节;在 GBK 编码下占 2 个字节。假设浏览器用 UTF-8 把“张三”编码成字节流发到服务器,服务器却用 ISO-8859-1 去解码,ISO-8859-1 是一种单字节字符集,会把每个字节都当成一个西欧字符,于是三个字节被拆成三个“半成品”,再显示出来就是乱码。反过来,如果服务器用 UTF-8 解码但浏览器发的是 GBK 字节,也会得到乱码。
所以解决问题前,先确定“整条链路”的字符集。一个典型的请求链路包括:
- 浏览器页面本身的编码(HTML
<meta charset>或响应头)。 - 表单提交时对字段值编码使用的字符集。
- URL 编码查询参数时使用的字符集。
- Servlet 容器解码 URL 和 body 时使用的字符集。
- 服务端代码里字符串读取、返回值输出使用的字符集。
任一段不一致,都可能乱码。下面逐个拆解。
4.2 POST 请求乱码:setCharacterEncoding 是首选解法
POST 请求体参数乱码,最好排查。在读取任何参数之前设置:
request.setCharacterEncoding("UTF-8");这句话的作用是告诉容器:请求体里的表单参数按 UTF-8 解码。它只对请求体有效,对 URL 上的查询字符串无效。这一点务必记牢。
如果你用的是 Tomcat 8.0 及以上版本,只要前端页面和表单本身也是 UTF-8,这行代码基本就能解决 POST 中文乱码。Tomcat 7 及更早版本,默认请求体解码字符集是 ISO-8859-1,更需要显式设置。
有些项目用了 Spring MVC,它的CharacterEncodingFilter已经统一处理了这层逻辑,所以你会发现不写setCharacterEncoding也没乱码。但既然是看 Servlet 笔记,还是要理解底层原理,不能只停留在框架黑盒里。
4.3 GET 请求乱码:问题大多在 Tomcat 的 URIEncoding
GET 参数在 URL 上,请求行本身不携带明确的字符集标识。Tomcat 解码 URL 里的查询参数时,使用的是server.xml中 Connector 的URIEncoding属性。
在 Tomcat 8.0 及以上版本,URIEncoding默认已经是 UTF-8,所以 GET 中文一般没问题。但在老版本 Tomcat、或者某些二次开发过的容器发行版里,默认可能是 ISO-8859-1,于是出现中文乱码。这时可以打开 Tomcat 的conf/server.xml,找到 Connector 节点:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />修改后重启 Tomcat,GET 参数里的中文通常就好了。
还有一种局面是代码已经发布到线上,不方便改server.xml,或者调用方在 URL 里传入的是经过编码的中文,容器却用错误的字符集解码了一次。这种情况下可以手动把参数值“还原”:
String raw = request.getParameter("name"); byte[] bytes = raw.getBytes("ISO-8859-1"); String name = new String(bytes, "UTF-8");意思是:先用 ISO-8859-1 把乱码字符串还原成字节数组,因为这些字节本来是正确的 UTF-8 字节,只是被错误地按 ISO-8859-1 解码了;再用 UTF-8 重新解码,得到正确字符串。
这是一个很经典的补救套路,面试也常考。但实际开发中我不建议依赖它,因为一旦两端编码不止一个字符集,这种“猜来猜去”的转码很容易翻车。最稳妥的还是统一服务端容器和客户端页面的字符集。
4.4 响应乱码:别忘了设置 response 的编码
参数取对了,输出又乱了,这种情况也别奇怪。response也有自己的编码。
向浏览器输出中文时,response.getWriter()会按照 response 的字符编码输出。默认情况下可能是 ISO-8859-1,那写到页面上的中文就会变成??或乱码。解决办法是在获取PrintWriter之前设置:
response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8");第一句设置响应体字符集,第二句设置 Content-Type,同时告诉浏览器用 UTF-8 解析页面。两个配合使用,效果最稳定。如果只设置Content-Type,部分容器也能推导出编码,但显式写setCharacterEncoding更保险。
4.5 一个更省事的方案:写统一编码过滤器
在每个 Servlet 里重复写编码设置,很容易漏。正确做法是写一个 Filter,把请求和响应的字符集统一设置好:
public class EncodingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8"); chain.doFilter(request, response); } @Override public void destroy() { } }然后在web.xml里配置:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>com.demo.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这样所有请求都会经过过滤器,POST 请求体的乱码问题基本被挡在门外。但要注意:过滤器里的request.setCharacterEncoding("UTF-8")仍然不能解决 GET 参数乱码,因为前面说过,它只对请求体有效。GET 的乱码要么靠 Tomcat 的URIEncoding,要么靠手动转码。这个边界一定要清楚,别以为加了过滤器就一劳永逸。
5. 多值参数:Checkbox、多选列表和重复同名参数
5.1 什么场景会出现同名多值参数
一个参数名对应多个值,在 HTTP 协议里是合法的。最常见的场景:
- HTML 表单中的 checkbox 多选,所有勾选项的
name相同。 <select multiple>多选下拉框。- 同一表单里有两个或者多个隐藏域用了同一个
name。 - 手动拼接 URL 时重复写
?tag=java&tag=mysql。
如果你只关心第一个值,用getParameter没问题。但多选场景下,用户可能勾了三个爱好,你却只存了一个,这显然是 bug。所以遇到这类参数,要立刻想到getParameterValues。
5.2 使用 getParameterValues 接收数组
前端表单:
<form action="/param" method="post"> <input type="checkbox" name="hobby" value="java"> Java <input type="checkbox" name="hobby" value="basketball"> 篮球 <input type="checkbox" name="hobby" value="music"> 音乐 <button type="submit">提交</button> </form>服务端接收:
String[] hobbies = request.getParameterValues("hobby"); if (hobbies != null) { for (String hobby : hobbies) { System.out.println(hobby); } }这里必须判空。如果用户一个都没勾,POST 请求里根本没有hobby这个字段,getParameterValues返回null,直接 for 循环会空指针。返回值是null而不是空数组,这一点非常反直觉,新手经常栽在这里。
如果你想把数组转成逗号分隔的字符串,可以用 Java 8 的String.join:
String hobbyStr = String.join(",", hobbies);但如果hobbies是 null,这里照样空指针,所以还是得先判空。
5.3 用 getParameterMap 处理更复杂的同名结构
当多个同名参数和不同名参数混在一起,逐个 get 会显得很乱。统一使用getParameterMap可以一次性获取:
Map<String, String[]> params = request.getParameterMap(); String[] hobbies = params.get("hobby");还有一个使用细节:getParameterMap返回的 Map 是只读的,你不能往里面put新的参数。如果需要增加参数,只能自己创建一个新的Map,把原数据和新增数据放进去,再传给业务层。框架内部(比如 Spring MVC)会对参数做二次封装,但原生 Servlet 环境下不要试图修改这个 Map。
另外,如果你的页面里有“不选任何 checkbox 就提交”的情况,建议在前端做一次校验,提示用户至少选择一项。服务端可以容忍空值,但业务上通常不允许。这不是 Servlet 的问题,而是参数校验策略问题,但处理不当确实会导致一个“服务端好像拿到了 null”的悬案。
6. 一个完整示例:基于 web.xml 的 Servlet 获取 GET/POST 参数并处理乱码
6.1 编写 Servlet 类
这里我给出一个直接用web.xml注册 Servlet 的完整示例,不依赖注解,兼容性也更好。先创建一个普通 Java 类:
package com.demo.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; public class ParamServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(request, response); } private void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { // 统一设置编码,放在任何参数读取和输出之前 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8"); String username = request.getParameter("username"); String[] hobbies = request.getParameterValues("hobby"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><meta charset=\"UTF-8\"></head>"); out.println("<body>"); out.println("<h1>参数接收结果</h1>"); out.println("<p>username: " + (username == null ? "" : username) + "</p>"); out.println("<p>hobby: " + (hobbies == null ? "" : Arrays.toString(hobbies)) + "</p>"); out.println("</body>"); out.println("</html>"); } }注意doGet和doPost都调用了同一个handleRequest,这样无论前端用什么方式提交,处理逻辑都一致。对 GET 请求来说,request.setCharacterEncoding("UTF-8")实际不会影响 URL 参数,但放在这里并不冲突,而且能保证万一请求体里也有内容时不会出乱子。
6.2 配置 web.xml 映射
在src/main/webapp/WEB-INF/web.xml中配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>ParamServlet</servlet-name> <servlet-class>com.demo.servlet.ParamServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ParamServlet</servlet-name> <url-pattern>/param</url-pattern> </servlet-mapping> </web-app><servlet-name>可以随意起,但要保证下面两个标签里的名字一致。<url-pattern>决定了访问路径,比如项目上下文路径是servlet-demo,那么访问地址就是:
http://localhost:8080/servlet-demo/param如果你使用注解方式,只需要在 Servlet 类上写@WebServlet("/param"),但既然要讲web.xml方式,我建议你把它和注解方式都尝试一遍,感受两种方式的异同。注解方式更简洁,但web.xml方式在项目需要统一管理 Servlet 映射、并且不想改动类文件时更有优势。
6.3 编写前端表单页面
在webapp目录下放一个form.html:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>参数提交测试</title> </head> <body> <form action="param" method="post"> <div> <label>用户名:</label> <input type="text" name="username"> </div> <div> <label>爱好:</label> <input type="checkbox" name="hobby" value="java"> Java <input type="checkbox" name="hobby" value="basketball"> 篮球 <input type="checkbox" name="hobby" value="music"> 音乐 </div> <button type="submit">提交</button> </form> </body> </html>method="post",提交后请求会发给同目录下的param,也就是/servlet-demo/param。如果你想测试 GET,把method改成get,地址栏会显示?username=xxx&hobby=java&hobby=music,也能得到同样的结果。
6.4 部署运行与测试效果
把项目部署到 Tomcat 的webapps目录,或者直接在 IDEA 里配置 Tomcat 后启动。打开浏览器访问form.html,输入中文用户名,勾选多个爱好,提交。
正常情况下的输出:
username: 张三 hobby: [java, basketball]如果用 POST 提交出现乱码,先确认handleRequest开头有没有写request.setCharacterEncoding("UTF-8")。如果用 GET 提交出现乱码,检查 Tomcat 的server.xml中 Connector 的URIEncoding是否为UTF-8。如果页面输出乱码,检查response.setContentType("text/html; charset=UTF-8")有没有在getWriter()之前调用。
这一套流程走通后,你已经具备了一个最小可用的 Servlet 参数接收模型。后面无论项目里加了多少框架,底层都跑不出这个范围。
7. 常见问题与排查技巧实录
7.1 获取到的参数总是 null
如果request.getParameter("xxx")返回null,先从这几个方向排查:
| 可能原因 | 排查方法 |
|---|---|
表单字段没有name属性 | 检查 HTML,id和name是不同的。 |
| 提交方式不匹配 | 参数在 body 里但 Servlet 只写了doGet,或反之。 |
form 的enctype不是 urlencoded | 改成application/x-www-form-urlencoded,文件上传除外。 |
| Servlet 映射路径不对 | 确认请求 URL 和<url-pattern>一致。 |
| 参数名拼写大小写不一致 | HTTP 参数名是大小写敏感的,手动检查一遍。 |
| 过滤器或前置操作提前读取了 body | 不要在读取参数前调用getReader()或getInputStream()。 |
推荐先写一行日志把getParameterMap()全量打出来,看看容器到底收到了哪些参数。如果 Map 本身就是空的,问题大多在请求侧;如果 Map里有但getParameter取不到,那可能是方法调用顺序或其他代码意外消费了参数表。
7.2 getParameter 和 getInputStream 互相干扰
这是一个真实发生过的案例:同事写一个接口,想同时读取请求体里的 JSON,又想用getParameter拿一个 URL 参数,结果前端的 JSON 数据死活读不到。原因就是请求体只能读一次。
Servlet 容器在第一次调用getParameter()时,如果判断请求是表单格式,会主动去解析 body 并把参数放入参数表,底层输入流的状态就被改变了。此时再调用getInputStream()或getReader(),得到的流是“空”的。
反过来,如果你先调用getReader()读取了 body,再调用getParameter()获取表单参数,参数表也是空。所以一个请求里,普通表单参数和原始请求体只能二选一。如果确实需要同时使用,只能在读取 body 前,通过request.getParameter获取 URL 参数?也不行,因为 URL 查询字符串在参数表里,body 解析不会影响 URL 参数。实际上安全做法是:先调用getParameter拿 URL 上的参数,这个操作可能会消费 body,所以先读body再取URL参数才是正道?严格来说,只要调用了getParameter,容器可能消费 body;但你只取 URL 参数时容器不知道你是否需要 body,它可能不会消费 body?规范允许容器惰性解析。可实际情况复杂,最稳妥的就是在读取 body 前把 URL 参数记录完?不,这又不能调用getParameter。所以建议:这种接口不要让 URL 上的普通参数参与业务,把你要的数据全部放 header 或 body 里,避免混用。
如果非要解析 body 里的表单并同时保留查询参数,可以自己从getInputStream读原始 body,手动URLDecoder解码,同时用getQueryString解析 URL 参数,完全绕开getParameter。这样代码会复杂,但至少不是玄学。
7.3 GET 和 POST 参数同时传递时的优先级
有些接口会同时接收 URL 上的查询参数和 body 里的表单参数,比如:
POST /param?source=web HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=zhangsan&source=app此时参数表里会有两个source,一个来自 URL,一个来自 body。Servlet 规范没有强制规定同名参数谁在前,不同容器实现可能有差异。多数情况下getParameter("source")会返回先被解析的那个值,但不要依赖这个顺序。
这里给出一个实用建议:公共参数(如source)放 header,业务参数放 body 或 URL,尽量避免同名参数来自两个位置。如果无法避免,就统一用getParameterValues("source")自己处理多个值,而不是猜默认优先级。
7.4 开发工具控制台中文乱码,不一定是代码问题
有时候代码里已经正确处理了编码,但 IDEA 的 Tomcat 日志窗口还是输出乱码。这很可能是 IDE 的编码设置和 Tomcat 日志输出编码不一致造成的。
可以试试在 IDEA 的 Tomcat 运行配置里,添加 VM options:
-Dfile.encoding=UTF-8如果无效,再检查项目源码文件编码是否为 UTF-8。IDEA 里右下角能看到文件编码,或者到Settings -> Editor -> File Encodings,把Global Encoding、Project Encoding、Default encoding for properties files都设为 UTF-8。编译编码也要同步,避免 class 文件本身就是乱码。
这一类乱码属于开发环境问题,和 Servlet 参数接收无关,但会干扰你判断真正的业务乱码。我每次遇到乱码,都会先区分是请求参数乱码、响应输出乱码,还是日志窗口乱码。三种乱码的修复位置完全不同,不要急着改代码。
7.5 前后端分离时代的 JSON 参数怎么获取
现在很多项目不再用传统表单提交,而是用fetch或者axios直接发 JSON:
fetch('/param', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: '张三', age: 20 }) });这时request.getParameter("username")返回null,因为 body 不是表单格式,Tomcat 不会把它解析进参数表。你需要读原始输入流:
BufferedReader reader = request.getReader(); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } String requestBody = sb.toString();然后交给 JSON 工具解析,比如 Fastjson、Jackson、Gson:
JSONObject jsonObject = JSON.parseObject(requestBody); String username = jsonObject.getString("username");如果你用的是 Spring MVC,@RequestBody已经封装了这部分逻辑。但使用原生 Servlet 时,理解“表单参数”和“原始请求体”的区别,处理 JSON 接口时才能知道为什么getParameter拿不到数据。
我个人在处理这类问题时,习惯先把请求头打印出来,看Content-Type到底是不是application/json。这样能迅速判断该走参数表还是该读流,能省下不少排查时间。
最后说一个自己的习惯
写原生 Servlet 这几年,我最大的体会是:参数获取相关的 bug,绝大多数不是方法不会用,而是“编码没统一”和“调用顺序没控制住”。setCharacterEncoding要放在读取参数或输出之前,getReader和getParameter不要混用,看到“中文乱码”先分清楚是请求乱码、响应乱码还是控制台乱码,这些问题都能快速定位。
还有一个可以提升效率的习惯:不管多简单的 Servlet,我都会在入口打印getParameterMap()或原始请求体,至少把入参留痕。线上排查问题时,这一段日志能告诉你用户真正提交了什么,比盯着浏览器地址栏猜有效得多。参数这块的知识很基础,但基础不牢,后面用框架封装得再高级,出了问题照样要回到底层来找答案。