• SpringMVC系列-5 消息转换器


    背景

    SpringMVC系列的第五篇介绍消息转换器,本文讨论的消息转换指代调用Controller接口后,对结果进行转换处理的过程。
    内容包括介绍自定义消息转换器、SpringMVC常见的消息转换器、Spring消息转换器工作原理等三部分。

    本文以 SpringMVC系列-2 HTTP请求调用链SpringMVC系列-4 参数解析器 为基础,对相同内容不再重述。

    1.自定义消息转换器

    自定义消息转换器,需要实现HttpMessageConverter接口,该接口定义如下:

    public interface HttpMessageConverter<T> {
    	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    
    	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
    
    	List<MediaType> getSupportedMediaTypes();
    	
    	// ⚠️:read相关逻辑不是本文关注的部分
    	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    	T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    有三个比较重要的方法:
    (1) getSupportedMediaTypes方法返回该解析器支持的MIME媒体类型;
    (2) canWrite方法判断该解析器能否将目标类型的对象转化为指定的MIME媒体类型;
    (3) write方法将目标对象转化为mediaType的二进制流并写入到outputMessage流对象中。

    自定义消息转换器:

    public class UserInfoHttpMessageConverter implements HttpMessageConverter<UserInfo> {
        @Override
        public boolean canWrite(Class clazz, MediaType mediaType) {
            return clazz == UserInfo.class;
        }
    
        @Override
        public List<MediaType> getSupportedMediaTypes() {
            return Collections.singletonList(MediaType.APPLICATION_JSON);
        }
    
        @Override
        public void write(UserInfo userInfo, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
            String result = userInfo.getId() + "_" + userInfo.getName() + "_" + LocalDateTime.now();
            outputMessage.getBody().write(result.getBytes(StandardCharsets.UTF_8));
        }
        //...read Ignore
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    该自定义转换器表示可以将UserInfo类型的消息以"application/json"媒体格式写出。

    将自定义的消息转换器注册到SpringMVC:

    @Configuration
    public class MyWebMvcConfigurer implements WebMvcConfigurer {
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(0, new UserInfoHttpMessageConverter());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:这里通过SpringMVC的配置类WebMvcConfigurer进行注册,注册原理在本文第三章中说明。

    用例涉及的Controller接口和基础类:

    @RestController
    @RequestMapping("/api/user")
    public class UserInfoController {
        @GetMapping("/query")
        public UserInfo query() {
            return new UserInfo().setName("test_sy").setId(28);
        }
    }
    
    @Data
    @Accessors(chain = true)
    public class UserInfo {
        private Integer id;
    
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    使用postman调用结果如下所示:
    在这里插入图片描述

    2.消息转换器

    SpringBoot版本为2.3.2.RELEASE

    2.1 框架内置的消息解析器

    框架内置的消息解析器支持的MIME类型分布如下所示:
    ByteArrayHttpMessageConverter:用于处理字节数组(byte array)的转换。

    ByteArrayHttpMessageConverter
        application/octet-stream
        */*
    
    • 1
    • 2
    • 3

    StringHttpMessageConverter:用于处理字符串的转换。

    StringHttpMessageConverter
        text/plain
        */*
       
    StringHttpMessageConverter
        text/plain
        */*
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ResourceHttpMessageConverter:用于处理Spring Resource的实现类的转换。Spring Resource是一个抽象类,它封装了对各种资源(如文件、数据库连接等)的操作。

    ResourceHttpMessageConverter
        */*
    
    • 1
    • 2

    ResourceRegionHttpMessageConverter:这个类是ResourceHttpMessageConverter的子类,它用于处理Resource的某个特定区域(如文件的某个部分)。

    ResourceRegionHttpMessageConverter
        */*
    
    • 1
    • 2

    SourceHttpMessageConverter:用于处理javax.xml.transform.Source的转换。javax.xml.transform.Source是用于XML转换的接口。

    SourceHttpMessageConverter
        application/xml   
        text/xml 
        application/*+xml
    
    • 1
    • 2
    • 3
    • 4

    AllEncompassingFormHttpMessageConverter:用于处理表单提交请求,能解析复杂的form表单,包括文件上传等。

    AllEncompassingFormHttpMessageConverter
        application/x-www-form-urlencoded
        multipart/form-data
        multipart/mixed
    
    • 1
    • 2
    • 3
    • 4

    MappingJackson2HttpMessageConverter:用于处理JSON序列化和反序列化。

    MappingJackson2HttpMessageConverter
        application/json   
        application/*+json
    
    MappingJackson2HttpMessageConverter
        application/json
        application/*+json
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Jaxb2RootElementHttpMessageConverter:这个类使用JAXB(Java Architecture for XML Binding)进行XML序列化和反序列化。

    Jaxb2RootElementHttpMessageConverter
        application/xml
        text/xml
        application/*+xml
    
    • 1
    • 2
    • 3
    • 4

    上述内置转换器中包括2个StringHttpMessageConverter和2个MappingJackson2HttpMessageConverter。转换器的顺序决定了其优先级,因此第二个StringHttpMessageConverter和MappingJackson2HttpMessageConverter处于失效状态:
    [1] HttpMessageConvertersAutoConfiguration自动装配类引入的StringHttpMessageConverter替代了默认的StringHttpMessageConverter(SpringMVC框架自带),区别是前者默认字符集为UTF_8,后者为ISO_8859_1

    [2] JacksonHttpMessageConvertersConfiguration自动装配类引入的MappingJackson2HttpMessageConverter替代了默认的MappingJackson2HttpMessageConverter。区别是使用其内部实现序列化和反序列化的ObjectMapper对象来自全局Bean对象(来自JacksonAutoConfiguration自动装配类引入的ObjectMapper)。因此在配置文件中对spring.jackson属性的配置可以体现在MappingJackson2HttpMessageConverter转换器上。

    2.2 MappingJackson2HttpMessageConverter转换器

    (1) 匹配方法
    由于MappingJackson2HttpMessageConverter是GenericHttpMessageConverter接口的实现类,匹配时根据canWrite(Type, Class, MediaType)方法进行:

    @Override
    public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
    	return canWrite(clazz, mediaType);
    }
    
    • 1
    • 2
    • 3
    • 4

    上述方法实现时吞掉了Type类型的参数, 调用重载的canWrite(Class, MediaType)方法:

    @Override
    public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
    if (!canWrite(mediaType)) {
    	return false;
    }
    if (mediaType != null && mediaType.getCharset() != null) {
    	Charset charset = mediaType.getCharset();
    	if (!ENCODINGS.containsKey(charset.name())) {
    		return false;
    	}
    }
    
    AtomicReference<Throwable> causeRef = new AtomicReference<>();
    if (this.objectMapper.canSerialize(clazz, causeRef)) {
    	return true;
    }
    logWarningIfNecessary(clazz, causeRef.get());
    return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    该方法可以分为三个部分:
    [1] 调用canWrite(MediaType)判断媒体类型是否支持:

    protected boolean canWrite(@Nullable MediaType mediaType) {
    	if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
    		return true;
    	}
    	for (MediaType supportedMediaType : getSupportedMediaTypes()) {
    		if (supportedMediaType.isCompatibleWith(mediaType)) {
    			return true;
    		}
    	}
    	return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果mediaType为空或者*/*或者与消息解析器支持的类型匹配则返回true;框架预置MappingJackson2HttpMessageConverter时,支持的MediaType已确定,为application/jsonapplication/*+json

    [2] 判断编码类型是否符合, 支持的编码格式有UTF-8,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,US-ASCII

    if (mediaType != null && mediaType.getCharset() != null) {
    	Charset charset = mediaType.getCharset();
    	if (!ENCODINGS.containsKey(charset.name())) {
    		return false;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    MediaType对象的Charset为空时,默认支持;

    [3] 调用ObjectMapper的canSerialize方法判断是否可被序列化;

    AtomicReference<Throwable> causeRef = new AtomicReference<>();
    if (this.objectMapper.canSerialize(clazz, causeRef)) {
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4

    (2) 写方法

    @Override
    public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    	final HttpHeaders headers = outputMessage.getHeaders();
    	// 添加Content-type: application/json
    	addDefaultHeaders(headers, t, contentType);
    	if (outputMessage instanceof StreamingHttpOutputMessage) {
    		StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
    		streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
    			@Override
    			public OutputStream getBody() {
    				return outputStream;
    			}
    			@Override
    			public HttpHeaders getHeaders() {
    				return headers;
    			}
    		}));
    	} else {
    		writeInternal(t, type, outputMessage);
    		outputMessage.getBody().flush();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    write方法包含两个逻辑步骤:添加默认头域和写操作,写操作的实际执行方法在writeInternal中:

    @Override
    protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    	MediaType contentType = outputMessage.getHeaders().getContentType();
    	 // 默认为UTF_8类型
    	JsonEncoding encoding = getJsonEncoding(contentType);
    	JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
    	writePrefix(generator, object);
    
    	Object value = object;
    	Class<?> serializationView = null;
    	FilterProvider filters = null;
    	JavaType javaType = null;
    
    	if (object instanceof MappingJacksonValue) {
    		MappingJacksonValue container = (MappingJacksonValue) object;
    		value = container.getValue();
    		serializationView = container.getSerializationView();
    		filters = container.getFilters();
    	}
    	if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
    		javaType = getJavaType(type, null);
    	}
    
    	ObjectWriter objectWriter = (serializationView != null ?
    			this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
    	if (filters != null) {
    		objectWriter = objectWriter.with(filters);
    	}
    	if (javaType != null && javaType.isContainerType()) {
    		objectWriter = objectWriter.forType(javaType);
    	}
    	SerializationConfig config = objectWriter.getConfig();
    	if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
    			config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
    		objectWriter = objectWriter.with(this.ssePrettyPrinter);
    	}
    	objectWriter.writeValue(generator, value);
    
    	writeSuffix(generator, object);
    	generator.flush();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    上述方法可以分为三个步骤:添加前缀(如果有,内置的对象无前缀)、写内容、添加后缀(如果有,内置的对象无前缀),操作完全基于objectMapper对象;关于ObjectMappr的API用法不是本文的重点,不进行赘述。

    3.工作原理

    框架内置的消息转换器为处理HTTP请求和响应提供了强大的支持,基本可以满足项目的需要。这些转换器在容器启动时进行实例化和设置,后被保存在RequestMappingHandlerAdapter对象的messageConverters属性中。
    当HTTP请求到达后,RequestMappingHandlerAdapter会构造一个ServletInvocableHandlerMethod对象,
    且该对象拥有来自RequestMappingHandlerAdapter的消息转换器。
    ServletInvocableHandlerMethod与HttpMessageConveter的关系图如下所示:
    在这里插入图片描述当HTTP请求被DispatcherServlet接受时,调用链会进入RequestMappingHandlerAdapterinvokeHandlerMethod方法,构造ServletInvocableHandlerMethod对象并调用invokeAndHandle方法:

    public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    	 // 反射调用Controller接口获取返回结果
    	 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    	 
    	 //... 
    	
    	 //将ModelAndViewContainer对象设置为请求未处理状态
    	 mavContainer.setRequestHandled(false);
    	
    	 //处理结果
    	 this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    说明:
    从Controller接口获取返回结果后,将结果处理工作委托给了returnValueHandlers属性,该属性是HandlerMethodReturnValueHandlerComposite组合类型,内部维持了一个List returnValueHandlers列表;因此handleReturnValue实际会根据匹配关系分派到指定的HandlerMethodReturnValueHandler中。
    框架内置的HandlerMethodReturnValueHandler和匹配关系如下:

    ModelAndView及其子类->ModelAndViewMethodReturnValueHandler
    Model及其子类->ModelMethodProcessor
    View及其子类->ViewMethodReturnValueHandler
    ResponseEntity及其子类或(ResponseEntity包裹)->ResponseBodyEmitterReturnValueHandler
    StreamingResponseBody及其子类或(ResponseEntity包裹)->StreamingResponseBodyReturnValueHandler
    HttpEntity,ResponseEntity->HttpEntityMethodProcessor
    HttpHeaders及其子类->HttpHeadersReturnValueHandler
    Callable及其子类->CallableMethodReturnValueHandler
    DeferredResult、ListenableFuture、CompletionStage及其子类->DeferredResultMethodReturnValueHandler
    WebAsyncTask及其子类->AsyncTaskMethodReturnValueHandler
    ModelAttribute注解->ModelAttributeMethodProcessor
    方法或类被ResponseBody注解->RequestResponseBodyMethodProcessor
    void,CharSequence及其子类->ViewNameMethodReturnValueHandler
    Map及其子类->MapMethodProcessor

    本文重点关注RequestResponseBodyMethodProcessor, 该结果处理器的匹配规则如下:

    public boolean supportsReturnType(MethodParameter returnType) {
    	return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
    			returnType.hasMethodAnnotation(ResponseBody.class));
    }
    
    • 1
    • 2
    • 3
    • 4

    即方法或者类被@ResponseBody注解的Controller接口使用RequestResponseBodyMethodProcessor。

    当请求进入RequestResponseBodyMethodProcessor的handleReturnValue方法后:

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    	// 将该HTTP请求标记为已处理
    	mavContainer.setRequestHandled(true);
    	// 从webRequest获取HttpServletRequest的代理类
    	ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    	// 从webRequest获取HttpServletResponse的代理类
    	ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    	
    	// Try even with null return value. ResponseBodyAdvice could get involved.
    	writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    核心逻辑在writeWithMessageConverters方法:

    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    	// ...
    }
    
    • 1
    • 2
    • 3

    该方法较长,主要步骤如下:
    (1)获取返回对象类型,并使用Object对象接收返回对象

    Object body;
    Class<?> valueType;
    Type targetType;
    if (value instanceof CharSequence) {
        // 字符类型,则直接进行转换
        body = value.toString();
        valueType = String.class;
        targetType = String.class;
    } else {
        body = value;
        valueType = getReturnValueType(body, returnType);
        targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意:valueType为对象实际类型,不包括泛型信息;targetType包含泛型信息。
    如:

    public Map<String, Integer> getMap() {
        return new HashMap<>();
    }
    
    • 1
    • 2
    • 3

    valueType为java.util.HashMap;而 targetType表示java.util.Map

    (2)InputStreamResource和Resource资源类型的特殊处理(Ignore);

    (3)协商媒体类型,确定媒体类型

    HttpServletRequest request = inputMessage.getServletRequest();
    // 获取HTTP请求头中接收的媒体类型,代表客户端要求的MIME类型[标注1]
    List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
    // 从所有的消息转换器中取媒体类型交集,代表服务器可以处理的媒体类型
    List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    
    if (body != null && producibleTypes.isEmpty()) {
        throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
    }
    // 从服务器支持的媒体类型中筛选出客户端要求的MIME类型
    List<MediaType> mediaTypesToUse = new ArrayList<>();
    for (MediaType requestedType : acceptableTypes) {
        for (MediaType producibleType : producibleTypes) {
            if (requestedType.isCompatibleWith(producibleType)) {
                mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
            }
        }
    }
    
    if (mediaTypesToUse.isEmpty()) {
        if (body != null) {
            throw new HttpMediaTypeNotAcceptableException(producibleTypes);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
        }
        return;
    }
    // 排序,按照品质因子进行[标注2]
    MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
    for (MediaType mediaType : mediaTypesToUse) {
        if (mediaType.isConcrete()) {
            selectedMediaType = mediaType;
            break;
        } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
            selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
            break;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    上述逻辑有两个地方需要补充说明一下:
    [1] getAcceptableMediaTypes方法从HttpServletRequest对象中获取客户端允许的MIME类型,由于框架内置的媒体协商器是HeaderContentNegotiationStrategy,即从请求头中的ACCEPT字段获取MIME类型;
    [2] Accept代表客户端允许的媒体类型,客户端可以同时支持多种类型的资源,且可通过品质因数进行排序,如下所示:
    Accept: text/html;q=0.1,application/xhtml+xml;q=0.2,application/xml;q=0.3,application/json;q=0
    Note: 不接受application/json类型,按照期望排序可接收text/htmlapplication/xhtml+xmlapplication/xml;类型
    即q值越大,表示期望值越高。另外,出Accept外,Accept-Charset(字符集)、Accept-Encoding(压缩算法)、Accept-Language(国际化)在HTTP媒体协商过程也可携带品质因子.

    (4)选择消息解析器,进行消息处理

    // 删除选中的MIME的品质因子(即q值)
    selectedMediaType = selectedMediaType.removeQualityValue();
    // 遍历HttpMessageConverter,寻找第一个匹配的消息解析器处理body对象(待返回结果)
    for (HttpMessageConverter<?> converter : this.messageConverters) {
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);
        // [标注1]
        if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {
            body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);
            if (body != null) {
                Object theBody = body;
                // [标注2]
                addContentDispositionHeader(inputMessage, outputMessage);
                if (genericConverter != null) {
                    genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                } else {
                    ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                }
            }
            return;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    代码按照遍历+匹配+处理的思路铺开,逻辑比较清晰。有两个地方需要补充说明一下:
    [1] 按照消息解析器是HttpMessageConverter还是GenericHttpMessageConverter,会使用不同的canWrite进行判断,后者多一个参数;write也有区别。
    [2] addContentDispositionHeader用于为文件请求添加Content-Disposition头域,用于指示文件的名称和下载方式。取值范围有inlineattachmentinline表示文件直接浏览器中显示文本attachment表示文件下载到本地。

    (5)异常场景处理
    未匹配到消息处理器的场景,抛出异常。

    ----以上为所有内容----

  • 相关阅读:
    node实战——koa实现文件下载和图片/pdf/视频预览(node后端储备知识)
    Linux合集之Linux实用运维脚本分享及Linux CPU的上下文切换
    cmd 命令关闭占用端口
    matlab simulink球杆控制系统的模糊PID控制设计
    基于内容的图像检索系统设计与实现--颜色信息--纹理信息--形状信息--PHASH--SHFT特征点的综合检测项目,包含简易版与完整版的源码及数据!
    元宇宙电商-NFG系统,解决了数字藏品市场的哪些痛点?
    Shell脚本-位置参数(命令行参数)
    LNMP环境:揭秘负载均衡与高可用性设计
    图神经网络入门 (GNN, GCN)
    Spark中RDD常见的算子:Value 类型、双 Value 类型、Key - Value 类型
  • 原文地址:https://blog.csdn.net/Sheng_Q/article/details/133231390