Завантаження файлу з весняних контролерів


387

У мене є вимога, де мені потрібно завантажити PDF з веб-сайту. PDF-код потрібно генерувати в коді, який, на мою думку, був би комбінацією фрейммаркера та рамки генерації PDF, наприклад iText. Будь-який кращий спосіб?

Однак моя основна проблема полягає в тому, як я можу дозволити користувачу завантажувати файл через контролер Spring?


2
Варто зазначити, що Spring Framework сильно змінився з 2011 року, тож ви також можете це зробити реагуючим способом - ось приклад
Krzysztof

Відповіді:


397
@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
public void getFile(
    @PathVariable("file_name") String fileName, 
    HttpServletResponse response) {
    try {
      // get your file as InputStream
      InputStream is = ...;
      // copy it to response's OutputStream
      org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException ex) {
      log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
      throw new RuntimeException("IOError writing file to output stream");
    }

}

Взагалі кажучи, коли у вас є response.getOutputStream(), ви можете там написати що завгодно. Ви можете передавати цей вихідний потік як місце для розміщення створеного PDF у ваш генератор. Крім того, якщо ви знаєте, який тип файлу ви надсилаєте, ви можете встановити

response.setContentType("application/pdf");

4
Це майже те, що я збирався сказати, але, ймовірно, слід також встановити заголовок типу відповіді на щось відповідне для файлу.
GaryF

2
Так, тільки що відредагував публікацію. У мене були створені різні типи файлів, тому я залишив його в браузері, щоб визначити тип вмісту файла за його розширенням.
Інфеліго

Забув flushBuffer, завдяки твоєму допису я побачив, чому моя не працює :-)
Ян Володимир Мостерт

35
Якась конкретна причина використовувати Apache IOUtilsзамість Spring FileCopyUtils?
Powerlord


290

Мені вдалося передати цю лінію за допомогою вбудованої підтримки у весні за допомогою ResourceHttpMessageConverter. Це встановить довжину вмісту та тип вмісту, якщо він може визначити тип mime

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}

10
Це працює. Але файл (.csv-файл) відображається у браузері та не завантажується - як змусити браузер завантажити?
chzbrgla

41
Ви можете додати продукцію = MediaType.APPLICATION_OCTET_STREAM_VALUE до @RequestMapping для примусового завантаження
Девід Каго

8
Також слід додати <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> до messageConverters list (<mvc: annotation-driven> <mvc: message-converters>)
Sllouyssgort

4
Чи є спосіб встановити Content-Dispositionзаголовок таким чином?
Ральф

8
У мене не було потреби в цьому, але я думаю, ви можете додати HttpResponse як параметр до методу, а потім "response.setHeader (" Вміст-диспозиція "," вкладення; ім'я файлу = деякий файл.pdf ");"
Скотт Карлсон

82

Ви повинні мати можливість записати файл у відповідь безпосередньо. Щось на зразок

response.setContentType("application/pdf");      
response.setHeader("Content-Disposition", "attachment; filename=\"somefile.pdf\""); 

а потім записати файл у вигляді двійкового потоку response.getOutputStream(). Не забудьте зробити response.flush()в кінці, і це має зробити.


8
Хіба не весняний спосіб встановити такий тип вмісту? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Чорний

4
@Francis Що робити, якщо ваша програма завантажує різні типи файлів? Відповідь Lobster1234 дозволяє динамічно встановити диспозицію вмісту.
Роза

2
це правда @Rose, але я вважаю, що краще було б визначати різні кінцеві точки за форматом
Чорний

3
Гадаю, ні, тому що це не масштабується. Зараз ми підтримуємо десяток типів ресурсів. Ми можемо підтримувати більше типів файлів на основі того, що користувачі хочуть завантажити, у такому випадку ми можемо отримати так багато кінцевих точок, які по суті роблять те саме. IMHO має бути лише одна кінцева точка завантаження, і вона обробляє безліч типів файлів. @Francis
Роза

3
це абсолютно "масштабовано", але ми можемо погодитися не погоджуватися, чи це найкраща практика
Чорний

74

З Spring 3.0 ви можете використовувати HttpEntityоб'єкт повернення. Якщо ви користуєтесь цим, то вашому контролеру не потрібен HttpServletResponseоб'єкт, і тому його легше протестувати. Окрім цього, ця відповідь відносно дорівнює Інфеліго .

