Java Spring - динамически сгенерированный ответ на загрузку CSV-файла зависает

На сайте моей компании есть несколько таблиц, которые нужно экспортировать в CSV-файл.
Существуют различные параметры, поэтому CSV-файл необходимо создавать динамически по запросу.

Моя проблема в том, что после нажатия на загрузку ответ зависает и ждет создания всего файла (что может занять некоторое время) и только затем загружает весь файл в одно мгновение.

Я использую AngularJS, поэтому я использую window.location = <url_for_file_download>, чтобы браузер загрузил файл.

На стороне сервера я использую Java Spring и выполнил все инструкции, которые смог найти в Интернете, чтобы создать контроллер загрузки файлов.

Мой код контроллера выглядит примерно так:

@RequestMapping(value = "http://yada.yada.yada/csv/myFile.csv", method = RequestMethod.GET)
public @ResponseBody
void getCustomers(HttpServletResponse response,
                         @RequestParam(required = false) String someParameters) 
                         throws NotAuthorizedException, IOException {  
// set headers
setHeaders(response);
// generate writer
CSVWriter write = generateWriter(response);
// get data
List<String[]> data = getData();
// write and flush and all that
.
.
.
}

Мой код для установки заголовков ответа:

response.setContentType("text/csv;charset=utf-8");
response.setHeader("Content-Disposition","attachment; filename=\"" + fileName + ".csv\"");

Я также попытался добавить следующие заголовки:

response.setHeader("Transfer-Encoding", "Chunked");
response.setHeader("Content-Description", "File Transfer");

и я также попытался установить Content-type на "application/octet-stream".

Обратите внимание, что я не добавляю заголовок Content-length, так как файл еще не существует и записывается на лету.

Для записи файла csv я использую OpenCSV, и мой код выглядит следующим образом:

OutputStream resOs = response.getOutputStream();
OutputStream buffOs = new BufferedOutputStream(resOs);
OutputStreamWriter outputWriter = new OutputStreamWriter(buffOs,"UTF-8");
CSVWriter writer = new CSVWriter(outputWriter);

Я перебираю данные и пишу так:

for (String[] row: data) {
    writer.writeNext(line);
}

(Это не совсем код, но это нечто большее, чем то, что происходит в коде)

И в конце смываю и закрываю:

writer.flush();
writer.close();

Я также пробовал сбрасывать после каждой строки, которую я пишу.

Так почему же файл не передается до того, как он будет записан? Почему мой браузер (Google Chrome) загружает файл в одно мгновение после долгого ожидания? И как я могу это исправить.

Надеюсь, я добавил достаточно кода, если чего-то не хватает, просто сообщите мне, и я постараюсь добавить это сюда.

Огромное спасибо заранее.


person jonny bordo    schedule 23.02.2015    source источник
comment
это код, который устанавливает ответ и записывает в поток responseoutput в контроллере?   -  person Paul John    schedule 23.02.2015
comment
@PaulJohn Да, извините, если это было непонятно. Но я использую некоторые вспомогательные функции, которым я передаю объект httpServletResponse, чтобы установить заголовки и создать модуль записи. Я отредактирую вопрос.   -  person jonny bordo    schedule 23.02.2015
comment
@jonnybordo ты нашел решение этой проблемы? У меня такой же.   -  person medvedev1088    schedule 22.12.2016
comment
@medvedev1088 Нет, извините, не видел.   -  person jonny bordo    schedule 22.12.2016
comment
@jonnybordo проверь, полезен ли мой ответ.   -  person medvedev1088    schedule 22.12.2016


Ответы (3)


Можете ли вы попробовать вернуть нулевое значение в свой java

вернуть ноль;

Или вы также можете попробовать код ниже 1. Код JQuery при нажатии кнопки отправки

$(document).ready( function() {
     $('#buttonName').click(function(e){
    $("#formName").submit();
     //alert("The file ready to be downloaded");

});
});

Ваш код контроллера

@RequestMapping(value="/name",method=RequestMethod.POST)                


public ModelAndView downloadCSV(ModelMap model,HttpSession session,@ModelAttribute(value="Pojo") Pojo pojo
            ,HttpServletRequest request,HttpServletResponse response){

----------------some code----------------

response.setContentType("application/csv");   
("application/unknown"); 
response.setHeader("content-disposition","attachment;filename =filename.csv"); 
ServletOutputStream  writer = response.getOutputStream();   
            logger.info("downloading contents to csv");

            writer.print("A");
            writer.print(',');
            writer.println("B");            

        for(int i=0;i<limit;i++){

                writer.print(""+pojo.get(i).getA());
                writer.print(',');
                writer.print(pojo.get(i).getB()); 
                writer.println();   
        }

        writer.flush();
        writer.close();

---------------some code-----------
return null;
}

Надеюсь это поможет

