资讯详情

资讯详情

Struts案例源码全解析:从XML配置到Action链路与拦截器

简介这是一份基于 Java 的 Struts 框架案例源码包面向正在学习 MVC 架构和 Struts 的 Java Web 开发者可用于理解企业级分层开发中模型、视图、控制器的协作方式以及常见拦截器、表单处理与用户权限配置等实践场景。整个压缩包共 735 个文件大小约 199.89MB包含 220 个 JAR 包、74 个 XML 配置文件、71 个 Java 类文件和 71 个 Java 源文件、61 个 JSP 页面以及项目文件、属性文件和类路径文件等从依赖库到页面展示均覆盖齐全。文件类型划分清晰便于在 IDE 中对照查看 Struts 的配置与代码调用关系。通过这份源码可以系统学习控制器与业务逻辑的解耦方式、声明式配置的常见写法、JSP 页面的数据展示技巧并可直接参考其包结构搭建自己的 Struts 项目。目前已有 91 人学习下载适合作为 Struts 入门到进阶的对照练习资源。1. 从Struts案例源码入手前先搞清这套734个文件的真实价值很多人拿到Struts案例源码习惯性先搜索Struts是什么结果看了半天概念还是不知道项目里为什么塞了220个JAR包、74个XML配置和71个Java类。我的建议是反过来先按目录把文件类型过一遍再对着web.xml和struts-config.xml读Action链你会发现在纯Servlet时代被硬编码搞得很痛苦的东西在Struts里其实是一套声明式路由。这套案例适合两类人一是准备Java面试、需要讲清楚MVC和Model2区别的候选人二是维护老系统、被迫和Struts 1.x/2.x共存的后端工程师。如果你以为它能直接跑成现代REST服务那先调整预期——它更像一份能动手复现的架构教材。2. 解剖Struts案例的工程结构与配置从web.xml到struts-config.xml2.1 目录结构与文件类型统计拿到源码后先不要急着用IDE打开。我习惯在命令行下用find和awk做一次文件归类因为这份案例的734个文件里真正决定运行行为的是XML配置和Java类而大量的JAR包和Manifest文件只是历史遗留的依赖痕迹。以下命令可以快速得到文件类型分布find . -type f | awk -F. {if (NF1) ext$NF; else extnone; count[ext]} END {for (e in count) print count[e], e} | sort -rn | head -20这里的awk -F.是按点拆分文件名取最后一个字段作为扩展名count[ext]做统计sort -rn按数量倒序排列。head -20截取前20行防止输出太多干扰判断。在常见解压场景下输出会类似这样220 jar 74 xml 71 class 71 java 61 jsp 42 prefs 29 manifest 28 properties 21 project 20 classpath这些数字和摘要描述基本吻合。需要留意的是prefs和manifest文件多数来自IDE比如Eclipse的工作区快照并不是运行时必需真正影响Struts启动的是web.xml、struts-config.xml、struts.xml取决于版本以及ApplicationResources.properties。所以如果你是做代码审计优先看src目录和WebRoot/WEB-INF不要被220个JAR包的数量吓到。下列表格列出了案例中各类文件的实际用途方便后续排查时定位文件类型数量运行时是否必需主要作用jar220依赖必需提供Struts核心库、日志、数据库驱动等xml74部分必需web.xml、struts-config.xml、validator等class71编译产物对应Java源文件编译后的字节码java71源码必需Action、FormBean、拦截器、Servicejsp61视图必需用户界面与表单展示properties28资源必需国际化消息、数据库配置、日志配置prefs/manifest/project/classpath若干非必需IDE工程描述与历史打包信息2.2 核心配置文件逐个拆解Struts案例里通常同时存在struts-config.xmlStruts 1.x或struts.xmlStruts 2.x。这份案例的正文里出现了RegisterAction和SimpleInterceptor这两个类名同时存在于两个版本中但从「拦截器」的命名习惯来看它更可能是基于Struts 2.x的约定或Struts 1.x的Interceptor插件扩展。我先把两者都给你捋一遍。先看Struts 1.x最典型的struts-config.xml骨架?xml version1.0 encodingUTF-8? !DOCTYPE struts-config PUBLIC -//Apache Software Foundation//DTD Struts Configuration 1.3//EN http://struts.apache.org/dtds/struts-config_1_3.dtd struts-config form-beans form-bean nameuserForm typecom.demo.UserForm/ /form-beans action-mappings action path/register typecom.demo.RegisterAction nameuserForm scoperequest validatetrue input/register.jsp forward namesuccess path/welcome.jsp/ forward nameerror path/register.jsp/ /action /action-mappings message-resources parameterApplicationResources/ /struts-config配置逻辑分三段form-beans定义表单类action-mappings声明URL到Action的映射message-resources指定国际化资源文件。这里面最容易忽略的是scoperequest它决定了ActionForm的存放范围。如果改成session用户在页面上填到一半的数据会在跳转后仍然存在这在分页查询场景下可以省去手动传参的麻烦但也会增大内存压力——这也是我经常在Java面试题里被问到的点。如果是Struts 2.x配置会换成struts.xml结构变成包package和拦截器栈interceptor stackstruts package namedefault extendsstruts-default interceptors interceptor namesimpleLogger classcom.demo.interceptor.SimpleInterceptor/ interceptor-stack nameregisterStack interceptor-ref namedefaultStack/ interceptor-ref namesimpleLogger/ /interceptor-stack /interceptors action nameregister classcom.demo.RegisterAction2 result namesuccess/welcome.jsp/result result nameinput/register.jsp/result interceptor-ref nameregisterStack/ /action /package /struts在Struts 2中每个Action默认会套用defaultStack它里面已经包含了参数封装、异常拦截、模式转换等基础能力。案例里的SimpleInterceptor是为了演示如何精确定制Action执行前后的逻辑。配置里一旦自定义了interceptor-ref默认栈就不会自动生效所以必须手动把defaultStack放进去——这是新手最容易踩的坑我之前在排查线上问题时发现有人把默认栈去掉后所有请求的HttpServletRequest参数全部为空因为参数拦截器不再执行。2.3 表单校验与数据流转的声明式实现Struts的一大卖点是数据校验可以脱离Java代码用XML完成。在Struts 1.x里你可以在struts-config.xml中引入validator-rules.xml和validation.xml给某个FormBean定义字段规则form nameuserForm field propertyusername dependsrequired,minlength,maxlength arg0 keylabel.username/ arg1 nameminlength key${var:minlength} resourcefalse/ arg2 namemaxlength key${var:maxlength} resourcefalse/ var var-nameminlength/var-name var-value3/var-value /var var var-namemaxlength/var-name var-value20/var-value /var /field /form这套声明式校验的好处是页面上的错误提示语和字段规则集中管理业务人员改文案不需要重新编译Java类。原理层面Struts会通过ActionServlet把请求转发给RequestProcessor在processValidate阶段调用Validator插件捕获校验失败后转入input指定的页面。在Struts 2.x中校验文件命名是ActionClass-validation.xml并放在Action类同包下。比如RegisterAction2-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 namepassword field-validator typerequiredstring message密码不能为空/message /field-validator field-validator typestringlength param nameminLength6/param param namemaxLength18/param message密码长度必须在6到18位之间/message /field-validator /field /validators这里的typerequiredstring会在actionContext里对字符串做trim后判断和直接判null有区别如果用户输入三个空格requiredstring会拦截而普通required依赖Ognl类型转换后可能直接报类型错误。所以我在处理表单类项目时更倾向用Struts 2的stringlength配合requiredstring而不是依赖前端H5校验——因为它会在服务端生效绕过了篡改页面绕过校验的可能。3. 核心Java类与Action链路的实现解析3.1 UserAction与RegisterAction2的分工这个案例的类列表中UserAction出现了四次User.class出现两次RegisterAction和RegisterAction2同时存在。这说明案例里很可能保留了两个版本一套是Struts 1.x的ActionActionForm写法一套是Struts 2.x的ActionSupport XML配置写法。它们解决的问题相同接收HTTP请求、封装参数、调用业务逻辑、决定跳转。我们先看Struts 1.x风格的UserActionpublic class UserAction extends Action { Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserForm userForm (UserForm) form; User user new User(); user.setUsername(userForm.getUsername()); user.setPassword(userForm.getPassword()); UserService service new UserService(); boolean saved service.register(user); if (saved) { request.setAttribute(user, user); return mapping.findForward(success); } return mapping.findForward(error); } }execute方法签名的四个参数是理解Struts 1的关键mapping代表当前Action在配置文件中的映射信息form是已经被Struts组装好的表单对象request/response和Servlet API共用。返回值ActionForward就是转发令牌它和struts-config.xml里的forward对应。注意这个方法里没有任何直接response.getWriter()写HTML的代码视图渲染完全交给JSP这就是MVC中Controller层的职责边界。而Struts 2.x中的RegisterAction2通常是这样public class RegisterAction2 extends ActionSupport { private User user; private UserService userService new UserService(); Override public String execute() throws Exception { if (userService.isUsernameExists(user.getUsername())) { addFieldError(username, 用户名已存在); return INPUT; } userService.register(user); return SUCCESS; } public User getUser() { return user; } public void setUser(User user) { this.user user; } }Struts 2的Action不再继承繁琐的基类逻辑参数通过getter/setter自动注入页面表单里的user.username会映射到Action的user属性及其嵌套属性。这里有一个重要区别在Struts 1里Action是单例多个请求共享同一个Action实例所以不能在execute里写实例状态在Struts 2里每个请求会创建一个新的Action对象写实例字段是安全的。这个差异经常被拿来考Java基础也决定了两种模式下的线程安全设计。3.2 用户注册流程的源码级走读完整跑通一次注册请求是理解案例的最佳路径。假设用户提交/register.doStruts 1.x的请求链路是Web容器 - ActionServlet - RequestProcessor - ActionForm封装 - 校验 - UserAction.execute - 业务Service - ActionForward - JSP渲染我把这个链路拆成五个步骤第一步ActionServlet读取struts-config.xml建立path - ActionMapping的注册表。 第二步请求到达后RequestProcessor根据请求路径匹配/register然后从session或request中取出指定scope的UserForm调用reset()清空旧数据。 第三步RequestProcessor把表单参数逐个写入UserForm的对应属性执行validate()或XML校验。 第四步校验通过后调用UserAction.execute()返回ActionForward。 第五步RequestDispatcher.forward()转发到对应的JSP渲染视图。这里最容易被忽视的是reset()方法。如果你没有在UserForm里重写reset()那么当用户从列表页点击「新建」时上一次操作残留的表单数据会继续存在造成界面回显数据错乱。所以案例里但凡涉及编辑场景的FormBean基本都会重写reset()public class UserForm extends ActionForm { private String username; private String password; Override public void reset(ActionMapping mapping, HttpServletRequest request) { this.username null; this.password null; } }这段代码的意图是每次请求先用null覆盖旧值再让Struts自动填充新值。如果是分页查询场景你可能希望保留pageNo字段不重置那就只在reset()里重置业务字段。这是我读过大量老项目后总结出来的规范reset()只清业务字段不清理分页、排序等查询条件。否则用户翻到第三页时点击搜索按钮后突然回到第一页这种体验非常糟糕。3.3 自己实现一个SimpleInterceptor拦截器栈的执行顺序案例里的SimpleInterceptor是理解Struts 2拦截器机制的好素材。它的骨架一般长这样public class SimpleInterceptor implements Interceptor { Override public void init() { // 拦截器实例初始化常用于加载资源 System.out.println(SimpleInterceptor init); } Override public String intercept(ActionInvocation invocation) throws Exception { System.out.println(before action: invocation.getProxy().getActionName()); String result invocation.invoke(); System.out.println(after action, result result); return result; } Override public void destroy() { // 释放资源 System.out.println(SimpleInterceptor destroy); } }核心是invocation.invoke()这一行会触发拦截器链中的下一个拦截器最终进入Action的execute()方法。在它之前写的代码是前置增强在它之后写的代码是后置增强。如果你做过Java动态代理会发现它们思路完全一致——拦截器就是责任链模式的动态代理实现。这也解释了为什么Struts 2的官方文档里经常提到拦截器栈就是AOP思想在Web层的一种落地。拦截器栈的执行顺序有一个容易出错的细节struts.xml里声明的顺序决定了前置执行顺序而后置执行顺序是逆序的。比如interceptor-ref namefirstInterceptor/ interceptor-ref namesimpleInterceptor/实际执行顺序是firstInterceptor.before - simpleInterceptor.before - action.execute - simpleInterceptor.after - firstInterceptor.after所以我在写日志、事务、权限这些横切逻辑时会刻意把firstInterceptor用于最外层事务simpleInterceptor用于业务日志。如果你把事务拦截器放在内层一旦外层拦截器提前返回事务可能根本不会开启。这也是我面试Java岗位时喜欢问的问题给一段配置让你推导拦截器执行顺序答错的人往往对AOP代理链路理解不够深。4. JSP视图层与Model2模式的联动61个JSP页面怎么排布4.1 从ActionForward到JSP的跳转关系61个JSP页面看起来很多但按功能归类后无非是「录入页、列表页、结果页、错误页、公共片段」五类。在Struts 1.x中JSP通过html:form、bean:write等TagLib和Model交互而不是直接写% %脚本。比如注册表单% taglib uri/WEB-INF/tlds/struts-html.tld prefixhtml % % taglib uri/WEB-INF/tlds/struts-bean.tld prefixbean % html:form action/register.do focususername p bean:message keylabel.username/ html:text propertyusername size20/ html:errors propertyusername/ /p p bean:message keylabel.password/ html:password propertypassword size20/ html:errors propertypassword/ /p html:submit value注册/ /html:formaction/register.do会与struts-config.xml中action path/register匹配property对应UserForm的属性名html:errors负责输出saveErrors里保存的字段错误。这套标签体系解决了JSP里String拼接HTML的痛点也是后来JavaServer Faces等组件化思路的前身。在Struts 2.x中JSP里的标签换成了s:form、s:textfield、s:fielderror但跳转目录的约定更简单/register.jsp直接就是struts.xml里result的路径。我通常会把所有视图放在/WEB-INF/pages下防止用户直接访问JSP绕过Action。这样做之后struts.xml里的result要写成/WEB-INF/pages/register.jsp目录隔离更安全。下面按照实际项目经验将61个JSP页面按功能做了分类统计页面功能典型文件与Action的关联表单录入页register.jsp, login.jsp通过html:form或s:form提交列表展示页userList.jsp, roleList.jsp使用bean:write循环或s:iterator结果提示页welcome.jsp, error.jsp展示Action返回的message公共片段header.jsp, footer.jsp使用% include %或jsp:include错误处理页404.jsp, 500.jspweb.xml中error-page指定4.2 EL表达式与表单回显的配合Struts案例的JSP页面会大量用到EL表达式因为Action往request里塞了user对象后JSP需要把它展示出来。一个典型的welcome.jsp片段% page contentTypetext/html;charsetUTF-8 languagejava % html body h2注册成功/h2 p用户名${user.username}/p p密码长度${fn:length(user.password)}/p /body /html${user.username}会依次从pageScope、requestScope、sessionScope、applicationScope中查找名为user的对象然后调用其getUsername()方法。这和Struts 1时代用bean:write nameuser propertyusername/等价但更简洁。这里有一个容易踩的坑如果Action返回SUCCESS但user对象为nullEL会直接输出空字符串页面不报错排查起来很费劲。所以我会在Action里加一个非空判断返回INPUT而不是硬着头皮渲染。4.3 JSP资源引用与404定位技巧老案例里的JSP通常通过相对路径引用CSS和JS在Tomcat下放到WebRoot后路径经常出问题。我一般用${pageContext.request.contextPath}拼接绝对上下文路径link relstylesheet href${pageContext.request.contextPath}/css/style.css如果页面样式丢失或链接404先检查web.xml里welcome-file的配置再看过滤器是否拦截了静态资源。案例里的web.xml如果配置了/*映射就会把所有请求都交给Struts导致css和js拿不到。常见做法是加一个静态资源Servlet或改映射路径。我会用curl验证资源是否被Struts拦截curl -I http://localhost:8080/struts-demo/css/style.css返回200没问题返回404或302说明请求被路由到了Action映射这时需要在web.xml中给静态资源添加一段servlet-mapping或者把资源放到独立路径下。在一些老案例中还会出现JSP页面引用了其他项目里才存在的图片这种跨项目硬引用在拆解源码时一眼就能看出来因为它们通常写成/images/xxx.png而没有带上上下文路径这也能解释为什么明明有61个JSP页面实际线上却经常找不到样式。5. 把案例改造成Maven工程再上线的三个关键改动5.1 用Maven收敛220个JAR包案例自带220个JAR包直接搬到生产环境会埋下版本冲突隐患。我通常的做法是先建一个pom.xml把源码里的import和struts-config.xml里的类名提取出来按依赖收敛。Struts 2.x的核心依赖如下dependency groupIdorg.apache.struts/groupId artifactIdstruts2-core/artifactId version2.5.30/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdorg.apache.struts/groupId artifactIdstruts2-spring-plugin/artifactId version2.5.30/version /dependencyprovided作用域的servlet-api不会被打进WAR避免和Tomcat自带版本冲突。如果案例中使用的是Struts 1.x则换成struts-core、struts-taglib和struts-extras。迁移后原WebRoot/WEB-INF/lib下的JAR全部删除最终打包体积能从上百MB降到十几MB。收敛依赖时可以用mvn dependency:tree检查是否有重复的commons-logging和log4j版本这两个库是Struts老项目的版本冲突重灾区。5.2 统一配置编码和数据源案例里28个properties文件大多是中英文资源包ApplicationResources.properties里建议显式声明编码label.username用户名 label.password密码同时在struts.xml里开启常量constant namestruts.i18n.encoding valueUTF-8/ constant namestruts.devMode valuefalse/如果案例用JDBC直连数据库我会把数据库连接池参数提取到db.properties再用Spring管理DataSource。这样改造后原来散落的DriverManager.getConnection()调用全部替换成DataSource.getConnection()避免每次请求都建立物理连接。这是一个很关键的Java基础优化点也是面试常问的连接池概念落地点。改造完成后可以在启动日志中看到连接池初始化的信息如果页面请求超过1万次通过jstat观察JVM的GC频率能明显感觉到连接复用带来的性能提升。5.3 用Jetty快速跑起来并验证拦截器Maven化之后可以直接用Jetty插件启动不需要本地装Tomcatmvn clean package mvn jetty:run -Djetty.http.port8080启动日志里如果看到SimpleInterceptor init说明拦截器已被容器实例化。随后访问http://localhost:8080/register打开控制台应当按顺序看到before action和after action两行日志并且after action后跟着resultsuccess。最后再用curl验证登录态和参数传递curl -X POST http://localhost:8080/register \ -d user.usernametesteruser.password123456 \ -c cookies.txt -L-L跟随重定向-c保存Cookie。如果响应里出现注册成功页面说明Action、拦截器、校验、JSP这四层链路已经全部打通。如果响应里出现404先看Jetty的日志里有没有No action mapped for namespace / and action name register有的话就是struts.xml中namespace不匹配。想同时跑两个实例做联调时可以再启动一个端口比如mvn jetty:run -Djetty.http.port8081用同一个浏览器对比不同节点下的Session和Cookie作用域差异。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →