[Java Spring Boot] Como fazer seu endpoint retornar um arquivo CSV

brunbs

Bruno Barbosa

Posted on November 21, 2023

[Java Spring Boot] Como fazer seu endpoint retornar um arquivo CSV

Introdução

Imagine uma situação em que você tem um banco de dados extremamente longo com informações sobre estudantes.
Seu cliente solicitou que você disponibilize um endpoint para retornar todos os estudantes cadastrados.
Você pode combinar com o front end para retornar essa informação de forma paginada pois apresentar milhares de dados direto na tela seria terrível!

Mas, por algum motivo, o cliente também quer ter à disposição a lista completa com todas as informações, porém em formato CSV para que consiga visualizar no Excel.

Caso você queira ver como retornar informações paginadas utilizando java spring boot, você pode ver este outro artigo que escrevi clicando aqui.

Vamos focar, então, em como desenvolver esse endpoint que retornará um arquivo CSV com a lista de todos os estudantes.

Você pode ver o repositório deste artigo clicando aqui.
Neste repositório você irá encontrar uma branch main, contendo um controller que retornar todas as informações de estudantes, sem paginação.

A branch que terá nosso desenvolvimento do endpoint de retorno do csv é a branch feature/csv-downloader

Vou rapidamente apresentar as principais classes do nosso pequeno projeto.

Desenvolvimento

Entidade Estudante:



@Data
@Builder
public class StudentEntity {

    private Integer registration;

    private String name;
    private Integer grade;

}



Enter fullscreen mode Exit fullscreen mode

O mais simples possível, nosso objetivo é construir um CSV, pra isso vamos utilizar uma classe simples de entidade.

Para poder criar arquivo CSV vamos precisar de duas dependências no nosso POM.xml:



        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.3</version>
        </dependency>


Enter fullscreen mode Exit fullscreen mode

Agora vamos ver como é o service que irá gerar o csv, vou colocar:

Classe StudentServiceImpl.class:

Vamos criar um método chamado getStudentsCSV, esse método irá receber um objeto do tipo HttpServletResponse, que vamos utilizar para comunicação HTTP, podendo adicionar informações de header e nosso arquivo CSV a ser retornado para o navegador.



@Override
    public void getStudentsCSV(HttpServletResponse response) {
        List<StudentEntity> students = StudentRepository.findAll();


    }


Enter fullscreen mode Exit fullscreen mode

Por enquanto tudo o que ele faz é receber a lista de estudantes do repository.
Vamos agora criar alguns métodos privados que serão responsáveis por criar nosso arquivo csv, nossa planilha, nossas linhas.

Método createSheet:



private XSSFSheet createSheet(XSSFWorkbook workbook) {
        XSSFSheet sheet = workbook.createSheet("Student Report");
        sheet.setColumnWidth(0, 4000);
        sheet.setColumnWidth(1, 4000);
        sheet.setColumnWidth(2, 4000);
        return sheet;
    }


Enter fullscreen mode Exit fullscreen mode

Esse método recebe um Workbook (Pasta de Trabalho), dentro dele vamos criar nossa planilha utilizando o método createSheet, que recebe o nome da planilha que vamos criar (Uma pasta de trabalho pode ter várias planilhas dentro).
após criada a planilha, podemos personalizar as colunas dentro dela, no nosso caso vamos alterar o comprimento das 3 primeiras colunas para 4000.

Certo, agora que temos nosso método que cria a planilha, vamos criar o método que cria células na nossa planilha, sendo o mais genérico possível:

Método createCell:



    private void createCell(XSSFRow row, int index, String value) {
        XSSFCell cell = row.createCell(index);
        cell.setCellValue(value);
    }


Enter fullscreen mode Exit fullscreen mode

Esse método recebe um objeto do tipo XSSFRow que representa uma linha da nossa planilha, recebe também um índice para saber qual a posição da célula na linha, e um value que seria o conteúdo que aquela célula receberá.

Então vamos criar um objeto do tipo XSSFCell e vamos utilizar o método createCell do objeto XSSFRow para criar uma célula, passando o índice, ou seja, a coluna que a célula estará.
E, por fim, vamos utilizar o método setCellValue do nosso XSSFCell que criamos antes para colocar o conteúdo dentro.
Bem simples.