person Thomas    schedule 23.02.2015
comment
Спасибо, Томас. На самом деле я пытался вернуть null ModelAndView, но это не помогло. Что касается JQuery, я предпочитаю избегать его использования, поскольку я использую AngularJS. Я также попытался загрузить файл, добавив скрытый iframe с src, установленным для создания файла csv. - person jonny bordo; 23.02.2015
comment
Один быстрый вопрос. Каков размер вашего загруженного CSV-файла (в МБ). Из вашего сообщения я понимаю, что вы извлекаете данные для загрузки из таблиц БД. Итак, когда вы выполняете этот запрос вручную, сколько времени требуется для извлечения всех записей. - person Thomas; 23.02.2015
comment
В том то и дело, что я не могу определить размер файла заранее, так как генерирую файл на лету. в настоящее время я работаю с небольшими файлами по 200-300 Кб, но в будущем они могут вырасти до мегабайт. В зависимости от данных. - person jonny bordo; 23.02.2015
comment
если его размер превышает 20 МБ, и если вы не хотите, чтобы ваш пользовательский интерфейс ждал обработки и загрузки всех данных, то я думаю, что лучший вариант - обернуть эту логику в какой-то асинхронный вызов (например, поток). Например, если вы используя thread , вы можете обернуть логику загрузки csv в методе запуска. Так что, как только пользователь нажмет кнопку загрузки csv, запрос будет обработан в бэкэнде, не влияя на часть пользовательского интерфейса. - person Thomas; 23.02.2015
comment
Я предполагаю, что мне придется разделить процесс на два запроса: один, чтобы начать создание файла и сохранить его в статическом месте, вернуть URL-адрес, а второй - получить файл. Спасибо за помощь Томас. - person jonny bordo; 23.02.2015

Контроллер будет ждать записи ответа, прежде чем ответ будет отправлен обратно клиенту.

Вот хороший пост с несколькими подходами/вариантами, изложенными

Загрузка файла из контроллеров Spring

В этом посте рассказывается о периодической очистке вывода, чтобы ускорить загрузку.

как загружать большие файлы без проблем с памятью в java

Если все, что вы пытаетесь сделать, это сообщить пользователю, что загрузка файла выполняется и скоро должна быть завершена, я думаю, что индикатор состояния выполнения Ajax может быть вашим решением.

  1. Запустите вызов ajax на серверную часть, чтобы сгенерировать файл

  2. Показывать индикатор прогресса пользователю, пока файл создается на стороне сервера

  3. как только ответ доступен, файл предоставляется пользователю.

Я думаю, что здесь изучается что-то подобное POST-запрос через Spring MVC

Надеюсь это поможет!

Спасибо, Пол.

person Paul John    schedule 23.02.2015
comment
Привет, Пол. Спасибо, я действительно видел этот пост. Я на самом деле в значительной степени использую то, что они предлагают. Но, как вы сказали, контроллер ждет, пока ответ будет полностью записан, прежде чем он будет отправлен обратно, и это как раз моя проблема. Я хочу, чтобы файл был отправлен во время записи. Я не уверен, что вижу решение этой проблемы в этом посте. - person jonny bordo; 23.02.2015
comment
Привет, Джонни, однажды зафиксированный ответ не может быть изменен. Ваша конечная цель — попытаться загрузить файл быстрее, верно? Или вам абсолютно необходимо иметь возможность сортировать файл в потоковом режиме? - person Paul John; 23.02.2015
comment
Привет, Пол, моя проблема в том, что файл может быть большим, поэтому его загрузка займет некоторое время (я говорю о 10 или, может быть, больше секунд). Моя проблема в том, что, поскольку файл загружается мгновенно, у пользователя нет никаких указаний на то, что файл был запрошен. Вот почему я хочу передать файл в потоковом режиме, чтобы он сразу запускался, и пользователь видел прогресс на странице загрузки браузера. - person jonny bordo; 23.02.2015
comment
как насчет того, чтобы сделать вызов на сервер ajax-вызовом, а затем показать пользователю индикатор выполнения, ожидая ответа сервера. - person Paul John; 23.02.2015
comment
Спасибо за ответ Павел. Я не могу точно загрузить файл с запросом ajax, но мне, возможно, просто придется разделить его на два отдельных запроса: один, чтобы начать создание файла, и другой, чтобы получить файл из статического местоположения. - person jonny bordo; 23.02.2015
comment
Прохладный. Я думаю, что сообщение, о котором я упоминал выше, поможет вам сделать что-то очень похожее. - person Paul John; 23.02.2015

Я столкнулся с той же проблемой. Код, который не работал для меня, был

@RequestMapping(value = "/test")
 public void test(HttpServletResponse response)
            throws IOException, InterruptedException {
    response.getOutputStream().println("Hello");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("How");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("are");
    response.getOutputStream().flush();
    Thread.sleep(2000);
    response.getOutputStream().println("you");
    response.getOutputStream().flush();
}

Виновником был ShallowEtagHeaderFilter. Когда этот фильтр включен, ответ отправляется одним фрагментом. Когда этот фильтр отключен, ответ отправляется несколькими порциями.

Из этого потока Tomcat не очищает буфер ответа, это выглядит как другой возможным виновником может быть GzipFilter

person medvedev1088    schedule 22.12.2016
comment
Спасибо @medvedev1088! Это может быть как раз то решение, которое мне нужно. Однако я столкнулся с этой проблемой на своей предыдущей работе, поэтому я не могу проверить, работает ли она в среде, в которой я работал. Надеюсь, я смогу где-нибудь протестировать это решение и принять ваш ответ. Я сделаю все возможное. Спасибо! - person jonny bordo; 25.12.2016