Допустим, у нас есть приложение на Spring c клиентом, который скачивает файлы из другого сервиса. Но в ответ нам приходит не только файл, а form-data: в одной части содержится мета-информация в json, в другой - нужный файл. Это выглядит примерно так:
--2wXcH4LBAoACUj6RpsmX_J2ME0xS1e7k1
Content-Disposition: form-data; name="files"; filename="{49943A8A-38DB-42A3-94F9-D546C82D4618}"
Content-Type: application/octet-stream
Content-Length: 11733
...D��q��A�Bb@�R��{/�dC�B~�9��nGIF89a� <- содержимое файла
--2wXcH4LBAoACUj6RpsmX_J2ME0xS1e7k1
Content-Disposition: form-data; name="body"
Content-Type: application/json
{"id":"{49943A8A-38DB-42A3-94F9-D546C82D4618}","name":"some-file.txt","contentType":"application/octet-stream;charset=UTF-8","resultCode":0}
--2wXcH4LBAoACUj6RpsmX_J2ME0xS1e7k1--
Я не нашел подходящих решений в интернете, поэтому хочу выложить, что удалось реализовать самостоятельно. Такая инструкция сэкономила бы мне несколько часов. Два варианта:
С использованием javax mail
С использованием spring web client
С использованием javax mail
Подключил в проект зависимость, которая позволила преобразовать ответ в MimeMultipart, который может достать из ответа и json и файл.
Схематично клиент с обработчиком ответа может выглядеть так (обработчики для исключений и других нестандартных случаев можно написать на свое усмотрение):
/*
зависимости javax mail
*/
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
@Service
@AllArgsConstructor
public class ClientWithRestTemplate {
private final RestTemplate restTemplate;
// метод - клиент
public void downloadFile() throws MessagingException, IOException {
ResponseEntity<byte[]> response = restTemplate
.postForEntity("http://host", new HttpEntity<>(body, headers),
byte[].class); // в теле получиаем массив байт,
// который обработаем с помощью javax.mail
handleMultipart(response.getBody());
}
// метод - обработчик ответа
private void handleMultipart(byte[] formData) throws MessagingException, IOException {
ByteArrayDataSource datasource = new ByteArrayDataSource(formData, "multipart/form-data");
MimeMultipart multipart = new MimeMultipart(datasource);
int count = multipart.getCount();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = multipart.getBodyPart(i); // получаем текущую часть ответа
Object content = bodyPart.getContent(); // выдергиваем из части content чтобы...
byte[] byteArray = SerializationUtils.serialize(content); // опять получить массив байт из него
String[] contentType = bodyPart.getHeader("Content-Type");
// по заголовку проверяем, что это находится в этой части: файл или json
if ("application/json".equals(contentType[0])) {
String jsonString = new String(byteArray); // если json, то преобразуем к строке
} else if ("application/octet-stream".equals(contentType[0])) {
String name = bodyPart.getFileName(); // если файл, то к ресурсу
// то преобразуем содержимое к ресурсу
Resource resource = new ByteArrayResource(byteArray) {
@Override
public String getFilename() { return name; }
};
}
}
}
}
С использованием Spring web client
В примере ответ блочится с самого начала, но если нужно, можно пробрасывать Mono и дальше. В этом способе важно то, что можно преобразовать ответ к мапе и далее работать уже с ней.
Схематично клиент в обработчиком ответа может выглядеть так (обработчики для исключений и других нестандартных случаев также можно написать на свое усмотрение):
public class ClientWebClientSpring {
// метод - клиент
public void downloadFile() {
WebClient client = WebClient.create();
// в теле получаем мапу, а не массив байт
ResponseEntity<MultiValueMap<String, Part>> rs =
client
.mutate() // важно использовать кастомный кодек чтобы потом получилось преобразовать в мапу
.codecs(clientCodecConfigurer -> clientCodecConfigurer.customCodecs()
.register(new MultipartHttpMessageReader(new DefaultPartHttpMessageReader()))
)
.build()
.post()
.uri("http://server-url")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(SomeDto.class))
.retrieve()
.toEntity(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {
})
.block();
handleMultipart(rs.getBody());
}
// метод - обработчик ответа от сервера
private void handleMultipart(MultiValueMap<String, Part> map) {
// берем в мапе часть с json и преобразуем результат к строке
Part body = map.get("body").get(0);
String jsonString = new String(getByteArray(body));
// берем в мапе часть с файлом и преобразуем результат к ресурсу
Part file = map.get("files").get(0);
byte[] fileByteArray = getByteArray(file);
Resource resource = new ByteArrayResource(fileByteArray) {
@Override // можно придумать другой способ получить имя файла
public String getFilename() { return name; }
};
}
private byte[] getByteArray(Part part) {
return DataBufferUtils.join(part.content())
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
}).block();
}
}
Этот вариант является комбинацией ответа со stackoverflow и этой статьи с медиума.
Заключение
Возможно я предложил неидеальное решение, но как и писал выше, ничего подходящего найти не удалось. Если у вас есть опыт, как вы обрабатывали такие ответы на java, поделитесь им в комментариях.