简介:Struts2、Spring和Velocity是Java Web开发中经典的技术组合,分别承担MVC架构中的控制层、业务层与视图层职责。Struts2处理请求调度与动作执行,Spring通过依赖注入实现松耦合的Bean管理与服务集成,Velocity则提供简洁的模板引擎以分离HTML与业务逻辑。本项目整合三大框架,构建高效、可扩展的企业级Web应用,涵盖配置文件管理、Action调用、Service注入及动态页面渲染等核心流程,适用于如iHand类模块化系统的开发,助力开发者掌握JavaEE主流框架的协同工作原理与实际应用。
Struts2 + Spring + Velocity 架构深度整合实战:从请求入口到视图渲染的全链路解析
在智能家居设备日益复杂的今天,确保无线连接的稳定性已成为一大设计挑战……等等,这可不是我们要讲的故事。我们今天要聊的是一个“老派但经典”的技术组合—— Struts2 + Spring + Velocity 。
是的,你没看错,这不是考古报告,而是一次对传统企业级 Java Web 架构的深度复盘与优化之旅。虽然如今 Spring Boot、React、Vue 已经成为主流,但在银行、电信、ERP 等大型遗留系统的维护中,这套“三件套”依然活跃在一线战场。理解它,不仅是为了修 bug,更是为了读懂那些年写下的千行代码背后的设计哲学 🧠。
那么问题来了:
当一个 HTTP 请求打进来时,它是如何穿越层层框架,最终变成你浏览器里那一页 HTML 的?
为什么 Action 要设成prototype?
Velocity 到底是怎么把$user.name变成“张三”的?
Spring 是怎么悄无声息地把 Service 塞进你的 Action 里的?
别急,咱们这就从头捋一遍,带你走完这条完整的“请求高速公路”。🚗💨
框架协同的艺术:MVC 分离不是口号,是肌肉记忆
先来点轻松的。想象一下你在一家餐厅点餐:
- 服务员(Struts2) :负责接待你,听清你要什么菜,然后下单给厨房;
- 厨师长(Spring) :管理所有食材和厨子,知道谁会做红烧肉、谁擅长凉拌菜;
- 菜单模板(Velocity) :不管你点的是川菜还是粤菜,最后都得按标准格式打印出来,不能乱排版。
这就是 MVC 的本质:控制、业务、视图各司其职,互不干扰。
Struts2 扮演的就是那个穿制服的服务员。它的核心控制器 StrutsPrepareAndExecuteFilter (以前叫 FilterDispatcher )就像门口的迎宾,任何请求进来都会被它拦下:“您好,请问要点哪个 action?” 😊
public class UserAction extends ActionSupport {
private UserService userService; // 这个 service 从哪来?后面揭晓!
public String execute() {
userList = userService.getAllUsers();
return SUCCESS;
}
}
注意!这里的 userService 并没有用 new UserServiceImpl() 来创建。这是关键—— 对象的创建权交给了别人 ,也就是传说中的“控制反转”(IoC)。谁拥有这个权力?当然是 Spring 啦!
至于返回的 SUCCESS ,也不是直接输出 HTML,而是告诉系统:“去拿一个叫 success.vm 的模板来渲染吧。” 这就是典型的 MVC 解耦设计:Action 不关心页面长什么样,只负责准备数据;页面也不关心数据从哪来,只管展示。
整套流程下来,干净利落,职责分明。而这,正是这套古老架构至今仍值得学习的原因之一。
Spring 容器的秘密:Bean 是怎么“活”起来的?
说到 Spring,很多人第一反应是“依赖注入”,但真正让它强大的,是整个 Bean 生命周期管理体系 。我们可以把它比作一个“生命工厂”:原料进来,经过一系列工序,最终产出可用的对象。
启动那一刻:ApplicationContext 的诞生
一切始于 ApplicationContext 的初始化。你可以用 XML、注解,或者 Java Config 来驱动它。比如下面这段代码:
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
当这行代码执行时,Spring 就开始了自己的表演:
- 读取
applicationContext.xml - 解析
<bean>标签,注册 Bean 定义; - 实例化单例 Bean(默认预加载);
- 执行依赖注入(DI);
- 调用初始化方法(如
@PostConstruct); - 容器就绪,可以对外提供服务。
这个过程可以用一张 Mermaid 流程图清晰呈现:
graph TD
A[启动 ApplicationContext] --> B[加载配置元数据]
B --> C{配置类型?}
C -->|XML| D[解析 beans 标签]
C -->|Java Config| E[扫描 @Configuration 类]
C -->|注解| F[启用 @ComponentScan]
D --> G[注册 BeanDefinition]
E --> G
F --> G
G --> H[实例化单例 Bean]
H --> I[执行依赖注入]
I --> J[调用初始化方法]
J --> K[容器就绪]
是不是很像一条自动化生产线?而且你会发现,Spring 提供了多种“容器型号”:
| 特性 | BeanFactory | ApplicationContext |
|---|---|---|
| 延迟加载 | 是 | 否(默认预初始化单例) |
| 事件传播 | 不支持 | 支持 |
| 国际化消息源 | 不支持 | 支持 |
| 资源访问抽象 | 不完整 | 完整(ResourceLoader) |
| AOP 自动代理 | 手动配置 | 自动注册 |
所以,在绝大多数场景下,我们都说:“选 ApplicationContext ,闭眼入。”
Bean 的一生:从出生到销毁的九个阶段
你以为 Bean 创建出来就完事了?Too young too simple!
一个典型的单例 Bean 在 Spring 容器中要经历整整 9 个生命周期阶段 ,堪称“Java 对象的一生”。
- 实例化 (Instantiation):通过反射调用构造函数。
- 属性填充 (Populate Properties):注入依赖项。
- Aware 接口回调 :如
ApplicationContextAware.setApplicationContext()。 - 前置处理 (BeanPostProcessor.beforeInitialization)
- 初始化方法调用 :
-@PostConstruct
-InitializingBean.afterPropertiesSet()
-init-method配置 - 后置处理 (BeanPostProcessor.afterInitialization)
- 可用状态 :正式上线,接受调用。
- 销毁前回调 :
-@PreDestroy
-DisposableBean.destroy()
-destroy-method配置 - 容器关闭时执行销毁
来看个例子感受一下顺序:
@Component
public class LifecycleBean implements InitializingBean, DisposableBean, ApplicationContextAware {
@PostConstruct
public void postConstruct() {
System.out.println("Step 5: @PostConstruct called");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Step 5: afterPropertiesSet() called");
}
@PreDestroy
public void preDestroy() {
System.out.println("Step 8: @PreDestroy called");
}
@Override
public void destroy() throws Exception {
System.out.println("Step 8: destroy() called");
}
public void customInit() {
System.out.println("Custom init method via XML config");
}
public void customDestroy() {
System.out.println("Custom destroy method via XML config");
}
}
配合 XML 设置 init-method="customInit" 和 destroy-method="customDestroy" ,运行结果如下:
Step 3: ApplicationContext injected
Step 5: @PostConstruct method called
Step 5: afterPropertiesSet() called
Custom init method via XML config
...
Step 8: @PreDestroy method called
Step 8: destroy() called
Custom destroy method via XML config
看到了吗?三个“初始化”方法居然都被触发了!那它们的执行顺序是啥?
👉 答案是: @PostConstruct → afterPropertiesSet() → init-method
同理,销毁也是类似顺序。这也是为什么建议优先使用 @PostConstruct/@PreDestroy ——标准 JSR-250 注解,跨容器兼容性更好。
⚠️ 小贴士:Java 9+ 默认不再包含
javax.annotation包,记得加依赖:
xml <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency>
作用域之争:singleton vs prototype,到底该用哪个?
Spring 支持多种作用域,但最常用的就两个: singleton 和 prototype 。
| Scope | 描述 | 使用场景 |
|---|---|---|
| singleton | 每容器唯一实例 | 大多数无状态服务 |
| prototype | 每次请求都创建新实例 | Action、DTO 等有状态对象 |
| request | 每个 HTTP 请求一个实例 | Web 层组件 |
| session | 每个用户会话一个实例 | 用户偏好设置 |
重点来了: Struts2 的 Action 必须设为 prototype !
为什么?因为每个用户的请求都应该有独立的状态空间。如果你把 Action 设成单例,A 用户登录的信息可能会被 B 用户看到——想想都吓人😱。
<bean id="userAction"
class="com.example.action.UserAction"
scope="prototype">
<property name="userService" ref="userService"/>
</bean>
或者用注解:
@Scope("prototype")
@Component
public class UserAction { ... }
如果不这么做,后果可能就是:
Instance hash: 123456789
Instance hash: 123456789 ← 哎?两次请求同一个实例?
一旦发现哈希值相同,就得立刻检查配置,否则线上出问题是早晚的事。
XML 还是注解?一场持续多年的“路线之争”
随着 Spring 注解驱动开发的普及,开发者常常陷入选择困难症:继续用 XML,还是全面转向注解?
其实两者各有千秋:
| 维度 | XML 配置 | 注解配置 |
|---|---|---|
| 可维护性 | 集中式,易统一调整 | 分散式,需全局搜索 |
| 编译检查 | 无 | 有 |
| 学习曲线 | 明确但繁琐 | 直观但需记忆注解含义 |
| 动态切换实现 | 修改配置文件即可 | 需重新编译 |
| 适合场景 | 大型企业系统、遗留迁移 | 新项目、微服务 |
我的建议是: 混合使用,扬长避短 ✅
- 业务组件用
@Component,@Service标注; - 数据源、事务管理器等基础设施用 XML 或 Java Config;
- 多环境用
@Profile控制:
@Profile("prod")
@ImportResource("classpath:datasource-prod.xml")
public class ProdConfig {}
@Profile("dev")
@Configuration
public class DevConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(H2).build();
}
}
这样既保证灵活性,又不失可控性。
来看看趋势数据就知道了👇
pie
title Bean 声明方式使用趋势(2020–2024)
“纯 XML” : 8
“XML + 注解” : 35
“纯注解 + Java Config” : 57
超过一半的新项目已经拥抱纯注解模式。不过,在需要精细控制 Bean 创建顺序或集成第三方库时,XML 仍有不可替代的优势。
Velocity:不只是模板引擎,更是“安全隔离墙”
现在轮到我们的“压轴嘉宾”登场了 —— Velocity。
作为 Apache 的老牌模板引擎,它不像 JSP 允许嵌入任意 Java 代码,而是通过一套简洁的 VTL(Velocity Template Language)强制实现逻辑与表现分离。
这意味着: 你在 .vm 文件里写不了 System.exit(0) ,也搞不出 while(true) 死循环 。👏
这对安全性来说简直是福音。试想一下,如果运营人员能编辑模板,你还敢让他们随便写 Java 代码吗?当然不敢!但 Velocity 让他们可以在安全范围内自由发挥。
模板是怎么被“吃掉”又“吐出来”的?
当你调用 mergeTemplate() 时,Velocity 启动四步流程:
- 资源定位 :根据
ResourceLoader查找.vm文件; - 语法分析 :构建 AST(抽象语法树);
- 上下文绑定 :将数据注入节点;
- 输出生成 :遍历 AST 写入 Writer。
整个过程如下图所示:
flowchart TD
A[开始 mergeTemplate] --> B{模板是否已缓存?}
B -- 是 --> C[从缓存获取Template对象]
B -- 否 --> D[调用ResourceLoader.loadResource]
D --> E[创建InputStreamReader]
E --> F[Parser.parse()生成AST]
F --> G[构建Template实例并放入缓存]
G --> H[创建InternalContextAdapter]
H --> I[AST.render(context, writer)]
I --> J[遍历节点执行渲染逻辑]
J --> K[输出至Writer]
K --> L[返回渲染结果]
其中 InternalContextAdapter 是个重要角色,它包装了原始 VelocityContext ,并在渲染期间提供额外信息(如模板名、行号),还支持自定义事件监听。
性能优化三大招:缓存、工具类、避免深层嵌套
在高并发场景下,模板渲染可能成为瓶颈。但 Velocity 早已备好杀手锏:
第一招:开启模板缓存
file.resource.loader.cache = true
file.resource.loader.modification.check.interval = 0
生产环境必须开缓存,关掉修改检测,减少 I/O 开销。
第二招:封装高频操作为工具类
别让模板干脏活累活!
public class DateTool {
public String format(Date date) {
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
}
context.put("dateTool", new DateTool());
模板里直接写 $dateTool.format($order.createTime) ,清爽又高效。
第三招:限制 #foreach 规模,防止 OOM
大数据集一定要分页处理,别一股脑塞进去:
#foreach($product in $products)
#if($foreach.count > 50)## break not supported!
#break
#end
<li>$product.name</li>
#end
哦对,VTL 不支持 #break 和 #continue 😅,只能靠条件判断绕过去。
VTL 语法精要:变量、条件、循环三板斧
变量引用规则
| 引用类型 | VTL语法示例 | 对应Java操作 |
|---|---|---|
| 属性访问 | $user.name | user.getName() |
| 方法调用 | $list.size() | list.size() |
| 数组/列表索引 | $array[0] | array.get(0) 或 array[0] |
| Map访问 | $map["key"] 或 $map.key | map.get("key") |
| 安静引用 | $!missingVar | 不存在时不报错,输出空字符串 |
特别提醒: $!user.name 和 $user.name 差别很大!后者找不到变量会原样输出 $user.name ,前端看起来就像一堆占位符没替换,调试起来头疼死了。推荐开发期用严格模式:
runtime.references.strict=true
这样一旦引用失败就会抛异常,快速定位问题。
条件判断: #if 的艺术
#if($user.age >= 18)
<p>欢迎成年人用户。</p>
#else
<p>您尚未达到法定年龄。</p>
#end
支持 && , || , ! ,还能识别 null 和空集合为 false。
多分支用 #elseif 更优雅:
#if($score >= 90)
优秀
#elseif($score >= 80)
良好
#elseif($score >= 60)
及格
#else
不及格
#end
比嵌套 #if 清晰多了。
循环进阶技巧
#foreach 内建了不少元变量:
-
$foreach.index: 从 0 开始 -
$foreach.count: 从 1 开始 -
$foreach.first/$foreach.last: 首尾判断 -
$foreach.hasNext: 是否还有下一个
常用于表格斑马纹:
<tr class="$foreach.even ? 'even' : 'odd'">
或者面包屑导航加分隔符:
#foreach($crumb in $breadcrumbs separator=" > ")
<a href="$crumb.url">$crumb.title</a>
#end
连末尾多余的分隔符都帮你处理好了,贴心吧?❤️
整合之路:Struts2 如何牵手 Spring?
这才是重头戏。两个框架怎么打通任督二脉?
web.xml:一切开始的地方
首先得让 Spring 容器随应用启动:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
ContextLoaderListener 会在 Servlet 初始化前加载 Spring 上下文,并存入 ServletContext ,供后续组件使用。
紧接着,Struts2 的过滤器登场:
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
这个过滤器拦截所有请求,解析 struts.xml ,找到对应的 Action。
struts2-spring-plugin:幕后功臣
真正实现整合的,是这个插件:
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>2.5.30</version>
</dependency>
它会自动将 Struts2 的 ObjectFactory 替换为 SpringObjectFactory 。从此以后,每当 Struts2 想创建 Action,都会问一句:“Spring,帮我造一个。”
// org.apache.struts2.spring.StrutsSpringObjectFactory
public Object buildBean(Class clazz, Map<String, Object> extraContext) {
if (appContext.containsBean(clazz.getName())) {
return appContext.getBean(clazz.getName(), clazz);
} else {
return super.buildBean(clazz, extraContext);
}
}
聪明吧?找不到才自己动手,找到了就甩锅给 Spring 😎
Action 配置:从类名到 Bean 名
于是我们在 struts.xml 中可以这样写:
<action name="login" class="loginAction">
<result name="success">/welcome.vm</result>
</action>
这里的 class="loginAction" 不再是类名,而是 Spring 容器中注册的 Bean ID。
对应的 Spring 配置:
<bean id="loginAction" class="com.example.action.LoginAction" scope="prototype">
<property name="userService" ref="userService"/>
</bean>
这样一来,Action 就能享受 Spring 的全套服务了:DI、AOP、事务、缓存……统统安排上!
全链路追踪:一次请求的奇幻漂流
让我们最后一次串起所有环节,看看一个请求是如何完成它的使命的。
flowchart TD
A[Client Request] --> B{Filter: StrutsPrepareAndExecuteFilter}
B --> C[Parse Action Mapping in struts.xml]
C --> D[Instantiate Action via Spring Container]
D --> E[Invoke Action Method]
E --> F[Call @Autowired Service]
F --> G[DAO Access Database]
G --> H[Return Data to Action]
H --> I[Push Model to ValueStack]
I --> J[Resolve VelocityResult]
J --> K[Load .vm Template from File Loader]
K --> L[Merge Context with Template]
L --> M[Output HTML Response]
M --> N[Client Render Page]
每一步都在正确的人手里做正确的事:
- Struts2 控制流程;
- Spring 管理依赖;
- Velocity 负责展示;
- DAO 专注数据;
- Service 协调逻辑。
没有谁越界,也没有谁失职。这种高度结构化的协作模式,正是企业级应用稳定性的基石。
生产优化 checklist:上线前必做的五件事
最后送上一份实用清单,助你平稳落地:
✅ 开启模板缓存
file.resource.loader.cache = true
file.resource.loader.modificationCheckInterval = 3600
✅ 启用解析器池
parser.pool.size = 20
✅ 集中宏管理
velocimacro.library = macros.vm,ui-components.vm
✅ 保护敏感方法
禁用 getClass() 、 wait() 等危险方法调用,可在 security.properties 中配置。
✅ 日志监控到位
接入 Logback 或 Log4j2,记录模板加载失败、表达式错误等关键事件。
这种高度集成的设计思路,正引领着智能音频设备向更可靠、更高效的方向演进。
简介:Struts2、Spring和Velocity是Java Web开发中经典的技术组合,分别承担MVC架构中的控制层、业务层与视图层职责。Struts2处理请求调度与动作执行,Spring通过依赖注入实现松耦合的Bean管理与服务集成,Velocity则提供简洁的模板引擎以分离HTML与业务逻辑。本项目整合三大框架,构建高效、可扩展的企业级Web应用,涵盖配置文件管理、Action调用、Service注入及动态页面渲染等核心流程,适用于如iHand类模块化系统的开发,助力开发者掌握JavaEE主流框架的协同工作原理与实际应用。
7万+

被折叠的 条评论
为什么被折叠?