Якщо значення повернення вашого файлу pdf є масивом байтів (прочитайте другу частину моєї відповіді щодо інших повернених значень) :

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    byte[] documentBody = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(documentBody.length);

    return new HttpEntity<byte[]>(documentBody, header);
}

Якщо тип повернення вашого PDF Framework ( documentBbody) вже не є байтовим масивом (а також ні ByteArrayInputStream), то було б розумно НЕ зробити його спочатку байтовим масивом. Замість цього краще використовувати:

Приклад із FileSystemResource:

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    File document = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(document.length());

    return new HttpEntity<byte[]>(new FileSystemResource(document),
                                  header);
}

11
-1 це неодмінно завантажить весь файл у пам'ять, може легко викликати OutOfMemoryErrors.
Файсал Фероз

1
@FaisalFeroz: так, це правильно, але файл файлу все-таки створюється в пам'яті (див. Питання: "PDF-файл повинен бути створений в коді"). У будь-якому разі - яке ваше рішення, яке подолало цю проблему?
Ральф

1
Ви також можете використовувати ResponseEntity, який є супер HttpEntity, який дозволяє вказати код статусу відповіді http. Приклад:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa

@Amr Mostafa: ResponseEntityце підклас HttpEntity(але я його розумію) з іншого боку. 201 СТВОРЕНО - це не те, що я використовував би, коли повертаю лише перегляд даних. (див. w3.org/Protocols/rfc2616/rfc2616-sec10.html для 201 СТВОРЕНО )
Ральф

1
Чи є причина, чому ви замінюєте пробіли на підкреслення у назві файлу? Ви можете загорнути його в лапки, щоб надіслати фактичне ім'я.
Олександру Северину

63

Якщо ви:

  • Не хочуть завантажувати весь файл у a, byte[]перш ніж надсилати відповідь;
  • Хочете / потрібно надіслати / завантажити через InputStream;
  • Хочете мати повний контроль над типом Mime та назвою файлу;
  • Попросіть інших @ControllerAdviceвинятків для вас (чи ні).

Нижче наведений код:

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<FileSystemResource> downloadStuff(@PathVariable int stuffId)
                                                                      throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);
    long fileLength = file.length(); // this is ok, but see note below

    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentType("application/pdf");
    respHeaders.setContentLength(fileLength);
    respHeaders.setContentDispositionFormData("attachment", "fileNameIwant.pdf");

    return new ResponseEntity<FileSystemResource>(
        new FileSystemResource(file), respHeaders, HttpStatus.OK
    );
}

Щодо частини довжини файлу : File#length()у загальному випадку має бути достатньо хорошою, але я подумав, що я буду робити це спостереження, оскільки це може бути повільним , і в цьому випадку ви повинні зберігати його раніше (наприклад, у БД). Справи, які можуть бути повільними, включають: якщо файл великий, особливо якщо файл знаходиться у віддаленій системі або щось більш досконале, як це - база даних, можливо.



InputStreamResource

Якщо ваш ресурс не є файлом, наприклад, ви збираєте дані з БД, ви повинні використовувати InputStreamResource. Приклад:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);

Ви не радите використовувати клас FileSystemResource?
Стефан

Власне, я вважаю, що це нормально використовувати FileSystemResourceтам. Навіть бажано, якщо ваш ресурс - це файл . У цьому зразку FileSystemResourceможна використовувати там, де InputStreamResourceє.
acdcjunior

Про частину підрахунку довжини файлу: Якщо ви переживаєте, не будьте. File#length()має бути досить хорошим у загальному випадку. Я щойно згадував про це, тому що це може бути повільним , особливо якщо файл знаходиться у віддаленій системі або щось більш досконале, як-от таке - база даних, можливо ?. Але хвилюйтеся, якщо це стане проблемою (або якщо у вас є тверді докази, це стане), не раніше. Основний момент: ви докладаєте зусиль, щоб передати файл, якщо вам доведеться попередньо завантажити все це, то потокове завершення не має ніякої різниці, так?
acdcjunior

чому вищевказаний код не працює для мене? Він завантажує файл 0 байтів. Я перевірив і переконався в наявності перетворювачів ByteArray & ResourceMessage. Я щось пропускаю?
coding_idiot

