服务端如何处理GZIP压缩的POST请求的两种方法
背景
客户端发送的post请求里面的body数据是经过gzip压缩的,比较规范的客户端压缩之后会在请求头标识这是一个压缩请求如:Content-Encoding: gzip。也有一些老六客户端虽然带着压缩请求头标识实际上也没有压缩数据,对于这种情况我们也要进行兼容。经过验证有两种实现方法:1.前置过滤器 2.业务controller里面解压缩。
构建springboot工程模拟发送端和接收端如下
maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
发送端模拟
package com.testwork;
import com.google.common.collect.ImmutableMap;
import org.apache.http.client.entity.GzipCompressingEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
public class SendTest {
// private static final String TEST_URL = "http://localhost:10092/send_data/filter";
// private static final String TEST_URL = "http://localhost:10092/send_data/body";
private static final String TEST_URL = "http://localhost:10092/send_data/stream";
private static final Map<String,String> HEADER_MAP = ImmutableMap.<String, String>builder()
.put("Content-Encoding", "gzip")
.put("Cookie", "abcd")
.put("Content-Type", "application/json;charset=UTF-8")
.build();
private static final String DATA = "[{\"id\":1,\"desc\":\"测试数据....\"}]";
public static void main(String[] args) throws IOException {
int operateCode = 1;
switch (operateCode) {
case 1 :
encodeZipByHttpClient();
break;
case 2 :
decodeZipByHttpClient();
break;
case 3 :
encodeZipByRestTemplate();
break;
case 4 :
decodeZipByRestTemplate();
break;
default :
System.err.println("没有匹配的操作");
}
}
/**
* httpclient发送 加密数据
*
* @throws IOException
*/
private static void encodeZipByHttpClient() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(TEST_URL);
HEADER_MAP.forEach(httpPost::addHeader);
// 使用httclient发送数据需要指定编码不然服务端会乱码
httpPost.setEntity(new GzipCompressingEntity(new StringEntity(DATA, "UTF-8")));
CloseableHttpResponse response = httpClient.execute(httpPost);
System.out.println(response.getStatusLine().getStatusCode());
}
/**
* httpclient发送 正常数据
*
* @throws IOException
*/
private static void decodeZipByHttpClient() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(TEST_URL);
HEADER_MAP.forEach(httpPost::addHeader);
httpPost.setEntity(new StringEntity(DATA, "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
System.out.println(response.getStatusLine().getStatusCode());
}
private static void encodeZipByRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();
HEADER_MAP.forEach(httpHeaders::add);
byte[] gzipDataBytes = compressToGzip();
HttpEntity<byte[]> entity = new HttpEntity<>(gzipDataBytes, httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(TEST_URL, entity, String.class);
System.out.println(responseEntity.getStatusCode().value());
}
private static void decodeZipByRestTemplate() throws IOException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = new HttpHeaders();
HEADER_MAP.forEach(httpHeaders::add);
HttpEntity<String> entity = new HttpEntity<>(DATA, httpHeaders);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(TEST_URL, entity, String.class);
System.out.println(responseEntity.getStatusCode().value());
}
private static byte[] compressToGzip() {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gos = new GZIPOutputStream(baos)) {
gos.write(DATA.getBytes());
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("压缩数据错误", e);
}
}
}
服务端接收模拟
package com.testwork.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;
@Slf4j
@RestController
public class TestController {
/** 说明: 1.这里用的是全局过滤器将数据解压缩,还未到业务层
* 如果在具体某个controller里面处理,这里的入参接受可以是
* request.getInputStream or @RequestBody byte[] data
* 2. 如果这个两个入参同时存在,@RequestBody data 有值, 这个时候request.getInputStream就是null了。
*/
@PostMapping("/send_data/filter")
public ResponseEntity<String> dealDataByFilter(@RequestBody String data) {
log.info("数据为 {}", data);
return ResponseEntity.ok("success");
}
@PostMapping("/send_data/body")
public ResponseEntity<String> dealDataByBody(@RequestBody byte[] data) {
log.info("解密前 {}", data);
String unzipData = getUnzipDataByBody(data);
log.info("解密后 {}", unzipData);
return ResponseEntity.ok("success");
}
@PostMapping("/send_data/stream")
public ResponseEntity<String> dealDataByInputStream(HttpServletRequest request) throws IOException {
String unzipData = getUnzipDataByInputStream(request);
log.info("解密后 {}", unzipData);
return ResponseEntity.ok("success");
}
// @RequestBody byte[] data
private String getUnzipDataByBody(byte[] data) {
try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(data));
Reader reader = new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8)) {
char[] buffer = new char[2024];
StringBuilder builder = new StringBuilder();
int len;
while ((len = reader.read(buffer)) > 0) {
builder.append(buffer, 0, len);
}
return builder.toString();
} catch (IOException e) {
throw new RuntimeException("error decompress data", e);
}
}
// request.getInputStream
private String getUnzipDataByInputStream(HttpServletRequest request) throws IOException {
String data;
InputStream is = request.getInputStream();
PushbackInputStream pbi = new PushbackInputStream(is, 2);
// 读取前两个字节
byte[] header = new byte[2];
int bytesRead = pbi.read(header);
// 检查前两个字节是否是GZIP的魔数
boolean isGzip = bytesRead == 2 && header[0] == (byte) 0x1F && header[1] == (byte) 0x8B;
//复位
pbi.unread(header);
if (isGzip) {
// 如果是gzip压缩,进行解压处理
byte[] decompressedData = decompressGzip(pbi);
data = new String(decompressedData, StandardCharsets.UTF_8);
} else {
// 如果不是,直接处理
BufferedReader reader = new BufferedReader(new InputStreamReader(pbi, "UTF-8"));
String line;
StringBuilder builder = new StringBuilder();
while ((line = reader.readLine())!= null) {
builder.append(line);
}
data = builder.toString();
}
return data;
}
private byte[] decompressGzip(InputStream is) throws IOException {
GZIPInputStream gzipInputStream = new GZIPInputStream(is);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[2024];
int len;
while ((len = gzipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}
}
服务端处理方式一:前置过滤器(推荐)
前置过滤器的处理思路就是在到达业务controller之前在过滤器先判断流是否为gzip,如果是则将流解压缩,这里面其实涉及到inputStream的复位重用问题,通过查阅相关资料发现PushbackInputStream可以解决。
PushbackInputStream
类中的unread
方法允许你将已经从流中读取的字节重新推回到流的前端,以便这些字节可以被后续的读取操作再次读取。PushbackInputStream
并不是简单地深拷贝了一个新的流,而是作为过滤器流(FilterInputStream
)的一种,它包裹在现有输入流的外部,提供了额外的功能,即推回已读字节的能力。PushbackInputStream
不会创建一个包含流中所有数据的副本,而是维持了一个内部缓冲区,这个缓冲区用于临时存储被推回的字节。
package com.testwork.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
@Component
public class GzipFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if ("gzip".equalsIgnoreCase(request.getHeader("Content-Encoding")) && request.getRequestURI().contains("filter")) {
InputStream is = request.getInputStream();
PushbackInputStream pbi = new PushbackInputStream(is, 2);
// 读取前两个字节
byte[] header = new byte[2];
int bytesRead = pbi.read(header);
// 检查前两个字节是否是GZIP的魔数
boolean isGzip = bytesRead == 2 && header[0] == (byte) 0x1F && header[1] == (byte) 0x8B;
//复位
pbi.unread(header);
// 解压缩,构建新请求,这边要考虑到带了gzip但是没有压缩的场景
GzipHttpServletWrapper newRequest = new GzipHttpServletWrapper(request, pbi, isGzip);
filterChain.doFilter(newRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
package com.testwork.filter;
import org.springframework.mock.web.DelegatingServletInputStream;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.zip.GZIPInputStream;
public class GzipHttpServletWrapper extends HttpServletRequestWrapper {
private InputStream inputStream;
public GzipHttpServletWrapper(HttpServletRequest request, InputStream is, boolean isGzip) throws IOException {
super(request);
this.inputStream = isGzip ? new GZIPInputStream(is) : is ;
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new DelegatingServletInputStream(inputStream);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(inputStream));
}
}
测试截图
选择filter这个URL,执行测试方法如下:
服务端处理方式二:业务controller里面解压缩
1.使用@RequestBody byte[] data 进行解压缩
private String getUnzipDataByBody(byte[] data) {
try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(data));
Reader reader = new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8)) {
char[] buffer = new char[2024];
StringBuilder builder = new StringBuilder();
int len;
while ((len = reader.read(buffer)) > 0) {
builder.append(buffer, 0, len);
}
return builder.toString();
} catch (IOException e) {
throw new RuntimeException("error decompress data", e);
}
}
测试截图
选择body这个URL,执行测试方法如下:
2.使用HttpServletRequest request 进行解压缩
private String getUnzipDataByInputStream(HttpServletRequest request) throws IOException {
String data;
InputStream is = request.getInputStream();
PushbackInputStream pbi = new PushbackInputStream(is, 2);
// 读取前两个字节
byte[] header = new byte[2];
int bytesRead = pbi.read(header);
// 检查前两个字节是否是GZIP的魔数
boolean isGzip = bytesRead == 2 && header[0] == (byte) 0x1F && header[1] == (byte) 0x8B;
//复位
pbi.unread(header);
if (isGzip) {
// 如果是gzip压缩,进行解压处理
byte[] decompressedData = decompressGzip(pbi);
data = new String(decompressedData, StandardCharsets.UTF_8);
} else {
// 如果不是,直接处理
BufferedReader reader = new BufferedReader(new InputStreamReader(pbi, "UTF-8"));
String line;
StringBuilder builder = new StringBuilder();
while ((line = reader.readLine())!= null) {
builder.append(line);
}
data = builder.toString();
}
return data;
}
private byte[] decompressGzip(InputStream is) throws IOException {
GZIPInputStream gzipInputStream = new GZIPInputStream(is);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[2024];
int len;
while ((len = gzipInputStream.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}
测试截图
选择stream这个URL,执行测试方法如下:
总结
处理客户端发送gzip压缩的post请求,使用前置过滤器比较优雅,只需要写一份能够全局生效,比较推荐。如果有特殊需要,也可以使用方式二的两种方法实现。在这个过程中学习到了一个新的输入流PushbackInputStream,对于request.getInputStream流的复位重用有很好的效果,另外发送端的单元测试也实验了httpclient和restemplate两种发送压缩处理代码,特别地,对于httpclient来说构造的数据必须要指定编码,否则遇到中文会遇到乱码的现象。