Допустим, у нас есть приложение на 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--

Я не нашел подходящих решений в интернете, поэтому хочу выложить, что удалось реализовать самостоятельно. Такая инструкция сэкономила бы мне несколько часов. Два варианта:

  1. С использованием javax mail

  2. С использованием 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, поделитесь им в комментариях.

Комментарии (2)