Чому ви турбуєтесь про перетворювачі ByteArray & ResourceMessage?
acdcjunior

20

Цей код добре працює, щоб автоматично завантажити файл з весняного контролера, натиснувши на посилання на jsp.

@RequestMapping(value="/downloadLogFile")
public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
    try {
        String filePathToBeServed = //complete file name with path;
        File fileToDownload = new File(filePathToBeServed);
        InputStream inputStream = new FileInputStream(fileToDownload);
        response.setContentType("application/force-download");
        response.setHeader("Content-Disposition", "attachment; filename="+fileName+".txt"); 
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
        inputStream.close();
    } catch (Exception e){
        LOGGER.debug("Request could not be completed at this moment. Please try again.");
        e.printStackTrace();
    }

}

14

Нижче код працював у мене для створення та завантаження текстового файлу.

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<byte[]> getDownloadData() throws Exception {

    String regData = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
    byte[] output = regData.getBytes();

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("charset", "utf-8");
    responseHeaders.setContentType(MediaType.valueOf("text/html"));
    responseHeaders.setContentLength(output.length);
    responseHeaders.set("Content-disposition", "attachment; filename=filename.txt");

    return new ResponseEntity<byte[]>(output, responseHeaders, HttpStatus.OK);
}

5

Що я можу швидко придумати, це створити pdf та зберегти його у файлі webapp / downloads / <RANDOM-FILENAME> .pdf та надіслати пересилання в цей файл за допомогою HttpServletRequest

request.getRequestDispatcher("/downloads/<RANDOM-FILENAME>.pdf").forward(request, response);

або якщо ви можете налаштувати вирішувач перегляду чимось,

  <bean id="pdfViewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView" />
    <property name="order" value=”2″/>
    <property name="prefix" value="/downloads/" />
    <property name="suffix" value=".pdf" />
  </bean>

тоді просто повертайтеся

return "RANDOM-FILENAME";

1
Якщо мені потрібні два вирішальники перегляду, то як я можу також повернути ім'я резолювер або вибрати його в контролері ??
azerafati

3

Наступне рішення працює для мене

    @RequestMapping(value="/download")
    public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
        try {

            String fileName="archivo demo.pdf";
            String filePathToBeServed = "C:\\software\\Tomcat 7.0\\tmpFiles\\";
            File fileToDownload = new File(filePathToBeServed+fileName);

            InputStream inputStream = new FileInputStream(fileToDownload);
            response.setContentType("application/force-download");
            response.setHeader("Content-Disposition", "attachment; filename="+fileName); 
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
            inputStream.close();
        } catch (Exception exception){
            System.out.println(exception.getMessage());
        }

    }

2

щось на кшталт нижче

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
    try {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        InputStream is = loader.getResource("classpath:META-INF/resources/Accepted.pdf").getInputStream();
        IOUtils.copy(is, response.getOutputStream());
        response.setHeader("Content-Disposition", "attachment; filename=Accepted.pdf");
        response.flushBuffer();
    } catch (IOException ex) {
        throw new RuntimeException("IOError writing file to output stream");
    }
}

Ви можете показати PDF або завантажити його приклади тут


1

Якщо це комусь допоможе. Ви можете робити те, що запропонував Infeligo у відповідь, але просто ввести цей додатковий біт у код для примусового завантаження.

response.setContentType("application/force-download");


0

У моєму випадку я генерую якийсь файл на вимогу, тому також потрібно генерувати URL.

Для мене працює щось подібне:

@RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET, produces = "text/csv")
@ResponseBody
public FileSystemResource getFile(@PathVariable String filename) {
    String path = dataProvider.getFullPath(filename);
    return new FileSystemResource(new File(path));
}

Дуже важливим є введення mime, producesа також те, що це ім'я файлу є частиною посилання, тому вам доведеться використовувати @PathVariable.

HTML-код виглядає так:

<a th:href="@{|/dbreport/files/${file_name}|}">Download</a>

Де ${file_name}генерується Thymeleaf в контролері і є: result_20200225.csv, так що весь URL випереджати посилання: example.com/aplication/dbreport/files/result_20200225.csv.

Після натискання на посилання браузер запитує мене, що робити з файлом - зберегти чи відкрити.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.