การดาวน์โหลดไฟล์จากสปริงคอนโทรลเลอร์


387

ฉันมีข้อกำหนดที่ฉันต้องการดาวน์โหลด PDF จากเว็บไซต์ จำเป็นต้องสร้าง PDF ภายในโค้ดซึ่งฉันคิดว่าจะเป็นการรวมกันของ freemarker และกรอบการสร้าง PDF เช่น iText มีวิธีไหนที่ดีกว่านี้?

อย่างไรก็ตามปัญหาหลักของฉันคือฉันจะอนุญาตให้ผู้ใช้ดาวน์โหลดไฟล์ผ่าน Spring Controller ได้อย่างไร


2
เป็นมูลค่าการกล่าวขวัญว่า Spring Framework เปลี่ยนไปมากตั้งแต่ปี 2011 ดังนั้นคุณสามารถทำได้ด้วยวิธีที่ตอบโต้เช่นกัน - นี่คือตัวอย่าง
Krzysztof Skrzynecki

คำตอบ:


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
ใช่เพิ่งแก้ไขโพสต์ ฉันสร้างไฟล์ประเภทต่าง ๆ ดังนั้นฉันจึงทิ้งมันไว้ในเบราว์เซอร์เพื่อกำหนดประเภทเนื้อหาของไฟล์ตามส่วนขยาย
Infeligo

ลืม flushBuffer ขอบคุณโพสต์ของคุณฉันเห็นว่าทำไมฉันถึงไม่ทำงาน :-)
Jan Vladimir Mostert

35
มีเหตุผลใดที่จะใช้ Apache IOUtilsแทน Spring FileCopyUtilsหรือไม่?
Powerlord

3
นี่คือทางออกที่ดีกว่า: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin

290

ฉันสามารถสตรีมรายการนี้โดยใช้การสนับสนุนในตัวใน Spring ด้วย 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 เพื่อบังคับการดาวน์โหลด
David Kago

8
นอกจากนี้คุณควรเพิ่ม<bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" />ในรายการ messageConverters (<mvc: ขับเคลื่อนด้วยคำอธิบายประกอบ> <mvc: message-converter>)
Sllouyssgort

4
มีวิธีการตั้งค่าContent-Dispositionส่วนหัวด้วยวิธีนี้หรือไม่?
Ralph

8
ฉันไม่ต้องการสิ่งนั้น แต่ฉันคิดว่าคุณสามารถเพิ่ม HttpResponse เป็นพารามิเตอร์ในวิธีการและจากนั้น "response.setHeader (" Content-Disposition "," สิ่งที่แนบมา; filename = somefile.pdf ");"
Scott Carlson

82

คุณควรจะสามารถเขียนไฟล์ลงบนการตอบกลับได้โดยตรง สิ่งที่ต้องการ

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

response.getOutputStream()แล้วเขียนไฟล์เป็นกระแสไบนารี จำไว้ว่าให้ทำresponse.flush()ตอนท้ายและควรทำ


8
ไม่ใช่วิธี 'Spring' ในการตั้งค่าประเภทเนื้อหาเช่นนี้ @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Black

4
@ ฟรานซิสจะเกิดอะไรขึ้นถ้าแอปพลิเคชันของคุณดาวน์โหลดไฟล์ประเภทอื่น คำตอบของ Lobster1234 ช่วยให้คุณสามารถตั้งค่าการจัดการเนื้อหาแบบไดนามิก
โรส

2
นั่นเป็นความจริง @Rose แต่ฉันเชื่อว่ามันจะเป็นการปฏิบัติที่ดีกว่าในการกำหนดจุดสิ้นสุดที่แตกต่างกันต่อรูปแบบ
Black

3
ฉันเดาไม่ได้เพราะมันไม่สามารถปรับขนาดได้ ขณะนี้เรากำลังสนับสนุนทรัพยากรหลายสิบประเภท เราอาจรองรับไฟล์ประเภทมากขึ้นตามสิ่งที่ผู้ใช้ต้องการอัปโหลดในกรณีนั้นเราอาจจบลงด้วยจุดสิ้นสุดจำนวนมากที่ทำสิ่งเดียวกัน IMHO จะต้องมีจุดสิ้นสุดการดาวน์โหลดเพียงจุดเดียวและสามารถจัดการไฟล์ได้หลากหลายประเภท @Francis
โรส

