将Tomcat Servlet CSV表格展示代码迁移至Spring Boot框架
迁移Servlet CSV表格展示到Spring Boot的实现方案
一、核心重构思路:从单体Servlet到分层架构
原Servlet将请求处理、业务逻辑、视图渲染全耦合在一个类中,Spring Boot推荐分层拆分,职责更清晰:
- Controller:处理HTTP请求,调用业务层,传递数据到视图
- Service:封装文件读取、CSV解析等业务逻辑
- 视图层:用Thymeleaf替换硬编码HTML,彻底分离Java代码与视图
二、分步实现步骤
1. 初始化Spring Boot项目
- 用Spring Initializr选择依赖:
Spring Web、Thymeleaf、OpenCSV(保持和原项目一致的CSV解析库) - 将原项目的
css、js资源放到src/main/resources/static目录,printers文件夹(包含csv文件)放到src/main/resources下
2. 替换Servlet为Spring MVC Controller
把原Servlet的doGet逻辑拆分到Controller中,专注请求分发与数据传递:
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class TonerLevelController { private final PrinterService printerService; // 构造注入Service public TonerLevelController(PrinterService printerService) { this.printerService = printerService; } @GetMapping({"/", "/tonerLevels"}) public String showTonerLevels(Model model) { // 传递CSV解析后的数据与报告文件列表到视图 model.addAttribute("printerData", printerService.getPrinterData()); model.addAttribute("reportFiles", printerService.getReportFiles()); // 返回视图名称(对应templates目录下的html文件) return "toner-levels"; } }
3. 提取业务逻辑到Service层
将原Servlet里的文件列表获取、CSV读取解析逻辑封装到Service,专注业务实现:
import com.opencsv.CSVReader; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import java.io.File; import java.io.FileReader; import java.time.LocalDate; import java.util.*; import java.util.regex.Pattern; @Service public class PrinterService { private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$"); public List<List<String>> getPrinterData() { List<List<String>> data = new ArrayList<>(); try { // 读取resources目录下的printers/printers.csv ClassPathResource resource = new ClassPathResource("printers/printers.csv"); CSVReader csvReader = new CSVReader(new FileReader(resource.getFile())); String[] nextRecord; while ((nextRecord = csvReader.readNext()) != null) { data.add(Arrays.asList(nextRecord)); } } catch (Exception e) { e.printStackTrace(); } return data; } public List<Map<String, String>> getReportFiles() { List<Map<String, String>> files = new ArrayList<>(); try { // 读取reports目录下的文件 ClassPathResource resource = new ClassPathResource("printers/reports"); File dir = resource.getFile(); File[] fileList = dir.listFiles(); if (fileList != null) { Arrays.sort(fileList, Collections.reverseOrder()); for (File file : fileList) { String fileName = file.getName(); LocalDate date = LocalDate.parse(fileName.substring(0, fileName.length() - 4)); String month = date.getMonth().minus(1).toString(); String year = String.valueOf(date.getYear()); Map<String, String> fileInfo = new HashMap<>(); fileInfo.put("name", fileName); fileInfo.put("displayName", month + " " + year); files.add(fileInfo); } } } catch (Exception e) { e.printStackTrace(); } return files; } // 封装单元格格式化逻辑,视图层直接调用 public String formatCell(String cell) { if (IP_PATTERN.matcher(cell).matches()) { return "<a href=\"http://" + cell + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + cell + "</a>"; } else if (cell.matches("-?(0|[1-9]\\d*)")) { return String.valueOf(Integer.parseInt(cell)); } return cell; } // 封装表头格式化逻辑 public String formatHeader(String header) { List<String> percentageHeaders = Arrays.asList("Black", "Yellow", "Magenta", "Cyan", "K1", "K2"); if (percentageHeaders.contains(header)) { return header + " %"; } return header; } }
4. 用Thymeleaf实现视图分离
将原Servlet中硬编码的HTML迁移到src/main/resources/templates/toner-levels.html,用Thymeleaf渲染动态数据:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>KHS Printers</title> <link rel="stylesheet" th:href="@{/css/bootstrap.css}"> <link rel="stylesheet" th:href="@{/css/style.css}"> </head> <body onload="sortTable()"> <!-- 导航栏 --> <nav data-bs-theme="dark" class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container-fluid"> <a class="navbar-brand">KHS Printers</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav nav-pills me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link active" aria-current="page" th:href="@{/tonerLevels}">Toner Levels</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/MonoPrints}">Mono Prints</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/MonoCopies}">Mono Copies</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/ColourPrints}">Colour Prints</a> </li> <li class="nav-item"> <a class="nav-link" th:href="@{/ColourCopies}">Colour Copies</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Monthly Reports</a> <ul class="dropdown-menu"> <form action="Monthly" method="post"> <li th:each="file : ${reportFiles}"> <button type="submit" class="dropdown-item" name="report" th:value="${file.name}" th:text="${file.displayName}"></button> </li> </form> </ul> </li> </ul> </div> </div> </nav> <!-- 数据表格 --> <table class="table table-dark table-hover"> <thead> <tr> <th th:each="header : ${printerData[0]}" th:text="${@printerService.formatHeader(header)}"></th> </tr> </thead> <tbody class="table-group-divider"> <tr th:each="row, rowStat : ${printerData}" th:if="${rowStat.index > 0}"> <td th:each="cell : ${row}" th:utext="${@printerService.formatCell(cell)}"></td> </tr> </tbody> </table> <script th:src="@{/js/bootstrap.bundle.js}"></script> <script th:src="@{/js/script.js}"></script> </body> </html>
5. 文件路径适配Spring Boot
原Servlet依赖Tomcat的catalina.base路径,Spring Boot中直接用ClassPathResource读取resources目录下的文件,脱离容器依赖,更通用。
6. 其他功能迁移
原Servlet中的其他导航项(如MonoPrints、MonoCopies等),可按照相同模式新建对应的Controller和Service,复用CSV读取逻辑。
三、额外优化建议
- 异常处理:用Spring的
@ControllerAdvice实现全局异常处理,替换e.printStackTrace() - 缓存优化:给
getPrinterData()添加@Cacheable注解,CSV文件不频繁更新时提升性能 - 实体类映射:将CSV行映射为
Printer实体类,替代List<String>,提升代码可读性 - 配置化:将文件路径、CSV分隔符等配置到
application.properties,方便后续修改
内容的提问来源于stack exchange,提问作者Roy503
相关产品推荐
相关产品推荐

