Para este ejemplo use la librería Apache POI que permite generar documentos de Office, el problema principal de la solución fue bajar los datos de una consulta a una BD que podia regresar muchos registros, por lo que no podíamos dejar esperando al usuario a que se generara completo el archivo Excel para que pudiera ser descargado.
La solución fue generar un archivo excel vacío con la cabecera necesaria para enviar el setContentType y el navegador queda esperando a que el ServletOutputStream termine de enviar datos Bytes x Bytes.
mvc-dispatcher-servlet.xml
unchecked
classpath:message
Encabezado de archivos XLSX, como se puede ver en la imagen todo archivo XLS tiene como Header los Byte: {0x50, 0x4b, 0x03, 0x04, 0x14, 0x00};
Metodo que Baja el archivo Bytes x Bytes
private void doDownload(String accion, String fecha, String fechainicio, String fechafinal, String compania, HttpServletRequest request, HttpServletResponse response) throws IOException {
String ftemporal = fecha;
//XLS
SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk
String mensaje = accion.equals("B") == true ? "Bloqueo" : "Desbloqueo";
String[] ftempo = ftemporal.split("/"); //{DD,MM,YYYY}
Sheet sheet = wb.createSheet("ReporteMora_" + mensaje + "_" + ftempo[2] + ftempo[1] + ftempo[0]);
OutputStream out = response.getOutputStream();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-disposition", "attachment;filename=\"" + sheet.getSheetName() + ".xlsx\"");
final byte headerBytes[] = new byte[]{0x50, 0x4b, 0x03, 0x04, 0x14, 0x00};
out.write(headerBytes);
response.flushBuffer();
//encabezado
//data
Row encabezado = sheet.createRow(0);
sheet.autoSizeColumn(0);
Cell cell = encabezado.createCell(0);
cell.setCellValue((String) "IMEI");
cell = encabezado.createCell(1);
cell.setCellValue((String) "MARCA");
cell = encabezado.createCell(2);
cell.setCellValue((String) "MODELO");
cell = encabezado.createCell(3);
cell.setCellValue((String) "FECHA_INGRESO");
cell = encabezado.createCell(4);
cell.setCellValue((String) "ACCION");
cell = encabezado.createCell(5);
cell.setCellValue((String) "TIPO_TECNOLOGIA");
cell = encabezado.createCell(6);
cell.setCellValue((String) "USUARIO");
ReportesDelegate reporteEstados = new ReportesDelegate();
int rownum = 1;
out.flush();
try {
/*
Metodo que recibe la fecha en formato dd/mm/YYYY
finicio en formato dd/mm/yy hh24:mi:ss
ffin en fomrato dd/mm/yy hh24:mi:ss
accion B o D //bloqueo desbloqueo
compania en codigo_compania x ej 219 claro
retorna una lista del reporte cobranza en detalle.
*/
List resultadoEstado = reporteEstados.getDetalleCobranza(fecha, fechainicio + " 00:00:00", fechafinal + "23:59:59", accion, compania);
int tamaniolista=resultadoEstado.size();
for (ReporteCobranza r : resultadoEstado) {
Row row = sheet.createRow(rownum++);
Cell cellda = row.createCell(0);
cellda.setCellValue(r.getImei().toString());
cellda = row.createCell(1);
cellda.setCellValue((String) r.getMarca());
cellda = row.createCell(2);
cellda.setCellValue((String) r.getModelo());
cellda = row.createCell(3);
cellda.setCellValue((String) r.getFechaIngreso());
cellda = row.createCell(4);
cellda.setCellValue((String) r.getAccion());
cellda = row.createCell(5);
cellda.setCellValue((String) r.getTipoTecnologia());
cellda = row.createCell(6);
cellda.setCellValue((String) r.getUsuario());
}
for (int i = 0; i < tamaniolista; i++) {
sheet.autoSizeColumn(i);
}
} catch (Exception e) {
System.out.println("Error reporte:" + e);
}
CustomOutputStream out2 = new CustomOutputStream(out, headerBytes.length);
wb.write(out2);
out.flush();
out.close();
wb.dispose();
}
Considere el uso de la versión Streaming de POI. Esto cargará un subconjunto del archivo en la memoria según sea necesario. Es el método recomendado cuando se trata de archivos de gran tamaño:
SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk
Al ecribir el encabezado uso el "sheet.autoSizeColumn", pero solo lo hago en el encabezado, ya que si se integra en el ciclo de creacion este reduce considerablemente la rapidez con que se genera el archivo EXCEL.
Consultar Documentacion relacionada del metodo
En las siguientes lineas creo un OutputStream que ocupare para enviar solo el encabezado del archivo y despues cuando empieze a llegar las ROW las enviare al navegador, la velocidad de descarga dependera de la conexion entre el servidor de Aplicaciones y la aplicacion y el tiempo de la Base de datos, considerando la cantidad de informacion que manejaba se pueden estimar en archivos de 150 MB promedio. Generar un archivo asi en una aplicaciones WEB es un tiempo considerable que se le debe hacer esperar al usuario, la mejor solucion fue el CustomOutputStream.
Por ultimo el " response.flushBuffer" con esto se vacia el buffer y se envia al response la informacion. En la primera vuelco de buffer es solo el HEADER generico de un archivo EXCEL XLSX.
OutputStream out = response.getOutputStream();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-disposition", "attachment;filename=\"" + sheet.getSheetName() + ".xlsx\"");
final byte headerBytes[] = new byte[]{0x50, 0x4b, 0x03, 0x04, 0x14, 0x00};
out.write(headerBytes);
response.flushBuffer();
Controlador de SPRING que activa la Descarga
import cl.claro.aplicaciones.webberr.modelo.CobranzaModel;
import cl.clarochile.aplicaciones.berr.configuracion.dao.entities.Compania;
import cl.clarochile.aplicaciones.berr.configuracion.dao.entities.Estado;
import cl.clarochile.aplicaciones.berr.configuracion.delegate.ConfiguracionDelegate;
import cl.clarochile.aplicaciones.berr.consultas.to.HistoricoVistaTO;
import cl.clarochile.aplicaciones.berr.reportes.to.CobranzaResumen;
import cl.clarochile.aplicaciones.berr.reportes.to.DatoBitacoraProceso;
import cl.clarochile.aplicaciones.berr.reportes.to.ReporteCobranza;
import cl.clarochile.aplicaciones.berr.service.delegate.ReportesDelegate;
import cl.clarochile.infraestructura.exceptions.BussinesFault;
import cl.clarochile.infraestructura.exceptions.TechnicalFault;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@RequestMapping(value = "/descargaDetalleCobranza")
public void descargaDetalleCobranza(@RequestParam(value = "compania") String compania,
@RequestParam(value = "fecha") String fecha,
@RequestParam(value = "fechainicio") String fechainicio,
@RequestParam(value = "fechafinal") String fechafinal,
@RequestParam(value = "accion") String accion,
HttpServletRequest request, HttpServletResponse response) throws Exception {
doDownload(accion, fecha, fechainicio, fechafinal, compania, request, response);
}
Clase que extiende ServletOutputStream para evitar volver a enviar el Header del archivo XLSX
class CustomOutputStream extends ServletOutputStream {
OutputStream os;
int skip = 0;
int n = 0;
public CustomOutputStream(OutputStream out, int skip) {
this.os = out;
this.skip = skip;
}
@Override
public void write(int b) throws IOException {
if (n < skip) {
n++;
return;
}
os.write(b);
}
}
ReporteCobranza.jsp