3
มันเป็น "ปรับขนาดได้" อย่างแน่นอน แต่เราสามารถตกลงที่จะไม่เห็นด้วยว่ามันเป็นวิธีปฏิบัติที่ดีที่สุด
ดำ

74

ด้วย Spring 3.0 คุณสามารถใช้HttpEntityวัตถุส่งคืน หากคุณใช้สิ่งนี้คอนโทรลเลอร์ของคุณไม่จำเป็นต้องมีHttpServletResponseวัตถุดังนั้นจึงง่ายต่อการทดสอบ ยกเว้นนี้คำตอบนี้คือเท่ากับเมื่อเทียบกับหนึ่ง Infeligo

หากค่าส่งคืนของเฟรมเวิร์ก 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
Faisal Feroz

1
@FaisalFeroz: ใช่ถูกต้องแล้ว แต่ไฟล์เอกสารนั้นถูกสร้างขึ้นในหน่วยความจำ (ดูคำถาม: "PDF จะต้องสร้างขึ้นภายในรหัส") อย่างไรก็ตามอะไรคือทางออกของคุณที่เอาชนะปัญหานี้?
Ralph

1
คุณยังสามารถใช้ ResponseEntity ซึ่งเป็นสุดยอดของ HttpEntity ซึ่งช่วยให้คุณระบุรหัสสถานะการตอบสนอง http ตัวอย่าง:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa

@Amr Mostafa: ResponseEntityเป็น subclass ของHttpEntity(แต่ฉันเข้าใจ) ในทางตรงกันข้าม 201 CREATED ไม่ใช่สิ่งที่ฉันจะใช้เมื่อฉันกลับไปดูข้อมูล (ดูw3.org/Protocols/rfc2616/rfc2616-sec10.htmlสำหรับ 201 CREATED )
Ralph

1
มีเหตุผลที่คุณกำลังเปลี่ยนช่องว่างด้วยการขีดเส้นใต้ในชื่อไฟล์หรือไม่? คุณสามารถใส่ไว้ในเครื่องหมายคำพูดเพื่อส่งชื่อจริง
Alexandru Severin

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()ควรจะดีพอในกรณีทั่วไป แต่ฉันคิดว่าฉันจะทำการสังเกตนี้เพราะมันอาจจะช้าซึ่งในกรณีนี้คุณควรเก็บมันไว้ก่อนหน้านี้ (เช่นใน DB) ในบางกรณีอาจรวมถึงช้า: หากไฟล์มีขนาดใหญ่โดยเฉพาะอย่างยิ่งหากไฟล์อยู่ในระบบระยะไกลหรือมีเนื้อหาเพิ่มเติมเช่นนั้น - ฐานข้อมูลอาจเป็นไปได้ว่า



InputStreamResource

หากทรัพยากรของคุณไม่ได้เป็นไฟล์เช่นคุณสามารถเลือกข้อมูลขึ้นมาจาก DB InputStreamResourceคุณควรใช้ ตัวอย่าง:

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

คุณไม่แนะนำให้ใช้คลาส FileSystemResource ใช่ไหม
Stephane

ที่จริงแล้วฉันเชื่อว่ามันโอเคที่จะใช้ที่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 / ดาวน์โหลด / <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

นี่อาจเป็นคำตอบที่มีประโยชน์

การส่งออกข้อมูลเป็นรูปแบบ pdf ที่หน้าได้หรือไม่

เมื่อขยายไปถึงสิ่งนี้การเพิ่มเนื้อหาการจัดการเป็นสิ่งที่แนบมา (ค่าเริ่มต้น) จะดาวน์โหลดไฟล์ หากคุณต้องการดูคุณต้องตั้งค่าเป็นแบบอินไลน์


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 behing example.com/aplication/dbreport/files/result_20200225.csvทั้งหมด:

หลังจากคลิกที่ลิงค์เบราว์เซอร์ถามฉันว่าจะทำอย่างไรกับไฟล์ - บันทึกหรือเปิด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.