首页 > 其他分享 >Spring Web : FormHttpMessageConverter

Spring Web : FormHttpMessageConverter

时间:2023-08-13 21:32:09浏览次数:67  
标签:Web return FormHttpMessageConverter Spring charset MediaType private part null

概述

FormHttpMessageConverter是Spring Web提供的用于读写一般HTML表单数据的HttpMessageConverter实现类,也可以写multipart数据,但是不能读取multipart数据。

具体来讲,FormHttpMessageConverter 可以 :

  • 读写application/x-www-form-urlencoded媒体类型数据:MultiValueMap MultiValueMap<String, String>
  • 写multipart/form-data媒体类型数据:MultiValueMap MultiValueMap<String, Object>

注意,不能读该类型数据,也就是不能用于处理文件上传。

当FormHttpMessageConverter写multipart数据时,它还可以使用其他的HttpMessageConverter用于写每一个不同类型的MIME部分(part)。缺省情况下,用于写不同类型MIME部分(part)的HttpMessageConverter是一个StringHttpMessageConverter(字符集缺省使用UTF-8),一个ResourceHttpMessageConverter,还有一个ByteArrayHttpMessageConverter。当然使用者也可以通过#setPartConverters指定自己的HttpMessageConverter集合。

源代码

源代码版本 : spring-web-5.1.5.RELEASE

package org.springframework.http.converter;

// 省略 imports 

