Spring Boot Controller export an Excel

陌路散爱 提交于 2021-02-06 09:20:09

问题


I have a java/spring boot application where I want to build an API endpoint that creates and returns a downloadable excel file. Here is my controller endpoint:

@RestController
@RequestMapping("/Foo")
public class FooController {
    private final FooService fooService;

    @GetMapping("/export")
    public ResponseEntity export() {
        Resource responseFile = fooService.export();

        return ResponseEntity.ok()
                             .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="+responseFile.getFilename())
                             .contentType(MediaType.MULTIPART_FORM_DATA)
                             .body(responseFile);
    }
}

Then the service class

public class FooService {
  public Resource export() throws IOException {
    StringBuilder filename = new StringBuilder("Foo Export").append(" - ")
                                                            .append("Test 1.xlsx");
    return export(filename);
  }

  private ByteArrayResource export(String filename) throws IOException {
      byte[] bytes = new byte[1024];
      try (Workbook workbook = generateExcel()) {
          FileOutputStream fos = write(workbook, filename);
          fos.write(bytes);
          fos.flush();
          fos.close();
      }

      return new ByteArrayResource(bytes);
  }

  private Workbook generateExcel() {
      Workbook workbook = new XSSFWorkbook();
      Sheet sheet = workbook.createSheet();

      //create columns and rows

      return workbook;
  }

  private FileOutputStream write(final Workbook workbook, final String filename) throws IOException {
      FileOutputStream fos = new FileOutputStream(filename);
      workbook.write(fos);
      fos.close();
      return fos;
  }  
}

This code successfully creates the proper excel file using the Apache POI library. But this won't return it out of the controller properly because ByteArrayResource::getFilename always returns null:

/**
 * This implementation always returns {@code null},
 * assuming that this resource type does not have a filename.
 */
@Override
public String getFilename() {
    return null;
}

What type of resource can I use to return the generated excel file?


回答1:


Since you are using ByteArrayResource, you can use the below controller code assuming that the FooService is autowired in the controller class.

@RequestMapping(path = "/download_excel", method = RequestMethod.GET)
public ResponseEntity<Resource> download(String fileName) throws IOException {

ByteArrayResource resource = fooService.export(fileName);

return ResponseEntity.ok()
        .headers(headers) // add headers if any
        .contentLength(resource.contentLength())
        .contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
        .body(resource);
}



回答2:


Basically , there are few points that you first need to understand & then decide what you want to do ,

1.Is excel creation on disk needed or can you stream it from memory?

If its a download pop up, user might keep it open for long time & memory be occupied during that period ( disadvantage of in memory approach ) .

Secondly, if generated file has to be new for each request ( i.e. data to be exported is different ) then there is no point in keeping it at disk ( disadvantage of in disk approach ) .

Thirdly, it will be hard for an API code to do disk clean up because you never know in advance as when user will finish up his down load ( disadvantage of in disk approach ) .

Answer by Fizik26 is this In - Memory approach where you don't create a file on disk. . Only thing from that answer is that you need to keep track of length of array out.toByteArray() & that can easily be done via a wrapper class.

2.While downloading a file , your code needs to stream a file chunk by chunk - thats what Java streams are for. Code like below does that.

return ResponseEntity.ok().contentLength(inputStreamWrapper.getByteCount())
            .contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
            .cacheControl(CacheControl.noCache())
            .header("Content-Disposition", "attachment; filename=" + "SYSTEM_GENERATED_FILE_NM")
            .body(new InputStreamResource(inputStreamWrapper.getByteArrayInputStream()));

and inputStreamWrapper is like ,

public class ByteArrayInputStreamWrapper {
    private ByteArrayInputStream byteArrayInputStream;
    private int byteCount;


    public ByteArrayInputStream getByteArrayInputStream() {
    return byteArrayInputStream;
    }


    public void setByteArrayInputStream(ByteArrayInputStream byteArrayInputStream) {
    this.byteArrayInputStream = byteArrayInputStream;
    }


    public int getByteCount() {
    return byteCount;
    }


    public void setByteCount(int byteCount) {
    this.byteCount = byteCount;
    }

}

Regarding file name, if file name is not an input to end point - that means ..its system generated ( a combination of constant string plus a variable part per user ). I am not sure why you need to get that from resource.

You won't need this wrapper if use - org.springframework.core.io.ByteArrayResource




回答3:


You have to set the file name to the response header using Content-disposition. Try this

@GetMapping("/export")
public ResponseEntity export(HttpServletResponse response) {
        fooService.export(response);      
}

Change your service method like this

public Resource export(HttpServletResponse response) throws IOException {
    StringBuilder filename = new StringBuilder("Foo Export").append(" - ")
                                                        .append("Test 1.xlsx");
   return export(filename, response);
}

private void export(String filename,  HttpServletResponse response) throws IOException {
      try (Workbook workbook = generateExcel()) {
          FileOutputStream fos = write(workbook, filename);
          IOUtils.copy(new FileInputStream(fos.getFD()),               
                                     servletResponse.getOutputStream());//IOUtils is from apache commons io
          response.setContentType("application/vnd.ms-excel");
          response.setHeader("Content-disposition", "attachment; filename=" + filename);
     }catch(Exception e) {
       //catch if any checked exception
     }finally{
        //Close all the streams
     }
}



回答4:


Letting controller know is always better what it is going to write using ReponseEntity. At service level just create and play around the objects. @RestController or @Controller doesn't matter here.

What you are looking forward for in your controller is somewhat like this (sample) -

@GetMapping(value = "/alluserreportExcel")
public ResponseEntity<InputStreamResource> excelCustomersReport() throws IOException {
    List<AppUser> users = (List<AppUser>) userService.findAllUsers();
    ByteArrayInputStream in = GenerateExcelReport.usersToExcel(users);
    // return IO ByteArray(in);
    HttpHeaders headers = new HttpHeaders();
    // set filename in header
    headers.add("Content-Disposition", "attachment; filename=users.xlsx");
    return ResponseEntity.ok().headers(headers).body(new InputStreamResource(in));
}

Generate Excel Class -

public class GenerateExcelReport {

public static ByteArrayInputStream usersToExcel(List<AppUser> users) throws IOException {
...
...
//your list here
int rowIdx = 1;
        for (AppUser user : users) {
            Row row = sheet.createRow(rowIdx++);

            row.createCell(0).setCellValue(user.getId().toString());
            ...
        }

  workbook.write(out);
  return new ByteArrayInputStream(out.toByteArray());

and finally, somewhere, in your view -

<a href="<c:url value='/alluserreportExcel'  />"
                target="_blank">Export all users to MS-Excel</a>

For full example, take a peek - here, here and here.




回答5:


You can use this :

 headers.add("Content-Disposition", "attachment; filename=NAMEOFYOURFILE.xlsx");
ByteArrayInputStream in = fooService.export();
return ResponseEntity
            .ok()
            .headers(headers)
            .body(new InputStreamResource(in));

It will download the Excel file when you call this endpoint.

In your export method in your service, you have to return something like that :

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
        workbook.write(out);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return new ByteArrayInputStream(out.toByteArray());


来源:https://stackoverflow.com/questions/52078128/spring-boot-controller-export-an-excel

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!