J'ai une application de démarrage Java/Spring où je veux créer un point de terminaison API qui crée et renvoie un fichier Excel téléchargeable. Voici mon point de terminaison de contrôleur:
@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);
}
}
Ensuite, la classe de service
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;
}
}
Ce code crée avec succès le fichier Excel approprié à l'aide de la bibliothèque de POI Apache. Mais cela ne le fera pas sortir correctement du contrôleur car ByteArrayResource::getFilename
renvoie toujours null:
/**
* This implementation always returns {@code null},
* assuming that this resource type does not have a filename.
*/
@Override
public String getFilename() {
return null;
}
Quel type de ressource puis-je utiliser pour renvoyer le fichier Excel généré?
Puisque vous utilisez ByteArrayResource
, vous pouvez utiliser le code de contrôleur ci-dessous en supposant que le FooService
est câblé automatiquement dans la classe du contrôleur.
@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);
}
Vous devez définir le nom de fichier sur l'en-tête de réponse à l'aide de Content-disposition
. Essaye ça
@GetMapping("/export")
public ResponseEntity export(HttpServletResponse response) {
fooService.export(response);
}
Modifiez votre méthode de service comme ceci
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
}
}
Fondamentalement, il y a peu de points que vous devez d'abord comprendre et ensuite décider ce que vous voulez faire,
1 .La création d'Excel sur disque est-elle nécessaire ou pouvez-vous la diffuser depuis la mémoire?
Si c'est un téléchargement, l'utilisateur peut le garder ouvert pendant longtemps et occuper de la mémoire pendant cette période (inconvénient de l'approche en mémoire).
Deuxièmement, si le fichier généré doit être nouveau pour chaque demande (c'est-à-dire que les données à exporter sont différentes), il est inutile de le conserver sur le disque (inconvénient de l'approche dans le disque).
Troisièmement, il sera difficile pour un code API de nettoyer le disque car vous ne savez jamais à l'avance quand l'utilisateur terminera son téléchargement (inconvénient de l'approche dans le disque).
Fizik26 répond par cette approche In - Memory où vous ne créez pas de fichier sur le disque. . La seule chose de cette réponse est que vous devez garder une trace de la longueur du tableau out.toByteArray()
et cela peut facilement être fait via une classe wrapper.
2 .Lors du téléchargement d'un fichier, votre code doit diffuser un fichier morceau par morceau - c'est à cela que Java sont destinés. Le code comme ci-dessous le fait.
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()));
et inputStreamWrapper
est comme,
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;
}
}
En ce qui concerne le nom de fichier, si le nom de fichier n'est pas une entrée vers le point final, cela signifie que son système est généré (une combinaison de chaîne constante plus une partie variable par utilisateur). Je ne sais pas pourquoi vous devez obtenir cela de la ressource.
Vous n'aurez pas besoin de ce wrapper si vous utilisez - org.springframework.core.io.ByteArrayResource
Faire savoir au contrôleur est toujours mieux ce qu'il va écrire en utilisant ReponseEntity. Au niveau du service, il suffit de créer et de jouer autour des objets. @RestController ou @Controller n'a pas d'importance ici.
Ce que vous attendez avec impatience dans votre contrôleur ressemble un peu à ceci (exemple) -
@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));
}
Générer une classe Excel -
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());
et enfin, quelque part, à votre avis -
<a href="<c:url value='/alluserreportExcel' />"
target="_blank">Export all users to MS-Excel</a>
Vous pouvez utiliser ceci:
headers.add("Content-Disposition", "attachment; filename=NAMEOFYOURFILE.xlsx");
ByteArrayInputStream in = fooService.export();
return ResponseEntity
.ok()
.headers(headers)
.body(new InputStreamResource(in));
Il télécharge le fichier Excel lorsque vous appelez ce point de terminaison.
Dans votre méthode d'exportation dans votre service, vous devez retourner quelque chose comme ça:
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
workbook.write(out);
} catch (IOException e) {
e.printStackTrace();
}
return new ByteArrayInputStream(out.toByteArray());