文章
问答
冒泡
SpringMVC 对multipart/related (RFC2387) 的Response的支持

      springmvc中只支持接受multipart形式的数据,却无法返回这个类型的数据,故添加对response的支持

      步骤如下。

       1. 定义用于返回的对象

@Data
public class MultipartRelatedOutput extends MultipartOutput {
    private String startInfo;

    public OutputPart getRootPart() {
        return getParts().get(0);
    }

    public OutputPart addPart(Object entity, MediaType mediaType,
                              String contentId, String contentTransferEncoding) {
        OutputPart outputPart = super.addPart(entity, mediaType);
        if (contentTransferEncoding != null)
            outputPart.getHeaders().add("Content-Transfer-Encoding", contentTransferEncoding);
        if (contentId != null)
            outputPart.getHeaders().add("Content-ID", contentId);
        return outputPart;
    }
}
@Data
public class OutputPart {
    private MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
    private Object entity;
    private Class<?> type;
    private Type genericType;
    private MediaType mediaType;
    private String filename;
    private boolean utf8Encode;

    public OutputPart(final Object entity, final Class<?> type, final Type genericType, final MediaType mediaType) {
        this(entity, type, genericType, mediaType, null);
    }

    public OutputPart(final Object entity, final Class<?> type, final Type genericType, final MediaType mediaType, final String filename) {
        this(entity, type, genericType, mediaType, null, false);
    }

    public OutputPart(final Object entity, final Class<?> type, final Type genericType, final MediaType mediaType, final String filename, final boolean utf8Encode) {
        this.entity = entity;
        this.type = type;
        this.genericType = genericType;
        this.mediaType = mediaType;
        this.filename = filename;
        this.utf8Encode = utf8Encode;
    }
 
}

        2. 配置HttpMessageConverter

@Component
public class MultipartDicomConverter implements HttpMessageConverter<MultipartRelatedOutput> {
    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = new MediaType(MediaType.MULTIPART_RELATED, DEFAULT_CHARSET);
    private List<MediaType> supportedMediaTypes;

    public MultipartDicomConverter() {
        supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
        supportedMediaTypes.add(DEFAULT_FORM_DATA_MEDIA_TYPE);
    }

    @Override
    public final void write(@NotNull final MultipartRelatedOutput multipartRelatedOutput,
                            @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {

        final HttpHeaders headers = outputMessage.getHeaders();
        addDefaultHeaders(headers, multipartRelatedOutput, contentType);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(outputStream ->
                    writeInternal(multipartRelatedOutput, outputMessage.getBody(), contentType));
        } else {
            writeInternal(multipartRelatedOutput, outputMessage.getBody(), contentType);
        }
    }


    protected void addDefaultHeaders(HttpHeaders headers, MultipartRelatedOutput multipartRelatedOutput,
                                     @Nullable MediaType mediaType) {
        headers.clear();
        for (OutputPart outputPart : multipartRelatedOutput.getParts()) {
            if (outputPart.getHeaders().get("Content-ID") == null) {
                outputPart.getHeaders().add("Content-ID", generateContentID());
            }
        }
        OutputPart rootOutputPart = multipartRelatedOutput.getRootPart();
        Map<String, String> mediaTypeParameters = new TreeMap<>(String::compareToIgnoreCase);
        mediaTypeParameters.putAll(rootOutputPart.getMediaType().getParameters());
        if (mediaTypeParameters.containsKey("boundary")) {
            multipartRelatedOutput.setBoundary(mediaTypeParameters.get("boundary"));
        } else {
            mediaTypeParameters.put("boundary", new String(multipartRelatedOutput.getBoundary().getBytes(), StandardCharsets.US_ASCII));
        }
        mediaTypeParameters.put("start", rootOutputPart.getHeaders().getFirst("Content-ID"));
        mediaTypeParameters.put("type", rootOutputPart.getMediaType().getType() + "/" + rootOutputPart.getMediaType().getSubtype());
        if (multipartRelatedOutput.getStartInfo() != null) {
            mediaTypeParameters.put("start-info", multipartRelatedOutput.getStartInfo());
        }
        headers.putAll(rootOutputPart.getHeaders());
        headers.set(CONTENT_TYPE, mediaType + "; " + mediaTypeParameters.entrySet()
                .stream().map(entry -> entry.getKey() + "=" + entry.getValue())
                .collect(Collectors.joining("; ")));
    }


    protected void writeInternal(MultipartRelatedOutput multipartOutput, OutputStream entityStream, MediaType contentType)
            throws IOException {
        String boundary;
        if (StringUtils.isNotBlank(contentType.getParameter("boundary"))) {
            boundary = contentType.getParameter("boundary");
        } else {
            boundary = multipartOutput.getBoundary();
        }
        assert boundary != null;
        byte[] boundaryBytes = boundary.getBytes();
        for (OutputPart part : multipartOutput.getParts()) {
            HttpHeaders headers = new HttpHeaders();
            writePart(entityStream, boundaryBytes, part, headers);
        }
        writeEnd(entityStream, boundaryBytes);
    }

    @SuppressWarnings(value = "unchecked")
    protected void writePart(OutputStream outputStream, byte[] boundaryBytes, OutputPart part, HttpHeaders headers)
            throws IOException {
        writeBoundary(outputStream, boundaryBytes);

        headers.addAll(part.getHeaders());
        headers.setContentType(part.getMediaType());

        String headerStr = headers.entrySet().stream()
                .map(entry -> {
                    List<String> values = entry.getValue();
                    return entry.getKey() + ":" + (values.size() == 1 ? values.get(0) : String.join(", ", values));
                }).collect(Collectors.joining("\r\n")) + "\r\n";
        outputStream.write(headerStr.getBytes());
        writeNewLine(outputStream);
        headers.clear();

        //todo 根据对象自定义序列号方式
        byte[] bytes = serialize(part.getEntity()).getBytes();

        StreamUtils.copy(bytes, outputStream);


        writeNewLine(outputStream);
    }



    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');
    }


    public static String generateContentID() {
        return generateContentIDFromAddrSpec(generateRFC822AddrSpec());
    }

    public static String generateContentIDFromAddrSpec(String addrSpec) {
        return "<" + addrSpec + ">";
    }


    public static String generateRFC822AddrSpec() {
        return UUID.randomUUID() + "@springmvc-multipart";
    }


    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return MultipartRelatedOutput.class.isAssignableFrom(clazz) && canWrite(mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return supportedMediaTypes;
    }


    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;
    }

    @Override
    public MultipartRelatedOutput read(Class<? extends MultipartRelatedOutput> clazz,
                                       HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        throw new UnsupportedOperationException(getClass().getSimpleName() + " does not support read to HTTP body.");
    }
}

3 使用示例

   @RequestMapping(value = "/xxx/{studyUID}", method = RequestMethod.GET, produces = {MediaType.MULTIPART_RELATED_VALUE})
    public MultipartRelatedOutput xxx(@PathVariable("studyUID") String studyUID) {
        return  null
    }


关于作者

小小鼠标垫
哼嗬哈嘿
获得点赞
文章被阅读