public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {

	/**
	 * The default charset used by the converter. 缺省使用字符集 utf-8
	 */
	public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    // 缺省支持的HTTP FORM 的MIME 格式 : application/x-www-form-urlencoded + utf-8
	private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE =
			new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);


	private List<MediaType> supportedMediaTypes = new ArrayList<>();

    // 记录用于操作 multipart 时,写各个 part 使用的 HttpMessageConverter 的集合
	private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();

   // 处理二进制字节和字符串之间转换时要使用的字符集,缺省使用 UTF-8 
	private Charset charset = DEFAULT_CHARSET;

	@Nullable
	private Charset multipartCharset;


	public FormHttpMessageConverter() {
        // 添加缺省支持的 MIME 类型 :
        // 1. application/x-www-form-urlencoded
        // 2. multipart/form-data
		this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
		this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

       // 缺省使用字符集 ISO-8859-1 的 StringHttpMessageConverter
		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		// 设置缺省使用的 multipart data  HttpMessageConverter
		this.partConverters.add(new ByteArrayHttpMessageConverter());
		this.partConverters.add(stringHttpMessageConverter);
		this.partConverters.add(new ResourceHttpMessageConverter());

		// 检查 this.partConverters 中的每个 AbstractHttpMessageConverter , 
		// 如果该 AbstractHttpMessageConverter/ 没有被设置缺省字符集,
		// 则将其缺省字符集设置为 this.charset,此时为 utf-8 
		applyDefaultCharset();
	}


	/**
	 * Set the list of  MediaType objects supported by this converter.
	 */
	public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
		this.supportedMediaTypes = supportedMediaTypes;
	}

	@Override
	public List<MediaType> getSupportedMediaTypes() {
		return Collections.unmodifiableList(this.supportedMediaTypes);
	}

	/**
	 * Set the message body converters to use. These converters are used to
	 * convert objects to MIME parts.
	 */
	public void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
		Assert.notEmpty(partConverters, "'partConverters' must not be empty");
		this.partConverters = partConverters;
	}

	/**
	 * Add a message body converter. Such a converter is used to convert objects
	 * to MIME parts.
	 */
	public void addPartConverter(HttpMessageConverter<?> partConverter) {
		Assert.notNull(partConverter, "'partConverter' must not be null");
		this.partConverters.add(partConverter);
	}

	/**
	 * 设置请求/响应Content-Type头部没有明确设置字符集时读写HTML FORM数据
     * 使用的字符集,不设置的话缺省使用 UTF-8
     *
     * Set the default character set to use for reading and writing form data when
	 * the request or response Content-Type header does not explicitly specify it.
	 * As of 4.3, this is also used as the default charset for the conversion
	 * of text bodies in a multipart request.
	 * As of 5.0 this is also used for part headers including
	 * "Content-Disposition" (and its filename parameter) unless (the mutually
	 * exclusive) #setMultipartCharset is also set, in which case part
	 * headers are encoded as ASCII and filename is encoded with the
	 * "encoded-word" syntax from RFC 2047.
	 * By default this is set to "UTF-8".
	 */
	public void setCharset(@Nullable Charset charset) {
		if (charset != this.charset) {
			this.charset = (charset != null ? charset : DEFAULT_CHARSET);
			applyDefaultCharset();
		}
	}

	/**
	 * Apply the configured charset as a default to registered part converters.
	 * 检查 this.partConverters 中的每个 AbstractHttpMessageConverter , 如果该 AbstractHttpMessageConverter
     * 没有被设置缺省字符集,则将其缺省字符集设置为 this.charset
	 */
	private void applyDefaultCharset() {
		for (HttpMessageConverter<?> candidate : this.partConverters) {
			if (candidate instanceof AbstractHttpMessageConverter) {
				AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
				// Only override default charset if the converter operates with a charset to begin with...
				if (converter.getDefaultCharset() != null) {
					converter.setDefaultCharset(this.charset);
				}
			}
		}
	}

	/**
	 * Set the character set to use when writing multipart data to encode file
	 * names. Encoding is based on the "encoded-word" syntax defined in RFC 2047
	 * and relies on MimeUtility from "javax.mail".
	 * As of 5.0 by default part headers, including Content-Disposition (and
	 * its filename parameter) will be encoded based on the setting of
	 * #setCharset(Charset) or UTF-8 by default.
	 * @since 4.1.1
	 * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
	 */
	public void setMultipartCharset(Charset charset) {
		this.multipartCharset = charset;
	}


    // 是否可以读取某种指定类型的MediaType mediaType :
    // 1. 通过检查 supportedMediaTypes 是否包含 mediaType 判断
    // 2. null mediaType 被支持
    // 3. 不能读取 multipart/form-data 数据, 对此做了特殊处理    
	@Override
	public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
			return false;
		}
		if (mediaType == null) {
			return true;
		}
		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
			// We can't read multipart....
			if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) 
                && supportedMediaType.includes(mediaType)) {
				return true;
			}
		}
		return false;
	}
   
    // 是否可以写入某种指定类型的MediaType mediaType :
    // 1. 通过检查 supportedMediaTypes 是否跟指定类型 mediaType 兼容判断
    // 2. null mediaType 被支持
    // 3. */* mediaType 被支持
	@Override
	public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
		if (!MultiValueMap.class.isAssignableFrom(clazz)) {
			return false;
		}
		if (mediaType == null || MediaType.ALL.equals(mediaType)) {
			return true;
		}
		for (MediaType supportedMediaType : getSupportedMediaTypes()) {
			if (supportedMediaType.isCompatibleWith(mediaType)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,
			HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

       // 获取  inputMessage 消息的内容类型到 contentType
		MediaType contentType = inputMessage.getHeaders().getContentType();
       // 获取  inputMessage 消息内容类型 contentType 指定的字符集,
       // 如果 contentType 未指定字符集,使用当前对象的属性 this.charset , 缺省是 UTF-8
		Charset charset = (contentType != null && contentType.getCharset() != null ?
				contentType.getCharset() : this.charset);
       // 使用字符集 charset 将消息体字节读取成字符串,记录到 body         
		String body = StreamUtils.copyToString(inputMessage.getBody(), charset);

       // 对消息体进行分析,因为该类仅支持读取HTTP FORM数据,这里按照 HTTP FORM数据的在
       // HTTP request body 中的格式进行分析
		String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
		MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
		for (String pair : pairs) {
			int idx = pair.indexOf('=');
			if (idx == -1) {
				result.add(URLDecoder.decode(pair, charset.name()), null);
			}
			else {
				String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
				String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
				result.add(name, value);
			}
		}
		return result;
	}

    // 写 FORM 数据到 outputMessage 消息体
	@Override
	@SuppressWarnings("unchecked")
	public void write(MultiValueMap<String, ?> map, @Nullable MediaType contentType, 
        HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		if (!isMultipart(map, contentType)) {
			writeForm((MultiValueMap<String, Object>) map, contentType, outputMessage);
		}
		else {
			writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
		}
	}


	private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
		if (contentType != null) {
			return MediaType.MULTIPART_FORM_DATA.includes(contentType);
		}
		for (String name : map.keySet()) {
			for (Object value : map.get(name)) {
				if (value != null && !(value instanceof String)) {
					return true;
				}
			}
		}
		return false;
	}

    // 写非 multipart FORM 数据到 outputMessage 消息体
	private void writeForm(MultiValueMap<String, Object> formData, @Nullable MediaType contentType,
			HttpOutputMessage outputMessage) throws IOException {

		contentType = getMediaType(contentType);
		outputMessage.getHeaders().setContentType(contentType);

		Charset charset = contentType.getCharset();
		Assert.notNull(charset, "No charset"); // should never occur

		final byte[] bytes = serializeForm(formData, charset).getBytes(charset);
		outputMessage.getHeaders().setContentLength(bytes.length);

		if (outputMessage instanceof StreamingHttpOutputMessage) {
			StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
			streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
		}
		else {
			StreamUtils.copy(bytes, outputMessage.getBody());
		}
	}

	private MediaType getMediaType(@Nullable MediaType mediaType) {
		if (mediaType == null) {
			return DEFAULT_FORM_DATA_MEDIA_TYPE;
		}
		else if (mediaType.getCharset() == null) {
			return new MediaType(mediaType, this.charset);
		}
		else {
			return mediaType;
		}
	}

	protected String serializeForm(MultiValueMap<String, Object> formData, Charset charset) {
		StringBuilder builder = new StringBuilder();
		formData.forEach((name, values) ->
				values.forEach(value -> {
					try {
						if (builder.length() != 0) {
							builder.append('&');
						}
						builder.append(URLEncoder.encode(name, charset.name()));
						if (value != null) {
							builder.append('=');
							builder.append(URLEncoder.encode(String.valueOf(value), charset.name()));
						}
					}
					catch (UnsupportedEncodingException ex) {
						throw new IllegalStateException(ex);
					}
				}));

		return builder.toString();
	}

    // 写 multipart FORM 数据到 outputMessage 消息体
	private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
			throws IOException {

		final byte[] boundary = generateMultipartBoundary();
		Map<String, String> parameters = new LinkedHashMap<>(2);
		if (!isFilenameCharsetSet()) {
			parameters.put("charset", this.charset.name());
		}
		parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));

		MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
		HttpHeaders headers = outputMessage.getHeaders();
		headers.setContentType(contentType);

		if (outputMessage instanceof StreamingHttpOutputMessage) {
			StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
			streamingOutputMessage.setBody(outputStream -> {
				writeParts(outputStream, parts, boundary);
				writeEnd(outputStream, boundary);
			});
		}
		else {
			writeParts(outputMessage.getBody(), parts, boundary);
			writeEnd(outputMessage.getBody(), boundary);
		}
	}

	/**
	 * When #setMultipartCharset(Charset) is configured (i.e. RFC 2047,
	 * "encoded-word" syntax) we need to use ASCII for part headers or otherwise
	 * we encode directly using the configured  #setCharset(Charset).
	 */
	private boolean isFilenameCharsetSet() {
		return (this.multipartCharset != null);
	}

   // 写 multipart FORM 数据中的各个 part 到 outputMessage 消息体的输出流 os
	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);
					writePart(name, getHttpEntity(part), os);
					writeNewLine(os);
				}
			}
		}
	}

   // 写 multipart FORM 数据中的某个 part 到 outputMessage 消息体的输出流 os 
	@SuppressWarnings("unchecked")
	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();
		for (HttpMessageConverter<?> messageConverter : this.partConverters) {
			if (messageConverter.canWrite(partType, partContentType)) {
				Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
				HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
				multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
				if (!partHeaders.isEmpty()) {
					multipartMessage.getHeaders().putAll(partHeaders);
				}
				((HttpMessageConverter<Object>) messageConverter).write(partBody, 
																		partContentType, 
																		multipartMessage);
				return;
			}
		}
		throw new HttpMessageNotWritableException(
				"Could not write request: no suitable HttpMessageConverter " +
				"found for request type [" + partType.getName() + "]");
	}

	/**
	 * Generate a multipart boundary.
	 * This implementation delegates to
	 * MimeTypeUtils#generateMultipartBoundary().
	 */
	protected byte[] generateMultipartBoundary() {
		return MimeTypeUtils.generateMultipartBoundary();
	}

	/**
	 * Return an HttpEntity for the given part Object.
	 * @param part the part to return an HttpEntity for
	 * @return the part Object itself it is an HttpEntity,
	 * or a newly built HttpEntity wrapper for that part
	 */
	protected HttpEntity<?> getHttpEntity(Object part) {
		return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<>(part));
	}

	/**
	 * Return the filename of the given multipart part. This value will be used for the
	 * Content-Disposition header.
	 * The default implementation returns Resource#getFilename() if the part is a
	 * Resource, and null in other cases. Can be overridden in subclasses.
	 * @param part the part to determine the file name for
	 * @return the filename, or null if not known
	 */
	@Nullable
	protected String getFilename(Object part) {
		if (part instanceof Resource) {
			Resource resource = (Resource) part;
			String filename = resource.getFilename();
			if (filename != null && this.multipartCharset != null) {
				filename = MimeDelegate.encode(filename, this.multipartCharset.name());
			}
			return filename;
		}
		else {
			return null;
		}
	}


	private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
		os.write('-');
		os.write('-');
		os.write(boundary);
		writeNewLine(os);
	}

	private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
		os.write('-');
		os.write('-');
		os.write(boundary);
		os.write('-');
		os.write('-');
		writeNewLine(os);
	}

	private static void writeNewLine(OutputStream os) throws IOException {
		os.write('\r');
		os.write('\n');
	}


	/**
	 * Implementation of org.springframework.http.HttpOutputMessage used
	 * to write a MIME multipart.
	 */
	private static class MultipartHttpOutputMessage implements HttpOutputMessage {

		private final OutputStream outputStream;

		private final Charset charset;

		private final HttpHeaders headers = new HttpHeaders();

		private boolean headersWritten = false;

		public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
			this.outputStream = outputStream;
			this.charset = charset;
		}

		@Override
		public HttpHeaders getHeaders() {
			return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
		}

		@Override
		public OutputStream getBody() throws IOException {
			writeHeaders();
			return this.outputStream;
		}

		private void writeHeaders() throws IOException {
			if (!this.headersWritten) {
				for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
					byte[] headerName = getBytes(entry.getKey());
					for (String headerValueString : entry.getValue()) {
						byte[] headerValue = getBytes(headerValueString);
						this.outputStream.write(headerName);
						this.outputStream.write(':');
						this.outputStream.write(' ');
						this.outputStream.write(headerValue);
						writeNewLine(this.outputStream);
					}
				}
				writeNewLine(this.outputStream);
				this.headersWritten = true;
			}
		}

		private byte[] getBytes(String name) {
			return name.getBytes(this.charset);
		}
	}


	/**
	 * Inner class to avoid a hard dependency on the JavaMail API.
	 */
	private static class MimeDelegate {

		public static String encode(String value, String charset) {
			try {
				return MimeUtility.encodeText(value, charset, null);
			}
			catch (UnsupportedEncodingException ex) {
				throw new IllegalStateException(ex);
			}
		}
	}

}

