HTTP协议上传文件
在 HTTP 协议中,Content-Type 头部用于指示资源的类型。一般的服务端框架,都内置了自动解析 HTTP 数据格式的功能,它们通常根据 Content-Type 来获知数据的编码格式,从而对请求体进行解析。
在客户端发送到服务端的请求中,有 4 种常见的 Content-Type 类型:
application/x-www-form-urlencoded:浏览表单默认的编码方式。将数据按照 k1=v1&k2=v2 格式组装,并对特殊符号进行转义。
multipart/form-data:上传文件时的编码方式。需要指定一个复杂的boundary(分隔符),避免和内容冲突。
application/json:请求体是序列化后的 json 字符串
text/plain:普通文本
综上:使用HTTP上传文件时,在请求头中指定 Content-Type=Content-Type: multipart/form-data; boundary=(一个复杂的分隔符)
httpclient上传文件
httpclient有多个版本,其中4.x、5.x 版本的 API 较为接近,与 3.x 版本差异较大。接下来文章会以 4.x 版本为例,介绍 httpclient 上传文件的细节。
添加相关 pom 依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.12</version>
</dependency>
4.3版本后,原有的文件上传方法 MultipartEntity 已废弃,推荐使用 httpmime 下的 MultipartEntityBuilder。
httpclient 的使用方法
创建 HttpClient 对象。
创建 HttpUriRequest 对象,指定请求 url。HttpGet、HttpPost 分别用于发送 get 和 post 请求。
设置请求参数。url 中的参数,在上一步中,通过 URIBuilder.addParameter 设置;请求体中的参数,通过 HttpPost.setEntity 方法设置。
调用 httpClient.execute(httpRequestBase, context) 发起请求,返回一个 HttpResponse。
调用 httpResponse.getEntity() 获取响应体。
关闭 HttpClient 对象,释放资源。
以上步骤是使用的逻辑顺序,实际代码顺序可能会有些不同。
代码示例
public class HttpUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpUtil.class);
private static final ContentType STRING_CONTENT_TYPE = ContentType.create("text/plain", StandardCharsets.UTF_8);
public static FacadeResponse multipartPost(String url, Map<String, String> headers, Map<String, Object> paramMap) {
// 创建 HttpPost 对象
HttpPost httpPost = new HttpPost(url);
// 设置请求头
if (MapUtils.isNotEmpty(headers)) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
httpPost.setHeader(key, value);
}
}
// 设置请求参数
if (MapUtils.isNotEmpty(paramMap)) {
// 使用 MultipartEntityBuilder 构造请求体
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
//设置浏览器兼容模式
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
//设置请求的编码格式
builder.setCharset(Consts.UTF_8);
// 设置 Content-Type
builder.setContentType(ContentType.MULTIPART_FORM_DATA);
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
String key = entry.getKey();
Object value = paramMap.get(key);
// 添加请求参数
addMultipartBody(builder, key, value);
}
HttpEntity entity = builder.build();
// 将构造好的 entity 设置到 HttpPost 对象中
httpPost.setEntity(entity);
}
return execute(httpPost, null);
}
private static void addMultipartBody(MultipartEntityBuilder builder, String key, Object value) {
if (value == null) {
return;
}
// MultipartFile 是 spring mvc 接收到的文件。
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
try {
builder.addBinaryBody(key, file.getInputStream(), ContentType.MULTIPART_FORM_DATA, file.getOriginalFilename());
} catch (IOException e) {
LOGGER.error("read file err.", e);
}
} else if (value instanceof File) {
File file = (File) value;
builder.addBinaryBody(key, file, ContentType.MULTIPART_FORM_DATA, file.getName());
} else if (value instanceof List) {
// 列表形式的参数,要一个一个 add
List<?> list = (List<?>) value;
for (Object o : list) {
addMultipartBody(builder, key, o);
}
} else if (value instanceof Date) {
// 日期格式的参数,使用约定的格式
builder.addTextBody(key, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value));
} else {
// 使用 UTF_8 编码的 ContentType,否则可能会有中文乱码问题
builder.addTextBody(key, value.toString(), STRING_CONTENT_TYPE);
}
}
private static FacadeResponse execute(HttpRequestBase httpRequestBase, HttpContext context) {
CloseableHttpClient httpClient = HttpClients.createDefault();
// 使用 try-with-resources 发起请求,保证请求完成后资源关闭
try (CloseableHttpResponse httpResponse = httpClient.execute(httpRequestBase, context)) {
// 处理响应头
Map<String, List<String>> headers = headerToMap(httpResponse.getAllHeaders());
// 处理响应体
HttpEntity httpEntity = httpResponse.getEntity();
if (httpEntity != null) {
String entityContent = EntityUtils.toString(httpEntity, Consts.UTF_8);
return new FacadeResponse(httpRequestBase.getRequestLine().getUri(), httpResponse.getStatusLine().getStatusCode(), headers, entityContent);
}
} catch (Exception ex) {
LOGGER.error("http execute failed.", ex);
}
return new FacadeResponse(httpRequestBase.getRequestLine().getUri(), HttpStatus.INTERNAL_SERVER_ERROR.value(), null, "http execute failed.");
}
/**
* 将headers转map
*
* @param headers 头信息
* @return map
*/
private static Map<String, List<String>> headerToMap(Header[] headers) {
if (null == headers || headers.length == 0) {
return Collections.emptyMap();
}
Map<String, List<String>> map = new HashMap<>();
for (Header header : headers) {
map.putIfAbsent(header.getName(), Lists.newArrayList(header.getValue()));
}
return map;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FacadeResponse implements Serializable {
// url, 包含?参数
private String url;
// 返回状态码
private int statusCode;
// 返回头信息
private Map<String, List<String>> headers;
// 返回entity内容
private String entityContent;
}
后记
这篇文章中的代码是笔者翻阅了许多资料后写出的。过程中遇到了 httpclient API 变更的问题,列表参数传递的问题,中文乱码的问题等等。
从上文的代码中,我们可以发现,使用 httpclient 上传文件时,代码还是相对复杂的,有许多注意点。那有没有一个简洁的 HTTP 工具类呢?
笔者后来发现了 hutool 提供的 HttpUtil,可以非常方便地上传下载文件,推荐读者们去尝试一下。