Tải xuống một tập tin từ bộ điều khiển mùa xuân


387

Tôi có một yêu cầu khi tôi cần tải xuống bản PDF từ trang web. PDF cần phải được tạo trong mã, mà tôi nghĩ sẽ là sự kết hợp giữa freemarker và khung tạo PDF như iText. Còn cách nào tốt hơn không?

Tuy nhiên, vấn đề chính của tôi là làm cách nào để cho phép người dùng tải xuống tệp thông qua Bộ điều khiển mùa xuân?


2
Điều đáng nói là Spring Framework đã thay đổi rất nhiều kể từ năm 2011, vì vậy bạn cũng có thể làm điều đó theo cách phản ứng - đây là một ví dụ
Krzysztof Skrzynecki

Câu trả lời:


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");
    }

}

Nói chung, khi bạn có response.getOutputStream(), bạn có thể viết bất cứ điều gì ở đó. Bạn có thể truyền luồng đầu ra này làm nơi đặt tệp PDF được tạo vào trình tạo của mình. Ngoài ra, nếu bạn biết loại tệp bạn đang gửi, bạn có thể đặt

response.setContentType("application/pdf");

4
Đây là khá nhiều những gì tôi sắp nói, nhưng có lẽ bạn cũng nên đặt tiêu đề loại phản hồi thành một cái gì đó phù hợp cho tệp.
GaryF

2
Đúng, chỉ cần chỉnh sửa bài viết. Tôi đã tạo ra nhiều loại tệp khác nhau, vì vậy tôi để nó cho trình duyệt để xác định loại nội dung của tệp bằng phần mở rộng của nó.
Infeligo

Quên flBuffer, nhờ vào bài đăng của bạn, tôi đã thấy lý do tại sao của tôi không hoạt động :-)
Jan Vladimir Mostert

35
Bất kỳ lý do cụ thể nào để sử dụng Apache IOUtilsthay vì Spring FileCopyUtils?
Powerlord

3

290

Tôi đã có thể truyền phát dòng này bằng cách sử dụng hỗ trợ tích hợp trong Spring với ResourceHttpMessageConverter. Điều này sẽ đặt độ dài nội dung và loại nội dung nếu nó có thể xác định loại 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
Những công việc này. Nhưng tệp (tệp .csv) được hiển thị trong trình duyệt và không được tải xuống - làm cách nào tôi có thể buộc trình duyệt tải xuống?
chzbrgla

41
Bạn có thể thêm sản xuất = MediaType.APPLICATION_OCTET_STREAM_VALUE vào @RequestMapping để buộc tải xuống
David Kago

8
Ngoài ra, bạn nên thêm <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> vào danh sách messageConverters (<mvc: điều khiển chú thích> <mvc: chuyển đổi tin nhắn>)
Sllouyssgort

4
Có cách nào để đặt Content-Dispositiontiêu đề với cách này không?
Ralph

8
Tôi không có nhu cầu về điều đó, nhưng tôi nghĩ rằng bạn có thể thêm httpResponse làm tham số cho phương thức và sau đó "answer.setHeader (" Xử lý nội dung "," tệp đính kèm; filename = somefile.pdf ");"
Scott Carlson

82

Bạn sẽ có thể viết các tập tin trên phản hồi trực tiếp. Cái gì đó như

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

và sau đó viết tệp dưới dạng luồng nhị phân trên response.getOutputStream(). Hãy nhớ làm response.flush()ở cuối và điều đó nên làm.


8
không phải là cách 'Mùa xuân' để đặt loại nội dung như thế này sao? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Đen

4
@Francis nếu ứng dụng của bạn tải xuống các loại tệp khác nhau thì sao? Câu trả lời của Lobster1234 cho phép bạn tự động thiết lập bố trí nội dung.
Hoa hồng

2
đó là sự thật @Rose, nhưng tôi tin rằng sẽ tốt hơn nếu xác định các điểm cuối khác nhau cho mỗi định dạng
Đen

