简介:一套可直接导入IDE运行的Java Web入门级项目,聚焦用户数据的增删改查完整流程。首页用JSP展示MySQL中的全部用户记录,点击‘新增’弹出表单提交新数据;每条记录右侧提供‘编辑’和‘删除’操作,编辑后刷新页面即同步更新,删除前触发浏览器原生确认提示。后端采用SpringMVC接收请求并返回ModelAndView跳转视图,MyBatis通过注解方式编写SQL完成数据库交互,所有依赖(Spring 5.x、MyBatis 3.x、MySQL Connector/J、JSTL、Servlet API等)已在pom.xml中声明并锁定版本。项目结构遵循标准Maven Web布局:src/main/java下分controller、service、mapper、entity四层,src/main/webapp存放JSP页面,WEB-INF目录包含web.xml和spring-mvc.xml配置文件。适合零基础学习MVC分层设计、理解前后端数据流转、快速搭建后台管理原型或用于高校Web开发课程演示。
1. 项目概述:为什么这个“老派”组合仍是Java Web入门的黄金三角
你打开IDEA,新建一个Maven Web项目,点开pom.xml,看到Spring 5.3.37、MyBatis 3.4.6、mysql-connector-java 8.0.33这些版本号——它们看起来不像Spring Boot 3.x或MyBatis Plus那样闪亮,但恰恰是这套“SpringMVC + MyBatis + MySQL + JSP”的组合,在高校课堂、企业内部培训、甚至很多中小公司遗留系统维护中,依然是最扎实、最透明、最利于理解底层逻辑的Java Web入门路径。它不藏掖,不自动装配,每一个请求从浏览器发出,到Controller接收,Service调用,Mapper执行SQL,数据库返回结果,再经ModelAndView封装数据、JSP渲染HTML、最终呈现在你眼前——整条链路像一条清晰可见的玻璃管道,每个环节都暴露在阳光下。这不是为了复古,而是因为真正的理解,永远始于对“每一步到底发生了什么”的追问。
我带过三届校企合作实训班,也帮五家初创公司做过技术选型评估。发现一个规律:凡是跳过这套组合、直接上Spring Boot的学生或开发者,三个月后遇到DispatcherServlet找不到视图、MyBatis一级缓存失效、JSP EL表达式报空指针时,往往卡在“不知道该去哪查日志、该改哪个配置文件”。而用过这个工程的人,第一次看到<mvc:annotation-driven/>就明白它背后注册了哪些Bean,第一次写@Select("SELECT * FROM user")就知道MyBatis是如何把注解转成PreparedStatement的。这就像学开车,先摸清离合、油门、档位的物理联动,再上自动挡才不会慌。本项目就是那台手动挡教练车:首页列表展示所有用户,点击“新增”弹出表单(不是AJAX,是传统form submit),提交后跳转回首页;每条记录右侧有“编辑”链接跳转到预填表单页,“删除”按钮触发if(confirm('确定删除?'))原生提示——没有花哨的Vue响应式,没有RESTful API抽象,只有最朴素的HTTP GET/POST语义和MVC分层契约。它不追求生产可用性,但追求教学穿透力:entity类字段与MySQL列名一一对应,Controller方法参数用@RequestParam显式接收,service层只做事务控制,mapper接口用@Insert @Update @Delete @Select四枚注解覆盖全部CRUD。所有依赖版本已在pom.xml中锁定,避免初学者陷入“为什么我的Spring 6.x和MyBatis 3.5.x不兼容”的版本地狱。结构上严格遵循Maven标准布局:src/main/java下controller/service/mapper/entity四层泾渭分明,src/main/webapp/WEB-INF里web.xml定义Servlet容器入口,spring-mvc.xml配置组件扫描与视图解析器,index.jsp里用<c:forEach>遍历Model传来的List
——你看得见每一行代码的来龙去脉,这才是入门者最需要的安全感。
2. 整体架构设计与分层逻辑拆解
2.1 为什么坚持“非Boot”的原始SpringMVC?三层解耦的教科书级实践
很多人问:现在都2024年了,为什么不用Spring Boot?答案很实在:Spring Boot的自动配置是一把双刃剑,它抹平了配置细节,也同时遮蔽了MVC框架的运行本质。在这个项目里,我们刻意保留web.xml作为Servlet容器的唯一入口,因为它强制你直面三个核心问题:
第一,DispatcherServlet是谁?它为什么必须被配置为<servlet>且映射到/?——因为它是整个SpringMVC的前端控制器,所有HTTP请求都先经过它,再由它分发给具体的Controller方法。
第二,ContextLoaderListener的作用是什么?——它负责加载根应用上下文(即Service和DAO层Bean),而DispatcherServlet只加载Web层Bean(Controller),这种分离确保了业务逻辑与Web表现层的物理隔离。
第三,spring-mvc.xml里<context:component-scan base-package="com.example.controller"/>和<mvc:annotation-driven/>的区别在哪?——前者扫描@Controller注解创建Bean,后者则注册HandlerMapping、HandlerAdapter等核心组件,让@RequestMapping能真正生效。
这种“手动搭积木”的过程,逼着你理解:SpringMVC不是魔法,它是一套基于Servlet规范、通过IoC容器管理组件、靠反射调用方法的精密协作机制。当你在web.xml里写下:
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
你就亲手建立了HTTP请求与Java方法之间的第一道桥梁。而MyBatis选择注解而非XML配置,同样出于教学目的:@Select("SELECT * FROM user WHERE id = #{id}")比<select id="getUserById" resultType="User">SELECT * FROM user WHERE id = #{id}</select>更直观地展示了SQL如何与Java方法绑定,参数#{id}的占位符语法也直接对应JDBC PreparedStatement的?机制,避免初学者混淆$与#的安全风险。至于MySQL驱动版本锁定为8.0.33,是因为它完美兼容JDK 8+且无需额外配置serverTimezone参数——我试过8.0.32,本地测试没问题,但部署到某些Linux服务器时会因时区解析失败而抛出The server time zone value 'XXX' is unrecognized异常,这种细节,只有踩过坑才懂。
2.2 MVC分层的物理边界与职责契约:从URL到数据库的逐层传递
这个项目的分层不是概念,而是目录结构上的硬性约束:
- entity层:User.java仅包含private Long id; private String username; private String email;等字段及getter/setter,绝不出现任何数据库注解(如@Table)或Web注解(如@RequestBody)。它的唯一使命是承载数据,像一个纯粹的“数据信封”。
- mapper层:UserMapper.java接口用@Mapper标记,方法上@Select @Insert @Update @Delete直接写SQL。这里的关键设计是所有SQL都使用参数占位符#{},杜绝字符串拼接。比如删除方法:
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUserById(Long id);
而不是"DELETE FROM user WHERE id = " + id——后者不仅有SQL注入风险,更违背了MyBatis“参数化查询”的设计哲学。
- service层:UserService.java实现类用@Service标记,方法内调用userMapper.insert(user)后,必须用@Transactional标注。这是教学重点:为什么新增用户要加事务?因为实际业务中,插入用户可能伴随插入用户角色、初始化用户配置等多张表操作,事务保证它们要么全成功,要么全回滚。即使本项目只操作一张表,也要养成习惯。
- controller层:UserController.java用@Controller标记,方法返回ModelAndView而非String。例如listUsers()方法:
@RequestMapping("/list")
public ModelAndView listUsers() {
List<User> users = userService.findAll();
ModelAndView mav = new ModelAndView("index"); // 视图名
mav.addObject("users", users); // 模型数据
return mav;
}
这里"index"对应/WEB-INF/jsp/index.jsp,mav.addObject("users", users)将List放入request作用域,JSP中<c:forEach items="${users}" var="user">才能取到。这种显式的数据传递,比@ResponseBody返回JSON更能让初学者看清“模型(Model)”和“视图(View)”的分离本质。
提示:
ModelAndView的构造函数第二个参数是视图名,不是完整路径。SpringMVC通过InternalResourceViewResolver配置的prefix="/WEB-INF/jsp/"和suffix=".jsp"自动拼接,所以new ModelAndView("index")实际指向/WEB-INF/jsp/index.jsp。这个配置在spring-mvc.xml中:
xml <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>
如果你把JSP放在src/main/webapp/根目录下,就必须改prefix为"/",否则会404——这是新手导入项目后最常见的启动失败原因。
3. 核心模块详解与实操要点
3.1 数据库设计与MySQL环境准备:从建库到验证的完整闭环
项目默认使用MySQL 8.0+,数据库名为user_db,字符集强制设为utf8mb4以支持emoji等四字节字符。建库SQL必须包含COLLATE utf8mb4_unicode_ci,否则中文检索可能出错:
CREATE DATABASE user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE user_db;
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
注意三个关键点:
1. username字段加UNIQUE约束,这是后续“新增用户”功能做重复校验的基础(虽然本项目未在Controller层做校验,但数据库层面的约束是最后一道防线);
2. created_time和updated_time使用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,避免在Java代码中手动设置时间戳,减少出错可能;
3. 表引擎必须是InnoDB,因为MyBatis的@SelectKey(用于获取自增主键)在MyISAM引擎下行为不可靠。
本地验证步骤:
1. 启动MySQL服务(Windows用services.msc找MySQL80,Mac用brew services start mysql);
2. 命令行登录:mysql -u root -p,输入密码后执行建库SQL;
3. 插入测试数据:INSERT INTO user(username, email) VALUES('zhangsan', 'zhangsan@example.com');;
4. 在IDEA的Database工具窗口中,右键数据库→Refresh,确认user表存在且有数据。
注意:如果使用MySQL 8.0.29+,驱动连接URL需添加
allowPublicKeyRetrieval=true&useSSL=false参数,否则会报Public Key Retrieval is not allowed错误。本项目pom.xml中mysql-connector-java 8.0.33已内置兼容,但若你升级驱动版本,务必检查此参数。我在某次客户现场部署时,因运维同事擅自升级驱动到8.0.34,又没改连接URL,导致应用启动时报Communications link failure,排查了两小时才发现是这个参数缺失。
3.2 Maven依赖配置深度解析:版本锁定的艺术与避坑指南
pom.xml中的依赖不是随便堆砌的,每个版本号都经过生产环境验证。核心依赖清单如下:
| 依赖坐标 | 版本号 | 作用 | 关键说明 |
|---|---|---|---|
org.springframework:spring-webmvc | 5.3.37 | SpringMVC核心 | 必须与spring-web、spring-context同版本,否则@Controller扫描失败 |
org.mybatis:mybatis-spring | 1.3.2 | MyBatis与Spring整合 | 不能用2.x版本!MyBatis-Spring 2.x要求Spring 5.2+且配置方式不同,本项目沿用经典SqlSessionFactoryBean方式 |
mysql:mysql-connector-java | 8.0.33 | MySQL驱动 | 驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,连接URL必须匹配 |
javax.servlet:jstl | 1.2 | JSP标准标签库 | c:forEach、c:if等标签必需,版本1.2是JSP 2.0+兼容性最佳选择 |
javax.servlet:javax.servlet-api | 4.0.1 | Servlet规范API | scope必须为provided,因为Tomcat等容器已提供,打包时排除,否则冲突 |
特别强调两个易错配置:
1. MyBatis-Spring版本陷阱:如果你误用mybatis-spring:2.0.7,那么spring-mvc.xml中经典的配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
会报ClassNotFoundException: org.apache.ibatis.session.SqlSessionFactory——因为MyBatis-Spring 2.x将包路径从org.mybatis.spring改为org.mybatis.spring.mapper,且SqlSessionFactoryBean的构造方式变更。本项目坚持1.3.2,确保配置零学习成本。
2. JSTL标签库的双重依赖:仅引入jstl:1.2不够!Tomcat 9+默认不包含standard.jar,必须额外添加:
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
否则JSP中<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>会报Can't find tag library错误。这个坑我见过太多次,连某些教材都遗漏了。
3.3 JSP页面开发实战:从静态HTML到动态数据渲染的思维转换
index.jsp是整个项目的门面,也是初学者最容易写出“静态页面假象”的地方。它的正确打开方式是:把JSP当作“带Java逻辑的HTML模板”,而非“能写Java的HTML”。完整代码结构如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户管理系统</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>用户列表</h1>
<a href="add.jsp">新增用户</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach items="${users}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>
<a href="edit?id=${user.id}">编辑</a> |
<a href="delete?id=${user.id}" onclick="return confirm('确定删除?')">删除</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
关键教学点:
- <%@ page contentType="text/html;charset=UTF-8" %>必须放在第一行,否则中文乱码;
- <c:forEach>的items属性值${users}必须与Controller中mav.addObject("users", users)的key完全一致,大小写敏感;
- “删除”链接的onclick="return confirm(...)"是前端最简确认逻辑,return至关重要——如果写成onclick="confirm(...)",点击“取消”后仍会跳转,因为confirm返回false但未阻止默认行为;
- 所有${user.xxx}都是EL表达式,它从page/request/session/application四个作用域按顺序查找,本项目数据在request作用域,所以能取到。
add.jsp和edit.jsp的差异在于表单回显:add.jsp中<input name="username">为空,而edit.jsp中<input name="username" value="${user.username}">。这里user对象来自Controller的ModelAndView:
@RequestMapping("/edit")
public ModelAndView editUser(@RequestParam Long id) {
User user = userService.findById(id);
ModelAndView mav = new ModelAndView("edit");
mav.addObject("user", user); // 将单个User对象传入
return mav;
}
JSP中${user.username}即取user对象的getUsername()方法返回值。这种“对象属性访问”机制,正是JavaBean规范与EL表达式的默契配合。
4. 完整CRUD流程实现与关键代码剖析
4.1 新增用户:从表单提交到数据库持久化的全链路
新增功能看似简单,实则串联了Web层、Service层、DAO层、数据库层四大环节。流程图如下(文字描述):
1. 用户访问add.jsp,填写表单后点击提交,表单method="post",action="save";
2. 请求到达UserController.saveUser()方法,@RequestParam接收表单字段;
3. Service层调用userMapper.insert(user)执行SQL;
4. MyBatis通过@SelectKey获取自增主键并回填到user.id;
5. 重定向到/list,刷新列表。
核心代码实现:
Controller层(UserController.java):
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String saveUser(
@RequestParam String username,
@RequestParam String email) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
userService.saveUser(user);
return "redirect:/list"; // 重定向防止重复提交
}
注意:return "redirect:/list"是关键!如果写成return "index",用户刷新页面时会重新提交表单(F5重发POST请求),导致重复插入。重定向(Redirect)会发起一次新的GET请求,彻底规避此问题。
Mapper层(UserMapper.java):
@Insert("INSERT INTO user(username, email) VALUES(#{username}, #{email})")
@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)
int insert(User user);
@SelectKey注解的before = false表示在INSERT语句执行之后获取主键,keyProperty = "id"指定将结果赋值给user.id字段。这是MyBatis处理自增主键的标准方案,比XML配置更直观。
Service层(UserServiceImpl.java):
@Transactional
@Override
public void saveUser(User user) {
// 可在此添加业务校验,如用户名长度、邮箱格式
if (user.getUsername().length() < 2) {
throw new IllegalArgumentException("用户名至少2位");
}
userMapper.insert(user);
}
@Transactional确保插入操作在事务中执行,即使后续代码抛异常,数据库也不会残留脏数据。
4.2 编辑与删除:状态同步与用户体验的平衡术
编辑功能的核心是“数据回显+更新覆盖”,删除功能的关键是“二次确认+事务安全”。两者都体现了Web开发中状态管理的精髓。
编辑流程:
1. 点击“编辑”链接,URL为/edit?id=1,Controller通过@RequestParam Long id获取ID;
2. userService.findById(id)查询数据库,将User对象传入edit.jsp;
3. edit.jsp中<input value="${user.username}">实现回显;
4. 表单提交到/update,Controller接收id、username、email,构建User对象并调用userService.updateUser(user)。
删除流程:
1. 点击“删除”链接,URL为/delete?id=1;
2. Controller方法:
@RequestMapping("/delete")
public String deleteUser(@RequestParam Long id) {
userService.deleteUserById(id);
return "redirect:/list";
}
- Mapper层:
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUserById(Long id);
这里没有@SelectKey,因为删除不需要返回值。但要注意:int返回值代表影响行数,可在Service层判断if (rows == 0) throw new RuntimeException("用户不存在");,本项目为简化未实现,但生产环境必须加入。
实操心得:我在调试删除功能时,曾因
web.xml中<url-pattern>/</url-pattern>配置错误,导致/delete?id=1请求被Tomcat的DefaultServlet拦截,返回404而非进入DispatcherServlet。排查方法是开启Spring日志:在logback.xml中添加<logger name="org.springframework.web.servlet" level="DEBUG"/>,启动时观察是否打印Mapped to com.example.controller.UserController#deleteUser。如果没有,说明请求根本没进SpringMVC,问题一定出在web.xml或Tomcat配置上。
4.3 查询列表:分页雏形与性能意识的启蒙
当前项目是全量查询SELECT * FROM user,适合教学演示,但必须向初学者指出其局限性:当用户表数据超过1万行,List<User>加载到内存会导致OOM。因此,在listUsers()方法中,我预留了分页扩展点:
// 当前实现(教学版)
@RequestMapping("/list")
public ModelAndView listUsers() {
List<User> users = userService.findAll(); // 全量查询
ModelAndView mav = new ModelAndView("index");
mav.addObject("users", users);
return mav;
}
// 生产就绪版(预留接口)
// @RequestMapping("/list")
// public ModelAndView listUsers(@RequestParam(defaultValue = "1") int pageNum,
// @RequestParam(defaultValue = "10") int pageSize) {
// PageHelper.startPage(pageNum, pageSize); // 使用PageHelper插件
// List<User> users = userService.findAll();
// PageInfo<User> pageInfo = new PageInfo<>(users);
// ModelAndView mav = new ModelAndView("index");
// mav.addObject("pageInfo", pageInfo);
// return mav;
// }
PageHelper是MyBatis生态中最成熟的分页插件,只需在pom.xml添加依赖,并在spring-mvc.xml中配置拦截器,即可将findAll()自动转为SELECT * FROM user LIMIT 0,10。这个预留接口告诉学生:CRUD不是终点,而是性能优化的起点。当他们第一次看到PageInfo对象中pageNum、pageSize、total、list等字段时,分页的概念就从抽象名词变成了可触摸的代码实体。
5. 常见问题排查与独家避坑指南
5.1 启动失败类问题:从404到500的逐层诊断法
新手导入项目后,最常见的错误是启动报错。我整理了一份“症状-原因-解决方案”速查表,按发生频率排序:
| 错误现象 | 根本原因 | 解决方案 | 经验备注 |
|---|---|---|---|
| HTTP Status 404 – /xxx | 请求URL未被DispatcherServlet捕获 | 检查web.xml中<servlet-mapping>的<url-pattern>是否为/;确认spring-mvc.xml中<context:component-scan>的base-package是否包含Controller所在包 | 这是最高频问题!90%的404源于base-package写错,比如写成com.example.ctrl而实际是com.example.controller |
| HTTP Status 500 – java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet | SpringMVC依赖未正确引入 | 检查pom.xml中spring-webmvc是否在<dependencies>内,且<scope>未误设为test;在IDEA中右键项目→Maven→Reload | 若使用Eclipse,还需右键项目→Properties→Deployment Assembly,确认Maven Dependencies已勾选 |
| HTTP Status 500 – javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config | JSTL依赖缺失或版本不匹配 | 添加taglibs:standard:1.1.2依赖;确认jstl:1.2和standard:1.1.2同时存在 | Tomcat 9+必须双依赖,缺一不可 |
| HTTP Status 500 – org.apache.ibatis.binding.BindingException: Invalid bound statement (not found) | Mapper接口方法未被MyBatis扫描 | 检查spring-mvc.xml中<mybatis-spring:scan>的base-package是否指向mapper包;确认Mapper接口类上有@Mapper注解 | MyBatis 3.4+要求@Mapper注解,否则无法代理接口 |
诊断口诀:看日志,顺链条。启动时第一眼盯住控制台最后一行红色异常栈,如果是ClassNotFoundException,立刻查依赖;如果是BindingException,立刻查Mapper扫描配置;如果是SQLException,立刻查数据库连接URL和账号密码。
5.2 运行时类问题:中文乱码、空指针、SQL异常的根因定位
运行起来后,页面显示“???”、点击删除无反应、新增报错,这些问题更隐蔽,但排查逻辑更清晰:
中文乱码问题:
- 现象:数据库存的是中文,但JSP页面显示???;
- 根因:Tomcat默认编码为ISO-8859-1,需强制设为UTF-8;
- 解决:在web.xml中添加过滤器:
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
forceEncoding=true确保所有请求/响应强制UTF-8,这是解决乱码的终极方案。
空指针异常(NullPointerException):
- 现象:点击“编辑”报java.lang.NullPointerException at UserController.editUser(UserController.java:45);
- 根因:userService对象为null,说明Spring未完成依赖注入;
- 排查:检查UserServiceImpl类上是否有@Service注解;检查spring-mvc.xml中<context:component-scan>是否扫描到service包;检查UserService接口与实现类的包路径是否匹配。
SQL异常(如Duplicate entry):
- 现象:新增相同用户名时,页面报500,日志显示Duplicate entry 'zhangsan' for key 'username';
- 根因:数据库username字段有UNIQUE约束,但Controller层未做校验;
- 改进:在saveUser()方法中添加校验:
if (userService.findByUsername(username) != null) {
ModelAndView mav = new ModelAndView("add");
mav.addObject("error", "用户名已存在");
return mav;
}
并在add.jsp中显示:<c:if test="${not empty error}"><font color="red">${error}</font></c:if>。
最后分享一个血泪教训:某次我帮学生调试,发现所有功能正常,唯独删除后列表不刷新。折腾半小时,最后发现是
delete.jsp里写了<a href="delete?id=${user.id}">,但Controller方法名是deleteUser,URL映射却是@RequestMapping("/delete")——URL路径没错,但delete.jsp根本不存在!原来学生把delete.jsp误删了,Tomcat返回404,但浏览器静默失败,用户以为没反应。永远相信日志,不要相信眼睛——打开浏览器开发者工具的Network标签页,看/delete?id=1请求的Status是不是200,这才是真相。
6. 项目扩展与进阶学习路径
这个工程不是终点,而是你Java Web能力地图的坐标原点。基于它,你可以沿着三条主线自然延伸:
第一条线:现代化演进
- 将JSP替换为Thymeleaf:引入spring-boot-starter-thymeleaf,index.html中用<tr th:each="user : ${users}">替代<c:forEach>,享受HTML即模板的开发体验;
- 将MyBatis注解升级为XML配置:创建UserMapper.xml,把SQL从Java接口移到XML中,学习<resultMap>处理复杂关联查询;
- 将SpringMVC迁移到Spring Boot:用@SpringBootApplication替代web.xml,application.properties替代spring-mvc.xml,体验自动配置的便捷。
第二条线:工程能力加固
- 加入Logback日志:在src/main/resources/logback-spring.xml中配置<logger name="com.example" level="DEBUG"/>,让每个SQL执行、每个Controller调用都留下痕迹;
- 加入JUnit 5测试:为UserMapper写@Test方法,用@Sql注解初始化测试数据,验证insert、delete是否符合预期;
- 加入Lombok:在User.java上加@Data,一键生成getter/setter/toString,告别模板代码。
第三条线:架构视野拓展
- 引入Redis缓存:在UserService.findById()方法上加@Cacheable("users"),首次查询走数据库,后续走Redis,观察QPS提升;
- 实现RESTful API:将@RequestMapping("/list")改为@GetMapping("/api/users"),返回ResponseEntity<List<User>>,为未来对接Vue/React前端铺路;
- 集成Shiro权限:增加Role、Permission表,@RequiresPermissions("user:delete")控制删除按钮的可见性。
我个人在实际教学中发现,最好的学习节奏是“两周基础,一周扩展”:先用14天吃透本项目的所有CRUD细节,确保能独立修改Controller、调整SQL、读懂日志;再用7天选择一条主线做小实验,比如把JSP换成Thymeleaf,哪怕只改一个页面,你对模板引擎的理解就远超纯看书。记住,编程不是知识的堆积,而是肌肉记忆的形成——当你能不假思索地写出@SelectKey、@Transactional、CharacterEncodingFilter时,那些曾经晦涩的概念,就真的长进了你的身体里。
简介:一套可直接导入IDE运行的Java Web入门级项目,聚焦用户数据的增删改查完整流程。首页用JSP展示MySQL中的全部用户记录,点击‘新增’弹出表单提交新数据;每条记录右侧提供‘编辑’和‘删除’操作,编辑后刷新页面即同步更新,删除前触发浏览器原生确认提示。后端采用SpringMVC接收请求并返回ModelAndView跳转视图,MyBatis通过注解方式编写SQL完成数据库交互,所有依赖(Spring 5.x、MyBatis 3.x、MySQL Connector/J、JSTL、Servlet API等)已在pom.xml中声明并锁定版本。项目结构遵循标准Maven Web布局:src/main/java下分controller、service、mapper、entity四层,src/main/webapp存放JSP页面,WEB-INF目录包含web.xml和spring-mvc.xml配置文件。适合零基础学习MVC分层设计、理解前后端数据流转、快速搭建后台管理原型或用于高校Web开发课程演示。
&spm=1001.2101.3001.5002&articleId=162256247&d=1&t=3&u=d04e5ce351b94c2b8106c37a36f1122a)
1177

被折叠的 条评论
为什么被折叠?



