1. 项目概述:一个看似“老派”却极富教学价值的全栈闭环实践
你点开这个标题,第一反应可能是:“Servlet?JSP?Tomcat?这不都是十年前的技术栈吗?”——没错,单看技术名词,它确实带着浓重的教科书气息。但恰恰是这套被很多新人绕着走、甚至被部分面试官轻描淡写带过的组合,构成了Java Web开发最底层、最扎实的“肌肉记忆”。它不依赖Spring Boot自动装配的魔法,不靠前端框架封装好的API调用,而是把HTTP请求怎么来、响应怎么回、数据库连接怎么建、数据怎么从一行SQL变成页面上一个表格里的文字,全部掰开揉碎,一环扣一环地摆在你面前。我带过不少刚从培训班出来的同学,他们能熟练写MyBatis注解、能配置Spring Cloud网关,但一问“request.setAttribute()和session.setAttribute()的区别在哪儿?为什么JSP里用EL表达式取值要加${}而不是直接写变量名?”,很多人卡壳。这不是知识盲区,而是对Web基础链路理解的断层。这个“用户信息列表展示”案例,就是一块磨刀石:它用最朴素的Servlet接收请求,用最原始的JDBC Template管理连接(背后是Druid连接池),用BeanUtils完成对象属性映射,最后用JSP+JSTL在浏览器端渲染出一张干净的表格。整个过程没有黑盒,每一步你都能在控制台看到日志,在数据库里查到记录,在浏览器Network面板里抓到完整的HTTP报文。它解决的不是“如何快速上线一个高并发系统”的问题,而是“当所有高级框架都失效时,你是否还知道Web的本质是什么”这个根本命题。适合谁?适合所有想真正搞懂Java Web底层逻辑的初学者、转行者,也适合那些已经会用Spring但想补全知识图谱的中级开发者。它不教你炫技,只帮你把地基夯得再实一点。
2. 整体架构设计与技术选型逻辑拆解
2.1 为什么是Servlet+JSP,而不是Spring MVC或Thymeleaf?
这个问题我每次讲到这个案例都会被问到。答案很实在:可控性与教学透明度。Spring MVC的DispatcherServlet像一个精密的瑞士钟表,它内部有HandlerMapping、HandlerAdapter、ViewResolver等十几种组件协同工作。你配好@Component和@RequestMapping,请求就到了,但中间发生了什么?参数怎么绑定的?ModelAndView怎么被解析成HTML的?这些细节被层层封装,对新手而言是“方便”,但也是“迷雾”。而Servlet,就是一个纯粹的Java类,它只做一件事:实现service()方法,接收HttpServletRequest和HttpServletResponse两个对象。你必须手动调用request.getParameter("id")去取参数,必须手动调用response.getWriter().write("<html>...")去写响应。这种“笨办法”强迫你直面HTTP协议本身。JSP同理。它本质是一个Servlet的语法糖,JSP文件在第一次被访问时,会被Tomcat的Jasper引擎编译成一个继承自HttpServlet的Java类。你在JSP里写的<%= user.getName() %>,最终会变成编译后Java类里的out.print(user.getName())。这意味着,你写的每一行JSP,都能在work/Catalina/localhost/yourapp/org/apache/jsp/目录下找到对应的.java源码。这种“所见即所得”的可追溯性,是任何现代模板引擎都难以提供的教学优势。选择它,不是守旧,而是为了让你在学“开车”之前,先亲手拆装一遍发动机。
2.2 Druid连接池为何不可替代?JDBC Template又解决了什么痛点?
很多人初学JDBC,写的是这样的代码:
Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DriverManager.getConnection(url, user, pwd); ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?"); ps.setLong(1, userId); rs = ps.executeQuery(); // 处理结果集... } catch (SQLException e) { e.printStackTrace(); } finally { if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); // 这里极易出错! }这段代码的问题在于:资源泄漏风险极高。close()方法可能抛出SQLException,如果在finally块里再嵌套try-catch,代码会变得极其臃肿。更致命的是,DriverManager.getConnection()每次都会创建一个全新的物理连接,而数据库连接是昂贵的资源,频繁创建销毁会拖垮性能。Druid正是为了解决这两个问题而生。它不是一个简单的连接池,而是一个集监控、防御、统计于一体的数据库连接管理器。它内部维护了一个连接池(默认初始8个,最大20个),当你调用dataSource.getConnection()时,它返回的不是物理连接,而是一个DruidPooledConnection的代理对象。这个代理对象在你调用close()时,并不会真的关闭物理连接,而是将连接归还给池子,供下一次复用。同时,Druid内置了SQL防火墙,能自动拦截' OR '1'='1这类注入语句;它还能实时监控慢SQL、连接泄漏、SQL执行时间分布,这些数据都能通过一个内置的Web控制台(http://localhost:8080/druid)直观看到。而JDBC Template,则是Spring对JDBC的一次优雅封装。它把上面那段冗长的try-catch-finally模板代码,抽象成了一个execute()、query()、update()等方法。你只需要关心SQL和参数,资源的获取、释放、异常转换(将SQLException转为Spring的DataAccessException)全部由它代劳。比如查询用户列表,一行代码搞定:
List<User> users = jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));这里BeanPropertyRowMapper就是利用反射,自动将ResultSet的每一列,按名字映射到User类的同名属性上。没有手动rs.getLong("id")、rs.getString("name")的繁琐。所以,Druid + JDBC Template的组合,是“基础设施稳定可靠”与“业务代码简洁清晰”的完美平衡。
2.3 BeanUtils:那个被低估的“对象搬运工”
在Web开发中,一个高频场景是:用户提交一个表单(比如注册页),后端需要把表单的name、email、age等参数,赋值给一个User对象的对应属性。最原始的做法是:
User user = new User(); user.setName(request.getParameter("name")); user.setEmail(request.getParameter("email")); user.setAge(Integer.parseInt(request.getParameter("age")));这不仅重复、易错(类型转换容易抛NumberFormatException),而且一旦User类的属性增多,代码量会指数级增长。Apache Commons BeanUtils库,就是为了解决这个“对象间属性拷贝”的通用问题。它的核心方法BeanUtils.copyProperties(dest, orig),能通过Java反射机制,自动遍历orig对象的所有getter方法,再在dest对象上寻找同名的setter方法,完成赋值。它甚至能处理嵌套属性(如user.getAddress().getCity())和类型转换(自动将字符串"25"转为int)。在这个案例里,它被用于两个关键环节:一是将HttpServletRequest中的参数,一键填充到User对象(用于新增或修改);二是将数据库查询出的User对象列表,传递给JSP页面时,JSP里的EL表达式${user.name}能直接取值,这背后依赖的正是JavaBean规范——User类必须有getName()和setName(String name)方法,而BeanUtils正是基于此规范工作的。它不是一个炫酷的框架,但它像空气一样无处不在,让枯燥的“搬数据”工作变得无声无息。
2.4 Tomcat:不只是容器,更是你的第一个“生产环境”模拟器
很多初学者把Tomcat当成一个“运行Java Web项目的工具”,仅此而已。但如果你深入进去,会发现它本身就是一本活的HTTP服务器教科书。当你把一个user-list.war包丢进webapps目录,Tomcat会自动解压、加载web.xml(或扫描@WebServlet注解)、初始化ServletContext、启动Filter链、最后将请求路由到你的UserListServlet。这个过程,完整复现了一个真实Web服务器的生命周期。更重要的是,Tomcat的配置文件server.xml,让你能亲手调整HTTP端口(默认8080)、设置虚拟主机、配置SSL证书(虽然本案例不用)、甚至定义不同的Connector来支持BIO/NIO/APR三种I/O模型。对于学习者而言,调试Tomcat的过程,就是调试整个Web请求链路的过程。你可以在logging.properties里把日志级别调成FINE,然后在catalina.out里看到每一个请求的详细处理步骤;你可以在web.xml里配置<error-page>,观察404、500错误是如何被统一捕获和展示的。它不像Docker或K8s那样复杂,但它足够真实,足够让你建立起对“一个Web应用从启动到响应”的完整心智模型。把它当作一个“微型生产环境”来对待,远比把它当成一个“一键启动的黑盒子”有价值得多。
3. 核心模块详解与实操要点
3.1 数据库设计:从ER图到建表语句的落地思考
一个看似简单的“用户信息列表”,其数据库设计却暗藏玄机。我们不能只想着“建个user表,放id、name、email就行”。实际开发中,你需要考虑扩展性、一致性、可读性。我通常会这样设计:
| 字段名 | 类型 | 是否为空 | 默认值 | 注释 |
|---|---|---|---|---|
id | BIGINT UNSIGNED | NOT NULL | - | 主键,自增,用BIGINT是为了未来数据量大时避免溢出 |
username | VARCHAR(50) | NOT NULL | '' | 用户名,唯一索引,不允许空字符串 |
email | VARCHAR(100) | NOT NULL | '' | 邮箱,唯一索引,需校验格式 |
password | VARCHAR(100) | NOT NULL | '' | 密码,必须加密存储(MD5/BCrypt),绝不存明文 |
real_name | VARCHAR(20) | NULL | NULL | 真实姓名,允许为空 |
age | TINYINT UNSIGNED | NULL | NULL | 年龄,用TINYINT足够(0-255) |
status | TINYINT | NOT NULL | 1 | 状态:1-启用,0-禁用,便于软删除 |
create_time | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 创建时间,自动填充 |
update_time | DATETIME | NOT NULL | CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间,自动更新 |
建表语句如下(MySQL 5.7+):
CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名', `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码(加密后)', `real_name` varchar(20) DEFAULT NULL COMMENT '真实姓名', `age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-启用,0-禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) USING BTREE, UNIQUE KEY `uk_email` (`email`) USING BTREE, KEY `idx_status` (`status`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';提示:
utf8mb4是必须的!它支持完整的Unicode字符,包括emoji。而旧的utf8在MySQL里其实是utf8mb3,不支持4字节字符,会导致插入微信昵称(含emoji)时报错。USING BTREE显式指定索引类型,是良好习惯。
为什么要有status字段而不是直接DELETE?这是“软删除”的经典实践。物理删除会丢失历史数据,且无法恢复。而将status设为0,查询时加上WHERE status = 1,既保证了数据安全,又不影响业务逻辑。create_time和update_time的自动填充,省去了在Java代码里手动设置时间的麻烦,且保证了时间戳的准确性(由数据库服务器提供,而非应用服务器)。
3.2 Druid连接池配置:从XML到Java Config的平滑过渡
Druid的配置有两种主流方式:XML(传统)和Java Config(Spring风格)。本案例采用XML,因为它更符合Servlet/JSP时代的原生风格,也更容易被初学者理解。配置文件druid.properties放在src/main/resources目录下:
# 数据库连接基本信息 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username=root password=123456 # 连接池核心参数 initialSize=5 minIdle=5 maxActive=20 maxWait=60000 timeBetweenEvictionRunsMillis=60000 minEvictableIdleTimeMillis=300000 validationQuery=SELECT 1 testWhileIdle=true testOnBorrow=false testOnReturn=false # 配置监控统计拦截的filters,去掉后监控界面sql无法统计 filters=stat,wall,log4j # 配置slowSqlMonitor slowSqlMillis=1000关键参数解读:
initialSize=5:应用启动时,池子里就准备好5个连接,避免首次请求时的连接创建延迟。minIdle=5:池中最小空闲连接数。当空闲连接少于5个时,Druid会自动创建新连接补充。maxActive=20:池中最大活跃连接数。超过20个请求,后续请求会进入等待队列,直到有连接被释放。timeBetweenEvictionRunsMillis=60000:每60秒运行一次“空闲连接回收”任务,检查哪些连接空闲太久(minEvictableIdleTimeMillis=300000即5分钟),并将其关闭。validationQuery=SELECT 1:这是健康检查SQL。Druid会定期执行它,确保连接还活着。MySQL用SELECT 1,Oracle用SELECT 1 FROM DUAL。filters=stat,wall,log4j:这是Druid的灵魂。stat开启监控统计;wall是SQL防火墙,能防御基本SQL注入;log4j将日志输出到Log4j。
注意:
serverTimezone=Asia/Shanghai是必须的!否则Java程序读取MySQL的DATETIME字段时,会因时区不一致导致时间错乱(比如数据库存的是2023-10-01 10:00:00,Java里读出来却是2023-10-01 18:00:00)。allowPublicKeyRetrieval=true&useSSL=false是MySQL 8.0+驱动的新要求,用于简化本地开发的SSL配置。
在UserDao类中,我们通过DruidDataSourceFactory.createDataSource()方法加载这个配置:
public class UserDao { private static DataSource dataSource; static { try { // 加载druid.properties文件 Properties props = new Properties(); InputStream is = UserDao.class.getClassLoader().getResourceAsStream("druid.properties"); props.load(is); // 创建数据源 dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("Druid DataSource init failed", e); } } // 后续的query、update方法都使用这个dataSource }这个静态代码块,保证了dataSource在UserDao类第一次被加载时就初始化完成,且全局唯一,符合连接池的设计初衷。
3.3 Servlet核心逻辑:从URL映射到MVC分层的演进
UserListServlet是整个流程的入口。它的职责非常明确:接收HTTP GET请求,调用DAO获取数据,将数据存入request域,然后转发(forward)到JSP页面。代码结构如下:
@WebServlet("/user/list") public class UserListServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 调用DAO,查询所有用户 List<User> userList = userDao.findAll(); // 2. 将数据存入request域,供JSP页面使用 request.setAttribute("userList", userList); // 3. 请求转发到user_list.jsp,注意:是forward,不是redirect! request.getRequestDispatcher("/WEB-INF/jsp/user_list.jsp").forward(request, response); } }这里有几个关键点必须厘清:
@WebServlet("/user/list"):这是一个注解,它告诉Tomcat,当收到/user/list路径的请求时,就交给这个Servlet处理。它替代了古老的web.xml配置,是Servlet 3.0+的标准。request.setAttribute()vsrequest.getSession().setAttribute():前者是将数据绑定到当前这次HTTP请求的生命周期内,数据只在本次请求-响应周期有效,转发(forward)时可以传递给下一个资源(JSP)。后者是将数据绑定到用户的Session会话中,数据会一直存在,直到Session超时或被手动销毁。列表展示是典型的“一次请求,一次展示”,用request域完全足够,且更安全(避免数据污染Session)。forward()vsredirect():forward()是服务器端的内部跳转,浏览器地址栏的URL不会改变,且request域中的数据可以传递过去。redirect()是客户端重定向,服务器会返回一个302状态码和新的Location头,浏览器会发起第二次请求,因此request域数据会丢失。本案例必须用forward(),否则JSP页面拿不到userList。
实操心得:我见过太多同学在这里踩坑。他们用
response.sendRedirect("/user_list.jsp"),结果JSP页面里${userList}取不到值,然后疯狂查EL表达式语法,其实根源在跳转方式错了。记住一个口诀:“数据要传,就用forward;URL要变,才用redirect”。
3.4 JSP页面渲染:从原始HTML到JSTL+EL的现代化写法
user_list.jsp位于/WEB-INF/jsp/目录下(放在WEB-INF里是为了安全,防止用户直接通过URL访问JSP源码)。它的核心任务是:将request域里的userList集合,渲染成一个HTML表格。早期的JSP写法是混杂Java代码的:
<% List<User> userList = (List<User>) request.getAttribute("userList"); %> <table> <tr><th>ID</th><th>用户名</th><th>邮箱</th><th>操作</th></tr> <% for (User user : userList) { %> <tr> <td><%= user.getId() %></td> <td><%= user.getUsername() %></td> <td><%= user.getEmail() %></td> <td><a href="delete?id=<%= user.getId() %>">删除</a></td> </tr> <% } %> </table>这种写法的问题是:Java代码和HTML严重耦合,难以维护,且容易产生XSS漏洞(如果user.getUsername()里包含<script>标签,就会被执行)。现代JSP的最佳实践是:彻底剥离Java代码,只用JSTL(JSP Standard Tag Library)和EL(Expression Language)。首先,在JSP顶部声明taglib:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>然后,用JSTL的<c:forEach>标签遍历集合:
<table border="1"> <thead> <tr> <th>ID</th> <th>用户名</th> <th>邮箱</th> <th>真实姓名</th> <th>年龄</th> <th>状态</th> <th>创建时间</th> <th>操作</th> </tr> </thead> <tbody> <c:choose> <c:when test="${empty userList}"> <tr><td colspan="8" align="center">暂无用户数据</td></tr> </c:when> <c:otherwise> <c:forEach items="${userList}" var="user"> <tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.email}</td> <td>${user.realName}</td> <td>${user.age}</td> <td> <c:if test="${user.status == 1}">启用</c:if> <c:if test="${user.status == 0}">禁用</c:if> </td> <td><fmt:formatDate value="${user.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/></td> <td> <a href="delete?id=${user.id}">删除</a> | <a href="edit?id=${user.id}">编辑</a> </td> </tr> </c:forEach> </c:otherwise> </c:choose> </tbody> </table>这里体现了几个重要思想:
<c:forEach>:替代了for循环,items指向EL表达式${userList},var定义了循环变量user,在循环体内就可以用${user.xxx}取值。<c:choose>/<c:when>/<c:otherwise>:替代了if-else,用于条件判断。test="${empty userList}"是JSTL的空值判断,比userList == null || userList.size() == 0更简洁安全。<fmt:formatDate>:专门用于日期格式化。value是Date对象,pattern指定了显示格式。这比在Java代码里用SimpleDateFormat格式化后再传给JSP要优雅得多。- EL表达式的自动转义:
${user.username}默认会对特殊字符(如<,>)进行HTML转义,输出为<、>,从而天然防御XSS攻击。这是JSP 2.0+的默认行为,无需额外配置。
注意:JSTL库需要在
pom.xml中引入:
<dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>否则JSP页面会报org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved错误。
4. 完整实操流程与关键环节实现
4.1 项目结构搭建:Maven骨架与目录约定
一个规范的Java Web项目,其目录结构是工程化的第一步。我们使用Maven来管理依赖和构建,项目结构如下:
user-management/ ├── pom.xml # Maven核心配置文件 ├── src/ │ ├── main/ │ │ ├── java/ # Java源代码 │ │ │ └── com/example/web/ # Servlet、DAO、实体类 │ │ │ ├── servlet/ │ │ │ │ └── UserListServlet.java │ │ │ ├── dao/ │ │ │ │ └── UserDao.java │ │ │ ├── domain/ │ │ │ │ └── User.java # JavaBean实体类 │ │ │ └── util/ │ │ │ └── JdbcUtils.java # (可选)自定义JDBC工具类 │ │ ├── resources/ # 配置文件 │ │ │ ├── druid.properties # Druid连接池配置 │ │ │ └── log4j.properties # 日志配置 │ │ └── webapp/ # Web资源根目录 │ │ ├── WEB-INF/ │ │ │ ├── web.xml # (可选)Servlet 2.5+的部署描述符 │ │ │ └── jsp/ # JSP页面存放目录 │ │ │ ├── user_list.jsp │ │ │ └── user_edit.jsp │ │ └── static/ # 静态资源(CSS, JS, 图片) │ │ ├── css/ │ │ │ └── style.css │ │ └── js/ │ │ └── common.js │ └── test/ # 单元测试代码 └── target/ # Maven编译输出目录(自动生成)pom.xml是整个项目的“心脏”,它定义了项目坐标、依赖、插件和构建配置。以下是本案例的核心依赖片段:
<dependencies> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> <!-- provided表示由Tomcat提供,打包时不包含 --> </dependency> <!-- JSP API --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Druid Connection Pool --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <!-- Spring JDBC Template --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.31</version> </dependency> <!-- Apache Commons BeanUtils --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency> <!-- Log4j for logging --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies> <build> <plugins> <!-- Maven Compiler Plugin: 指定Java版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <!-- Maven War Plugin: 打包成WAR文件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> </plugin> </plugins> </build>关键说明:
<scope>provided</scope>用于javax.servlet-api和javax.servlet.jsp-api。因为Tomcat本身就提供了这些API的实现,如果打包时也包含进去,会导致类冲突(ClassCastException)。Maven会识别这个scope,在编译和测试时提供该依赖,但在最终的WAR包中不包含它。
4.2 User实体类:严格遵循JavaBean规范的编写
User.java是整个数据流转的载体,它必须严格遵守JavaBean规范,否则BeanUtils和JSP EL都无法正常工作。规范的核心是:
- 私有属性(private)
- 公共的无参构造方法(public User())
- 每个属性都有对应的公共getter和setter方法(public String getName(), public void setName(String name))
- getter/setter方法的命名必须符合驼峰规则
package com.example.web.domain; import java.util.Date; // 必须有无参构造方法! public class User { private Long id; private String username; private String email; private String password; private String realName; private Integer age; private Integer status; private Date createTime; private Date updateTime; // 无参构造方法,必须有! public User() {} // getter和setter方法,必须一一对应,且首字母大写 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRealName() { return realName; } public void setRealName(String realName) { this.realName = realName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } // 可选:重写toString(),方便调试 @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + '}'; } }注意:
Integer和Long是包装类,它们可以为null,而int和long是基本类型,不能为null。在数据库中,age和status字段允许为NULL(NULLABLE),所以Java中必须用包装类,否则ResultSet.getInt("age")遇到NULL时会抛出SQLException。Date类型同理,对应数据库的DATETIME。
4.3 UserDao实现:JDBC Template的实战应用
UserDao是数据访问层,它封装了所有与数据库交互的逻辑。我们使用JdbcTemplate来简化JDBC操作。JdbcTemplate本身不管理连接,它需要一个DataSource。我们已经在UserDao的静态代码块中初始化了Druid的DataSource,现在只需在构造方法中注入它:
package com.example.web.dao; import com.example.web.domain.User; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.util.List; public class UserDao { private JdbcTemplate template; // 构造方法注入DataSource public UserDao(DataSource dataSource) { this.template = new JdbcTemplate(dataSource); } // 查询所有用户 public List<User> findAll() { String sql = "SELECT * FROM user WHERE status = 1 ORDER BY create_time DESC"; return template.query(sql, new BeanPropertyRowMapper<>(User.class)); } // 根据ID查询单个用户 public User findById(Long id) { String sql = "SELECT * FROM user WHERE id = ? AND status = 1"; try { return template.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id); } catch (EmptyResultDataAccessException e) { // 如果没查到,返回null,而不是抛异常 return null; } } // 新增用户 public int save(User user) { String sql = "INSERT INTO user(username, email, password, real_name, age, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; return template.update(sql, user.getUsername(), user.getEmail(), user.getPassword(), // 注意:此处应为加密后的密码! user.getRealName(), user.getAge(), user.getStatus(), user.getCreateTime(), user.getUpdateTime()); } // 更新用户 public int update(User user) { String sql = "UPDATE user SET username=?, email=?, real_name=?, age=?, status=?, update_time=? WHERE id=?"; return template.update(sql, user.getUsername(), user.getEmail(), user.getRealName(), user.getAge(), user.getStatus(), user.getUpdateTime(), user.getId()); } // 删除用户(软删除) public int delete(Long id) { String sql = "UPDATE user SET status=0, update_time=? WHERE id=?"; return template.update(sql, new Date(), id); } }关键点解析:
template.query():用于查询,返回一个List<T>。BeanPropertyRowMapper是核心,它根据User.class的属性名,自动匹配ResultSet的列名。template.queryForObject():用于查询单个对象。如果ResultSet为空(即没查到),它会抛出EmptyResultDataAccessException。我们在findById()中捕获它,并返回null,这是一种更友好的API设计。template.update():用于INSERT、UPDATE、DELETE操作,返回受影响的行数(int)。- SQL中的
?占位符:这是预编译(Prepared Statement)