别再只做增删改查了!用这个图书馆管理系统项目,深入理解JSP+Servlet核心机制
从图书馆管理系统实战中掌握JSP/Servlet进阶技巧1. 为什么我们需要重新审视这个项目每次看到新入行的开发者用JSPServlet完成图书馆管理系统时我总会想起自己当年的经历——功能实现了但代码却像意大利面条一样纠缠不清。这个看似简单的项目实际上蕴含着Java Web开发的核心思想而大多数教程只教会了我们如何完成却没告诉我们如何写好。让我们暂时放下那些基础的增删改查来聊聊如何通过这个项目真正理解JSP/Servlet的精髓。你已经知道怎么用%%写Java代码也明白request.getParameter()的用法但你是否思考过为什么MVC模式在Web开发中如此重要直接拼接SQL语句会带来什么灾难会话管理除了记住用户名还能做什么连接池真的只是性能优化这么简单吗这个项目将成为我们深入这些问题的跳板。不同于简单的功能实现我们将重点关注代码质量、安全性和可维护性——这些才是区分初级和中级开发者的关键。2. MVC模式在JSP/Servlet中的正确打开方式2.1 解剖原始代码的结构问题原始代码中最典型的反模式就是在JSP中直接处理业务逻辑和数据库操作。比如这样的代码% String sql SELECT * FROM Book WHERE book_name LIKE %request.getParameter(keyword)%; ResultSet rs db.query(sql); while(rs.next()) { % tr td%rs.getString(book_no)%/td td%rs.getString(book_name)%/td /tr % } %这种写法至少有三大问题SQL注入风险直接拼接用户输入职责混乱视图层处理数据访问难以维护HTML与Java代码混杂2.2 重构为清晰的MVC结构让我们用标准的MVC模式重构图书查询功能src/ ├── controller/ │ └── BookController.java ├── model/ │ ├── Book.java │ └── BookService.java └── view/ └── bookList.jspBookController.java核心代码protected void doGet(HttpServletRequest request, HttpServletResponse response) { String keyword request.getParameter(keyword); ListBook books new BookService().searchBooks(keyword); request.setAttribute(books, books); request.getRequestDispatcher(/view/bookList.jsp).forward(request, response); }BookService.java中的关键改进public ListBook searchBooks(String keyword) { String sql SELECT * FROM Book WHERE book_name LIKE ?; try (Connection conn DataSource.getConnection(); PreparedStatement stmt conn.prepareStatement(sql)) { stmt.setString(1, %keyword%); ResultSet rs stmt.executeQuery(); // 结果集转换为Book对象列表 return convertToBookList(rs); } catch (SQLException e) { throw new RuntimeException(查询失败, e); } }这种结构的优势显而易见关注点分离各层职责明确安全性提升使用PreparedStatement防止SQL注入可测试性增强业务逻辑可以单独测试可扩展性更好添加新功能不影响现有结构3. 会话管理的艺术与陷阱3.1 深入理解Session机制原始项目中只用Session存储了用户IDrequest.getSession().setAttribute(Scard_no, Scard_no);这虽然简单但存在几个潜在问题会话固定攻击风险没有在登录时重置Session ID信息暴露直接将数据库主键暴露给客户端缺乏超时控制可能导致会话长期有效3.2 安全增强的会话管理方案改进后的登录处理// 登录验证成功后 HttpSession session request.getSession(false); if (session ! null) { session.invalidate(); // 使旧会话失效 } session request.getSession(true); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 // 不直接暴露数据库ID UserToken token new UserToken(user.getId(), user.getRole()); session.setAttribute(currentUser, token); // 设置防CSRF令牌 session.setAttribute(csrfToken, UUID.randomUUID().toString());关键改进点会话固定防护登录时创建新会话最小信息原则不直接暴露数据库主键超时控制避免会话长期有效安全增强添加CSRF防护令牌3.3 会话数据的合理使用在视图层(JSP)中我们可以通过EL表达式安全地访问会话数据div classuser-info 欢迎${currentUser.name} c:if test${currentUser.role ADMIN} a href/admin管理后台/a /c:if /div会话使用的最佳实践存储尽量少的数据Session存储在服务器内存敏感信息不要直接存储在Session中考虑使用Redis等外部存储实现分布式Session重要操作前重新验证用户身份4. 数据库访问的进阶实践4.1 从基础DBUtil到连接池原始代码中的DBUtil直接创建连接public class DBUtil { public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这种简单实现在高并发场景下会导致连接创建开销大每次请求都新建连接资源耗尽风险未及时关闭的连接会堆积性能瓶颈连接创建成为系统瓶颈4.2 使用HikariCP实现高性能连接池引入现代连接池解决方案public class DataSource { private static final HikariDataSource ds; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:sqlserver://localhost:1433;databaseNamestu_library); config.setUsername(sa); config.setPassword(password); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); ds new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }连接池配置要点参数推荐值说明maximumPoolSizeCPU核心数*2 磁盘数最大连接数minimumIdlemaximumPoolSize/2最小空闲连接connectionTimeout30000(ms)获取连接超时时间idleTimeout600000(ms)空闲连接超时4.3 使用Try-With-Resources确保资源释放改进后的数据库访问模式public ListBook findAll() { String sql SELECT * FROM Book; try (Connection conn DataSource.getConnection(); PreparedStatement stmt conn.prepareStatement(sql); ResultSet rs stmt.executeQuery()) { return convertToBookList(rs); } catch (SQLException e) { throw new RuntimeException(查询失败, e); } }资源管理的关键点使用Try-With-Resources自动关闭资源在finally块中显式关闭资源(Java 7以下)确保每个Connection都有对应的关闭操作避免在循环中创建连接5. 前端与后端的优雅交互5.1 参数传递的多种方式原始项目中大量使用类似代码String bookNo request.getParameter(book_no);这种方式虽然简单但存在类型安全问题。我们可以做得更好改进方案1使用工具类进行参数转换public class RequestUtils { public static int getIntParameter(HttpServletRequest request, String name) { String value request.getParameter(name); try { return value ! null ? Integer.parseInt(value) : 0; } catch (NumberFormatException e) { throw new BadRequestException(参数格式错误: name); } } }改进方案2使用POJO自动绑定public class BookQuery { private String name; private String author; private String press; // getters/setters } // 在Controller中 BookQuery query new BookQuery(); BeanUtils.populate(query, request.getParameterMap());5.2 统一的JSON API设计对于现代Web应用JSON API是更好的选择WebServlet(/api/books) public class BookApiController extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { BookQuery query parseQuery(request); ListBook books bookService.search(query); response.setContentType(application/json); new ObjectMapper().writeValue(response.getWriter(), new ApiResult(true, success, books)); } }对应的前端AJAX调用$.get(/api/books, {keyword: Java}, function(data) { if (data.success) { renderBooks(data.result); } else { showError(data.message); } });5.3 错误处理的统一方案原始项目中的错误处理比较随意% try { % !-- 业务代码 -- % } catch (Exception e) { % 发生错误% e.getMessage() % % } %改进后的全局错误处理web.xml配置error-page error-code404/error-code location/error/notFound/location /error-page error-page exception-typejava.lang.Exception/exception-type location/error/serverError/location /error-pageErrorController示例protected void doGet(HttpServletRequest request, HttpServletResponse response) { Throwable exception (Throwable)request.getAttribute(javax.servlet.error.exception); if (isApiRequest(request)) { sendJsonError(response, exception); } else { request.setAttribute(error, exception); request.getRequestDispatcher(/WEB-INF/views/error.jsp).forward(request, response); } }6. 安全防护不容忽视6.1 SQL注入防护再探讨虽然我们已经使用了PreparedStatement但还有一些进阶技巧使用JPA/Hibernate等ORM框架Entity Table(name Book) public class Book { Id private String no; private String name; // 其他字段和getter/setter } // 查询示例 String jpql SELECT b FROM Book b WHERE b.name LIKE :keyword; ListBook books em.createQuery(jpql) .setParameter(keyword, %keyword%) .getResultList();使用JdbcTemplate命名参数String sql SELECT * FROM Book WHERE name LIKE :keyword; MapString, Object params new HashMap(); params.put(keyword, %keyword%); ListBook books jdbcTemplate.queryForList(sql, params, Book.class);6.2 XSS防护策略原始项目直接输出用户输入td%book.getName()%/td改进方案使用JSTL EL表达式tdc:out value${book.name}//td或者使用过滤器进行全局转义public class XssFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssRequestWrapper((HttpServletRequest)request), response); } } // XssRequestWrapper会重写getParameter等方法对输入进行转义6.3 CSRF防护实现在关键操作中添加CSRF令牌验证表单中添加令牌form action/books/borrow methodpost input typehidden namecsrfToken value${csrfToken} !-- 其他表单字段 -- /form服务器端验证protected void doPost(HttpServletRequest request, HttpServletResponse response) { String sessionToken (String)request.getSession().getAttribute(csrfToken); String requestToken request.getParameter(csrfToken); if (sessionToken null || !sessionToken.equals(requestToken)) { throw new SecurityException(CSRF token验证失败); } // 处理正常逻辑 }7. 项目结构与构建的现代化7.1 标准的Maven项目结构建议的项目结构library-system/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── config/ │ │ │ ├── controller/ │ │ │ ├── model/ │ │ │ ├── service/ │ │ │ └── util/ │ │ ├── resources/ │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── views/ │ │ └── static/ │ │ ├── css/ │ │ ├── js/ │ │ └── images/ │ └── test/ │ └── java/ └── pom.xml7.2 使用Servlet 3.0注解配置替代web.xml的注解配置WebServlet( name bookServlet, urlPatterns {/books, /books/*}, initParams { WebInitParam(name pageSize, value 10) } ) MultipartConfig( maxFileSize 1024 * 1024 * 5, maxRequestSize 1024 * 1024 * 10 ) public class BookController extends HttpServlet { // servlet代码 }7.3 日志记录的最佳实践使用SLF4JLogback替代System.outimport org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BookService { private static final Logger logger LoggerFactory.getLogger(BookService.class); public void saveBook(Book book) { try { // 保存逻辑 logger.info(成功保存图书: {}, book.getName()); } catch (Exception e) { logger.error(保存图书失败, e); throw new RuntimeException(e); } } }日志配置示例(logback.xml)configuration appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/library.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/library.%d{yyyy-MM-dd}.log/fileNamePattern /rollingPolicy encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender root levelINFO appender-ref refFILE / /root /configuration