Spring中使用RestTemplate实现Form表单提交原理解析
文章目录
今天刚好是个周末闲来无事,想起前两天工作中遇到的一个问题,我个人认为是个不错的案例,特此来记录下,
form表单数据提交
,需求是这样:通过POST表单数据提交以及文件上传。需求分析:其一是以POST表单数据提交数据;其二是以POST表单实现文件的上传。
此处我使用的Spring中的RestTemplate
来作为客户端请求的发起方。
1、使用RestTemplate
在Spring中注入RestTemplate对象
/**
* @author Mr.Gao
* @date 2023/6/13 14:50
* @apiNote:
*/
@Configuration
public class RestTemplateConfig {
/**
* 连接超时时间
*/
private static final int CONNECT_TIMEOUT = 5 * 1000;
/**
* 设置数据超时时间
*/
private static final int READ_TIMEOUT = 30 * 1000;
/**
* 从线程池中获取线程超时时间
*/
private static final int CONNECT_REQUEST_TIMEOUT = 10 * 1000;
/**
* 连接池最大生成连接数
*/
private static final int CONNECT_MAX_TOTAL = 1000;
/**
* 默认设置route最大连接数,同路由并发数(每个主机的并发)
*/
private static final int DEFAULT_MAX_PER_ROUTE = CONNECT_MAX_TOTAL / 2;
/**
* 请求连接池配置
*
* @return
*/
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
// --配置连接池管理器
PoolingHttpClientConnectionManager clientConnectionManager = new PoolingHttpClientConnectionManager();
clientConnectionManager.setMaxTotal(CONNECT_MAX_TOTAL);
clientConnectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
// --创建连接客户端
CloseableHttpClient closeableHttpClient = HttpClientBuilder.create()
// -连接池配置
.setConnectionManager(clientConnectionManager)
// --采用长连接策略
.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
// --连接配置
.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(READ_TIMEOUT).setConnectionRequestTimeout(CONNECT_REQUEST_TIMEOUT).build()).build();
return new HttpComponentsClientHttpRequestFactory(closeableHttpClient);
}
@Bean
public MappingJackson2HttpMessageConverter myMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.MULTIPART_FORM_DATA,
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.ALL));
return messageConverter;
}
/**
* RestTemplate配置Client
*
* @return
*/
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplateBuilder().build();
restTemplate.setRequestFactory(clientHttpRequestFactory());
// 配置转化器
restTemplate.getMessageConverters().add(myMappingJackson2HttpMessageConverter());
return restTemplate;
}
}
使用时先注入
RestTemplate
对象
@Autowired
private RestTemplate restTemplate;
2、工作原理
2.1 RestTemplate中注册消息转化器HttpMessageConverter
public RestTemplate() {
// 字节数组Http消息转化器
this.messageConverters.add(new ByteArrayHttpMessageConverter());
// 字符串Http消息转化器
this.messageConverters.add(new StringHttpMessageConverter());
// 此转换器可以读取所有媒体类型
this.messageConverters.add(new ResourceHttpMessageConverter(false));
// 用于 text/xml and application/xml, and application/*-xml.
this.messageConverters.add(new SourceHttpMessageConverter<>());
// 用于表单提交处理
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
// xml消息处理器
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
// json处理器
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
...
this.uriTemplateHandler = initUriTemplateHandler();
}
2.2 AllEncompassingFormHttpMessageConverter(处理表单提交)
2.2.1 FormHttpMessageConverter(父类构造器)
在初始化AllEncompassingFormHttpMessageConverter对象时,先调用父类构造器FormHttpMessageConverter来初始化支持的MediaType和HttpMessageConverts,
此处初始化的是当前自身的处理器(先预留个悬念)。
public FormHttpMessageConverter() {
// application/x-www-form-urlencoded
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
// multipart/form-data
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
// multipart/mixed
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
// 为AllEncompassingFormHttpMessageConverter的指定转化处理器
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(new StringHttpMessageConverter());
// 用来处理文件资源
this.partConverters.add(new ResourceHttpMessageConverter());
// 默认字符集为UTF-8
applyDefaultCharset();
}
2.2.2 AllEncompassingFormHttpMessageConverter初始化完毕
注意参数: this.partConverters,此处ResourceHttpMessageConverter
用于解析表单中参数为file类型的参数。
2.3 以RestTemplate的exchange方法展开论述
/**
* @param url : 请求的url地址
* @param method: 请求方式(GET/POST/PUT/PATCH等...)
* @param requestEntity: 请求信息实体(包含请求头(header)和请求体(requestBody))
* @param responseType: 返回类型(泛型,用来指定返回结果的数据类型)
* @param uriVariables: 可变参数(暂时可忽略)
* /
@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
2.3.1 解析exchange方法
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
throws RestClientException {
// 创建HttpEntityRequestCallback对象用来装载requestBody(请求体body)和responseType(响应类型)
RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
// 创建ResponseEntityResponseExtractor用来装载responseType(响应类型)
ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
// nonNull方法用来判断结果是否为空,若为空,则抛出异常IllegalStateException,提示: No result
// execute执行请求的方法
return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));
}
2.3.2 解析execute方法
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
// 将String类型url转化为URI
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
// 进入真正执行请求逻辑
return doExecute(expanded, method, requestCallback, responseExtractor);
}
2.3.3 解析doExecute方法
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
// url 和 method 进行非空判断
Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
// 通过ClientHttpRequestFactory工厂创建ClientHttpRequest,
// 我采用的自定义HttpComponentsClientHttpRequestFactory,客户端Client使用的CloseableHttpClient
// 具体的可以查看自定义配置:RestTemplateConfig配置类
ClientHttpRequest request = createRequest(url, method);
// requestCallback 不为空,执行doWithRequest方法(用来执行数据的封装)
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
// ClientHttpRequest执行请求发送
response = request.execute();
// 处理响应
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
response.close();
}
}
}
2.3.4 解析doWithRequest方法
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
// 实际表示的是对请求头的设置,request.getHeaders().setAccept(allSupportedMediaTypes);
super.doWithRequest(httpRequest);
// 获取到请求体
Object requestBody = this.requestEntity.getBody();
// 请求体为空
if (requestBody == null) {
HttpHeaders httpHeaders = httpRequest.getHeaders();
HttpHeaders requestHeaders = this.requestEntity.getHeaders();
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));
}
if (httpHeaders.getContentLength() < 0) {
httpHeaders.setContentLength(0L);
}
}
else {
// 请求体不为空
// 获取请求体的Class类对象
Class<?> requestBodyClass = requestBody.getClass();
Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
HttpHeaders httpHeaders = httpRequest.getHeaders();
HttpHeaders requestHeaders = this.requestEntity.getHeaders();
// 获取 content-type
MediaType requestContentType = requestHeaders.getContentType();
// 此处是对RestTemplate中的this.messageConverters进行遍历,上边分析所得
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<Object> genericConverter =
(GenericHttpMessageConverter<Object>) messageConverter;
if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
// 添加请求体中的Header信息
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));
}
// 如果日志级别为debug时输出日志
logBody(requestBody, requestContentType, genericConverter);
// 执行
genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
return;
}
}
else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
// 添加请求体中的Header信息
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));
}
// 如果日志级别为debug时输出日志
logBody(requestBody, requestContentType, messageConverter);
//
((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, httpRequest);
return;
}
}
String message = "No HttpMessageConverter for " + requestBodyClass.getName();
if (requestContentType != null) {
message += " and content type \"" + requestContentType + "\"";
}
throw new RestClientException(message);
}
}
2.3.4 执行AllEncompassingFormHttpMessageConverter的write方法
public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// MediaType.MULTIPART_FORM_DATA的type为multipart
if (isMultipart(map, contentType)) {
// MediaType类型为MULTIPART_FORM_DATA和MULTIPART_MIXED类型的数据
writeMultipart((MultiValueMap<String, Object>) map, contentType, outputMessage);
}
else {
// 处理MediaType类型APPLICATION_FORM_URLENCODED的数据
writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
}
}
2.3.5 执行FormHttpMessageConverter的writeMultipart方法
/**
* @param outputMessage : HttpComponentsClientHttpRequest实例
*/
private void writeMultipart(
MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
if (contentType == null) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
Map<String, String> parameters = new LinkedHashMap<>(contentType.getParameters().size() + 2);
parameters.putAll(contentType.getParameters());
// 生成boundary参数
byte[] boundary = generateMultipartBoundary();
if (!isFilenameCharsetSet()) {
if (!this.charset.equals(StandardCharsets.UTF_8) &&
!this.charset.equals(StandardCharsets.US_ASCII)) {
parameters.put("charset", this.charset.name());
}
}
// 设置boundary参数 且 boundary参数缺失会报错
parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
// Add parameters to output content type
contentType = new MediaType(contentType, parameters);
outputMessage.getHeaders().setContentType(contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
});
}
else {
// 将参数parts和boundary通过OutputStream写入
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
}
2.3.6 执行FormHttpMessageConverter的writeParts方法
/**
* @param os 输出流
* @param parts 请求体入参
* @parm boundary 参数
*/
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
// name为key,part为value
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
}
此处不由的发现:
自身转化器this.partConverters
和前边2.2.1节相呼应,此刻我心里的疑惑貌似已经有了答案,答案就是不同类型的字段都会匹配到适合的转化器来进行转化输出。
- 小结
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
// 获取参数的值
Object partBody = partEntity.getBody();
if (partBody == null) {
throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
}
// 获取对应参数的类型
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
// 通过this.partConverters匹配适合partType 类型的消息处理器
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
// 请求Header设置 Content-Disposition
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
// ResourceHttpMessageConverter.write()用来处理file类型数据
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
}
2.3.7 ResourceHttpMessageConverter解析参数类型支持哪些
- WritableResource
- ContextResource
- UrlResource
- FileUrlResource
- FileSystemResource(常用)
- ClassPathResource(常用)
- ByteArrayResource
- InputStreamResource
2.4 实现表单提交使用小结
- 1、MediaType为
APPLICATION_FORM_URLENCODED(表单提交),若表单数据中存在file类型,则需要更改为MULTIPART_FORM_DATA
- 2、RestTemplate中的HttpRequestEntity中的body参数类型为
MultiValueMap的实例
- 3、MultiValueMap中包含file类型的属性,则属性需要转化为
FileSystemResource, ClassPathResource等实现类即可。
- 4、补充点:表单提交数据时,
数据格式为键值对形式,例如:name=value1&name2=value2 而非json格式。
3、小结
以上是我对Spring中使用RestTemplate实现表单数据提交的原理追踪,写着写着不由得发现写多了(因为越聊越多,哈哈哈,根本聊不完),各位小伙伴看着理解,我尽可能的写了我的所有理解,希望能帮助到工作中的你。
如果存在错误的地方,望指正!!!
最后,如果你看到了这里,说明也是钻研技术的程序员一枚,希望各位小伙伴能一键三连哦!
每日一语: 每一个日出,都是新的希望;每一个日落,都是新的期待。保持积极,珍惜当下,每一天都是满满的正能量!