You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 17:00:55