Struts2实战:从登录注册到用户信息管理系统的完整实现解析
简介面向Java Web初学者的Struts2登录注册及用户信息管理系统资源包采用MVC分层架构完整覆盖用户注册、登录认证、会话保持、信息增删改查等流程直观演示Action、Result、拦截器、表单验证、SQL Server数据库接入等核心机制适合快速建立框架整体认知。压缩包共74个文件大小4.27MB包含13个Java源文件与对应class文件用于业务实现9个XML配置定义Action映射与拦截器栈6个JSP页面负责视图展示6个jar依赖库支撑框架运行同时附带数据库mdf/ldf文件、14个gif操作演示和一份图解文档基本涵盖从环境搭建到代码研读的完整素材。项目中的数据库脚本、配置文件与通用用户模块代码可直接复用有效缩短课程设计或毕业设计的开发周期。目前已有842人学习下载对Struts2初学者和需要快速完成Web项目的开发者具有不错的参考价值。 最近帮一个老朋友维护他大学时代的课设项目恰好就是“Struts2做的登陆注册及用户信息管理系统”这类经典题目。翻了翻代码又查了查现在网上流行的实现方式尤其是Vue前端配合后端接口的登录注册方案突然觉得有挺多东西值得沉淀下来。Struts2在当年是Java Web领域绝对的主力框架用它做登录注册和用户信息管理几乎是每个人的入门必修课。虽然现在新项目里已经很少见到它的身影但藏在它背后的MVC分层思想、拦截器机制、表单校验逻辑放到今天任何一个Web框架里都依然通用。这篇文章我会以一个完整可运行的Struts2用户管理系统为例把从需求拆解、数据库设计、Action编写、JSP页面渲染到权限控制的全过程都过一遍。不管你是还在上学的计算机专业学生还是刚开始接触Java Web开发的转行者甚至只是单纯想看看老项目代码到底在做什么的维护者这篇文章应该都能帮到你。我尽量把当年踩过的坑和实际的思考过程都交代清楚而不是只贴一堆配置文件。1. 项目定位与整体方案1.1 这套系统到底解决什么问题登录注册模块加上用户信息管理本质上覆盖了几乎所有Web应用的“地基”部分。你打开任何一家网站看到的“登录”“注册”“个人中心”背后就是这么一套逻辑在运转。而Struts2版本的实践意义在于它能把你从一个只会写Servlet的新手带进“框架思维”的世界——你开始考虑请求怎么路由、数据怎么封装、页面怎么跳转而不是在doGet和doPost里堆砌一堆重复代码。具体来说这套系统要完成的事情包括用户通过注册页面提交信息系统校验数据的合法性后写入数据库用户通过登录页面输入账号密码系统校验身份后把用户状态保存到Session登录后的用户可以查看自己的资料、修改密码、更新基本信息如果是管理员角色还能查看全部用户的列表并执行禁用、删除等操作。听起来不复杂但每一个环节拆开都有很多细节值得推敲比如密码要不要加密存储、Session超时怎么办、表单重复提交怎么防这些都是一开始容易被忽略的问题。1.2 为什么选Struts2而不是其他方案放在当年的大背景下选择Struts2的理由其实很直白。第一它比原始的Servlet封装得更高级Action类不再强制继承HttpServlet这意味着单元测试变得容易很多。第二Struts2内置了强大的拦截器栈像参数封装、文件上传、表单校验这些能力都是现成的不用自己造轮子。第三它的配置文件支持模块化管理不同的业务模块可以拆成多个XML文件方便多人协作。当然我也得说句公道话Struts2的缺点同样明显。它最大的问题是线程模型——每个请求都会创建一个新的Action实例虽然避免了线程安全问题但对象创建和销毁带来的开销并不小。另外一个让人头疼的地方是OGNL表达式和值栈的机制刚开始接触的时候完全不知道变量从哪里来、要往哪里去调试起来非常痛苦。不过这些“痛苦”反而逼着你去理解数据流转等你搞懂了值栈的原理再学Spring MVC的Model、Vue的状态管理都会顺很多——因为这些框架解决的是同一类问题只是换了一种更好的方式。1.3 技术栈选择与版本搭配这个项目的技术组合我建议这样配置Struts2核心框架用2.3系列因为2.5以后包名改动比较大很多老教程会直接跑不起来反而增加学习成本数据库用MySQL 5.7轻量也够用数据库连接池用DBCP或者C3P0均可我实际测试下来DBCP配置更简单适合新手持久层直接用JDBC或者Spring的JdbcTemplate不要引入Hibernate或者MyBatis——这个阶段的核心目的是理解MVC的流转过程过早引入ORM反而会混淆视线。还需要特别注意的是版本兼容问题。Servlet容器我用的Tomcat 7或8JDK用的1.7或1.8这套组合实测下来兼容性最好。如果你电脑上装的是Tomcat 9或者更高版本可能会遇到ClassNotFoundException这类依赖冲突问题因为Struts2的老版本依赖了Javassist等库在新容器上偶尔会出幺蛾子。建议直接开一个虚拟机或者用Docker跑一个干净的Tomcat 8环境能把很多莫名其妙的坑提前规避掉。2. 核心模块拆解与数据库设计2.1 需求边界怎么划做这种管理系统最怕的就是一上来就想到处都做“增强功能”。我建议第一期就把需求严格锁定在四个范围内注册、登录、用户信息查看与编辑、管理员用户列表管理。什么验证码、邮箱激活、第三方登录统统先放到二期再说。这样做的目的是让核心流程足够清晰每一条链路你都能从头到尾走通。注册模块的核心动作就是采集输入、校验合法性、写入数据库登录模块的核心动作是查询用户、比对凭证、初始化会话用户信息管理的核心动作是权限校验、读取数据、回显表单、更新数据管理员列表管理的核心动作是分页查询、状态变更、删除操作。把边界划清楚之后数据库表结构也就呼之欲出了。2.2 数据库表结构设计思路用户表的设计不需要太复杂但基础字段一定要完整。我设计的表结构如下字段名类型说明idINT(11) 主键自增用户唯一标识usernameVARCHAR(50) 唯一索引用户名passwordVARCHAR(64)密码MD5或SHA加密后存储emailVARCHAR(100)邮箱phoneVARCHAR(20)手机号roleTINYINT(1)角色1管理员 0普通用户statusTINYINT(1)状态1启用 0禁用create_timeDATETIME注册时间update_timeDATETIME信息更新时间这里有一个重要的设计习惯你得养成不要把明文密码存进数据库。哪怕这个项目只是练手也要从一开始就养成加密存储的习惯。当年我自己偷懒存过明文密码结果后来做项目对接、做报表统计的时候看到那些密码就心虚生怕泄露。技术上用JDK自带的MessageDigest做一次MD5加盐哈希就够了不需要引入太复杂的加密框架。加盐的方式也很简单取用户名加上一个固定字符串拼接后再做哈希就能有效对抗彩虹表攻击。角色和状态这两个字段也很有讲究。加上它们之后你的系统就有了基础的权限控制能力和账号管控能力不用额外去建权限表。管理员可以进入专门的用户列表页面操作所有账号普通用户登录后只能操作自己的信息。这就把“用户信息管理系统”和单纯的“登录注册Demo”区分开了项目的含金量完全不一样。2.3 目录结构怎么组织Struts2项目虽然是经典的分层架构但不同的人的组织方式差别还挺大的。我推荐按“控制器-业务-持久化”三层来组织同时把公共的工具类和实体类独立出来。一个典型的包结构是这样的com.example.demo ├── action # Action类接收请求并响应跳转 │ ├── UserAction.java │ └── AdminAction.java ├── service # 业务接口和实现处理核心逻辑 │ ├── UserService.java │ └── impl/UserServiceImpl.java ├── dao # 数据库访问层 │ ├── UserDao.java │ └── impl/UserDaoImpl.java ├── entity # 实体对应数据库表结构 │ └── User.java ├── util # 工具类例如MD5加密 │ └── MD5Util.java ├── interceptor # 自定义拦截器例如权限拦截器 │ └── LoginInterceptor.java └── common # 全局常量、返回结果封装等 └── Constants.java这层结构最大的好处是职责清晰。有人可能会问业务逻辑这么简单Service层不是多余的吗我的观点是哪怕当前只有登录注册两件事Service层也一定要保留。因为随着系统演化你十有八九要加邮箱验证、密码找回、操作日志这些功能这些逻辑如果都写在Action里那Action类很快就会膨胀成一个几百行的上帝类维护起来痛不欲生。从第一天起养成“Action只做路由Service只做业务DAO只做持久化”的习惯你后面写任何框架的项目都会受益。3. 关键代码实现与实操细节3.1 核心配置文件逐个说清楚Struts2的配置文件有好几个但最关键的无非是web.xml和struts.xml。web.xml里需要配置Struts2的过滤器它的作用是把所有符合规则的请求都拦截下来交给框架去处理。这里有个细节容易被忽略filter-mapping的url-pattern要写成/*而不是*.action。原因在于Struts2的静态资源处理机制和Action映射都需要经过过滤器如果写*.action那你后续配置的静态资源放行策略就会失效。不过随着版本更新有些写法也会有细微差异我实际项目里还是倾向于统一用/*然后在框架层面控制Action的访问路径。struts.xml则用来配置包、全局结果页、拦截器栈和具体的Action映射。给你看一个我实际用的精简配置注释我都标清楚了?xml version1.0 encodingUTF-8? !DOCTYPE struts PUBLIC -//Apache Software Foundation//DTD Struts Configuration 2.3//EN http://struts.apache.org/dtds/struts-2.3.dtd struts constant namestruts.devMode valuetrue / constant namestruts.i18n.encoding valueUTF-8 / package namedefault namespace/ extendsstruts-default !-- 自定义拦截器栈登录校验 -- interceptors interceptor nameloginInterceptor classcom.example.demo.interceptor.LoginInterceptor / interceptor-stack namemyStack interceptor-ref namedefaultStack / interceptor-ref nameloginInterceptor param nameexcludeMethodslogin,register/param /interceptor-ref /interceptor-stack /interceptors global-results result namelogin/login.jsp/result /global-results action nameuser_* classcom.example.demo.action.UserAction method{1} result namesuccess/index.jsp/result result nameinput/register.jsp/result interceptor-ref namemyStack / /action action nameadmin_* classcom.example.demo.action.AdminAction method{1} result namesuccess/admin/userList.jsp/result result nameinput/login.jsp/result interceptor-ref namemyStack / /action /package /struts这里有两个核心概念你阅读的时候要重点领会。第一个是user_*这种通配符映射方式它可以用{1}取出星号部分作为方法名也就是把URL和应用方法解耦了。第二个是拦截器栈的配置方式它允许你定义一套通用的前置逻辑比如登录校验然后灵活地对部分方法放行。这种“低侵入”的扩展方式是理解框架强大与否的关键所在。我在实际项目中就靠着这套配置轻松地给后台接口统一加上了操作日志和接口耗时统计完全不用改业务代码。3.2 Action类的写法与数据流转Action类是Struts2的数据流转枢纽理解它的关键在于“模型驱动”和“属性驱动”两种模式。属性驱动的写法比较直观每个表单字段对应Action中的一个同名属性Struts2会自动把请求参数填充进来。模型驱动则要求Action实现ModelDrivenT接口返回一个模型对象这样表单参数会直接填充到模型对象里。两者各有优劣但实际项目中我更推荐在Action中组合使用对于注册这种字段较多的场景使用模型驱动能少写很多Getter和Setter对于登录这种只有两个字段的场景属性驱动反而更直观。下面是一个注册方法的骨架你可以看到我是怎么在Action中完成业务调用的public class UserAction extends ActionSupport { private User user; private String confirmPassword; private UserService userService; public String register() throws Exception { // 基础非空校验交给validate()方法处理 // 检查用户名是否已存在 if (userService.isUsernameExist(user.getUsername())) { addFieldError(username, 用户名已被注册); return INPUT; } // 密码加密 user.setPassword(MD5Util.md5(user.getUsername() user.getPassword())); user.setCreateTime(new Date()); user.setRole(Constants.ROLE_USER); user.setStatus(Constants.STATUS_ACTIVE); userService.addUser(user); return SUCCESS; } Override public void validate() { if (user null || StringUtils.isEmpty(user.getUsername())) { addFieldError(username, 用户名不能为空); } if (StringUtils.isEmpty(user.getPassword())) { addFieldError(password, 密码不能为空); } if (StringUtils.isEmpty(confirmPassword) || !confirmPassword.equals(user.getPassword())) { addFieldError(confirmPassword, 两次输入的密码不一致); } } }我着重讲一下validate()方法。它是Struts2基于ActionSupport提供的声明式校验入口会在execute()之前自动执行。一旦出现了校验错误框架就会放弃执行业务方法直接跳转到input对应的视图页面同时通过s:fielderror标签把错误信息显示在页面上。我当年写这个系统时最常犯的错误就是忽略了user对象为空的判断。比如用户直接发一个没有参数体的POST请求Struts2可能连User对象都封装不出来这时候在validate()里直接调用user.getUsername()就会抛空指针异常。后来我在每个地方都加上了user null的防御判断这个问题才算根治。这个习惯我一直保留到现在无论是写Python代码还是Go代码都习惯在入口处校验参数对象的非空性。3.3 JSP页面的渲染与表单回显Struts2提供一个标签库比如s:form、s:textfield、s:password、s:submit等这些标签的方便之处在于它们能自动绑定Action的属性并支持错误信息的回显。比如你在validate()里添加了addFieldError(username, 用户名已被注册)在JSP页面里通过s:fielderror fieldNameusername /就能把错误信息展示出来用户不需要重新输入整个表单——这非常提升体验。不过我在实际开发中遇到的“回显异常”是另一类情况。你会发现如果你用s:textfield nameuser.username/Struts2能正确显示对象属性值但如果你的表单字段名写错比如少写了“user”前缀页面上就静默地什么都不显示也不报错。排查这种问题最有效的方法就是在Action的validate()或execute()方法里打断点观察user对象有没有正确接收到请求参数以及回显的数据到底在值栈的哪个位置。表单提交还有一个经典问题——重复提交。用户注册成功之后按一下F5刷新浏览器可能会重新提交表单导致数据库中插入两条一模一样的用户数据如果不判重的话。Struts2针对这个问题提供了s:token标签它会在表单中生成一个一次性令牌由框架内置的TokenInterceptor拦截器校验是否为重复请求。如果你感兴趣这是一个非常好的进阶练习点。3.4 拦截器如何实现登录状态校验登录拦截的核心逻辑很简单从Session里取当前登录用户如果能取到就放行取不到就跳转到登录页。但真要把这个逻辑和Struts2框架融合好有几点值得注意。自定义拦截器类需要实现Interceptor接口或继承AbstractInterceptor然后在intercept()方法中通过ActionInvocation对象控制流程。这里的关键技巧在于Action中有些方法是不需要登录就能访问的比如注册、登录这两个方法本身。我在前面那份struts.xml配置中已经通过excludeMethods参数排除了这两个方法。这个参数的具体设计是这样的拦截器内部会根据当前请求调用的方法名判断是否命中排除列表如果命中就直接放行。用excludeMethods的方式处理比在Interceptor内部硬编码判断要优雅得多。关于拦截器我最想强调的是执行顺序。Struts2的拦截器是按照配置顺序进栈再出栈的这也意味着如果你在拦截器内部对ActionInvocation执行了invoke()方法后续的拦截器会继续执行然后才是Action方法。框架默认的defaultStack里就包含了数据封装、参数校验、异常处理等多种拦截器。我当年为了搞清楚这个执行顺序特意在好几个拦截器里加上了System.out.println观察打印日志的顺序。后来总结出一句话拦截器的执行顺序遵循“洋葱模型”倒数第二个配置的拦截器会最先执行invoke之前的代码最后执行invoke之后的代码。理解了这一点写任何自定义中间件你都不会被执行顺序折磨了。3.5 分页查询用户列表的实现管理员的用户列表页面几乎必然要用到分页查询因为这能避免一次性从数据库拉出大量数据导致页面卡死。我当时自己手写了一个非常简易的分页逻辑没有引入分页插件因为要控制代码量。核心思路是计算总记录数算出总页数再根据当前页数查询对应页的数据。DAO层的查询SQL大概是下面这个模样SELECT * FROM t_user ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}为了让查询更灵活我可以动态拼接查询条件。当时我用的是JDBC的PreparedStatement来实现的配合一个简单的查询条件对象UserQuery把用户名、邮箱、角色等过滤条件都封装进去。虽然这套手写方式在数据量大了之后性能一般但对一个小型管理系统来说完全够用了。说实话写这一遍手写分页让我后来理解MyBatis的PageHelper插件原理时几乎零成本——所谓插件也不过是在SQL执行之前帮你算好LIMIT ?而已。我同时强烈建议你在分页查询的时候做一下“状态字段联动”比如管理员可以在列表页把某个用户禁用。禁用之后该用户在登录的时候就会被判定为“账号已被禁用”无法进入系统。这个问题看起来简单但它牵扯到登录校验逻辑和用户列表展示逻辑两层改动。很多人做完分页就以为大功告成结果漏掉状态字段的拦截判断等测试人员点出“我账号明明被禁了还能登录”的Bug时才追悔莫及。4. 常见问题与排查技巧实录4.1 环境配置类的经典坑Struts2老项目在环境配置上真的是坑多到防不胜防。我总结了几个最常见的、也是我每次搭建环境几乎必遇的问题症状原因解决方案页面报404且Tomcat日志无异常struts.xml中的action映射不存在或用例错误检查namespace和action name是否匹配注意通配符大小写类未找到异常ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter版本不对2.5后对应包名变更2.5版本将过滤器类改为org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter页面中文乱码请求编码未设置在web.xml配置CharacterEncodingFilter且设置tomcat URIEncodingUTF-8表单提交后Action的属性全为null字段name和Action属性不对应或缺少getter/setter检查字段名是否带正确的前缀如user.username并确保getter/setter方法命名规范静态资源无法加载Struts2拦截了所有请求包括JS/CSS/图片使用constant namestruts.action.excludePattern value/static/.*/静态资源排除这里面的中文乱码问题我多说一句。即便你配置了过滤器Tomcat本身对GET请求的URI解码可能仍默认使用ISO-8859-1所以说要改server.xml里的URIEncoding。如果你是用Spring Boot内嵌Tomcat那配置方式又不一样了需要显式在配置文件里指定server.tomcat.uri-encodingUTF-8。每次遇到乱码先分清是请求参数乱码还是页面显示乱码然后再针对性地解决效率最高。4.2 业务逻辑上的隐蔽Bug这类问题比环境问题更考验代码功底。我在这个项目中印象最深的是登录逻辑中的“用户状态校验缺失”问题。有一天同事跟我说“我账号被禁用了但我还能登录”。我排查了很久才发现原来的登录逻辑只校验了用户名和密码是否匹配完全没有检查status字段。修复方式很简单在比对密码之前加上一行代码if (user.getStatus() Constants.STATUS_DISABLED) { addActionError(该账号已被禁用请联系管理员); return ERROR; }但这个问题背后暴露的其实是“登录三要素”没有建立完整意识账号存在性、密码正确性、状态可用性三者缺一不可。后来我把这套判断逻辑写成了标准的checkLogin方法并且加了单元测试去覆盖禁用账号的场景才算彻底根治。另一个隐蔽Bug是Struts2重定向时的数据丢失。登录成功后如果你返回的是一个非转发的结果类型例如result typeredirect/index.jsp/result那么你在Action中设置的属性值不会自动带到目标页面因为重定向是新的请求。我当时在登录成功后在Action里设置了一条欢迎消息结果怎么都显示不出来查了半天才意识到是这个原因。解决办法是用redirectAction并且带参数传递或者在Session里保存消息页面显示后再清除。这算是初学者最容易忽视的一个知识点。4.3 调试技巧日志和断点的配合这算是我个人的一个调试习惯。Struts2开发的时候一定要把log4j.properties的级别调成DEBUG然后在控制台观察Struts2输出的处理流程日志。它能告诉你每个请求经过了哪些拦截器、Action方法返回了什么值、最终跳转到了哪个页面。很多初学者看日志一片刷屏就下意识关掉但实际上日志里包含非常宝贵的排查线索。配合Idea的断点调试方式你能在UserAction.register()方法调用前后完整观察到参数封装的情况。我一般调试业务的顺序是先在浏览器F12看请求参数是否正常、URL状态码是多少再到Idea打断点DEBUG看Action属性是否赋值正确最后看日志里的SQL语句和数据库实际结果是否对应。通过这套“请求面-代码面-数据面”三层排查法大多数问题都能在几分钟内定位而不是靠肉眼读代码碰运气。5. 从Struts2到VueRestful的演进思路5.1 前端渲染与后端渲染的本质差异Struts2的JSP方案本质上是服务端渲染后端通过Action处理完业务把数据放入值栈然后服务端生成最终的HTML响应给浏览器。这种模式的好处是SEO友好、首屏加载快但它也有很明显的劣势——每一次页面跳转都需要走一个完整的HTTP请求周期前后端代码耦合在一起JavaScript很难进行组件化和工程化管理。这几年用Vue实现的登录注册系统在网上特别火本质原因是开发者希望把渲染压力放在浏览器端后端只负责提供JSON数据接口。这种前后端分离模式的好处非常明显前端团队和后端团队可以并行开发同一套后端接口可以同时支持网页端、App端、小程序端。我最近在几个项目里都用Vue3配合Element Plus重新实现了这个用户管理系统体验确实顺滑很多。用户点击“登录”按钮前端通过Axios发一个POST请求到后端/api/login接口后端返回{code:0,data:{token:xxx,user:{...}}}前端拿这个响应直接更新页面状态整个过程页面完全不刷新。5.2 核心逻辑的对应关系如果你已经理解了Struts2那套逻辑学Vue后端分离其实只是一个知识迁移的过程。Action对应前端的API接口请求validate()方法对应前端的表单校验规则和后端的参数校验注解struts.xml的路由配置对应后端的RequestMapping注解值栈中的数据获取对应前端Axios响应对象中的response.data。要说最大的思维转变在哪里我认为是“状态管理”这一块。Struts2时代你只要往Session里塞一个用户对象就可以全局共享登录状态但Vue前端由于是单页应用你需要用Pinia或者Vuex这类状态管理库配合LocalStorage或Cookie来存储用户的Token然后在每个请求的请求头里把Token带上。虽然概念和实现方式变了但本质思路还是一模一样通过一种全局共享的机制让所有页面都知道“谁是我的当前用户”。5.3 老项目的改造建议这篇文章的读者里一定有人在维护一个类似的老项目可能还遇到过“这代码还能不能改”的困惑。我的建议是不要急着推倒重建更不要为了追新框架就强行改造。最稳妥的路线是先将Struts2的前后端接口剥离出来在原有的Action旁边增加返回JSON数据的方法比如返回一个纯JSON字符串前台页面改成用Ajax请求这些接口稳定运行一段时间后再逐渐用新的接口层替代旧的JSP页面。这个过渡方案能最大化保证业务连续性同时又为后续技术升级打下基础。如果你是在学习阶段我更建议你同时用Vue3把同一个登录注册页面写一遍然后对照着研究和Struts2版本的异同。我自己就是这么干的先写一遍Java后端的登录注册再用Vue写一遍前端页面对接一个Express或者Spring Boot后端。等两边都跑通了你会发现“登录注册”远远不只是两个页面那么简单——它涉及的密码学、会话管理、数据持久化、接口安全每一块单独拿出来都够研究好一阵子。能在这些基础模块上有深刻的理解以后无论框架怎么变你都能很快上手。最后再分享一个我做这类项目时的心法无论用框架还是不用框架把核心业务流程画成一张流程图再动手写代码永远是最省时间的一步。登录、注册、退出、改密、管理员封号每一条路径上有什么分支、什么异常、什么跳转逐条在纸上过一遍再敲键盘代码的质量会完全不一样。这也是被Struts2的拦截器机制和OGNL表达式“毒打”过之后我体会最深刻的一件事。本文还有配套的精品资源点击获取