3
Tôi đoán là không, vì nó không thể mở rộng. Chúng tôi hiện đang hỗ trợ hàng tá loại tài nguyên. Chúng tôi có thể hỗ trợ nhiều loại tệp hơn dựa trên những gì người dùng muốn tải lên trong trường hợp đó, chúng tôi có thể kết thúc với rất nhiều điểm cuối về cơ bản làm cùng một việc. IMHO chỉ có một điểm kết thúc tải xuống và nó xử lý vô số loại tệp. @Francis
Hoa hồng

3
nó hoàn toàn "có thể mở rộng", nhưng chúng tôi có thể đồng ý không đồng ý liệu đó có phải là cách thực hành tốt nhất hay không
Black

74

Với Spring 3.0, bạn có thể sử dụng HttpEntityđối tượng trả về. Nếu bạn sử dụng cái này, thì bộ điều khiển của bạn không cần một HttpServletResponseđối tượng, và do đó sẽ dễ kiểm tra hơn. Ngoại trừ điều này, câu trả lời này tương đối giống với câu trả lời của Infeligo .

Nếu giá trị trả về của khung pdf của bạn là một mảng byte (đọc phần thứ hai trong câu trả lời của tôi cho các giá trị trả về khác) :

@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);
}

Nếu kiểu trả về của Khung PDF ( documentBbody) của bạn chưa phải là một mảng byte (và cũng không có ByteArrayInputStream) thì bạn nên KHÔNG làm cho nó thành một mảng byte trước. Thay vào đó, tốt hơn là sử dụng:

ví dụ với 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 điều này sẽ không tải toàn bộ tập tin trong bộ nhớ có thể dễ dàng tạo ra OutOfMemoryErrors.
Faisal Feroz

1
@FaisalFeroz: có, điều này đúng, nhưng tài liệu tệp dù sao cũng được tạo trong bộ nhớ (xem câu hỏi: "PDF cần được tạo trong mã"). Dù sao - giải pháp của bạn đã khắc phục vấn đề này là gì?
Ralph

1
Bạn cũng có thể sử dụng FeedbackEntity, một siêu siêu HTTPEntity cho phép bạn chỉ định mã trạng thái http phản hồi. Ví dụ:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa

@Amr Mostafa: ResponseEntitylà một lớp con của HttpEntity(nhưng tôi hiểu rồi) mặt khác 201 CREATED không phải là thứ tôi sẽ sử dụng khi tôi chỉ trả về chế độ xem dữ liệu. (xem w3.org/Prot Protocol / rfc2616 / rfc2616-sec10.html cho 201 TẠO)
Ralph

1
Có một lý do tại sao bạn thay thế khoảng trắng bằng dấu gạch dưới trong tên tệp? Bạn có thể gói nó trong dấu ngoặc kép để gửi tên thực tế.
Alexandru Severin

63

Nếu bạn:

  • Không muốn tải toàn bộ tệp vào byte[]trước khi gửi phản hồi;
  • Muốn / cần gửi / tải xuống qua InputStream;
  • Muốn có toàn quyền kiểm soát Loại Mime và tên tệp được gửi;
  • Có những @ControllerAdvicengoại lệ khác cho bạn (hoặc không).

Mã dưới đây là những gì bạn cần:

@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
    );
}

Về phần chiều dài tệp : File#length()phải đủ tốt trong trường hợp chung, nhưng tôi nghĩ tôi nên thực hiện quan sát này vì nó có thể chậm , trong trường hợp đó bạn nên lưu trữ nó trước đó (ví dụ trong DB). Các trường hợp có thể chậm bao gồm: nếu tệp lớn, đặc biệt nếu tệp nằm trong một hệ thống từ xa hoặc một cái gì đó được xây dựng công phu hơn như thế - có thể là một cơ sở dữ liệu.



InputStreamResource