标签:Web,return,FormHttpMessageConverter,Spring,charset,MediaType,private,part,null
From: https://blog.51cto.com/u_15668812/7069840

相关文章

  • 自定义springboot-starter包
    https://www.cnblogs.com/yuansc/p/9088212.html 前言我们都知道可以使用SpringBoot快速的开发基于Spring框架的项目。由于围绕SpringBoot存在很多开箱即用的Starter依赖,使得我们在开发业务代码时能够非常方便的、不需要过多关注框架的配置,而只需要关注业务即可。例如我想......
  • 缓存套餐_Spring Cache _入门案例1
                    ......
  • 【愚公系列】2023年08月 攻防世界-Web(ics-02)
    (文章目录)前言SSRF(服务器端请求伪造)是一种攻击技术,攻击者通过构造恶意请求,欺骗服务器发起外部请求,获取服务器本应该不被直接访问的信息或服务。攻击者可利用SSRF进行一系列攻击,包括对内部资源进行扫描、窃取敏感信息、攻击内部系统等。SQL注入是一种常见的Web攻击技术,攻......
  • 【web_逆向02】某易云逆向注意点
    加密入口需要用到a,b,c三个函数a,不需要动,直接copy就行b,标准的第三方库,直接安装即可:npminstallcrypto-js千万不要去抠CryptoJS,希望各位记住这个格式.js的标准第三方库https://www.npmjs.com/c,有点麻烦,库太老了--npm找不到functionc(a,b,c){var......
  • spring事件
    1、SpringEvent参考CSDN-Spring中事件监听(通知)机制详解与实践【含原理】掘金-TransactionalEventListener使用场景以及实现原理,最后要躲个大坑demo项目hy-springEvent-demo【springEventdemo项目】数据库在项目目录的datebase里面SpringEvent用于对事件的监听与处理,他是对设计......
  • 【web_逆向01】环境安装
    node.js环境安装官网下载,直接下一步就行安装后,在cmd环境,测试pycharm运行js代码安装node.js插件,安装后记得重启pycharm即可python调用js代码运行,pyexecjs模块pipinstallpyexecjs使用importexecjs#请注意,安装的是pyexecjs.使用的是execjs##1.直接执......
  • Burp Suite Professional / Community 2023.9 (macOS, Linux, Windows) - Web 应用安
    BurpSuiteProfessional/Community2023.9(macOS,Linux,Windows)-Web应用安全、测试和扫描BurpSuiteProfessional,Test,find,andexploitvulnerabilities.请访问原文链接:https://sysin.org/blog/burp-suite-pro-2023/,查看最新版。原创作品,转载请保留出处。作者......
  • Web通用漏洞--RCE
    Web通用漏洞--RCE漏洞简介RCE远程代码/命令执行漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统。RCE漏洞也分为代码执行漏洞和命令执行漏洞,所谓代码执行即通过漏洞点注入参数而使用源代码进行相应的操作,所谓的命令执行就是通过漏洞点注入参数......
  • Web攻防--xxe实体注入
    web攻防--xxe实体注入漏洞简介XML外部实体注入(也称为XXE)是一种Web安全漏洞,允许攻击者干扰应用程序对XML数据的处理。它通常允许攻击者查看应用程序服务器文件系统上的文件,并与应用程序本身可以访问的任何后端或外部系统进行交互。在某些情况下,攻击者可以利用XXE漏洞联......
  • ctfshow--web入门--XXE
    ctfshow--web入门--XXEweb373源码<?phperror_reporting(0);libxml_disable_entity_loader(false); //允许加载外部实体$xmlfile=file_get_contents('php://input'); //使用php伪协议进行接收值,并进行文件读取if(isset($xmlfile)){$dom=newDOMDocument();......