Java Web基础教学工程:SpringMVC+MyBatis+MySQL实现用户管理全流程(含JSP界面与Maven依赖配置)

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接导入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.jspmav.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_timeupdated_time使用DEFAULT CURRENT_TIMESTAMPON 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-webmvc5.3.37SpringMVC核心必须与spring-webspring-context同版本,否则@Controller扫描失败
org.mybatis:mybatis-spring1.3.2MyBatis与Spring整合不能用2.x版本!MyBatis-Spring 2.x要求Spring 5.2+且配置方式不同,本项目沿用经典SqlSessionFactoryBean方式
mysql:mysql-connector-java8.0.33MySQL驱动驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,连接URL必须匹配
javax.servlet:jstl1.2JSP标准标签库c:forEachc:if等标签必需,版本1.2是JSP 2.0+兼容性最佳选择
javax.servlet:javax.servlet-api4.0.1Servlet规范APIscope必须为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.jspedit.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接收idusernameemail,构建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";
}
  1. 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对象中pageNumpageSizetotallist等字段时,分页的概念就从抽象名词变成了可触摸的代码实体。

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.DispatcherServletSpringMVC依赖未正确引入检查pom.xml中spring-webmvc是否在<dependencies>内,且<scope>未误设为test;在IDEA中右键项目→MavenReload若使用Eclipse,还需右键项目→PropertiesDeployment Assembly,确认Maven Dependencies已勾选
HTTP Status 500 – javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/ConfigJSTL依赖缺失或版本不匹配添加taglibs:standard:1.1.2依赖;确认jstl:1.2standard: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-thymeleafindex.html中用<tr th:each="user : ${users}">替代<c:forEach>,享受HTML即模板的开发体验;
- 将MyBatis注解升级为XML配置:创建UserMapper.xml,把SQL从Java接口移到XML中,学习<resultMap>处理复杂关联查询;
- 将SpringMVC迁移到Spring Boot:用@SpringBootApplication替代web.xmlapplication.properties替代spring-mvc.xml,体验自动配置的便捷。

第二条线:工程能力加固
- 加入Logback日志:在src/main/resources/logback-spring.xml中配置<logger name="com.example" level="DEBUG"/>,让每个SQL执行、每个Controller调用都留下痕迹;
- 加入JUnit 5测试:为UserMapper@Test方法,用@Sql注解初始化测试数据,验证insertdelete是否符合预期;
- 加入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权限:增加RolePermission表,@RequiresPermissions("user:delete")控制删除按钮的可见性。

我个人在实际教学中发现,最好的学习节奏是“两周基础,一周扩展”:先用14天吃透本项目的所有CRUD细节,确保能独立修改Controller、调整SQL、读懂日志;再用7天选择一条主线做小实验,比如把JSP换成Thymeleaf,哪怕只改一个页面,你对模板引擎的理解就远超纯看书。记住,编程不是知识的堆积,而是肌肉记忆的形成——当你能不假思索地写出@SelectKey@TransactionalCharacterEncodingFilter时,那些曾经晦涩的概念,就真的长进了你的身体里。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接导入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开发课程演示。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值