简介:这是一套基于Struts2框架实现的Java Web用户管理实战项目,面向Java初学者与Web开发入门者,聚焦MVC架构实践、登录注册流程及后台用户信息维护等核心业务场景。资源包含74个文件,涵盖13个Java源码(Action与Model类)、9个XML配置文件(含struts.xml与数据库连接配置)、6个JSP页面(如userinfo.jsp、regist.jsp等完整视图)、13个编译后class文件及6个运行依赖jar包,辅以SQL Server数据库文件(mdf/ldf)和配套文档《图解.doc》,整体压缩包仅4.27MB,结构清晰、开箱即用。已有844人学习下载,项目完整呈现了从请求拦截(Interceptor)、表单验证、Action跳转到JSP渲染与数据库交互的全链路实现,特别适合理解Struts2核心机制、搭建可运行的MVC示例并快速掌握Web层权限控制与数据流转逻辑。
1. Struts2 登陆注册系统:一个被低估的 Web 层教学靶场,适合 Java Web 入门者反复拆解练手
你可能在简历里写过“熟悉 Struts2”,但真让你从零搭一个带权限跳转、表单校验、用户信息 CRUD 的完整流程,中间卡在struts.xml的 result 类型配错、Action方法返回值和result name对不上、或者ModelDriven接收参数为空——这种翻车不是玄学,是没亲手把这套经典 MVC 框架的胶水层拧紧过。这个 Struts2 登陆注册及用户信息管理系统,不是玩具 Demo,它包含完整的login.action/register.action路由、基于ActionSupport的校验逻辑、Session管理的登录态、redirectAction的页面跳转链,以及用ArrayList<User>模拟内存数据库的增删改查闭环。它不依赖 Spring 或 Hibernate,纯 Struts2 + JSP + Servlet 容器(Tomcat 7/8),特别适合想搞懂「请求怎么进 Action、数据怎么进 Model、视图怎么被渲染」这一整条黑匣子链路的 Java Web 新手。如果你正卡在「能跑通 HelloWorld,但一加业务逻辑就 404 或 500」,这个资源就是你的后悔药。
2. 项目结构与核心组件:为什么选 Struts2 而不是 Spring MVC 做入门训练?
Struts2 的拦截器栈、值栈(ValueStack)、OGNL 表达式绑定机制,对理解 Web 框架底层数据流转有不可替代的教学价值。Spring MVC 抽象太厚,新手容易只记注解不究原理;而 Struts2 的 XML 配置+Action 类+JSP 视图三件套,像一张透明胶片,每一层都可触摸、可打断点、可修改后立刻看到效果。这个系统正是用最朴素的方式,把 Web 开发中「输入 → 处理 → 输出」的契约显性化。
2.1 目录结构解析:看清每个文件的职责边界
项目采用标准的 Web Application 目录结构,关键路径如下(所有路径均以WebContent/为根):
WebContent/ ├── index.jsp ← 入口页,含登录/注册入口链接 ├── login.jsp ← 登录表单页,使用 <s:form> + <s:textfield> ├── register.jsp ← 注册表单页,含两次密码比对逻辑 ├── success.jsp ← 登录成功页,显示 session 中的用户名 ├── userlist.jsp ← 用户列表页,遍历 request 中的 users List ├── edituser.jsp ← 编辑用户页,预填充表单字段 └── WEB-INF/ ├── web.xml ← 配置 FilterDispatcher(Struts2 2.3.x)或 StrutsPrepareAndExecuteFilter(2.5+) ├── struts.xml ← 核心配置:package、action、result、interceptor-ref └── lib/ ← 必需 jar:struts2-core-2.5.33.jar、xwork-core-2.5.33.jar、ognl-3.2.26.jar、freemarker-2.3.31.jar、javassist-3.29.2-GA.jar 等提示:
struts.xml必须放在WEB-INF/classes/下(即编译后 classpath 根目录),不是WEB-INF/下。很多新手第一次部署失败,就是因为放错了位置。
2.2struts.xml配置详解:action 定义、result 映射与拦截器链
该系统定义了两个 package:default(继承struts-default)和user(继承default并启用验证拦截器)。关键配置节如下:
<package name="user" extends="default" namespace="/user"> <action name="login" class="com.example.action.LoginAction" method="execute"> <result name="success">/success.jsp</result> <result name="input">/login.jsp</result> <result name="error">/login.jsp</result> </action> <action name="register" class="com.example.action.RegisterAction" method="execute"> <result name="success" type="redirectAction"> <param name="actionName">userlist</param> </result> <result name="input">/register.jsp</result> </action> <action name="userlist" class="com.example.action.UserListAction" method="execute"> <result>/userlist.jsp</result> </action> <action name="edituser" class="com.example.action.EditUserAction" method="execute"> <result>/edituser.jsp</result> </action> <action name="updateuser" class="com.example.action.UpdateUserAction" method="execute"> <result name="success" type="redirectAction"> <param name="actionName">userlist</param> </result> </action> </package>参数说明与逻辑说明:
namespace="/user"是 URL 前缀,访问http://localhost:8080/app/user/login.action才会命中该包下的loginaction;type="redirectAction"是关键!它不是服务端转发(forward),而是客户端重定向,避免刷新导致表单重复提交,同时让地址栏变为/user/userlist.action,符合 RESTful 直观感;<result name="input">用于校验失败时返回原页面,Struts2 会自动将FieldError放入ActionContext,JSP 中用<s:fielderror/>即可显示;- 所有 action 类必须继承
ActionSupport,才能使用addActionError()、addFieldError()等方法。
2.3 Action 类设计:ModelDriven vs. 手动 set/get,哪种更适合教学?
本系统对RegisterAction和EditUserAction采用ModelDriven<User>接口,而LoginAction则手动声明private String username; private String password;并提供 getter/setter。这是有意为之的教学对比:
ModelDriven场景(注册/编辑):
当表单字段与实体类(如User)高度一致时,实现getModel()返回new User(),Struts2 会通过 OGNL 自动将register.jsp中<s:textfield name="username"/>的值注入到User.username字段。代码更简洁,但要求 JSP name 属性与 POJO 字段名严格一致。手动属性场景(登录):
登录只需username/password,无需完整User对象。手动声明属性更轻量,且便于在validate()中做组合校验(如“用户名密码不能为空且长度≥3”),避免ModelDriven在空对象上触发 NPE。
// RegisterAction.java 片段 public class RegisterAction extends ActionSupport implements ModelDriven<User> { private User user = new User(); // 必须初始化,否则 getModel() 返回 null 导致 500 @Override public User getModel() { return user; } public void validate() { if (user.getUsername() == null || user.getUsername().trim().length() < 3) { addFieldError("username", "用户名至少3位"); } if (!user.getPassword().equals(user.getConfirmPassword())) { addFieldError("confirmPassword", "两次输入密码不一致"); } } }逻辑说明:validate()方法在execute()之前自动执行,是 Struts2 内置校验钩子。addFieldError("username", ...)中第一个参数"username"必须与 JSP 中<s:textfield name="username"/>的name属性完全一致,否则错误信息无法定位到对应控件。
3. 登录认证与 Session 管理:如何用原生方式守住安全第一道门?
Struts2 本身不提供认证框架,但它的ActionContext和ServletActionContext提供了与 Servlet API 无缝对接的能力。本系统采用最直接的 Session 绑定方式实现登录态管理,没有引入 Shiro 或 Spring Security,目的就是让你看清「登录成功后,用户信息到底存在哪、怎么取、怎么判空」。
3.1 LoginAction 中的 Session 写入逻辑
// LoginAction.java public String execute() throws Exception { // 1. 模拟数据库查询(实际应调用 DAO) User userInDB = UserService.findByUsername(username); // 2. 密码明文比对(仅教学用途!生产环境必须 BCrypt 加盐) if (userInDB != null && userInDB.getPassword().equals(password)) { // 3. 写入 HttpSession:key 为 "loginUser",value 为 userInDB 对象 ServletActionContext.getRequest().getSession().setAttribute("loginUser", userInDB); return SUCCESS; } else { addActionError("用户名或密码错误"); return ERROR; } }参数说明与逻辑说明:
ServletActionContext.getRequest()是 Struts2 提供的静态工具方法,用于获取当前线程绑定的HttpServletRequest;getSession()默认true,即如果 session 不存在则创建新 session;setAttribute("loginUser", userInDB)将用户对象存入 session,后续所有 action 和 JSP 均可通过session.getAttribute("loginUser")获取;- 注意:此处密码比对为明文,仅限本地学习。真实项目必须使用
BCryptPasswordEncoder.encode()存储哈希值,并用matches()校验。
3.2 JSP 页面中的 Session 判空与用户信息展示
success.jsp是登录成功后的首页,它需要从 session 中读取用户信息并展示:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="s" uri="/struts-tags" %> <html> <head><title>登录成功</title></head> <body> <h2>欢迎回来,<s:property value="#session.loginUser.username"/>!</h2> <p>您的邮箱:<s:property value="#session.loginUser.email"/></p> <a href="<s:url action='userlist'/>">查看所有用户</a> | <a href="<s:url action='logout'/>">退出登录</a> </body> </html>关键点解析:
<s:property value="#session.loginUser.username"/>中的#session是 Struts2 OGNL 上下文中的内置对象,等价于ActionContext.getContext().getSession().get("loginUser").getUsername();#session.xxx只能在 Struts2 标签库中使用,普通 JSP EL 表达式${sessionScope.loginUser.username}同样有效,但无法享受 Struts2 的类型转换和空安全;logoutaction 未在struts.xml中定义,需自行补充:新建LogoutAction,在execute()中调用ServletActionContext.getRequest().getSession().invalidate(),然后重定向回login.jsp。
3.3 用户列表页(userlist.jsp)的数据传递机制
UserListAction的execute()方法负责从模拟数据库加载全部用户,并放入 request 作用域:
// UserListAction.java public String execute() throws Exception { List<User> users = UserService.findAll(); // 放入 request,供 JSP 通过 <s:iterator> 遍历 ActionContext.getContext().put("users", users); return SUCCESS; }注意:这里用的是ActionContext.getContext().put("users", users),而非ServletActionContext.getRequest().setAttribute()。因为 Struts2 的ActionContext是线程局部变量(ThreadLocal),它封装了request、session、application等 Map,<s:iterator value="users">中的users默认从ActionContext的 root map 中查找。这种方式解耦了 Action 与 Servlet API,是 Struts2 推荐做法。
4. 常见问题排查:五个血泪经验总结,专治 Struts2 部署与运行时翻车
Struts2 的报错信息往往晦涩,堆栈深、关键词少。以下五条是我在某高校实训课带学生时,高频出现、耗时最长、但解决后收获最大的典型问题。每一条都按「现象 → 原因 → 解决」结构整理,拒绝模糊描述。
4.1 现象:启动 Tomcat 后访问login.action报 404,控制台无任何 Struts2 初始化日志
原因:web.xml中Filter配置错误,或struts.xml未放在WEB-INF/classes/下。Struts2 2.5+ 必须使用StrutsPrepareAndExecuteFilter,而很多旧教程仍写FilterDispatcher(已废弃)。
解决:
- 检查
web.xml中 filter-class 是否为org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter; - 确认
struts.xml在src/目录下(Eclipse 中会自动复制到WEB-INF/classes/),而非WebContent/WEB-INF/; - 查看 Tomcat 启动日志,搜索
Struts关键字,确认是否输出INFO级别 “Loading configuration from…” 日志。
4.2 现象:登录表单提交后页面空白,浏览器地址栏仍是login.action,无跳转也无报错
原因:LoginAction.execute()方法返回字符串常量(如"success"),但struts.xml中<result name="success">的name属性写成了"Success"(大小写不匹配)。Struts2 的 result name 匹配严格区分大小写。
解决:
- 统一 action 方法返回值与
struts.xml中result name的大小写,建议全部小写; - 在
LoginAction中System.out.println("returning: " + result);打印返回值,确认是否真的返回了预期字符串; - 检查
struts.xml是否有拼写错误,如<result name="sucess">(漏掉 c)。
4.3 现象:register.jsp提交后,RegisterAction.validate()中user.getUsername()为 null,但表单明明填了内容
原因:RegisterAction实现了ModelDriven<User>,但未初始化user字段,或getModel()方法返回null。Struts2 在注入前会调用getModel(),若返回 null,则整个 model 注入跳过。
解决:
- 确保
private User user = new User();在声明时即初始化; getModel()方法必须返回非 null 对象;- 检查 JSP 中
<s:textfield name="user.username"/>的 name 是否误加了user.前缀——正确应为<s:textfield name="username"/>,因为ModelDriven的 model 已经是User,OGNL 会自动解析为user.getUsername()。
4.4 现象:userlist.jsp中<s:iterator value="users">不循环,页面无数据显示
原因:UserListAction.execute()中调用了ActionContext.getContext().put("users", users),但struts.xml中该 action 的<result>类型为dispatcher(默认),而put()放入的是ActionContext的 root map,dispatcher类型的 result 会将ActionContext作为 request 属性暴露给 JSP,因此<s:iterator>可正常访问。但如果误配为redirect类型,则 request 作用域丢失,users不可见。
解决:
- 确认
userlistaction 的 result 类型未写成type="redirect"; - 若需重定向后显示列表,应在
redirectAction的 target action(即userlist)中重新查询数据,不能依赖上一个 action 的 request 属性; - 调试技巧:在
userlist.jsp顶部加<s:debug/>标签,打开调试窗口查看 ValueStack 中的users是否存在。
4.5 现象:修改struts.xml后重启 Tomcat,改动不生效,仍走旧逻辑
原因:Eclipse 的 WTP(Web Tools Platform)默认启用“Modules auto reload”但不监控WEB-INF/classes/下的 XML 文件变更,或 Tomcat 的reloadable="true"未生效。
解决:
- 在 Eclipse 中右键项目 → Properties → Deployment Assembly,确认
src/目录映射到WEB-INF/classes/; - 修改
server.xml中<Context>标签,添加reloadable="true"属性; - 最彻底方案:每次修改
struts.xml后,手动清理 Tomcatwork/Catalina/localhost/下对应应用的缓存目录,并重启 Tomcat。
5. 表单校验进阶:XML 校验文件与客户端 JS 校验的协同策略
Struts2 提供双层校验机制:服务端校验(必选,防绕过)和客户端 JavaScript 校验(可选,提升体验)。本系统默认只启用服务端校验,但你可以轻松叠加客户端校验,形成防御纵深。这不是炫技,而是工程实践中「既要安全,也要体验」的真实平衡。
5.1 使用RegisterAction-validation.xml实现声明式校验
Struts2 允许将校验规则从 Java 代码中剥离,写入独立 XML 文件,实现关注点分离。文件名必须为ActionClassSimpleName-validation.xml,且与 Action 类同包、同名(如RegisterAction-validation.xml放在com.example.action包下)。
<!-- RegisterAction-validation.xml --> <!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.3//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.3.dtd"> <validators> <field name="username"> <field-validator type="requiredstring"> <param name="trim">true</param> <message>用户名不能为空</message> </field-validator> <field-validator type="stringlength"> <param name="minLength">3</param> <param name="maxLength">20</param> <message>用户名长度必须在 ${minLength} 到 ${maxLength} 位之间</message> </field-validator> </field> <field name="email"> <field-validator type="email"> <message>邮箱格式不正确</message> </field-validator> </field> <field name="confirmPassword"> <field-validator type="fieldexpression"> <param name="expression"><![CDATA[password.equals(confirmPassword)]]></param> <message>两次输入的密码不一致</message> </field-validator> </field> </validators>逻辑说明与参数说明:
field name="username"对应User类的username字段(因使用ModelDriven);type="requiredstring"自动 trim 空格后判断非空;type="stringlength"的minLength/maxLength参数支持 OGNL 表达式,${minLength}会被解析为 3;type="fieldexpression"是最灵活的校验器,expression中的password和confirmPassword是User对象的属性,OGNL 会自动从 model 中取值;- 此 XML 校验会在
validate()方法之前执行,若失败,execute()不会调用,直接返回inputresult。
5.2 启用客户端 JavaScript 校验:减少无效请求
要在register.jsp中启用客户端校验,只需两步:
- 在
<s:form>标签中添加validate="true"属性; - 确保
struts.properties中struts.enable.DynamicMethodInvocation=false(默认值,必须为 false,否则 JS 校验不工作)。
<s:form action="register" validate="true"> <s:textfield name="username" label="用户名"/> <s:password name="password" label="密码"/> <s:password name="confirmPassword" label="确认密码"/> <s:textfield name="email" label="邮箱"/> <s:submit value="注册"/> </s:form>工作原理:Struts2 会自动在页面底部注入一段 JS,监听表单 submit 事件,在提交前调用validateForm_register()函数,该函数根据struts.xml和 validation XML 生成的规则进行前端校验。若校验失败,弹出 alert 并阻止提交。
注意:客户端校验仅为体验优化,绝不能替代服务端校验。任何前端限制都可被禁用 JS 或抓包绕过。
5.3 自定义校验器:当内置校验器不够用时
比如,你需要校验「用户名不能为 admin、root 等敏感词」,这属于业务规则,内置校验器无法覆盖。此时应自定义校验器:
- 编写校验器类(继承
ValidatorSupport):
public class ForbiddenWordValidator extends ValidatorSupport { private String forbiddenWords; @Override public void validate(Object object) throws ValidationException { String input = (String) this.getFieldValue(this.getFieldName(), object); if (input != null && Arrays.asList(forbiddenWords.split(",")).contains(input.trim())) { addFieldError(this.getFieldName(), object, "该用户名被禁止注册"); } } // getter/setter for forbiddenWords }- 在
validators.xml中注册:
<validator name="forbiddenword" class="com.example.validator.ForbiddenWordValidator"/>- 在
RegisterAction-validation.xml中使用:
<field name="username"> <field-validator type="forbiddenword"> <param name="forbiddenWords">admin,root,guest</param> <message>用户名不能为系统保留字</message> </field-validator> </field>关键点:自定义校验器的getFieldValue()方法会自动从ModelDriven的 model 中取值,无需手动处理对象层级。
6. 从部署到调试:一个工程师的日常习惯,帮你省下 80% 的排错时间
我带过的 A同学,在第一次部署这个 Struts2 系统时,花了 3 小时卡在404,最后发现是web.xml里filter-mapping的url-pattern写成了*.action(缺了/),应为/*或/。这种低级错误,老手不会犯,不是因为他们更聪明,而是有一套肌肉记忆般的检查清单。我把这套清单固化成每日必做的三件事,坚持半年后,部署成功率从 60% 提升到 98%。
6.1 启动前必查三张表:配置、路径、依赖
| 检查项 | 检查位置 | 正确示例 | 错误常见表现 |
|---|---|---|---|
| web.xml filter 配置 | WEB-INF/web.xml | <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> | 控制台无 Struts 初始化日志,404 |
| struts.xml 位置 | WEB-INF/classes/struts.xml(编译后路径) | Eclipse 中src/struts.xml→ 自动复制到classes/ | 修改配置不生效,始终走旧逻辑 |
| jar 包完整性 | WEB-INF/lib/ | 必含struts2-core-2.5.33.jar,ognl-3.2.26.jar,freemarker-2.3.31.jar,javassist-3.29.2-GA.jar | 启动报NoClassDefFoundError,如ognl.Node |
提示:用
jar -tf xxx.jar \| grep "ClassName"快速验证 jar 是否含指定类。例如jar -tf struts2-core-2.5.33.jar \| grep "StrutsPrepareAndExecuteFilter"。
6.2 运行时调试黄金三招:日志、断点、调试标签
第一招:开启 Struts2 DEBUG 日志
在src/log4j2.xml(或log4j.properties)中添加:<Logger name="com.opensymphony.xwork2" level="debug" additivity="false"> <AppenderRef ref="Console"/> </Logger>启动后控制台会输出每一步拦截器执行、OGNL 表达式解析、result 跳转详情,比堆栈更有指向性。
第二招:在 Action 的
execute()第一行打条件断点
条件设为getUsername() != null,这样只有真正提交的请求才会中断,避免被 Tomcat 启动时的探测请求干扰。第三招:JSP 中插入
<s:debug/>标签
放在任意 JSP 页面顶部,访问时会弹出浮动调试窗口,显示当前ValueStack中所有对象、ActionContext的session/request内容、OGNL 表达式求值结果。这是定位「数据传过去了没?」「为什么取不到?」的终极武器。
6.3 生产环境迁移 checklist:去掉教学痕迹,加固基础
这个系统是教学靶场,上线前必须做减法:
- 移除所有
System.out.println()和e.printStackTrace()—— 改用 SLF4J 记录 warn/error; - 密码存储必须升级—— 删除明文比对,引入
BCryptPasswordEncoder,数据库字段改为VARCHAR(100)存储哈希值; - 关闭开发模式——
struts.xml中添加<constant name="struts.devMode" value="false"/>,禁用struts.xml热加载,提升性能; - 精简拦截器栈—— 默认
defaultStack包含 12 个拦截器,生产环境可自定义精简版,移除debuggingInterceptor、profilingInterceptor等非必要项; - 静态资源分离—— CSS/JS/图片移出
WebContent/,托管到 Nginx,减轻 Tomcat 压力。
从那以后我每次接手一个 Struts2 项目,不管多急,都强制走一遍「三查三招一清单」:查配置、查路径、查依赖;用日志、用断点、用 debug 标签;最后对照迁移 checklist 过一遍。它不炫酷,但稳。希望帮到你。
本文还有配套的精品资源,点击获取