Nếu tài nguyên của bạn không phải là một tệp, ví dụ: bạn chọn dữ liệu từ DB, bạn nên sử dụng InputStreamResource. Thí dụ:

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

Bạn không khuyên nên sử dụng lớp FileSystemResource?
Stephane

Trên thực tế, tôi tin rằng nó là OK để sử dụng FileSystemResourceở đó. Nó thậm chí được khuyến khích nếu tài nguyên của bạn là một tập tin . Trong mẫu này, FileSystemResourcecó thể được sử dụng ở đâu InputStreamResource.
acdcjunior

Về phần tính toán độ dài tệp: Nếu bạn lo lắng, đừng. File#length()nên đủ tốt trong trường hợp chung. Tôi chỉ đề cập đến nó bởi vì nó có thể chậm , đặc biệt nếu tệp nằm trong một hệ thống từ xa hoặc một cái gì đó chi tiết hơn như thế - một cơ sở dữ liệu, có thể?. Nhưng chỉ lo lắng nếu nó trở thành một vấn đề (hoặc nếu bạn có bằng chứng cứng thì nó sẽ trở thành một), chứ không phải trước đây. Điểm chính là: bạn đang nỗ lực truyền phát tệp, nếu bạn phải tải trước tất cả trước đó, thì việc truyền phát kết thúc không có sự khác biệt, eh?
acdcjunior

Tại sao mã trên không làm việc cho tôi? Nó tải tập tin 0 byte. Tôi đã kiểm tra và chắc chắn rằng các trình chuyển đổi ByteArray & ResourceMessage đang ở đó. Tui bỏ lỡ điều gì vậy ?
mã hóa_idiot

Tại sao bạn lo lắng về bộ chuyển đổi ByteArray & ResourceMessage?
acdcjunior

20

Mã này đang hoạt động tốt để tải xuống một tệp tự động từ bộ điều khiển mùa xuân khi nhấp vào một liên kết trên 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

Mã dưới đây làm việc cho tôi để tạo và tải xuống một tệp văn bản.

@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

Những gì tôi có thể nhanh chóng nghĩ đến là, tạo pdf và lưu trữ nó trong webapp / download / <RANDOM-FILENAME> .pdf từ mã và gửi chuyển tiếp tới tệp này bằng cách sử dụng HttpServletRequest

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

hoặc nếu bạn có thể định cấu hình trình phân giải chế độ xem của mình một cái gì đó như,

  <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>

sau đó chỉ cần trở lại

return "RANDOM-FILENAME";

1
Nếu tôi cần hai trình phân giải chế độ xem, làm thế nào tôi cũng có thể trả về tên của trình phân giải hoặc chọn nó trong trình điều khiển ??
azerafati

3

Giải pháp sau đây phù hợp với tôi

    @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

một cái gì đó như dưới đây

@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");
    }
}

Bạn có thể hiển thị PDF hoặc tải xuống các ví dụ ở đây


1

Nếu nó giúp được ai. Bạn có thể làm những gì câu trả lời được chấp nhận bởi Infeligo đã đề xuất nhưng chỉ cần đặt thêm bit này vào mã để tải xuống bắt buộc.

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


0

Trong trường hợp của tôi, tôi đang tạo một số tệp theo yêu cầu, do đó cũng phải tạo url.

Đối với tôi làm việc như thế:

@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));
}

Rất quan trọng là loại mime trong producesvà cũng vậy, tên của tệp là một phần của liên kết để bạn phải sử dụng @PathVariable.

Mã HTML trông như thế:

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

Trường hợp ${file_name}được tạo bởi Thymeleaf trong bộ điều khiển và nghĩa là: result_20200225.csv, do đó toàn bộ liên kết ghi chú url là : example.com/aplication/dbreport/files/result_20200225.csv.

Sau khi nhấp vào liên kết trình duyệt hỏi tôi phải làm gì với tệp - lưu hoặc mở.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.