Struts2+Spring+Velocity企业级Web应用开发实战项目

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: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 就开始了自己的表演:

  1. 读取 applicationContext.xml
  2. 解析 <bean> 标签,注册 Bean 定义;
  3. 实例化单例 Bean(默认预加载);
  4. 执行依赖注入(DI);
  5. 调用初始化方法(如 @PostConstruct );
  6. 容器就绪,可以对外提供服务。

这个过程可以用一张 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 对象的一生”。

  1. 实例化 (Instantiation):通过反射调用构造函数。
  2. 属性填充 (Populate Properties):注入依赖项。
  3. Aware 接口回调 :如 ApplicationContextAware.setApplicationContext()
  4. 前置处理 (BeanPostProcessor.beforeInitialization)
  5. 初始化方法调用
    - @PostConstruct
    - InitializingBean.afterPropertiesSet()
    - init-method 配置
  6. 后置处理 (BeanPostProcessor.afterInitialization)
  7. 可用状态 :正式上线,接受调用。
  8. 销毁前回调
    - @PreDestroy
    - DisposableBean.destroy()
    - destroy-method 配置
  9. 容器关闭时执行销毁

来看个例子感受一下顺序:

@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 启动四步流程:

  1. 资源定位 :根据 ResourceLoader 查找 .vm 文件;
  2. 语法分析 :构建 AST(抽象语法树);
  3. 上下文绑定 :将数据注入节点;
  4. 输出生成 :遍历 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,记录模板加载失败、表达式错误等关键事件。


这种高度集成的设计思路,正引领着智能音频设备向更可靠、更高效的方向演进。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Struts2、Spring和Velocity是Java Web开发中经典的技术组合,分别承担MVC架构中的控制层、业务层与视图层职责。Struts2处理请求调度与动作执行,Spring通过依赖注入实现松耦合的Bean管理与服务集成,Velocity则提供简洁的模板引擎以分离HTML与业务逻辑。本项目整合三大框架,构建高效、可扩展的企业级Web应用,涵盖配置文件管理、Action调用、Service注入及动态页面渲染等核心流程,适用于如iHand类模块化系统的开发,助力开发者掌握JavaEE主流框架的协同工作原理与实际应用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

考虑可再生能源出力不确定性的商业园区用户需求响应策略(Matlab代码实现)内容概要:本文围绕“考虑可再生能源出力不确定性的商业园区用户需求响应策略”展开,结合Matlab代码实现,研究在可再生能源(如风电、光伏)出力具有不确定性的背景下,商业园区如何制定有效的需求响应策略以优化能源调度和提升系统经济性。文中可能涉及不确定性建模(如场景生成与缩减)、优化模型构建(如随机规划、鲁棒优化)以及需求响应机制设计(如价格型、激励型),并通过Matlab仿真验证所提策略的有效性。此外,文档还列举了大量相关的电力系统、综合能源系统优化调度案例与代码资源,涵盖微电网调度、储能配置、负荷预测等多个方向,形成一个完整的科研支持体系。; 适合人群:具备一定电力系统、优化理论和Matlab编程基础的研究生、科研人员及从事能源系统规划与运行的工程技术人员。; 使用场景及目标:①学习如何建模可再生能源的不确定性并应用于需求响应优化;②掌握使用Matlab进行商业园区能源系统仿真与优化调度的方法;③复现论文结果或开展相关课题研究,提升科研效率与创新能力。; 阅读建议:建议结合文中提供的Matlab代码实例,逐步理解模型构建与求解过程,重点关注不确定性处理方法与需求响应机制的设计逻辑,同时可参考文档中列出的其他资源进行扩展学习与交叉验证。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值