Spring中使用RestTemplate实现Form表单提交原理解析

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类型的参数。

AllEncompassingFormHttpMessageConverter初始化完毕

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方法

write方法
isMultipart执行的原因

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实现表单数据提交的原理追踪,写着写着不由得发现写多了(因为越聊越多,哈哈哈,根本聊不完),各位小伙伴看着理解,我尽可能的写了我的所有理解,希望能帮助到工作中的你。如果存在错误的地方,望指正!!!

最后,如果你看到了这里,说明也是钻研技术的程序员一枚,希望各位小伙伴能一键三连哦!

每日一语: 每一个日出,都是新的希望;每一个日落,都是新的期待。保持积极,珍惜当下,每一天都是满满的正能量!满满正能量

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值