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
}