学校教务系统老崩溃 教师用JSP技术重构学生选课成绩管理平台稳定运行零故障
一、崩溃前的那个深夜
2024年9月,开学第一周,李铭副教授坐在办公室里盯着屏幕上弹出的那个熟悉的错误页面发呆——”Java.lang.NullPointerException at line 256”。这是这个学期教务系统崩溃的第17次了。
作为计算机学院负责教务管理的教师,李铭眼睁睁看着学生们在选课系统前排队,系统却突然弹出一个白页,连错误原因都说不清楚。更糟糕的是,系统用的是十年前遗留的老代码,JSP页面里夹杂着大量硬编码的SQL语句,整个项目就像一栋年久失修的老楼,随时可能塌方。
“这样下去不行。”李铭对自己说,”要么修,要么重写。”
他翻出了旧系统的代码结构,粗略统计了一下:32个JSP页面,47个JavaBean,数据库有8张核心表,但代码量超过15000行,而且其中至少有40%是重复的拷贝粘贴代码。最致命的是,整个系统没有任何测试,部署全靠经验,出问题了就猜。
李铭决定自己来重构。
二、问题出在哪儿
在动手写代码之前,李铭花了一周时间做问题分析。他写了一个简单的日志监控脚本,把所有崩溃时的请求参数和堆栈信息都记录下来。
// 旧系统错误日志分析工具
public class ErrorAnalyzer {
public static void analyzeLog(String logFile) {
// 读取日志并分类错误
Map<String, Integer> errorTypes = new HashMap<>();
// 最常见的错误分布:
// NullPointerException - 34%(空指针,主要是未初始化对象)
// SQLException - 28%(SQL注入或连接泄露)
// ClassCastException - 18%(类型转换问题)
// OutOfMemoryError - 12%(内存泄漏)
// 其他 - 8%
}
}
通过数据分析,他发现主要问题集中在三个方面:
第一,连接池管理混乱。 旧系统的数据库连接完全靠手动获取和关闭,很多代码片段是这样的:
// 旧代码示例 - 典型的连接泄露
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM course WHERE id='" + courseId + "'");
// ... 处理结果 ...
// 注意:没有finally块,如果前面抛异常,连接永远不释放
这种写法在选课高峰时段,几十秒内就能把数据库连接池耗尽,导致系统直接挂掉。
第二,JSP页面承担了太多业务逻辑。 很多JSP页面长达几百行,Java代码和HTML代码混在一起,可读性极差:
<%-- 旧系统选课页面片段 --%>
<%@ page import="java.sql.*" %>
<%
String studentId = request.getParameter("sid");
String courseId = request.getParameter("cid");
// 直接拼接SQL,存在注入风险
String sql = "INSERT INTO enrollment VALUES('" + studentId + "','" + courseId + "')";
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/school", "root", "123456");
Statement stmt = conn.createStatement();
stmt.executeUpdate(sql);
// 直接输出HTML
out.println("<html><body>选课成功</body></html>");
%>
第三,没有任何异常处理和事务控制。 选课操作涉及多张表的数据修改(学生表、课程表、选课表),但旧系统根本没有事务管理,一旦中途出错,数据就处于不一致状态。
三、设计方案
李铭参考了经典的分层架构模式,设计了一个清晰的技术方案:
技术栈选择:
- 后端:JSP + Servlet + JavaBean(MVC架构)
- 数据库:MySQL 8.0
- 连接池:Apache DBCP2
- 前端:Bootstrap 5 + jQuery
- 部署:Tomcat 9
架构设计:
┌─────────────────────────────────────────┐
│ 表现层 (JSP) │
│ login.jsp | courseList.jsp | grade.jsp │
├─────────────────────────────────────────┤
│ 控制层 (Servlet) │
│ LoginServlet | CourseServlet | │
│ EnrollmentServlet | GradeServlet │
├─────────────────────────────────────────┤
│ 业务层 (Service) │
│ CourseService | EnrollmentService │
│ GradeService | UserService │
├─────────────────────────────────────────┤
│ 数据层 (DAO) │
│ CourseDAO | EnrollmentDAO | GradeDAO │
├─────────────────────────────────────────┤
│ 数据库 (MySQL) │
│ students | courses | enrollments | │
│ grades | teachers │
└─────────────────────────────────────────┘
李铭特别强调了数据访问层的重要性。他设计了一个通用的数据库工具类,用连接池替代手动的连接管理:
package com.edu.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import org.apache.commons.dbcp2.BasicDataSource;
/**
* 数据库连接工具类 - 使用连接池管理数据库连接
* 这是新系统的核心基础设施
*/
public class DBUtil {
// 连接池配置 - 从配置文件读取
private static BasicDataSource dataSource;
static {
dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/school_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8");
dataSource.setUsername("school_user");
dataSource.setPassword("sCh00l#2024!");
// 连接池配置 - 关键参数
dataSource.setInitialSize(10); // 初始连接数
dataSource.setMaxTotal(50); // 最大连接数
dataSource.setMaxIdle(20); // 最大空闲连接
dataSource.setMinIdle(5); // 最小空闲连接
dataSource.setMaxWaitMillis(30000); // 最大等待时间30秒
dataSource.setRemoveAbandoned(true); // 移除泄露连接
dataSource.setRemoveAbandonedTimeout(300); // 300秒后回收
dataSource.setLogAbandoned(true); // 记录泄露日志
}
/**
* 获取数据库连接
* @return Connection对象
* @throws SQLException 获取连接失败时抛出
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 关闭数据库资源 - 使用try-with-resources的标准写法
* @param conn 连接对象
* @param pstmt 预处理语句
* @param rs 结果集
*/
public static void closeResources(Connection conn, PreparedStatement pstmt, ResultSet rs) {
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (pstmt != null) {
try { pstmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
/**
* 获取连接池信息 - 用于监控和调试
*/
public static void printPoolStatus() {
System.out.println("=== 数据库连接池状态 ===");
System.out.println("当前活跃连接数: " + dataSource.getNumActive());
System.out.println("当前空闲连接数: " + dataSource.getNumIdle());
System.out.println("总创建连接数: " + dataSource.getTotalConnections());
System.out.println("====================");
}
}
这个工具类是整个系统的基石。通过连接池的配置,系统可以在选课高峰时动态管理数据库连接,避免旧系统那种”连接用完了就崩”的问题。
四、核心模块实现
4.1 学生选课模块
选课功能是系统最核心、也是最容易出问题的模块。李铭在设计时特别注重事务控制和并发处理:
package com.edu.service;
import com.edu.dao.EnrollmentDAO;
import com.edu.dao.CourseDAO;
import com.edu.model.Course;
import com.edu.model.Enrollment;
import com.edu.util.DBUtil;
import com.edu.util.TransactionUtil;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
* 选课业务逻辑层
* 负责选课的核心业务流程:检查资格、选课、更新容量
*/
public class EnrollmentService {
private EnrollmentDAO enrollmentDAO = new EnrollmentDAO();
private CourseDAO courseDAO = new CourseDAO();
/**
* 学生选课 - 核心方法,包含完整的事务控制
* @param studentId 学生ID
* @param courseId 课程ID
* @return 选课结果消息
*/
public String enrollCourse(String studentId, String courseId) {
Connection conn = null;
try {
// 开启事务
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 1. 检查课程是否存在且有容量
Course course = courseDAO.findById(conn, courseId);
if (course == null) {
return "课程不存在";
}
if (course.getEnrolled() >= course.getMaxCapacity()) {
return "课程已满员";
}
// 2. 检查学生是否已选此课程
if (enrollmentDAO.exists(conn, studentId, courseId)) {
return "已选过该课程";
}
// 3. 检查时间冲突
if (hasTimeConflict(conn, studentId, courseId)) {
return "课程时间与其他已选课程冲突";
}
// 4. 插入选课记录
Enrollment enrollment = new Enrollment();
enrollment.setStudentId(studentId);
enrollment.setCourseId(courseId);
enrollmentDAO.insert(conn, enrollment);
// 5. 更新课程已选人数
courseDAO.updateCapacity(conn, courseId, course.getEnrolled() + 1);
// 提交事务
conn.commit();
return "选课成功";
} catch (Exception e) {
// 发生异常,回滚事务
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
return "选课失败:" + e.getMessage();
} finally {
// 关闭连接,恢复自动提交
if (conn != null) {
try {
conn.setAutoCommit(true);
DBUtil.closeResources(conn, null, null);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/**
* 检查时间冲突
*/
private boolean hasTimeConflict(Connection conn, String studentId, String courseId) throws SQLException {
String sql = "SELECT COUNT(*) FROM courses c " +
"JOIN enrollments e ON c.id = e.course_id " +
"WHERE e.student_id = ? AND c.id != ? " +
"AND c.time_slot = (SELECT time_slot FROM courses WHERE id = ?)";
java.sql.PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, studentId);
pstmt.setString(2, courseId);
pstmt.setString(3, courseId);
ResultSet rs = pstmt.executeQuery();
rs.next();
int count = rs.getInt(1);
DBUtil.closeResources(null, pstmt, rs);
return count > 0;
}
}
这段代码有几个关键设计点:
第一,事务控制。 选课操作涉及两表修改(插入选课记录、更新课程容量),必须保证要么都成功,要么都失败。通过setAutoCommit(false)开启手动事务,在成功时commit,失败时rollback。
第二,冲突检测。 选课前检查时间冲突,避免学生选到同一时间段的课程。这个检查也是事务的一部分。
第三,安全的SQL写法。 全部使用PreparedStatement,避免SQL注入风险。旧系统那种字符串拼接的SQL在新系统里消失了。
4.2 Servlet控制器
控制层使用Servlet,负责接收请求、调用业务逻辑、转发响应:
package com.edu.servlet;
import com.edu.service.EnrollmentService;
import com.edu.model.Enrollment;
import com.edu.model.Student;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* 选课管理Servlet
* 处理所有选课相关的HTTP请求
*/
@WebServlet("/enrollment")
public class EnrollmentServlet extends HttpServlet {
private EnrollmentService enrollmentService = new EnrollmentService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
if ("list".equals(action)) {
// 查看已选课程列表
listEnrollments(request, response);
} else if ("remove".equals(action)) {
// 退选课程
removeCourse(request, response);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String action = request.getParameter("action");
if ("enroll".equals(action)) {
// 选课
enrollCourse(request, response);
}
}
/**
* 选课操作
*/
private void enrollCourse(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String studentId = request.getParameter("studentId");
String courseId = request.getParameter("courseId");
// 参数校验
if (studentId == null || studentId.trim().isEmpty() ||
courseId == null || courseId.trim().isEmpty()) {
response.sendRedirect("error.jsp?msg=参数不能为空");
return;
}
// 调用业务逻辑
String result = enrollmentService.enrollCourse(studentId, courseId);
if (result.equals("选课成功")) {
response.sendRedirect("enrollment?action=list&studentId=" + studentId);
} else {
// 记录错误信息
request.setAttribute("errorMsg", result);
request.setAttribute("studentId", studentId);
request.getRequestDispatcher("enrollForm.jsp").forward(request, response);
}
}
/**
* 查看已选课程
*/
private void listEnrollments(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String studentId = request.getParameter("studentId");
// 获取已选课程列表
List<Enrollment> enrollments = enrollmentService.getEnrollmentsByStudent(studentId);
request.setAttribute("enrollments", enrollments);
request.setAttribute("studentId", studentId);
request.getRequestDispatcher("enrollmentList.jsp").forward(request, response);
}
/**
* 退选课程
*/
private void removeCourse(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String studentId = request.getParameter("studentId");
String courseId = request.getParameter("courseId");
enrollmentService.dropCourse(studentId, courseId);
response.sendRedirect("enrollment?action=list&studentId=" + studentId);
}
}
Servlet的设计遵循单一职责原则,每个方法只处理一种操作。通过doGet和doPost分别处理查询和修改请求,符合RESTful的设计理念。
4.3 成绩管理模块
成绩管理涉及敏感的分数数据,李铭在权限控制上做了特别的设计:
package com.edu.dao;
import com.edu.model.Grade;
import com.edu.util.DBUtil;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* 成绩数据访问对象
* 负责成绩的增删改查操作
*/
public class GradeDAO {
/**
* 录入成绩 - 教师使用
* @param conn 数据库连接(由事务管理)
* @param grade 成绩对象
* @return 操作结果
*/
public boolean insert(Connection conn, Grade grade) throws SQLException {
String sql = "INSERT INTO grades (student_id, course_id, score, grade_point, teacher_id, submit_time) " +
"VALUES (?, ?, ?, ?, ?, NOW())";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, grade.getStudentId());
pstmt.setString(2, grade.getCourseId());
pstmt.setDouble(3, grade.getScore());
pstmt.setDouble(4, grade.getGradePoint());
pstmt.setString(5, grade.getTeacherId());
int rows = pstmt.executeUpdate();
DBUtil.closeResources(null, pstmt, null);
return rows > 0;
}
/**
* 修改成绩 - 需要原始分数验证
*/
public boolean update(Connection conn, Grade grade) throws SQLException {
String sql = "UPDATE grades SET score=?, grade_point=?, modify_time=NOW() " +
"WHERE student_id=? AND course_id=? AND score=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setDouble(1, grade.getScore());
pstmt.setDouble(2, grade.getGradePoint());
pstmt.setString(3, grade.getStudentId());
pstmt.setString(4, grade.getCourseId());
pstmt.setDouble(5, grade.getOriginalScore()); // 原始分数验证
int rows = pstmt.executeUpdate();
DBUtil.closeResources(null, pstmt, null);
return rows > 0;
}
/**
* 查询学生成绩
*/
public List<Grade> findByStudent(Connection conn, String studentId) throws SQLException {
List<Grade> grades = new ArrayList<>();
String sql = "SELECT g.*, c.name as course_name, c.credits, t.name as teacher_name " +
"FROM grades g " +
"JOIN courses c ON g.course_id = c.id " +
"JOIN teachers t ON g.teacher_id = t.id " +
"WHERE g.student_id = ? " +
"ORDER BY c.course_no";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, studentId);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
Grade grade = new Grade();
grade.setStudentId(rs.getString("student_id"));
grade.setCourseId(rs.getString("course_id"));
grade.setCourseName(rs.getString("course_name"));
grade.setCredits(rs.getDouble("credits"));
grade.setScore(rs.getDouble("score"));
grade.setGradePoint(rs.getDouble("grade_point"));
grade.setTeacherName(rs.getString("teacher_name"));
grade.setSubmitTime(rs.getTimestamp("submit_time"));
grades.add(grade);
}
DBUtil.closeResources(null, pstmt, rs);
return grades;
}
}
成绩模块的特点是:修改成绩需要验证原始分数,防止误操作或恶意篡改。这是一个很好的细节设计。
4.4 统一异常处理
为了让系统更健壮,李铭实现了一个全局异常过滤器:
package com.edu.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.sql.SQLException;
/**
* 全局异常处理过滤器
* 捕获所有未处理的异常,防止系统崩溃时直接暴露错误页面
*/
public class ExceptionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化代码
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
try {
// 设置响应编码
resp.setCharacterEncoding("UTF-8");
req.setCharacterEncoding("UTF-8");
// 继续处理请求
chain.doFilter(request, response);
} catch (Exception e) {
// 记录异常日志
System.err.println("=== 系统异常 ===");
System.err.println("请求URL: " + req.getRequestURI());
System.err.println("异常类型: " + e.getClass().getName());
System.err.println("异常信息: " + e.getMessage());
e.printStackTrace();
// 转发到统一错误页面
request.setAttribute("error", e);
request.getRequestDispatcher("/error.jsp").forward(request, response);
}
}
@Override
public void destroy() {
// 清理资源
}
}
这个过滤器在web.xml中配置,会拦截所有请求,确保任何未处理的异常都不会导致系统直接崩溃。
五、安全与性能优化
李铭还做了几个重要的优化:
防SQL注入: 所有数据库操作都使用PreparedStatement,彻底杜绝了SQL注入风险。
密码加密: 使用BCrypt对密码进行加密存储:
package com.edu.util;
import org.mindrot.jbcrypt.BCrypt;
/**
* 密码加密工具类
*/
public class PasswordUtil {
/**
* 加密密码
*/
public static String hashPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt(12));
}
/**
* 验证密码
*/
public static boolean checkPassword(String password, String hashedPassword) {
return BCrypt.checkpw(password, hashedPassword);
}
}
会话管理: 使用Session来管理用户登录状态,设置合理的超时时间:
// 登录成功后设置session
HttpSession session = request.getSession();
session.setAttribute("user", user);
session.setMaxInactiveInterval(3600); // 1小时超时
缓存优化: 对频繁查询但变化不快的数据(如课程列表)使用内存缓存:
package com.edu.cache;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
/**
* 简单的内存缓存
* 用于存储课程列表等频繁查询的数据
*/
public class CourseCache {
private static ConcurrentHashMap<String, List> cache = new ConcurrentHashMap<>();
private static long lastUpdateTime = 0;
private static final long CACHE_EXPIRE_TIME = 5 * 60 * 1000; // 5分钟过期
public static List getCourseList() {
long now = System.currentTimeMillis();
// 缓存过期则重新加载
if (cache.isEmpty() || (now - lastUpdateTime) > CACHE_EXPIRE_TIME) {
cache.put("all", new CourseDAO().findAll());
lastUpdateTime = now;
}
return cache.get("all");
}
/**
* 清除缓存 - 当数据更新时调用
*/
public static void invalidate() {
cache.clear();
lastUpdateTime = 0;
}
}
六、部署与测试
系统开发完成后,李铭进行了严格的测试:
压力测试: 使用JMeter模拟1000个并发用户进行选课操作,系统稳定运行,没有崩溃:
压力测试结果:
- 并发用户数:1000
- 平均响应时间:120ms
- 最大响应时间:450ms
- 错误率:0%
- 吞吐量:833 requests/second
功能测试: 覆盖了选课、退课、成绩查询、成绩录入等所有功能场景,测试用例200多个,全部通过。
安全测试: 使用SQLmap扫描SQL注入漏洞,使用Burp Suite检测会话安全问题,没有发现高危漏洞。
部署到生产环境后,李铭做了30天的稳定运行观察。每天系统都会自动生成运行报告:
=== 2024年10月系统运行报告 ===
系统运行天数:30天
总请求数:1,250,000
成功请求:1,249,850
失败请求:150(0.012%)
平均响应时间:98ms
最大并发用户数:856
数据库连接池平均使用率:35%
内存使用率:稳定在45%左右
系统可用性:99.988%
零崩溃记录
==========================
七、心得体会
李铭后来在学校的教师技术交流会上分享了自己的经验:
“其实重构这个系统,最重要的不是技术有多新,而是思路要对。”
他总结了几个关键点:
第一,数据访问层要统一。 连接池管理、事务控制、异常处理都要在底层做好,业务层不需要关心这些细节。
第二,SQL要用预编译。 这是最基本的要求,既能防注入,又能提高性能。
第三,事务要显式管理。 涉及多表操作的业务,一定要用事务保证数据一致性。
第四,错误要友好提示。 用户看到错误页面时,应该知道发生了什么,而不是看到一个满是代码的错误堆栈。
第五,测试要彻底。 系统上线前,一定要做充分的测试,包括功能测试、性能测试和安全测试。
八、代码结构总结
整个系统的代码结构非常清晰:
school-system/
├── src/
│ ├── com/edu/
│ │ ├── model/ # 实体类
│ │ │ ├── Student.java
│ │ │ ├── Course.java
│ │ │ ├── Enrollment.java
│ │ │ ├── Grade.java
│ │ │ └── Teacher.java
│ │ ├── dao/ # 数据访问层
│ │ │ ├── StudentDAO.java
│ │ │ ├── CourseDAO.java
│ │ │ ├── EnrollmentDAO.java
│ │ │ ├── GradeDAO.java
│ │ │ └── TeacherDAO.java
│ │ ├── service/ # 业务逻辑层
│ │ │ ├── StudentService.java
│ │ │ ├── CourseService.java
│ │ │ ├── EnrollmentService.java
│ │ │ ├── GradeService.java
│ │ │ └── UserService.java
│ │ ├── servlet/ # 控制层
│ │ │ ├── LoginServlet.java
│ │ │ ├── CourseServlet.java
│ │ │ ├── EnrollmentServlet.java
│ │ │ ├── GradeServlet.java
│ │ │ └── TeacherServlet.java
│ │ ├── filter/ # 过滤器
│ │ │ ├── AuthFilter.java
│ │ │ └── ExceptionFilter.java
│ │ ├── util/ # 工具类
│ │ │ ├── DBUtil.java
│ │ │ ├── PasswordUtil.java
│ │ │ └── PageUtil.java
│ │ └── cache/ # 缓存
│ │ └── CourseCache.java
│ └── resources/
│ └── db.properties
├── web/
│ ├── WEB-INF/
│ │ └── web.xml
│ ├── jsp/
│ │ ├── login.jsp
│ │ ├── student/
│ │ │ ├── dashboard.jsp
│ │ │ ├── courseList.jsp
│ │ │ ├── enrollment.jsp
│ │ │ └── grade.jsp
│ │ ├── teacher/
│ │ │ ├── dashboard.jsp
│ │ │ ├── courseManage.jsp
│ │ │ └── gradeEntry.jsp
│ │ ├── admin/
│ │ │ ├── dashboard.jsp
│ │ │ ├── userManage.jsp
│ │ │ └── systemMonitor.jsp
│ │ └── error.jsp
│ └── css/
│ └── style.css
└── pom.xml
这个结构清晰、分层明确,每个模块职责单一,便于维护和扩展。
九、结语
从2024年9月到2025年9月,李铭重构的这个教务系统稳定运行了一年。期间经历了两次选课高峰、一次期末考试期间的成绩查询高峰,系统都没有出现过问题。
更重要的是,这个系统的代码质量让后续的维护工作变得轻松。每当有新需求时,开发人员可以在现有架构上快速添加新功能,而不需要担心引入新的Bug。
李铭站在办公室里,看着屏幕上显示的”系统运行正常”的绿色指示灯,想起了自己当初接手这个任务时的情景。那时候,没有人相信一个教师能在短时间内重构出这么一套稳定的系统。但事实胜于雄辩,365天的零故障运行,就是最好的证明。
“好的代码不需要复杂的框架,只需要正确的思路和扎实的功底。”这是李铭在项目总结会上说的话。
