资讯动态

Struts2实战:登录注册与用户管理全流程解析

发布时间:2026/10/17 12:28:38 来源:尧图企业网站定制
简介一套基于 Struts2 框架的登录注册及用户信息管理系统 Java Web 项目面向初学 MVC 架构与 Struts2 的开发者帮助理解用户认证、表单验证及数据库交互等基础模块。资源包共 74 个文件大小 4.27MB包含 13 个 Java 源码、13 个编译后 class、9 个 XML 配置、6 个 JSP 页面及 6 个 jar 依赖库另有 SQL Server 数据库文件mdf/ldf与图解文档可直接导入 MyEclipse 运行。项目通过 struts.xml 定义 Action 与 Result配合拦截器实现权限检查和数据校验视图层用 JSP 展示用户列表、注册与修改页面后端以 SQL Server 存储用户信息。已有 843 人学习下载。压缩包内附使用说明文档、数据库脚本和完整目录结构便于按模块拆解学习结合描述中的知识点可掌握 Struts2 核心配置、MVC 分层、用户认证授权和 JDBC 操作等实用技能适合作为课程设计或入门练手参考。1. 一份Struts2老项目的正确打开方式登录、注册与用户管理全流程某开发者接手一个老业务系统时碰到一台只装了Tomcat和MySQL的老机器上面跑着一个用Struts2写的登录注册与用户信息管理系统。这个项目没有Spring、没有MyBatis所有请求转发和验证逻辑都写在struts.xml、Action类和JSP里。它要解决的事情很具体用户从注册页提交数据Action接收参数后交给DAO写进MySQL登录时把表单数据交给登录校验拦截成功就把用户信息放进Session登录后还能对用户数据做增删查改。这套流程就是传统JavaWeb的MVC经典链路。它适合刚学完Servlet、想换一种MVC写法的学生也适合天天写SpringBoot、想回头看看web.xml和filter-mapping怎么工作的开发。跑通后它本质上就是一份Struts2时代的最小可运行样本。整个项目拉下来大概能看到四件事Action如何被XML映射、值栈如何传递参数、用户登录状态如何用Session维护以及页面跳转的result如何配置。下文就按「先看结构、再拆代码、然后跑起来、最后动手改」的顺序来拆。2. 项目结构与技术栈三层架构里的Model、Action与JSP如何分工2.1 目录结构先搞清楚每个文件夹该放什么Struts2项目没有SpringBoot那种约定大于配置的自动扫描所有类都要显式放到classes目录下由构建工具去拷贝。这个项目的目录结构很典型解压后你会看到标准的JavaWeb布局WebContent/ ├── META-INF/ ├── WEB-INF/ │ ├── lib/ # 全部jar包struts2-core、xwork等 │ ├── web.xml # 核心Filter注册和欢迎页配置 │ ├── pages/ │ │ ├── login.jsp │ │ ├── register.jsp │ │ ├── list.jsp # 用户列表页 │ │ └── update.jsp # 编辑用户信息页 │ └── views/ # 部分版本把JSP放在这里与pages二选一 ├── css/ ├── js/ ├── index.jsp # 首页跳转 └── struts.xml # 注意位置放src下最终生成到classes这里最容易翻车的是struts.xml的位置。老项目里它通常不在WebContent下而是放在src根目录构建时被拷贝到classes/WEB-INF/classes路径下。如果你只用IDE打开WebContent目录找配置文件十有八九会误判项目缺文件。src/ ├── com/example/action/ │ ├── UserAction.java # 登录、注册、增删改查的Action入口 │ └── BaseAction.java # 抽取公共方法 ├── com/example/dao/ │ ├── UserDAO.java # JDBC数据操作层 │ └── DBUtil.java # 连接管理 ├── com/example/model/ │ └── User.java # 实体类对应user表 ├── struts.xml # 动作映射和result跳转 ├── jdbc.properties # 数据库连接参数 └── log4j2.properties # 日志输出配置分包逻辑一眼能看明白action层接收请求参数、调用dao层返回结果、在model里存放字段JSP负责展示。唯一和现在SpringMVC工程不同的地方在于Action里的每个方法名对应struts.xml里method属性一个Action类可以被多个动作映射复用这也是接下来要说的核心机制。2.2 技术栈与关键依赖为什么这个项目没有Spring技术栈非常朴素Struts2作为MVC控制器JDBC直接访问MySQLJSP做视图层前端只有原生JavaScript和CSS。没有MyBatis、没有Spring容器Action的实例化完全由Struts2框架自己搞定。依赖清单集中在lib目录常用的就是下面这几个依赖作用说明struts2-core-x.x.jarMVC核心框架提供Action、拦截器、值栈机制xwork-core-x.x.jar底层框架被struts2-core依赖处理OGNL表达式commons-fileupload文件上传支持本次未用到但框架默认加载mysql-connector-javaJDBC驱动版本要和MySQL对应jstl标签库JSP页面里迭代用户列表时使用log4j2日志排查Action是否正确执行版本匹配是这个项目最玄学的地方。struts2-core和xwork的版本一旦错位启动时报的异常非常难读常见报错是Unable to load configuration或者method不能解析。我一般直接锁定struts2-core 2.5.30搭配同期的xwork不要随便升到新版因为2.5之后部分标签库和struts-tags的解析逻辑变化很大老代码容易踩坑。2.3 配置文件逐个说明struts.xml、web.xml与jdbc.properties先看web.xml这里注册了Struts2的核心过滤器。Struts2从2.5版本开始强制要求使用StrutsPrepareAndExecuteFilter老版本的FilterDispatcher已被移除如果你的lib里同时存在新老两份jar启动时会报Filter映射冲突。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-nameStruts2 Login Demo/display-name filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app这段配置声明所有请求先经过Struts2过滤器由框架决定走哪个Action。url-pattern写/*是常见做法但如果你在同一个Web应用里同时挂了Servlet和Struts2这里就要小心Struts2过滤器会拦截所有请求路径包括静态资源。所以一般搭配的静态资源目录不放页面里而是单独用JSP的basePath指向。struts.xml是核心里面有动作映射与result的定义。每个action标签的name对应浏览器URL的路径class指定Action类method指定调用哪个方法result定义了方法返回后的跳转方式。以登录功能为例?xml version1.0 encodingUTF-8 ? !DOCTYPE struts PUBLIC -//Apache Software Foundation//DTD Struts Configuration 2.5//EN http://struts.apache.org/dtds/struts-2.5.dtd struts constant namestruts.devMode valuetrue / constant namestruts.i18n.encoding valueUTF-8 / package namedefault extendsstruts-default namespace/admin action namelogin classcom.example.action.UserAction methodlogin result namesuccess/WEB-INF/pages/list.jsp/result result nameerror/WEB-INF/pages/login.jsp/result /action action nameregister classcom.example.action.UserAction methodregister result namesuccess typeredirectActionlogin/result result nameerror/WEB-INF/pages/register.jsp/result /action action namelist classcom.example.action.UserAction methodlist result namesuccess/WEB-INF/pages/list.jsp/result /action /package /struts这里namespace/admin是关键参数。它意味着所有访问路径都要带/admin/login.action这样的前缀而不是直接login.action。很多新手在这个地方直接复制配置结果页面链接写的是login请求发出去变成404这就是namespace没对上。result的typeredirectAction表示注册成功后浏览器重新请求login动作而不是直接转发页面这样能避免表单重复提交。jdbc.properties承担数据库连接参数全部放在src根目录用于加载驱动我实际测试时建议用这种格式jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/userdb?useUnicodetruecharacterEncodingUTF-8useSSLfalse jdbc.usernameroot jdbc.password123456连接串里的characterEncodingUTF-8必须保留。注册中文用户名时如果漏了这个参数乱码会一路从页面传到数据库查错时非常痛苦。useSSLfalse是为了避开本地MySQL证书提示非生产环境加上没毛病。3. 核心模块拆解一个登录请求从JSP到DAO的完整链路3.1 登录页面与UserAction的初始化和execute方法登录页本身就是一个普通的JSP加一点点JavaScript校验。核心逻辑不在页面而在提交地址。页面里的form action指向/admin/login.actionname属性对应UserAction里的字段% page contentTypetext/html;charsetUTF-8 languagejava % html body form idloginForm action${pageContext.request.contextPath}/admin/login.action methodpost div 用户名input typetext nameuser.username idusername/ /div div 密码input typepassword nameuser.password idpassword/ /div div button typebutton onclicksubmitLogin()登录/button /div /form script function submitLogin() { var username document.getElementById(username).value.trim(); var password document.getElementById(password).value.trim(); if (username || password ) { alert(用户名和密码不能为空); return; } document.getElementById(loginForm).submit(); } /script /body /html这段代码里input的name是user.username它会在提交时被Struts2的参数拦截器读取通过OGNL表达式自动映射到UserAction的user字段User对象的username属性上。省略了request.getParameter那一整套模板代码这就是Struts2相对Servlet最直观的体验差异。对应的UserAction登录方法如下public class UserAction extends ActionSupport { private User user; private ListUser userList; private String message; private UserDAO userDAO new UserDAO(); public String login() throws Exception { if (user null || user.getUsername() null) { return INPUT; } User dbUser userDAO.findByUsername(user.getUsername()); if (dbUser ! null dbUser.getPassword().equals(user.getPassword())) { MapString, Object session ActionContext.getContext().getSession(); session.put(currentUser, dbUser); userList userDAO.findAll(); return SUCCESS; } message 用户名或密码错误; return ERROR; } // getter 和 setter 省略但必须存在 public User getUser() { return user; } public void setUser(User user) { this.user user; } public ListUser getUserList() { return userList; } public void setUserList(ListUser userList) { this.userList userList; } public String getMessage() { return message; } public void setMessage(String message) { this.message message; } }注意细节getUser/setUser必须成对出现少了任何一个Struts2的参数映射都会静默失效user会一直是null登录永远走INPUT分支。这个坑我踩过一整晚了才定位到是setter没写。方法返回的字符串常量如SUCCESS、ERROR、INPUT来自ActionSupport和struts.xml里result namesuccess的name字段一一对应。登录成功后把currentUser存入Session接着查出完整用户列表并放到userList属性里JSP端通过迭代userList渲染表格。3.2 注册功能与密码处理注册逻辑和登录几乎对称但多了三个环节验证两次密码是否一致、检查用户名是否重复、新增用户记录。DAO层的findByUsername在这个方法里展示了它的第二个用途重复校验public String register() throws Exception { if (user null || user.getPassword() null || user.getUsername() null) { message 请填写完整信息; return ERROR; } if (userDAO.findByUsername(user.getUsername()) ! null) { message 用户名已存在换一个吧; return ERROR; } int result userDAO.insert(user); if (result 0) { return SUCCESS; } else { message 注册失败请稍后重试; return ERROR; } }这个项目原样没有对密码做加密直接明文存数据库。这点拿出来看会觉得不安全但考虑到它是一个教学演示项目倒也能理解。如果你把它改成自己的毕业设计两个必改点是密码通过MD5或BCrypt加密后再落库加一个注册成功后的自动登录跳转。我在自己的版本里常用BCrypt加一层核心代码在service层多一次调用不改变Action的返回逻辑。3.3 用户信息管理列表查询、更新与删除列表查询在login里已经有了雏形更标准的做法是单独一个list方法方便登录后点击侧边栏菜单重新加载public String list() throws Exception { userList userDAO.findAll(); return SUCCESS; }参数全部通过struts.xml的action配置传递不用在每个链接后手写问号参数。但要注意如果userList是nullJSP迭代时会发生空指针异常所以在登录失效后直接访问/admin/list.action你会看到500页面而不是登录页。老项目中常见的补救办法是在list方法开头做一次Session检查如果currentUser不存在就重定向到login这比单纯依赖拦截器更直白。更新和删除分别是update和delete方法。更新页比较有意思因为它要回显原数据所以路径里通常带一个userIdAction通过改方法参数声明public String edit() throws Exception { int userId Integer.parseInt(ServletActionContext.getRequest().getParameter(userId)); user userDAO.findById(userId); return SUCCESS; } public String update() throws Exception { userDAO.update(user); return SUCCESS; }删除则是简单的userDAO.delete(user.getId())。在老项目里删除之后通常return SUCCESS转发到list页面因为转发不会刷新浏览器地址栏所以列表数据重新查询反而是更安全的做法。3.4 一次请求的生命周期从URL到JSP完整走查把上面几段代码串起来一个普通的登录请求在Struts2里会经历这样几个阶段。浏览器提交/admin/login.action请求先到达Tomcat被StrutsPrepareAndExecuteFilter截获框架根据namespace和action name匹配struts.xml中的login动作。匹配过程中参数拦截器把request参数注入到Action的user字段然后执行method指定的login方法。方法返回SUCCESS后框架读取对应的result配置正向转发到list.jspJSP从值栈中取得userList并循环渲染成表格。这个链路里有几个关键点值得展开。第一值栈中Action对象本身就是一个栈顶元素JSP里直接写userList能访问到Action的getUserList返回值这是OGNL的默认行为。第二result配置中不带type时默认是转发dispatcher带redirectAction时才会发起第二次请求。第三参数注入发生在Action执行之前任何方法里都能直接使用user对象不需要自己手动解析请求参数。如果要在登录后统一下发权限校验正确姿势是写一个自定义拦截器覆盖struts-default里的默认拦截器栈。比如这样public class LoginInterceptor extends AbstractInterceptor { Override public String intercept(ActionInvocation invocation) throws Exception { MapString, Object session invocation.getInvocationContext().getSession(); if (session.get(currentUser) null) { return login; } return invocation.invoke(); } }然后在struts.xml的package里配置default-interceptor-ref指向新拦截器栈。这样一个登录拦截就能覆盖整个admin包下的所有动作比在每个方法里写Session判断干净得多。4. 运行与调试构建部署、常见报错与避坑排查手册4.1 从零跑起来Eclipse或IDEA下的构建与部署这个老项目最常见的运行方式是整包导入IDE后直接发布到Tomcat。先说准备工作JDK1.8或以上版本Tomcat 8.5或9.xMySQL 5.7。数据库脚本要手动执行表结构大概是user表包含id、username、password、email、phone、create_time等字段。以IDEA为例导入WebContent目录为Web项目后首先检查Project Structure里的Artifacts是不是Web Application Exploded类型保证WEB-INF/classes目录存在然后执行Build菜单下的Rebuild Project。构建产物里必须出现struts.xml和jdbc.properties如果没有你要在Project Structure里把src目录标记为Sources Root。然后配置Tomcat# 在Tomcat的conf/context.xml中确认工作目录指向项目输出目录 Context docBase/Users/you/projects/login-demo/out/artifacts/login_demo_war_exploded path/login reloadabletrue/ # 启动Tomcat观察日志 catalina outTomcat启动后可以在浏览器直接访问http://localhost:8080/login/index.jspindex.jsp会自动跳转到login页面。如果页面404先确认访问路径是否带上了/login上下文。如果启动时异常多半是lib目录的jar包版本冲突用jar tf检查一下具体冲突的包名。4.2 五个常见报错按现象、原因到解决来排查第一个点击登录按钮没反应浏览器控制台提示404。现象是地址栏URL路径是/admin/login.action直接变成404。原因是struts.xml里namespace配置的是/admin但页面form的action写成了login.action两层路径没对齐。解决方法是统一以${pageContext.request.contextPath}/admin/login.action来拼路径或者在struts.xml里把namespace改成/二选一别混着来。第二个启动报Unable to load configuration。现象是Tomcat启动过程中直接抛异常后面的Action基本全部不可用。原因是struts2-core与xwork-core版本不匹配或者struts.xml里引用了不存在的拦截器类名称。解决方法是先检查lib目录里是否同时存在struts2-core和xwork两个包且版本一致然后逐行检查struts.xml的package配置注释掉多余拦截器再重新启动。第三个注册中文用户名变成问号。现象是数据库里显示一串???页面显示正常。原因是MySQL连接串没有设置编码同时JSP页面编码和数据库编码不一致。解决方法是先从jdbc.properties开始排查把driver连接串带上characterEncodingUTF-8再把数据库表和库的charset都改成了utf8mb4重启后数据恢复正常。第四个本地能跑部署到另一台电脑后CSS全部错乱。现象是页面能出来但样式和JS全部失效查看网络请求发现css文件夹找不到。原因是JSP里的样式引用用了相对路径比如css/style.css在多层目录下相对路径解析错误。解决方法是统一改成绝对路径在JSP头部加上String path request.getContextPath();再用${path}/css/style.css引用资源。第五个删除用户后列表显示的还是旧数据。现象是点了删除数据库记录确实没了但页面列表还是原来的样子。原因是Action删除后返回SUCCESS转发到同一个list.jsp页面转发不刷新浏览器页面拿到的是上一次查询的结果。解决方法是把删除的result改成typeredirectAction指向list动作强制浏览器发起新一轮list请求这样数据必然是最新的。4.3 日志排查技巧log4j2怎么辅助定位Action老项目的排错不依赖IDE调试器大部分问题都能在日志里找到直接线索。这个项目用的log4j2默认配置会输出到控制台和文件。如果某个Action始终返回500先看日志里有没有Method failed这种异常通常后面紧跟异常栈直接指出是哪一行代码出的问题。我的习惯是在Action类里加上一行类名日志启动后能看到每个Action的实例创建时间。方法执行前后手动打日志能快速判断参数是否注入成功。比如登录方法入口打印user.username如果打印出来是空说明参数映射挂了优先检查表单name属性和Action的setter方法命名。日志级别不要一直开着DEBUG生产环境或调试Windows环境下建议设成INFO否则会被Log4j的类加载日志刷屏。5. 二次开发给系统加一个带分页的用户列表与搜索筛选5.1 先明确改造目标缺什么补什么原始项目的用户列表是一次性全量查出所有用户没有分页也没有搜索。实际使用中只要数据量过百这种列表就没法用所以第一个改造点是把list方法改成分页查询。我在自己维护的版本里会新增一个PageBean操作类核心逻辑在DAO层通过LIMIT语句完成Action端接收page和keyword两个参数。下面给出完整可运行的DAO改造方案。public ListUser findByPage(int pageNum, int pageSize, String keyword) { ListUser list new ArrayListUser(); String sql SELECT id, username, email, phone FROM user WHERE username LIKE ? LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (pageNum - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setEmail(rs.getString(email)); u.setPhone(rs.getString(phone)); list.add(u); } } } catch (Exception e) { e.printStackTrace(); } return list; }这段代码的解释LIMIT的第一个参数是起始位置页面传进来的pageNum需要减一再乘pageSize。数据量大时这种物理分页是性价比最高的方案。代码里用了PreparedStatement来拼接参数不要用字符串拼接SQL否则用户名里的单引号会直接让查询崩掉。改造Action时只需要在list方法前面加两个参数接收private int pageNum 1; private int pageSize 10; private String keyword ; public String list() throws Exception { userList userDAO.findByPage(pageNum, pageSize, keyword); totalCount userDAO.count(keyword); pageCount (totalCount pageSize - 1) / pageSize; return SUCCESS; }参数由Struts2自动注入URL里传pageNum2keywordabc就能分别生效。count方法也很关键它统计过滤后的总条数用于计算总页数。改造后的JSP底部可以放上上一页、下一页按钮通过jsp的输出链接把pageNum携带过去。5.2 session失效后的重定向问题如果改完分页你会发现一个隐藏问题直接在GitHub把项目拉下来不一定能跑。别急我先把登录跳转和header部分独立成公共JSP片段然后为拦截器配置一个guest让它保持匿名最后用window.location在前端把session超时的用户弹回到登录页。这个问题不解决下一页按钮点击多次后session超时再点上一页就会被拦截器重返登录页但改了session路径就能避免。5.3 我的改造习惯从那以后我每次拿到一个老项目都会先把配置文件全部看一遍再碰代码而不是打开跑起来就完事。这个习惯的核心就两点第一先把struts.xml的包结构和拦截器栈画成一条请求链确认哪些Action缺Session校验第二一定要用版本锁定的方式管理jar包避免部署环境差异造成玄学问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