Agora que já temos um método genérico que cria células em uma linha da nossa planilha, podemos criar cabeçalhos e preencher linhas.

Vamos começar com o método que cria o cabeçalho:

Método createHeader:



private void createHeader(XSSFSheet sheet) {
        XSSFRow header = sheet.createRow(0);
        createCell(header, 0, "Registration");
        createCell(header, 1, "Name");
        createCell(header, 2, "Grade");
    }


Enter fullscreen mode Exit fullscreen mode

Esse método recebe uma objeto referente a planilha.
Primeiramente criamos um objeto XSSFRow que irá representar uma linha na nossa planilha, como vamos fazer um cabeçalho, vamos criar a primeira linha, por isso utilizaremos o método createRow da class XSSFSheet passando o valor 0.

Agora vamos utilizar o método createCell que criamos anteriormente passando: o header (pois este representa a linha da planilha, a coluna da célula e o conteúdo).
Estamos chamando esse método 3 vezes pois temos 3 colunas para adicionar:
Registration - A matrícula do estudante
Name - O nome do estudante
Grade - a nota do estudante

Agora que temos a nossa primeira linha da planilha (nosso cabeçalho) vamos popular as outras linhas com o retorno do repository, para isso, vamos criar um método chamado createRow:

Método createRow:



private void createRow(XSSFSheet sheet, List<StudentEntity> students) {
        XSSFRow row;
        int rowCounter = 1;

        for(var student : students) {
            row = sheet.createRow(rowCounter);
            createCell(row, 0, student.getRegistration().toString());
            createCell(row, 1, student.getName());
            createCell(row, 2, student.getGrade().toString());
            rowCounter++;
        }
    }


Enter fullscreen mode Exit fullscreen mode

Esse método recebe como parâmetro a nossa planilha (XSSFSheet) e uma lista de estudantes que retornou do nosso repository.
Primeiro vamos criar um objeto do tipo XSSFRow e um inteiro que será nosso contador de linhas da planilha, iniciando em 1. Ele irá iniciar em 1 pois a linha 0 já foi criada, é nosso cabeçalho.

Agora vamos iterar pela lista de estudantes onde, para cada registro de estudante, vamos criar uma linha utilizando o método createRow da classe XSSFSheet passando a linha que estamos criando.
E então vamos chamar o método createCell, criado por nós. Vamos passar para esse método nosso XSSFRow, a coluna que vamos preencher e o conteúdo. No caso, chamamos 3 vezes pois temos 3 atributos na entidade, sendo que a coluna 0 é a matrícula do estudante, a coluna 1 é o nome e a coluna 2 é a nota.
depois de fazermos as atribuições, incrementamos nosso contados de linhas para que, na próxima iteração, as informações sejam adicionadas em uma nova linha.

Muito bem, agora que temos as principais classes para criação da nossa planilha, vamos chamar esses métodos no nosso service lá no método de gerar relatório que será chamado pelo controller, ficando assim:



