1、简介 {#1简介}
Java Servlet 是一个服务端组件,用于处理客户端传入的 HTTP 请求,通常我们需要通过 Servlet 中的 HttpServletRequest
对象来获取到客户端提交的请求数据。
本文将带你了解在 Java Servlet 中读取 Payload(即请求体)数据的各种方法,以及最佳实践和注意事项。
2、理解 Request Payload {#2理解-request-payload}
Post 请求主要用于通过 HTTP 请求向服务器发送数据。这些数据可以是任何内容,从包含用户输入的表单数据到结构化数据如 JSON 和 XML,甚至是二进制文件。这些数据位于请求体中,与 URL 分开。这样可以实现更广泛和安全的数据传输。我们可以通过请求中的 Content-Type
Header 标识不同类型的数据。
常见的 Content-Type
包括:
- application/x-www-form-urlencoded:用于以键值对形式编码的表单数据
- application/json:用于 JSON 格式的数据
- application/xml:用于 XML 格式的数据
- text/plain:用于发送纯文本
- multipart/form-data:用于上传二进制文件和常规表单数据(form data)
3、读取 Post 请求体的方式 {#3读取-post-请求体的方式}
接下来,让我们看看从 POST Payload 中提取数据的不同方法。
3.1、使用 getParameter() 获取 URL 编码的表单数据 {#31使用-getparameter-获取-url-编码的表单数据}
我们可以使用 HttpServletRequest
接口提供的 getParameter()
方法,使用通过 POST 请求提交的参数名检索特定表单数据。该方法使用表单参数名作为参数,并以字符串(String
)形式返回相应的值。
举个例子:
@WebServlet(name = "FormDataServlet", urlPatterns = "/form-data")
public class FormDataServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String firstName = StringEscapeUtils.escapeHtml4(req.getParameter("first_name"));
String lastName = StringEscapeUtils.escapeHtml4(req.getParameter("last_name"));
resp.getWriter()
.append("Full Name: ")
.append(firstName)
.append(" ")
.append(lastName);
}
}
这种方法可以处理键值对,但不适合处理复杂的数据结构。
我们还使用了 apache commons 文本库中 StringEscapeUtils
类的 escapeHtml4()
方法,通过对输入进行过滤(对特殊字符进行编码),以防止 XSS 攻击。该库的依赖如下:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
3.2、读取原始的字符串请求体 {#32读取原始的字符串请求体}
为了提高灵活性,我们可以使用 HttpServletRequest
接口的 getReader()
方法访问 Servlet 请求原始的文本流。
该方法返回一个 BufferedReader
对象,我们可以逐行读取数据:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
StringBuilder payload = new StringBuilder();
try(BufferedReader reader = req.getReader()){
String line;
while ((line = reader.readLine()) != null){
payload.append(line);
}
}
resp.getWriter().append(countWordsFrequency(payload.toString()).toString());
}
需要注意:
- 在读取较大的请求体时需要小心,可能会内存溢出。
- 可能需要根据请求处理字符编码差异。
3.3、解析结构化数据格式(JSON、XML) {#33解析结构化数据格式jsonxml}
JSON 和 XML 等结构化数据格式被广泛用于客户端和服务器之间的数据交换。我们可以使用专用库将 Payload 解析为 Java 对象。
要解析 JSON 数据,我们可以使用 Jackson 或 Gson 等流行的库。在本例中,我们使用 Gson。其依赖如下:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
我们可以使用 BufferedReader
对象从 Request Body(请求体)中以纯文本形式读取 JSON Payload,然后使用 Gson
将其解析为 Java 对象。
解析 JSON 数据的示例如下:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String contentType = req.getContentType();
if (!("application/json".equals(contentType))) {
// 非 JSON 类的请求体
resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
"Invalid content type");
return;
}
try (BufferedReader reader = req.getReader()) {
Gson gson = new Gson();
Product newProduct = gson.fromJson(reader, Product.class);
resp.getWriter()
.append("Added new Product with name: ")
.append(newProduct.getName());
} catch (IOException ex) {
req.setAttribute("message", "There was an error: " + ex.getMessage());
}
}
我们应该在解析前验证 Content Type,以避免非法的数据格式和安全问题。
如果需要解析 XML 数据的话,可以使用 XStream 库。其 依赖 如下:
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.20</version>
</dependency>
要从 Request Body(请求体)中解析 XML,我们可以像读取 JSON Payload 一样,将其读作纯文本,然后使用 XStream
将其解析为 Java 对象。
解析 XML POST 请求的示例如下:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String contentType = req.getContentType();
if (!("application/xml".equals(contentType))) {
// 请求体的类型不是 XML
resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
"Invalid content type");
return;
}
try (BufferedReader reader = req.getReader()) {
XStream xStream = new XStream();
xStream.allowTypesByWildcard(new String[] { "com.baeldung.**" });
xStream.alias("Order", Order.class);
Order order = (Order) xStream.fromXML(reader);
resp.getWriter()
.append("Created new Order with orderId: ")
.append(order.getOrderId())
.append(" for Product: ")
.append(order.getProduct());
} catch (IOException ex) {
req.setAttribute("message", "There was an error: " + ex.getMessage());
}
}
3.4、处理 multipart/form-data {#34处理-multipartform-data}
multipart/form-data Content Type,专门用于处理包含二进制数据(如图片、视频或文档)和常规文本数据的表单,通常用来上传文件。
要处理 multipart/form-data 请求,我们必须用 @MultipartConfig
或在 web.xml
中配置 Servlet。
@MultipartConfig
提供了各种参数来控制文件上传行为,如 location
(临时存储目录)、maxFileSize
(单个上传文件的最大大小)和 maxRequestSize
(整个请求的最大大小):
@MultipartConfig(fileSizeThreshold = 1024 * 1024,
maxFileSize = 1024 * 1024 * 5,
maxRequestSize = 1024 * 1024 * 5 * 5)
public class FileUploadServlet extends HttpServlet {
// ...
}
在 Servlet 中,我们可以使用 getPart(String name)
方法获取 multipart/form-data 中特定 Part (部分)的信息,或者使用 getParts()
方法获取所有 Part 的信息。Part 接口提供了访问文件名、内容类型、大小和输入流等详细信息的方法。
使用 POST 请求上传文件示例如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String uploadPath = getServletContext().getRealPath("") +
File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
Part filePart = request.getPart("file");
if (filePart != null) {
String fileName = Paths.get(filePart.getSubmittedFileName())
.getFileName().toString();
if(fileName.isEmpty()){
response.getWriter().println("Invalid File Name!");
return;
}
if(!fileName.endsWith(".txt")){
response.getWriter().println("Only .txt files are allowed!");
return;
}
File file = new File(uploadPath, fileName);
try (InputStream fileContent = filePart.getInputStream()) {
Files.copy(fileContent, file.toPath(),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
response.getWriter().println("Error writing file: " +
e.getMessage());
return;
}
response.getWriter()
.println("File uploaded to: " + file.toPath());
} else {
response.getWriter()
.println("File upload failed!");
}
}
对于文件上传来说,在安全方面需要考虑些东西:
- 防止路径遍历
- 检查文件的类型
- 检查文件的大小
- 安全地写入文件
4、最佳实践和常见的问题 {#4最佳实践和常见的问题}
4.1、Content-Type 校验 {#41content-type-校验}
我们应始终验证传入请求的 Content-Type,以确保服务器正确处理请求。这有助于防止非法的数据格式和潜在的安全漏洞。
例如,如果我们的 Servlet 期望使用 JSON,我们就应该在处理之前检查 Content-Type Header 是否为 application/json
类型:
String contentType = req.getContentType();
if (!("application/json".equals(contentType))) {
resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
"Invalid content type");
return;
}
或者,可以使用 Apache Tika (内容检测和分析框架)实现一些更强大的功能。
4.2、异常处理 {#42异常处理}
在读取和处理 Payload 时,应始终执行适当的错误处理。这可以确保应用能够从容应对意外情况,提高程序的健壮性。
还应该通过 HTTP 状态代码提供有意义的信息,这有助于告诉开发人员和用户出了什么问题。
4.3、性能优化 {#43性能优化}
处理非常大的 Payload 会影响性能。为了优化,应该考虑限制传入请求的大小、流式处理数据以及避免数据复制。可以使用诸如 Apache Commons IO 这样的库,它可以帮助高效处理 Payload。
此外,还应确保 Servlet 不会执行阻塞操作,以免阻塞请求处理。
4.4、安全 {#44安全}
在处理 POST Payload 时,安全是一个重要的考虑因素。
一些关键做法包括:
- 数据验证:始终对输入的数据进行验证和过滤,以防止注入攻击。
- 认证和授权:确保只有授权用户才能访问某些端点。
- CSRF 保护:实现 CSRF(跨站请求伪造)保护,以防止用户不经意间从恶意网站发送未经授权的命令。
- 数据加密:使用 HTTPS 加密传输中的数据,保护敏感信息。
- 限制上传大小:设置上传文件的大小限制,防止拒绝服务攻击。
5、总结 {#5总结}
本文介绍了在 Servlet 中读取和解析 POST 请求 Payload 的各种方法,介绍了如何获取表单、JSON、XML 以及 Multipart 文件等各种类型请求的数据,最后还介绍了最佳实践及注意事项。
Ref:https://www.baeldung.com/java-servlet-post-request-payload