

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:经过差不多一个月的学习,我们这个项目已经接近尾声,今天是最后一张的内容,关于商家端的excel数据表的导出,后面我会挤出几天的时间对这个项目进行全面的总结,如果对你有帮助,请关注我。
一、为什么选择Apache POI?
在苍穹外卖项目中,虽然EasyExcel性能更好,但POI仍然是主流选择,原因如下:
-
功能最全面:支持复杂的Excel操作(公式、图表、宏等)
-
生态成熟:文档丰富,问题解决方案多
-
无需额外依赖:很多项目已包含POI依赖
-
精细控制:可以精确控制每个单元格的样式
二、技术依赖配置
Maven依赖
XML
xml
<!-- POI核心依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<!-- POI处理.xlsx格式 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 日期处理工具(可选) -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
三、完整实现步骤
第1步:定义导出VO类
java
java
package com.sky.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 营业额报表VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO {
private LocalDate date; // 日期
private BigDecimal turnover; // 营业额
private Integer orderCount; // 订单总数
private Integer validOrderCount; // 有效订单数
private String completionRate; // 完成率
}
/**
* 菜品销量VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishSalesVO {
private String dishName; // 菜品名称
private Integer salesCount; // 销量
private BigDecimal amount; // 销售额
private String category; // 分类
}
第2步:Service层实现
java
java
package com.sky.service.impl;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.DishMapper;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.DishSalesVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private DishMapper dishMapper;
@Override
public void exportTurnoverReport(LocalDate start, LocalDate end,
HttpServletResponse response) throws IOException {
// 1. 查询数据
List<TurnoverReportVO> turnoverData = getTurnoverData(start, end);
// 2. 创建工作簿
Workbook workbook = new XSSFWorkbook();
// 3. 创建样式
Map<String, CellStyle> styles = createStyles(workbook);
try {
// 4. 创建营业额报表Sheet
Sheet turnoverSheet = workbook.createSheet("营业额报表");
createTurnoverSheet(turnoverSheet, turnoverData, styles);
// 5. 设置响应头
String fileName = URLEncoder.encode(
String.format("营业额报表_%s至%s.xlsx", start, end),
"UTF-8"
);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition",
"attachment;filename=" + fileName);
// 6. 输出到响应流
workbook.write(response.getOutputStream());
workbook.close();
log.info("导出营业额报表成功,时间范围:{} 至 {}", start, end);
} catch (Exception e) {
log.error("导出营业额报表失败", e);
throw new RuntimeException("导出失败:" + e.getMessage());
} finally {
workbook.close();
}
}
/**
* 查询营业额数据
*/
private List<TurnoverReportVO> getTurnoverData(LocalDate start, LocalDate end) {
List<TurnoverReportVO> list = new ArrayList<>();
LocalDate currentDate = start;
while (!currentDate.isAfter(end)) {
// 查询当日数据
BigDecimal turnover = orderMapper.getTurnoverByDate(currentDate);
Integer orderCount = orderMapper.getOrderCountByDate(currentDate);
Integer validOrderCount = orderMapper.getValidOrderCountByDate(currentDate);
// 计算完成率
String completionRate = orderCount == 0 ? "0%" :
String.format("%.2f%%", (validOrderCount * 100.0 / orderCount));
TurnoverReportVO vo = TurnoverReportVO.builder()
.date(currentDate)
.turnover(turnover != null ? turnover : BigDecimal.ZERO)
.orderCount(orderCount != null ? orderCount : 0)
.validOrderCount(validOrderCount != null ? validOrderCount : 0)
.completionRate(completionRate)
.build();
list.add(vo);
currentDate = currentDate.plusDays(1);
}
return list;
}
/**
* 创建样式集合
*/
private Map<String, CellStyle> createStyles(Workbook workbook) {
Map<String, CellStyle> styles = new HashMap<>();
// 标题样式(加粗、居中、背景色)
CellStyle titleStyle = workbook.createCellStyle();
Font titleFont = workbook.createFont();
titleFont.setBold(true);
titleFont.setFontHeightInPoints((short) 14);
titleStyle.setFont(titleFont);
titleStyle.setAlignment(HorizontalAlignment.CENTER);
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
titleStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
titleStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
styles.put("title", titleStyle);
// 表头样式(加粗、居中、边框)
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
headerStyle.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
styles.put("header", headerStyle);
// 数据样式(右对齐、边框)
CellStyle dataStyle = workbook.createCellStyle();
dataStyle.setAlignment(HorizontalAlignment.RIGHT);
dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
dataStyle.setBorderTop(BorderStyle.THIN);
dataStyle.setBorderBottom(BorderStyle.THIN);
dataStyle.setBorderLeft(BorderStyle.THIN);
dataStyle.setBorderRight(BorderStyle.THIN);
styles.put("data", dataStyle);
// 货币样式
CellStyle currencyStyle = workbook.createCellStyle();
currencyStyle.cloneStyleFrom(dataStyle);
DataFormat format = workbook.createDataFormat();
currencyStyle.setDataFormat(format.getFormat("#,##0.00"));
styles.put("currency", currencyStyle);
// 百分比样式
CellStyle percentStyle = workbook.createCellStyle();
percentStyle.cloneStyleFrom(dataStyle);
percentStyle.setDataFormat(format.getFormat("0.00%"));
styles.put("percent", percentStyle);
return styles;
}
/**
* 创建营业额报表Sheet
*/
private void createTurnoverSheet(Sheet sheet, List<TurnoverReportVO> data,
Map<String, CellStyle> styles) {
// 1. 创建标题行
Row titleRow = sheet.createRow(0);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue("苍穹外卖营业额统计报表");
titleCell.setCellStyle(styles.get("title"));
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 4));
titleRow.setHeightInPoints(30);
// 2. 创建表头
String[] headers = {"日期", "营业额(元)", "订单总数", "有效订单数", "完成率"};
Row headerRow = sheet.createRow(1);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(styles.get("header"));
// 设置列宽
sheet.setColumnWidth(i, 4500);
}
// 3. 填充数据
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
int rowNum = 2;
for (TurnoverReportVO vo : data) {
Row row = sheet.createRow(rowNum);
// 日期
Cell dateCell = row.createCell(0);
dateCell.setCellValue(vo.getDate().format(dateFormatter));
dateCell.setCellStyle(styles.get("data"));
// 营业额
Cell turnoverCell = row.createCell(1);
turnoverCell.setCellValue(vo.getTurnover().doubleValue());
turnoverCell.setCellStyle(styles.get("currency"));
// 订单总数
Cell orderCountCell = row.createCell(2);
orderCountCell.setCellValue(vo.getOrderCount());
orderCountCell.setCellStyle(styles.get("data"));
// 有效订单数
Cell validCountCell = row.createCell(3);
validCountCell.setCellValue(vo.getValidOrderCount());
validCountCell.setCellStyle(styles.get("data"));
// 完成率
Cell rateCell = row.createCell(4);
rateCell.setCellValue(vo.getCompletionRate());
rateCell.setCellStyle(styles.get("data"));
rowNum++;
}
// 4. 添加统计行
Row summaryRow = sheet.createRow(rowNum);
Cell summaryLabel = summaryRow.createCell(0);
summaryLabel.setCellValue("合计");
summaryLabel.setCellStyle(styles.get("header"));
// 计算总营业额
BigDecimal totalTurnover = data.stream()
.map(TurnoverReportVO::getTurnover)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Cell totalTurnoverCell = summaryRow.createCell(1);
totalTurnoverCell.setCellValue(totalTurnover.doubleValue());
totalTurnoverCell.setCellStyle(styles.get("currency"));
// 计算总订单数
int totalOrders = data.stream()
.mapToInt(TurnoverReportVO::getOrderCount)
.sum();
Cell totalOrdersCell = summaryRow.createCell(2);
totalOrdersCell.setCellValue(totalOrders);
totalOrdersCell.setCellStyle(styles.get("header"));
// 计算总有效订单
int totalValidOrders = data.stream()
.mapToInt(TurnoverReportVO::getValidOrderCount)
.sum();
Cell totalValidCell = summaryRow.createCell(3);
totalValidCell.setCellValue(totalValidOrders);
totalValidCell.setCellStyle(styles.get("header"));
// 整体完成率
String totalRate = totalOrders == 0 ? "0%" :
String.format("%.2f%%", (totalValidOrders * 100.0 / totalOrders));
Cell totalRateCell = summaryRow.createCell(4);
totalRateCell.setCellValue(totalRate);
totalRateCell.setCellStyle(styles.get("header"));
}
}
第3步:导出菜品销量排行
java
java
/**
* 导出菜品销量排行
*/
public void exportTopDishes(LocalDate start, LocalDate end,
Integer topN, HttpServletResponse response) throws IOException {
// 查询Top N菜品
List<DishSalesVO> dishList = dishMapper.getTopDishes(start, end, topN);
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("热销菜品排行");
// 创建样式
Map<String, CellStyle> styles = createStyles(workbook);
// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {"排名", "菜品名称", "分类", "销量", "销售额(元)"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(styles.get("header"));
sheet.setColumnWidth(i, i == 1 ? 6000 : 4000);
}
// 填充数据
int rank = 1;
for (int i = 0; i < dishList.size(); i++) {
DishSalesVO vo = dishList.get(i);
Row row = sheet.createRow(i + 1);
// 排名
Cell rankCell = row.createCell(0);
rankCell.setCellValue(rank++);
rankCell.setCellStyle(styles.get("data"));
// 菜品名称
Cell nameCell = row.createCell(1);
nameCell.setCellValue(vo.getDishName());
nameCell.setCellStyle(styles.get("data"));
// 分类
Cell categoryCell = row.createCell(2);
categoryCell.setCellValue(vo.getCategory());
categoryCell.setCellStyle(styles.get("data"));
// 销量
Cell salesCell = row.createCell(3);
salesCell.setCellValue(vo.getSalesCount());
salesCell.setCellStyle(styles.get("data"));
// 销售额
Cell amountCell = row.createCell(4);
amountCell.setCellValue(vo.getAmount().doubleValue());
amountCell.setCellStyle(styles.get("currency"));
}
// 响应输出
String fileName = URLEncoder.encode(
String.format("热销菜品排行_%s至%s.xlsx", start, end), "UTF-8");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-disposition", "attachment;filename=" + fileName);
workbook.write(response.getOutputStream());
workbook.close();
}
第4步:Mapper层SQL实现
java
java
@Mapper
public interface OrderMapper {
/**
* 查询指定日期营业额
*/
@Select("SELECT COALESCE(SUM(amount), 0) FROM orders " +
"WHERE DATE(create_time) = #{date} " +
"AND status = 5")
BigDecimal getTurnoverByDate(LocalDate date);
/**
* 查询指定日期订单总数
*/
@Select("SELECT COUNT(*) FROM orders " +
"WHERE DATE(create_time) = #{date}")
Integer getOrderCountByDate(LocalDate date);
/**
* 查询指定日期有效订单数
*/
@Select("SELECT COUNT(*) FROM orders " +
"WHERE DATE(create_time) = #{date} " +
"AND status IN (5, 6)")
Integer getValidOrderCountByDate(LocalDate date);
}
@Mapper
public interface DishMapper {
/**
* 查询Top N热销菜品
*/
@Select("SELECT d.name as dish_name, " +
"SUM(od.number) as sales_count, " +
"SUM(od.amount) as amount, " +
"c.name as category " +
"FROM order_detail od " +
"LEFT JOIN dish d ON od.dish_id = d.id " +
"LEFT JOIN category c ON d.category_id = c.id " +
"LEFT JOIN orders o ON od.order_id = o.id " +
"WHERE DATE(o.create_time) BETWEEN #{start} AND #{end} " +
"AND o.status = 5 " +
"GROUP BY d.id, d.name, c.name " +
"ORDER BY sales_count DESC " +
"LIMIT #{topN}")
List<DishSalesVO> getTopDishes(LocalDate start, LocalDate end, Integer topN);
}
第5步:Controller层接口
java
java
@RestController
@RequestMapping("/admin/report")
@Api(tags = "报表管理")
@Slf4j
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/export/turnover")
@ApiOperation("导出营业额报表")
public void exportTurnoverReport(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate start,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,
HttpServletResponse response) {
try {
reportService.exportTurnoverReport(start, end, response);
} catch (IOException e) {
log.error("导出失败", e);
throw new RuntimeException("导出失败");
}
}
@GetMapping("/export/top-dishes")
@ApiOperation("导出热销菜品排行")
public void exportTopDishes(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate start,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,
@RequestParam(defaultValue = "10") Integer topN,
HttpServletResponse response) {
try {
reportService.exportTopDishes(start, end, topN, response);
} catch (IOException e) {
log.error("导出失败", e);
throw new RuntimeException("导出失败");
}
}
}
四、POI核心技术点
1. Workbook的选择
java
java
// HSSFWorkbook: 处理.xls格式(Excel 97-2003),最大行数65536
Workbook hssfWorkbook = new HSSFWorkbook();
// XSSFWorkbook: 处理.xlsx格式(Excel 2007+),最大行数1048576
Workbook xssfWorkbook = new XSSFWorkbook();
// SXSSFWorkbook: 处理大数据量,内存友好
Workbook sxssfWorkbook = new SXSSFWorkbook(100); // 内存中保留100行
2. 单元格样式详解
java
java
// 创建字体
Font font = workbook.createFont();
font.setFontName("微软雅黑");
font.setFontHeightInPoints((short) 12);
font.setBold(true);
font.setColor(IndexedColors.RED.getIndex());
font.setItalic(true);
font.setUnderline(Font.U_SINGLE);
// 创建样式
CellStyle style = workbook.createCellStyle();
style.setFont(font);
style.setAlignment(HorizontalAlignment.CENTER); // 水平居中
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中
style.setBorderTop(BorderStyle.THIN); // 上边框
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
// 设置数据格式
DataFormat format = workbook.createDataFormat();
style.setDataFormat(format.getFormat("yyyy-mm-dd")); // 日期格式
style.setDataFormat(format.getFormat("#,##0.00")); // 货币格式
style.setDataFormat(format.getFormat("0.00%")); // 百分比格式
3. 合并单元格
java
java
// 合并行:起始行,结束行,起始列,结束列
CellRangeAddress region = new CellRangeAddress(0, 0, 0, 4);
sheet.addMergedRegion(region);
// 判断单元格是否合并
boolean isMerged = sheet.isMergedRegion(rowNum, colNum);
4. 公式计算
java
java
// 添加Excel公式
Cell formulaCell = row.createCell(5);
formulaCell.setCellFormula("SUM(B2:B10)");
// 求和公式
formulaCell.setCellFormula("SUMIF(A2:A10, \"已完成\", C2:C10)");
// 平均值
formulaCell.setCellFormula("AVERAGE(B2:B10)");
// 注意:需要设置公式后刷新
workbook.setForceFormulaRecalculation(true);
五、难点与解决方案
难点1:大数据量导出内存溢出
java
java
@Service
public class LargeDataExportService {
/**
* 使用SXSSFWorkbook解决大数据量问题
*/
public void exportLargeData(HttpServletResponse response) throws IOException {
// SXSSFWorkbook: 内存中只保留100行,其余写入磁盘
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
Sheet sheet = workbook.createSheet("大数据报表");
// 分页查询数据
int pageSize = 2000;
int pageNum = 1;
int rowNum = 0;
while (true) {
// 分页查询
List<DataVO> pageData = queryData(pageNum, pageSize);
if (pageData.isEmpty()) {
break;
}
// 写入数据
for (DataVO data : pageData) {
Row row = sheet.createRow(rowNum++);
// 填充数据...
}
pageNum++;
// 定期清理临时文件
if (pageNum % 10 == 0) {
workbook.dispose();
}
}
// 响应输出
workbook.write(response.getOutputStream());
workbook.dispose(); // 清理临时文件
workbook.close();
}
}
难点2:复杂表头(多级表头)
java
java
public void createComplexHeader(Sheet sheet) {
// 创建第一级表头
Row row1 = sheet.createRow(0);
row1.createCell(0).setCellValue("基本信息");
row1.createCell(2).setCellValue("订单信息");
// 合并单元格
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 1));
sheet.addMergedRegion(new CellRangeAddress(0, 0, 2, 4));
// 创建第二级表头
Row row2 = sheet.createRow(1);
String[] headers = {"姓名", "年龄", "订单号", "金额", "状态"};
for (int i = 0; i < headers.length; i++) {
row2.createCell(i).setCellValue(headers[i]);
}
}
难点3:单元格样式性能优化
java
java
// 错误做法:每个单元格都创建新样式(内存浪费)
Cell cell = row.createCell(0);
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
cell.setCellStyle(style);
// 正确做法:复用样式
public class StyleCache {
private static Map<String, CellStyle> styleCache = new HashMap<>();
public static CellStyle getStyle(Workbook workbook, String styleName) {
return styleCache.computeIfAbsent(styleName, k -> {
CellStyle style = workbook.createCellStyle();
// 配置样式...
return style;
});
}
}
// 使用
CellStyle style = StyleCache.getStyle(workbook, "header");
cell.setCellStyle(style);
难点4:导出进度监控
java
java
@Component
public class ExportProgressMonitor {
private final Map<String, ExportTask> taskMap = new ConcurrentHashMap<>();
@Data
public static class ExportTask {
private String taskId;
private int totalCount;
private int processedCount;
private String status; // RUNNING, SUCCESS, FAILED
private String filePath;
}
public String startExport(ExportParams params) {
String taskId = UUID.randomUUID().toString();
ExportTask task = new ExportTask();
task.setTaskId(taskId);
task.setStatus("RUNNING");
taskMap.put(taskId, task);
// 异步执行导出
CompletableFuture.runAsync(() -> {
try {
String filePath = doExport(params, task);
task.setFilePath(filePath);
task.setStatus("SUCCESS");
} catch (Exception e) {
task.setStatus("FAILED");
}
});
return taskId;
}
public ExportTask getProgress(String taskId) {
return taskMap.get(taskId);
}
}
难点5:并发导出限流
java
java
@Component
public class ExportRateLimiter {
private final Semaphore semaphore = new Semaphore(3); // 最多3个并发
public void executeWithLimit(Runnable exportTask) {
if (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
try {
exportTask.run();
} finally {
semaphore.release();
}
}
}
// 使用
@Autowired
private ExportRateLimiter rateLimiter;
public void export(HttpServletResponse response) {
rateLimiter.executeWithLimit(() -> {
doExport(response);
});
}
六、最佳实践总结
1. 性能优化
-
大数据量使用
SXSSFWorkbook -
复用
CellStyle对象 -
分页查询,分批写入
-
使用
try-with-resources确保资源释放
2. 内存管理
java
java
// 及时清理临时文件
SXSSFWorkbook workbook = new SXSSFWorkbook(100);
try {
// 导出操作
} finally {
workbook.dispose(); // 清理磁盘临时文件
workbook.close(); // 关闭工作簿
}
3. 异常处理
java
java
try {
// 导出逻辑
} catch (IOException e) {
log.error("IO异常", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write("导出失败:" + e.getMessage());
} catch (Exception e) {
log.error("导出异常", e);
throw new BusinessException("导出失败");
}
4. 编码规范
-
文件名使用
URLEncoder.encode()防止中文乱码 -
设置正确的ContentType
-
添加导出日志记录
-
权限控制(只有管理员可导出)
通过以上实现,苍穹外卖系统可以稳定、高效地处理各种Excel导出需求,既保证了功能完整性,又解决了POI常见的内存问题。
值得注意的是,在实际的项目中,我们不需要在后端自己创建表格,设置字体,大小,标题,分行等等功能,我们通常是实现有一个模板文件,通过IO流把数据写入这个模板文件中。
补充:
HttpServletResponse 详解
HttpServletResponse 是 Java Web 开发中的一个核心接口,用于封装服务器对客户端的响应。简单来说,它就是服务器给浏览器(或其他客户端)回复信息的"信使"。
一、核心作用
1. 设置响应状态码
告诉客户端请求处理的结果状态
java
java
// 成功
response.setStatus(HttpServletResponse.SC_OK); // 200
// 资源未找到
response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404
// 服务器错误
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500
// 重定向
response.sendRedirect("/login.html"); // 自动设置302状态码
2. 设置响应头(Header)
告诉浏览器如何处理返回的内容
java
java
// 告诉浏览器返回的是图片
response.setHeader("Content-Type", "image/jpeg");
// 告诉浏览器下载文件(苍穹外卖导出Excel场景)
response.setHeader("Content-Disposition", "attachment;filename=report.xlsx");
// 禁用缓存(验证码场景)
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
3. 设置响应内容类型和编码
告诉浏览器内容的格式和字符集
java
java
// 设置返回HTML页面
response.setContentType("text/html;charset=UTF-8");
// 设置返回JSON数据
response.setContentType("application/json;charset=UTF-8");
// 设置返回Excel文件(苍穹外卖常用)
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
4. 获取输出流
向客户端写入实际的数据内容
java
java
// 字符流 - 用于返回文本(HTML、JSON等)
PrintWriter writer = response.getWriter();
writer.write("<html><body>Hello</body></html>");
// 字节流 - 用于返回二进制数据(图片、Excel、PDF等)
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(excelData);
二、在苍穹外卖导出Excel中的完整应用
java
java
@GetMapping("/export/turnover")
public void exportTurnoverReport(HttpServletResponse response) {
try {
// 1. 设置响应类型(告诉浏览器返回的是Excel文件)
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// 2. 设置字符编码
response.setCharacterEncoding("UTF-8");
// 3. 设置响应头(告诉浏览器下载该文件,而不是直接打开)
String fileName = URLEncoder.encode("营业额报表.xlsx", "UTF-8");
response.setHeader("Content-Disposition",
"attachment;filename=" + fileName);
// 4. 禁用缓存(防止浏览器缓存文件)
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
// 5. 获取输出流并写入Excel数据
Workbook workbook = new XSSFWorkbook();
// ... 创建Excel内容 ...
// 将Excel写入响应流
workbook.write(response.getOutputStream());
workbook.close();
} catch (IOException e) {
// 异常处理
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
三、常见使用场景
场景1:返回JSON数据(API接口)
java
java
@GetMapping("/user/{id}")
public void getUser(@PathVariable Long id, HttpServletResponse response) {
User user = userService.getById(id);
response.setContentType("application/json;charset=UTF-8");
try {
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(user));
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
// 更优雅的方式:使用@ResponseBody
@GetMapping("/user/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {
return userService.getById(id); // Spring自动处理响应
}
场景2:文件下载
java
java
@GetMapping("/download/image")
public void downloadImage(@RequestParam String path, HttpServletResponse response) {
try {
// 读取文件
File file = new File(path);
// 设置响应头
response.setContentType("image/jpeg");
response.setContentLengthLong(file.length());
response.setHeader("Content-Disposition",
"attachment;filename=" + file.getName());
// 写入输出流
FileInputStream fis = new FileInputStream(file);
ServletOutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
fis.close();
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
场景3:生成验证码图片
java
java
@GetMapping("/captcha")
public void getCaptcha(HttpServletResponse response) {
// 禁用缓存
response.setHeader("Cache-Control", "no-cache");
response.setContentType("image/jpeg");
// 生成验证码图片
BufferedImage image = new BufferedImage(100, 40, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// ... 绘制验证码 ...
try {
// 输出图片
ImageIO.write(image, "jpg", response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
场景4:重定向
java
java
@GetMapping("/old-url")
public void redirectOldUrl(HttpServletResponse response) {
try {
// 重定向到新地址
response.sendRedirect("/new-url");
// 或使用状态码+Header方式
// response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
// response.setHeader("Location", "/new-url");
} catch (IOException e) {
e.printStackTrace();
}
}
四、核心方法总结
| 方法分类 | 方法名 | 作用 |
|---|---|---|
| 状态码 | setStatus(int sc) |
设置HTTP状态码 |
sendError(int sc) |
发送错误状态码 | |
sendRedirect(String location) |
重定向(自动设置302) | |
| 响应头 | setHeader(String name, String value) |
设置响应头 |
addHeader(String name, String value) |
添加响应头(可重复) | |
setContentType(String type) |
设置内容类型 | |
setCharacterEncoding(String charset) |
设置字符编码 | |
setContentLength(int len) |
设置内容长度 | |
setDateHeader(String name, long date) |
设置日期类型响应头 | |
| 输出流 | getWriter() |
获取字符输出流(返回文本) |
getOutputStream() |
获取字节输出流(返回二进制) | |
| Cookie | addCookie(Cookie cookie) |
添加Cookie |
| 其他 | encodeURL(String url) |
URL编码(会话追踪) |
containsHeader(String name) |
是否包含指定响应头 |
五、注意事项
1. getWriter() 和 getOutputStream() 互斥
java
java
// 错误:不能同时使用
response.getWriter();
response.getOutputStream(); // 抛出IllegalStateException
// 正确:只能选择其一
// 返回文本用getWriter()
PrintWriter writer = response.getWriter();
// 返回二进制文件用getOutputStream()
ServletOutputStream os = response.getOutputStream();
2. 必须在提交响应前设置Header
java
java
// 正确顺序
response.setContentType("application/json");
response.setHeader("X-Custom", "value");
response.getWriter().write("data"); // 提交响应
// 错误:提交后设置无效
response.getWriter().write("data");
response.setHeader("X-Custom", "value"); // 无效
3. 文件下载时文件名中文乱码处理
java
java
// 不同浏览器兼容处理
String fileName = "报表.xlsx";
String userAgent = request.getHeader("User-Agent");
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
// IE浏览器
fileName = URLEncoder.encode(fileName, "UTF-8");
} else {
// 其他浏览器
fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
}
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
4. 大文件下载注意内存
java
java
// 错误:一次性读取到内存
byte[] data = Files.readAllBytes(Paths.get("large.zip"));
response.getOutputStream().write(data); // 内存溢出风险
// 正确:使用缓冲流分批写入
try (InputStream is = new FileInputStream("large.zip");
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
}
六、Spring MVC 中的简化
Spring MVC 提供了更简洁的方式,通常不需要直接操作 HttpServletResponse:
java
java
// 1. 返回JSON - 使用@ResponseBody
@GetMapping("/user")
@ResponseBody
public User getUser() {
return userService.getUser();
}
// 2. 返回文件 - 使用ResponseEntity
@GetMapping("/download")
public ResponseEntity<byte[]> download() {
byte[] data = generateExcel();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment;filename=report.xlsx")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(data);
}
// 3. 返回视图 - 自动渲染 @GetMapping("/page") public String page() { return "index"; // 返回视图名称 }
七、总结
HttpServletResponse 是服务器向客户端发送响应的核心对象,主要功能:
-
告诉浏览器响应的状态(成功、失败、重定向等)
-
告诉浏览器返回内容的类型(HTML、JSON、Excel、图片等)
-
告诉浏览器如何处理返回内容(下载、直接显示、缓存等)
-
实际传输数据内容(通过输出流)
在苍穹外卖项目中,它主要用于:
-
导出Excel报表(设置下载响应头,输出Excel文件)
-
返回API数据(设置JSON格式,输出数据)
-
文件上传后的预览(返回图片文件)
-
登录验证码(输出图片流)
结语:
如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!