@Override
    public void getStudentsCSV(HttpServletResponse response) {
        List<StudentEntity> students = StudentRepository.findAll();

        try(XSSFWorkbook workbook = new XSSFWorkbook()){
            String headerKey = "Content-Disposition";
            String headerValue = "attachment; filename=report.csv";
            response.setHeader(headerKey, headerValue);

            XSSFSheet sheet = createSheet(workbook);
            createHeader(sheet);
            createRow(sheet, students);

            ServletOutputStream out = response.getOutputStream();
            out.flush();
            workbook.write(out);
            out.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }


Enter fullscreen mode Exit fullscreen mode

OBS: aqui estamos lançando uma RuntimeException pois não estou preocupado com o tratamento de exceções aqui, acredito que o correto seja criar uma exception personalizada para possíveis erros e utilizar um handler para tratar essa exception.

Vamos entender o que está acontecendo aqui:

1 - Chamamos nosso repository que irá retornar todos os estudantes.
2 - Criamos nossa pasta de trabalho (Workbook), que é um objeto do tipo XSSFWorkbook.
3 - Criamos um header para adicionar no nosso HttpServletResponse. Esse header se chama "Content-Disposition" e tem como valor "attachment; filename=report.csv". Ele irá informar para o navegador que estamos retornando um anexo e já diz o nome do anexo que será salvo no computador do usuário.
4 - Chamamos nosso método de criar planilha
5 - Chamamos nosso método de criar o cabeçalho
6 - Chamamos nosso método de preencher as linhas com as informações de estudantes retornadas pelo repository
7 - Criamos um ServletOutputStream para retornar informações binárias para o nosso client. Esse objeto recebe o getOutputStream() a partir do nosso HttpServletResponse.
8 - Fazemos o flush() do ServletOutputStream que irá forçar a escrita das informações, adicionamos ele no nosso workbook e então encerramos com o método out()

Assim, esse service irá retornar nosso CSV. Nosso Service completo ficou da seguinte forma:



package com.csv.downloader.service.Impl;


import com.csv.downloader.domain.entity.StudentEntity;
import com.csv.downloader.domain.response.StudentResponse;
import com.csv.downloader.repository.StudentRepository;
import com.csv.downloader.service.StudentService;
import com.csv.downloader.util.StudentMapper;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    @Override
    public List<StudentResponse> getStudents() {
        List<StudentEntity> students = StudentRepository.findAll();
        return studentMapper.entityToResponseList(students);
    }

    @Override
    public void getStudentsCSV(HttpServletResponse response) {
        List<StudentEntity> students = StudentRepository.findAll();

        try(XSSFWorkbook workbook = new XSSFWorkbook()){
            String headerKey = "Content-Disposition";
            String headerValue = "attachment; filename=report.csv";
            response.setHeader(headerKey, headerValue);

            XSSFSheet sheet = createSheet(workbook);
            createHeader(sheet);
            createRow(sheet, students);

            ServletOutputStream out = response.getOutputStream();
            out.flush();
            workbook.write(out);
            out.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private XSSFSheet createSheet(XSSFWorkbook workbook) {
        XSSFSheet sheet = workbook.createSheet("Student Report");
        sheet.setColumnWidth(0, 4000);
        sheet.setColumnWidth(1, 4000);
        sheet.setColumnWidth(2, 4000);
        return sheet;
    }

    private void createHeader(XSSFSheet sheet) {
        XSSFRow header = sheet.createRow(0);
        createCell(header, 0, "Registration");
        createCell(header, 1, "Name");
        createCell(header, 2, "Grade");
    }

    private void createCell(XSSFRow row, int index, String value) {
        XSSFCell cell = row.createCell(index);
        cell.setCellValue(value);
    }

    private void createRow(XSSFSheet sheet, List<StudentEntity> students) {
        XSSFRow row;
        int rowCounter = 1;

        for(var student : students) {
            row = sheet.createRow(rowCounter);
            createCell(row, 0, student.getRegistration().toString());
            createCell(row, 1, student.getName());
            createCell(row, 2, student.getGrade().toString());
            rowCounter++;
        }
    }


}



Enter fullscreen mode Exit fullscreen mode

O controller que chama esse service é o StudentsController:

StudentController.class:



package com.csv.downloader.controller;

import com.csv.downloader.domain.response.StudentResponse;
import com.csv.downloader.service.StudentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("students")
@Tag(name = "Student Controller", description = "Endpoint for returning csv with a list of students")
public class StudentController {

@Autowired
private StudentService studentService;

@GetMapping("all")
@Operation(summary = "list all students")
ResponseEntity&lt;List&lt;StudentResponse&gt;&gt; getAllStudents() {
    return ResponseEntity.ok().body(studentService.getStudents());
}

@GetMapping("download")
@Operation(summary = "download a csv with all students info")
void getStudentsCSVReport(HttpServletResponse response) {
    studentService.getStudentsCSV(response);
}
Enter fullscreen mode Exit fullscreen mode

}

Enter fullscreen mode Exit fullscreen mode




Resultado

No final, o arquivo CSV ficou da seguinte forma:"

A CSV file running in Excel program showing the list of students with registration, name and grade

Para chamar o endpoint, basta rodar a aplicação e fazer um get para:
localhost:8080/students/download

ou acessar o swagger da aplicação:
http://localhost:8080/swagger-ui/index.html

Swagger image with the download endpoint running

Com isso finalizamos nossa implementação.
Espero que este artigo tenha ajudado vocês. Quaisquer dúvidas ou sugestões de melhorias podem comentar que ficarei feliz em responder.

💖 💪 🙅 🚩
brunbs
Bruno Barbosa

Posted on November 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related