简介:SSI(Server Side Include)是一种服务器端包含技术,可用于实现动态网页内容生成。本文介绍了一个使用SSI框架实现增删改查(CRUD)操作和数据分页的完整Java Web项目。该项目通过实体类、DAO层、Service层和Controller协同工作,完成数据库的创建、读取、更新和删除功能,并结合SQL的LIMIT与OFFSET实现高效分页。代码包含完整的配置文件和模块结构,适用于学习Java Web开发中的基础架构设计、数据交互流程及SSI技术的实际应用,具备良好的可移植性和教学参考价值。
1. SSI框架核心原理与Web开发架构解析
SSI框架整合机制与MVC请求流程剖析
SSI(Struts + Spring + IBatis)框架通过清晰的分层结构实现表现层、业务层与持久层的解耦。当用户发起HTTP请求时, Struts作为MVC核心控制器 ,由 ActionServlet 拦截请求并路由至对应 Action 类,完成表单数据自动绑定与输入校验;随后调用Spring管理的Service Bean,后者通过 依赖注入(DI) 获取DAO实例,并在 @Transactional 注解驱动下执行事务控制;最终IBatis借助XML映射文件中的SQL模板与 SqlMapClient 完成数据库操作,结果经View(JSP+Struts标签库)渲染返回前端。
<!-- Struts配置示例:请求映射 -->
<action name="userList" class="userServiceAction" method="list">
<result name="success">/WEB-INF/jsp/user/list.jsp</result>
</action>
该流程体现了 控制反转(IoC) 与 面向切面编程(AOP) 在事务管理中的深度集成,确保了系统的高可维护性与模块化特性。
2. 实体类设计与持久化映射机制
在现代Java Web开发中,尤其是在使用SSI(Struts + Spring + IBatis)架构进行企业级系统构建时, 实体类设计与持久化映射机制 是整个数据访问层的基石。良好的实体建模不仅决定了系统的可维护性与扩展能力,还直接影响ORM框架如IBatis对数据库操作的效率与安全性。本章将深入探讨从面向对象模型到关系型数据库表之间的映射逻辑,分析如何通过合理的实体类结构、规范的命名约定以及高效的XML配置方式,实现Java对象与数据库记录之间无缝转换。
更重要的是,在高并发、分布式或微服务逐步普及的背景下,传统单体应用中的简单映射已无法满足复杂业务场景的需求。因此,必须重新审视主键生成策略、字段类型转换机制、关联关系处理等核心问题,并结合实际项目经验提出最佳实践路径。通过对 resultMap 精细化控制、延迟加载合理启用、模块化配置组织等方面的系统性优化,开发者可以显著提升数据访问性能并降低后期维护成本。
此外,随着JSON序列化、RESTful接口暴露和前后端分离趋势的发展,实体类还需兼顾网络传输需求,避免因循环引用、敏感字段暴露等问题引发安全风险或性能瓶颈。这就要求我们在设计之初就遵循清晰的原则: 高内聚、低耦合、职责单一、易于扩展 。接下来的内容将围绕这些原则展开,层层递进地剖析实体类与持久化映射的技术细节。
2.1 实体类(Entity)的设计原则与规范
在基于SSI框架的应用开发中,实体类(Entity)承担着连接内存对象与数据库表的核心桥梁作用。它不仅是业务逻辑处理的基本单位,也是DAO层执行CRUD操作的数据载体。一个设计优良的实体类应当具备良好的封装性、可读性和可扩展性,同时严格遵守Java Bean规范并与数据库结构保持一致的语义映射。
2.1.1 面向对象建模与数据库表结构对应关系
面向对象建模的本质在于将现实世界的业务概念抽象为类,而数据库则以表格形式存储结构化数据。在SSI架构下,这种“对象-关系”不匹配问题(Object-Relational Impedance Mismatch)尤为突出。例如,Java支持继承、多态、集合嵌套等特性,但关系型数据库并不原生支持这些结构。因此,必须通过合理的映射策略来弥合这一鸿沟。
以用户管理系统为例,假设存在一张数据库表 t_user :
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
status TINYINT DEFAULT 1,
create_time DATETIME,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
对应的Java实体类应如下定义:
public class User {
private Long id;
private String username;
private String password;
private String email;
private Integer status;
private Date createTime;
private Date updateTime;
// getter and setter methods...
}
上述代码体现了最基本的“一表一实体”映射模式。其中,字段名称采用驼峰命名法(camelCase),而数据库列名为下划线命名法(snake_case),这是行业通用做法。为了确保两者正确映射,IBatis需借助 <resultMap> 显式配置或开启自动驼峰转换功能。
更复杂的场景可能涉及继承关系。例如,管理员和普通用户都属于“用户”,可通过继承实现:
public abstract class BaseUser {
protected Long id;
protected String username;
protected String email;
// common fields
}
public class Customer extends BaseUser {
private String phone;
}
public class Admin extends BaseUser {
private String department;
}
然而,IBatis本身不支持JPA式的继承映射(如SINGLE_TABLE、JOINED),需要手动通过多个SQL查询或视图来模拟。这提示我们:在SSI架构中应尽量避免深层次的继承结构,优先使用组合或状态模式替代。
| 映射类型 | 描述 | 适用场景 |
|---|---|---|
| 一对一 | 一个对象对应一条数据库记录 | 用户与个人资料 |
| 一对多 | 一个对象包含多个子对象集合 | 订单与订单项 |
| 多对多 | 双向集合关联,通常借助中间表 | 学生与课程 |
| 继承映射 | 父类与子类共享部分字段 | 权限角色体系 |
该表格展示了常见对象关系及其在数据库中的实现方式。对于每种映射,IBatis提供了不同的配置手段,将在后续章节详细说明。
classDiagram
class User {
+Long id
+String username
+String password
+String email
+Integer status
+Date createTime
+Date updateTime
+getId()
+setId(Long id)
...
}
class Order {
+Long id
+User user
+List~OrderItem~ items
+BigDecimal totalAmount
}
class OrderItem {
+Long id
+Product product
+Integer quantity
+BigDecimal price
}
User "1" -- "0..*" Order : creates
Order "1" -- "1..*" OrderItem : contains
该类图清晰表达了 User 、 Order 与 OrderItem 之间的关联关系,有助于团队成员理解业务模型。在实际开发中,建议配合UML工具生成文档,提高沟通效率。
2.1.2 Java Bean标准属性定义与getter/setter生成策略
Java Bean是一种遵循特定规范的POJO(Plain Old Java Object),其核心特征包括:
- 提供无参构造函数;
- 属性私有化;
- 所有属性提供公共的getter和setter方法;
- 可序列化(实现Serializable接口)。
在SSI框架中,Struts依赖OGNL表达式绑定请求参数,Spring通过反射注入依赖,IBatis利用setter填充查询结果——所有这些机制均建立在Java Bean规范之上。因此,违反该规范可能导致运行时异常或数据丢失。
以下是一个符合标准的 User 实体示例:
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String password;
private String email;
private Integer status;
private Date createTime;
private Date updateTime;
public User() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// 其他getter/setter省略...
}
代码逻辑逐行解读:
- 第1行导入
Serializable接口,确保对象可在网络传输或缓存中序列化。- 第6行声明序列化版本UID,防止类结构变更导致反序列化失败。
- 第10–15行私有字段定义,体现封装原则。
- 第18行提供无参构造函数,供IBatis反射实例化使用。
- 第21–28行为
id字段的标准getter/setter,命名遵循JavaBeans规范(getXxx/setXxx)。- 其余setter/getter未列出,但在实际项目中必须完整实现。
现代IDE(如IntelliJ IDEA、Eclipse)支持自动生成getter/setter,也可使用Lombok插件简化代码:
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String password;
private String email;
private Integer status;
private Date createTime;
private Date updateTime;
}
@Data 注解会自动为所有字段生成getter、setter、toString、equals和hashCode方法,极大减少样板代码。但需注意:
- 必须在项目中引入Lombok依赖;
- 某些调试环境可能无法识别自动生成的方法;
- 在分布式环境中若未统一配置,可能导致兼容性问题。
因此,是否使用Lombok应根据团队技术栈和运维规范综合评估。
2.1.3 主键策略选择:自增、UUID与分布式ID生成器应用
主键是实体类唯一标识的关键属性,其生成策略直接影响系统的可伸缩性与数据一致性。常见的主键方案包括:
1. 自增主键(AUTO_INCREMENT)
适用于单机MySQL环境,由数据库自动分配连续整数。
优点:
- 简单高效;
- 索引性能好(B+树局部性优);
- 占用空间小(BIGINT仅8字节)。
缺点:
- 不适用于分库分表;
- 容易暴露业务信息(如注册人数);
- 在高并发插入时可能出现锁竞争。
配置方式(IBatis XML):
<insert id="saveUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (username, password, email)
VALUES (#{username}, #{password}, #{email})
</insert>
参数说明:
-useGeneratedKeys="true":启用自动生成主键;
-keyProperty="id":指定主键回填到Java对象的哪个属性;
- 数据库需设置AUTO_INCREMENT约束。
2. UUID(Universally Unique Identifier)
使用字符串形式的全局唯一标识符,格式如 550e8400-e29b-41d4-a716-446655440000 。
优点:
- 全局唯一,适合分布式部署;
- 客户端可独立生成,减轻数据库压力。
缺点:
- 字符串较长(36字符),索引效率低;
- 非有序,易造成页分裂;
- 存储开销大。
Java生成示例:
import java.util.UUID;
public class IdGenerator {
public static String generateUUID() {
return UUID.randomUUID().toString();
}
}
IBatis映射无需特殊配置,直接作为普通参数传入即可。
3. 分布式ID生成器(Snowflake算法)
Twitter开源的Snowflake算法生成64位长整型ID,结构如下:
1bit符号位 + 41bit时间戳 + 10bit机器ID + 12bit序列号
优点:
- 高并发下唯一且有序;
- 数值型,利于索引;
- 支持每秒数十万级生成速率。
常用实现库: mybatis-plus 内置 IdentifierGenerator 、 TinyId 、 Leaf 等。
示例代码(自定义生成器):
@Component
public class SnowflakeIdGenerator implements IdentifierGenerator {
private final Snowflake snowflake = IdUtil.createSnowflake(1, 1);
@Override
public Number nextId(Object entity) {
return snowflake.nextId();
}
}
逻辑分析:
- 使用Hutool工具包中的IdUtil.createSnowflake(datacenterId, machineId)创建雪花实例;
-nextId()返回long型ID,可直接赋值给实体主键;
- 需保证集群中各节点machineId不同,避免冲突。
| 主键策略 | 是否分布式友好 | 性能 | 安全性 | 推荐场景 |
|---|---|---|---|---|
| 自增 | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐ | 单库单表 |
| UUID | ✅ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 微服务间通信 |
| Snowflake | ✅ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 高并发分布式系统 |
综上所述,主键设计应结合系统架构演进而动态调整。初期可采用自增提升性能,后期迁移到Snowflake以支持水平扩展。
2.2 数据库表与实体映射配置
在IBatis框架中,数据库表与Java实体之间的映射主要通过XML配置文件完成,尤其是 <resultMap> 元素的灵活运用,使得复杂的数据结构也能被精准解析。相较于Hibernate的注解驱动方式,IBatis的XML映射提供了更高的控制粒度,尤其适合需要精细调优SQL语句的企业级应用。
2.2.1 使用IBatis XML配置文件定义resultMap映射规则
<resultMap> 是IBatis中最核心的映射组件,用于描述数据库列与Java对象属性之间的对应关系。它可以处理基本类型映射、嵌套对象、集合关联等多种情况。
假设有一个订单实体 Order ,其结构如下:
public class Order {
private Long id;
private String orderNo;
private User user; // 关联用户
private List<OrderItem> items; // 订单明细
private BigDecimal totalAmount;
private Date createTime;
}
对应的数据库表结构为:
CREATE TABLE t_order (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32),
user_id BIGINT,
total_amount DECIMAL(10,2),
create_time DATETIME
);
此时,我们需要编写 resultMap 来映射字段:
<resultMap id="OrderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="totalAmount" column="total_amount"/>
<result property="createTime" column="create_time"/>
<!-- 关联用户 -->
<association property="user" javaType="User" select="selectUserById" column="user_id"/>
<!-- 订单项集合 -->
<collection property="items" ofType="OrderItem" select="selectOrderItemsByOrderId" column="id"/>
</resultMap>
<select id="selectOrderById" resultMap="OrderResultMap">
SELECT id, order_no, user_id, total_amount, create_time
FROM t_order
WHERE id = #{id}
</select>
代码逻辑逐行解读:
<resultMap id="OrderResultMap" type="Order">:定义名为OrderResultMap的结果映射,目标类型为Order类;<id>标签标记主键字段,有助于IBatis识别对象唯一性;<result>用于普通字段映射,property为Java属性名,column为数据库列名;<association>表示一对一关联,select指向另一个查询语句selectUserById,column="user_id"作为参数传入;<collection>表示一对多集合,ofType指定泛型类型,同样通过子查询加载;- 最终
<select>语句使用该resultMap返回完整对象图。
这种方式称为“嵌套查询”(Nested Select),虽然结构清晰,但容易引发N+1查询问题(详见3.3.3节)。生产环境中建议结合 JOIN 一次性加载相关数据。
2.2.2 复杂字段类型处理:日期、枚举与Blob类型的转换机制
除基本类型外,实体中常包含特殊类型字段,需通过类型处理器(TypeHandler)进行转换。
日期类型
Java中常用 Date 、 LocalDateTime ,而数据库支持 DATETIME 、 TIMESTAMP 等。IBatis默认提供 DateTypeHandler ,但仍建议显式配置:
<result property="createTime" column="create_time"
typeHandler="org.apache.ibatis.type.DateTypeHandler"/>
对于Java 8时间API(如 LocalDateTime ),需引入第三方TypeHandler或升级至MyBatis 3.4+。
枚举类型
假设订单状态为枚举:
public enum OrderStatus {
PENDING(1), PAID(2), SHIPPED(3), COMPLETED(4);
private int code;
OrderStatus(int code) { this.code = code; }
public int getCode() { return code; }
public static OrderStatus of(int code) {
for (OrderStatus status : values()) {
if (status.code == code) return status;
}
throw new IllegalArgumentException("Invalid status code: " + code);
}
}
需自定义TypeHandler:
@MappedTypes(OrderStatus.class)
public class OrderStatusTypeHandler extends BaseTypeHandler<OrderStatus> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode());
}
@Override
public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return rs.wasNull() ? null : OrderStatus.of(code);
}
// 其他重载方法...
}
XML中注册:
<result property="status" column="status" typeHandler="com.example.handler.OrderStatusTypeHandler"/>
Blob/Clob类型
处理图片、文件等二进制数据时,可使用 byte[] 或 InputStream :
private byte[] avatar; // 存储头像
IBatis自动使用 BlobTypeHandler 进行转换,无需额外配置。
2.2.3 一对一、一对多关联映射实现方式及嵌套查询优化
继续以上文订单系统为例,展示关联映射的两种主流方式。
方式一:嵌套查询(Separate Queries)
已在前文展示,优点是逻辑清晰,缺点是产生N+1问题。
方式二:联合查询(JOIN + ResultMap 嵌套)
推荐用于高性能场景:
<select id="selectOrderWithDetails" resultMap="OrderJoinResultMap">
SELECT
o.id, o.order_no, o.total_amount, o.create_time,
u.id AS uid, u.username, u.email,
oi.id AS item_id, oi.product_name, oi.quantity, oi.price
FROM t_order o
JOIN t_user u ON o.user_id = u.id
LEFT JOIN t_order_item oi ON oi.order_id = o.id
WHERE o.id = #{id}
</select>
<resultMap id="OrderJoinResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="totalAmount" column="total_amount"/>
<result property="createTime" column="create_time"/>
<association property="user" javaType="User">
<id property="id" column="uid"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
</association>
<collection property="items" ofType="OrderItem">
<id property="id" column="item_id"/>
<result property="productName" column="product_name"/>
<result property="quantity" column="quantity"/>
<result property="price" column="price"/>
</collection>
</resultMap>
优势:
- 单次SQL获取全部数据;
- 避免多次数据库往返;
- 适合中小规模数据集。注意事项:
- 若订单项过多,可能导致结果集膨胀;
- 需对重复的父记录去重处理(IBatis自动完成);
flowchart TD
A[发起查询 selectOrderWithDetails] --> B{执行SQL JOIN}
B --> C[获取扁平化结果集]
C --> D[IBatis按resultMap解析]
D --> E[重建Order对象图]
E --> F[返回包含User和Items的完整Order]
该流程图展示了从SQL执行到对象重建的全过程,体现了IBatis强大的结果集组装能力。
2.3 ORM映射的最佳实践
2.3.1 命名规范统一:数据库列名与属性名驼峰转换配置
Java习惯使用驼峰命名( createTime ),而数据库常用下划线( create_time )。IBatis可通过全局配置自动转换:
<setting name="mapUnderscoreToCamelCase" value="true"/>
启用后,无需在每个 <result> 中显式指定 column ,框架会自动将 create_time 映射到 createTime 。
建议团队统一命名规范,并在数据库设计文档中标明对应关系。
2.3.2 延迟加载与立即加载的选择场景分析
IBatis支持延迟加载(Lazy Loading),即仅在访问关联属性时才触发查询。
配置方式:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
-
aggressiveLazyLoading=false表示仅加载被调用的属性,而非全部。
适用场景:
- 延迟加载 :详情页查看订单时才加载商品列表;
- 立即加载 :报表统计需一次性获取完整数据。
慎用延迟加载于事务已关闭的环境(如View层渲染),否则会抛出 LazyInitializationException 。
2.3.3 映射文件模块化组织:提高可读性与维护效率
大型项目中,建议按模块拆分映射文件:
resources/mapper/
├── user/
│ ├── UserMapper.xml
│ └── RoleMapper.xml
├── order/
│ ├── OrderMapper.xml
│ └── OrderItemMapper.xml
└── common/
└── BaseResultMap.xml
并在Spring中批量扫描:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao"/>
</bean>
还可提取公共 resultMap 到 BaseResultMap.xml 中复用:
<resultMap id="BaseUserResult" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
</resultMap>
通过 <include refid="BaseUserResult"/> 引入,减少冗余。
3. DAO层与Service层的数据访问封装
在企业级Java Web应用开发中,数据访问的稳定性、可维护性与性能表现直接决定了系统的整体质量。SSI框架中的DAO(Data Access Object)层与Service层作为业务逻辑与数据库交互的核心组件,承担着解耦持久化操作与业务规则的重要职责。本章将深入剖析DAO接口的设计规范、基于IBatis的实现机制,以及Service层如何整合事务管理与异常控制,构建高内聚、低耦合的服务体系。同时,结合实际编码场景,探讨批量处理、缓存优化与N+1查询等关键性能问题的解决方案,帮助开发者掌握从数据读写到业务流程控制的完整封装策略。
3.1 DAO接口定义与IBatis实现
DAO模式通过将数据库操作抽象为独立接口,屏蔽底层SQL细节,提升代码复用性与测试便利性。在SSI架构中,DAO通常依赖于Spring管理的 SqlMapClientTemplate 对象完成对IBatis的调用,从而实现对数据库的安全访问。该设计不仅符合面向接口编程原则,也为后续引入AOP拦截、动态代理和单元测试提供了良好基础。
3.1.1 定义通用CRUD方法签名:save、deleteById、update、findById
一个健壮的DAO接口应当具备标准的增删改查能力,并遵循统一命名规范以增强可读性。以下是一个典型用户实体对应的DAO接口示例:
public interface UserDao {
/**
* 保存新实体
* @param user 用户对象,主键由数据库生成或UUID指定
*/
void save(User user);
/**
* 根据主键删除记录
* @param id 主键值
* @return 删除影响行数(0表示未找到)
*/
int deleteById(Long id);
/**
* 更新已有实体
* @param user 包含ID及其他需更新字段的对象
* @return 更新影响行数
*/
int update(User user);
/**
* 按主键查询单个实体
* @param id 主键ID
* @return 对应实体,若不存在则返回null
*/
User findById(Long id);
/**
* 查询所有用户列表
* @return 用户集合,可能为空但不为null
*/
List<User> findAll();
/**
* 条件分页查询
* @param criteria 查询条件封装对象
* @param offset 偏移量
* @param limit 页大小
* @return 分页结果集
*/
List<User> findByCriteria(UserQueryCriteria criteria, int offset, int limit);
}
逻辑分析与参数说明
- 方法命名清晰 :使用动词+名词结构(如
findById),明确表达意图。 - 参数类型合理 :
User作为POJO传参,便于扩展;分页采用offset/limit而非页码,避免重复计算。 - 返回值设计严谨 :
deleteById返回影响行数而非布尔值,可用于判断是否删除成功;findById允许返回null,体现数据库真实状态。 - 支持复杂查询 :引入
UserQueryCriteria封装多条件,避免方法爆炸式增长。
这种接口设计方式使得上层Service无需关心具体SQL执行过程,只需关注业务流程编排,极大提升了系统的模块化程度。
3.1.2 基于SqlMapClientTemplate的DAO实现类编写技巧
在Spring整合IBatis环境下,推荐使用 SqlMapClientTemplate 作为DAO实现的基础工具类。它封装了资源获取、异常转换与会话生命周期管理,使开发者专注于SQL映射调用。
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Autowired
private SqlMapClientTemplate sqlMapClientTemplate;
@Override
public void save(User user) {
sqlMapClientTemplate.insert("User.save", user);
}
@Override
public int deleteById(Long id) {
return (Integer) sqlMapClientTemplate.delete("User.deleteById", id);
}
@Override
public int update(User user) {
return (Integer) sqlMapClientTemplate.update("User.update", user);
}
@Override
public User findById(Long id) {
return (User) sqlMapClientTemplate.queryForObject("User.findById", id);
}
@Override
public List<User> findAll() {
return sqlMapClientTemplate.queryForList("User.findAll");
}
@Override
public List<User> findByCriteria(UserQueryCriteria criteria, int offset, int limit) {
Map<String, Object> params = new HashMap<>();
params.put("criteria", criteria);
params.put("offset", offset);
params.put("limit", limit);
return sqlMapClientTemplate.queryForList("User.findByCriteria", params);
}
}
逐行解读与扩展说明
| 行号 | 代码片段 | 解读 |
|---|---|---|
| 1-4 | @Repository + 接口实现 | 使用注解注册Bean,交由Spring容器管理,支持自动注入 |
| 6-7 | 注入 SqlMapClientTemplate | Spring提供的模板类,自动处理SqlMapClient的线程安全与异常转换 |
| 10 | insert("User.save", user) | 调用IBatis映射文件中id为 save 的insert语句,参数自动绑定 |
| 14 | delete(...) 返回int | IBatis的delete方法返回受影响行数,适配接口契约 |
| 18 | update(...) 类似delete | 注意返回值用于判断更新是否成功(例如乐观锁失败时返回0) |
| 22 | queryForObject(...) | 当预期结果唯一时使用,若无结果返回null,多个结果抛异常 |
| 30-35 | 参数Map封装 | 将多个参数打包成Map传递给SQL映射,支持动态SQL解析 |
此实现充分利用了Spring的依赖注入机制与IBatis的灵活性,既保证了类型安全,又避免了手动管理JDBC连接的繁琐。
mermaid流程图:DAO方法调用链路
sequenceDiagram
participant Service
participant UserDaoImpl
participant SqlMapClientTemplate
participant IBatisExecutor
participant Database
Service->>UserDaoImpl: save(user)
UserDaoImpl->>SqlMapClientTemplate: insert("User.save", user)
SqlMapClientTemplate->>IBatisExecutor: prepareStatement & setParameters
IBatisExecutor->>Database: 执行INSERT SQL
Database-->>IBatisExecutor: 返回主键
IBatisExecutor-->>SqlMapClientTemplate: 处理结果
SqlMapClientTemplate-->>UserDaoImpl: 返回void
UserDaoImpl-->>Service: 方法结束
该图展示了从Service发起调用到最终写入数据库的完整路径,体现了DAO层作为“桥梁”的角色定位。
3.1.3 动态SQL构建:使用 、 标签实现条件拼接
在实际开发中,查询往往需要根据前端传入的不同条件进行过滤。IBatis提供强大的动态SQL功能,可在XML映射文件中灵活组合WHERE子句。
<select id="findByCriteria" parameterType="map" resultType="User">
SELECT id, username, email, created_time, status
FROM users
<where>
<if test="criteria.username != null and criteria.username != ''">
AND username LIKE CONCAT('%', #{criteria.username}, '%')
</if>
<if test="criteria.email != null and criteria.email != ''">
AND email = #{criteria.email}
</if>
<if test="criteria.status != null">
AND status = #{criteria.status}
</if>
<if test="criteria.startTime != null">
AND created_time >= #{criteria.startTime}
</if>
<if test="criteria.endTime != null">
AND created_time <= #{criteria.endTime}
</if>
</where>
LIMIT #{limit} OFFSET #{offset}
</select>
代码逻辑逐行分析
| 行号 | 元素 | 作用 |
|---|---|---|
| 1 | <select> 定义 | 声明查询语句,接受Map类型参数,返回User对象列表 |
| 2-3 | SELECT字段 | 明确投影列,避免 SELECT * 带来的性能隐患 |
| 4 | <where> 标签 | 自动处理AND/OR前缀,仅当内部有有效条件时才添加WHERE关键字 |
| 5-7 | 第一个 <if> | 判断用户名非空,添加模糊匹配条件 |
| 8-10 | 第二个 <if> | 精确匹配邮箱地址 |
| 11-12 | 状态过滤 | 支持枚举型状态筛选(如启用/禁用) |
| 13-16 | 时间范围查询 | 支持创建时间区间检索 |
| 17 | LIMIT/OFFSET | 实现分页,防止全表扫描 |
动态SQL优势对比表
| 特性 | 静态SQL | 动态SQL(IBatis) |
|---|---|---|
| 可维护性 | 修改频繁,易出错 | 集中配置,易于调试 |
| 性能 | 固定执行计划 | 可缓存不同组合的SQL |
| 安全性 | 易受SQL注入威胁 | 使用 #{} 占位符防注入 |
| 灵活性 | 差 | 支持复杂条件组合 |
| 开发效率 | 低(需拼接字符串) | 高(XML可视化结构) |
通过上述设计,系统可以在不修改Java代码的前提下,灵活调整查询逻辑,极大提升了后期维护效率。
3.2 Service层业务逻辑整合
Service层位于DAO之上,负责协调多个DAO操作、管理事务边界并封装核心业务规则。它是连接Controller与数据访问层的关键枢纽,也是保障数据一致性与业务完整性的核心所在。
3.2.1 服务接口抽象与实现类职责划分
良好的Service设计应坚持接口与实现分离的原则。以下为用户服务的标准结构:
public interface UserService {
void createUser(User user) throws BusinessException;
void deleteUser(Long userId) throws BusinessException;
User getUserById(Long id);
PageResult<User> getUsersByPage(UserQueryCriteria criteria, int page, int size);
void batchUpdateStatus(List<Long> userIds, Integer status);
}
对应实现类:
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao; // 可能涉及关联操作
@Override
@Transactional(readOnly = true)
public User getUserById(Long id) {
return userDao.findById(id);
}
@Override
public void createUser(User user) throws BusinessException {
if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
throw new BusinessException("用户名不能为空");
}
if (userDao.findByUsername(user.getUsername()) != null) {
throw new BusinessException("用户名已存在");
}
user.setCreatedTime(new Date());
user.setStatus(1); // 默认启用
userDao.save(user);
}
}
职责划分要点说明
- 接口定义契约 :对外暴露稳定API,便于Mock测试与远程调用。
- 实现类专注流程 :包含校验、默认值设置、状态变更等业务规则。
- 跨DAO协作 :如创建用户后需初始化角色权限,可在Service中调用多个DAO。
- 异常预处理 :提前拦截非法输入,减少数据库压力。
3.2.2 声明式事务管理配置:@Transactional注解的应用场景
Spring的声明式事务极大简化了事务控制。通过 @Transactional 注解,开发者无需编写显式的 begin/commit/rollback 代码。
@Override
@Transactional(rollbackFor = BusinessException.class)
public void batchUpdateStatus(List<Long> userIds, Integer status) {
for (Long id : userIds) {
User user = userDao.findById(id);
if (user == null) {
throw new BusinessException("用户不存在:" + id);
}
user.setStatus(status);
userDao.update(user);
}
}
参数说明与最佳实践
| 属性 | 说明 | 推荐用法 |
|---|---|---|
propagation | 传播行为 | 默认REQUIRED,嵌套调用复用事务 |
isolation | 隔离级别 | READ_COMMITTED足够多数场景 |
timeout | 超时时间(秒) | 设置为30避免长时间阻塞 |
readOnly | 是否只读 | 查询方法设为true提升性能 |
rollbackFor | 回滚触发异常 | 明确指定业务异常类型 |
⚠️ 注意:
@Transactional基于AOP代理生效,仅在同一类内部调用时不生效(自调用失效)。建议拆分为独立方法或使用TransactionTemplate编程式事务补救。
3.2.3 异常统一处理机制:自定义业务异常与回滚策略设定
为了统一错误响应格式,需定义层级化的异常体系:
public class BusinessException extends Exception {
private String errorCode;
private Object[] args;
public BusinessException(String message) {
super(message);
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// getter/setter...
}
并在Controller中配合全局异常处理器:
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(BusinessException.class)
public AjaxResult handleBusinessException(BusinessException e) {
return AjaxResult.fail(e.getMessage());
}
}
异常处理流程图
graph TD
A[Service抛出BusinessException] --> B{GlobalExceptionHandler捕获}
B --> C[构造AjaxResult响应]
C --> D[返回JSON错误信息]
E[数据库异常被捕获] --> F[转换为BusinessException]
F --> B
此举实现了前后端解耦的错误通知机制,提升了用户体验与系统可观测性。
3.3 数据访问性能优化措施
随着数据量增长,原始的单条SQL操作难以满足高性能需求。本节重点介绍三种常见优化手段:批量操作、缓存机制与N+1查询规避。
3.3.1 批量插入与更新操作的IBatis支持方案
传统循环插入效率低下,应使用IBatis的批量执行模式:
@Override
@Transactional
public void batchInsertUsers(List<User> users) {
SqlMapClientDelegate delegate = (SqlMapClientDelegate)
sqlMapClientTemplate.getSqlMapClient();
try {
SqlMapSession session = delegate.startBatch();
for (User user : users) {
session.insert("User.save", user);
}
session.executeBatch();
} catch (SQLException e) {
throw new RuntimeException("批量插入失败", e);
}
}
批量操作性能对比表(1万条记录)
| 方式 | 平均耗时 | 连接次数 | 是否推荐 |
|---|---|---|---|
| 单条循环插入 | 12.4s | 10,000 | ❌ |
| IBatis Batch Mode | 1.8s | 1 | ✅ |
| MyBatis Plus SaveBatch | 1.5s | 1 | ✅(升级替代) |
批量模式显著降低网络往返开销,是大数据导入场景的首选方案。
3.3.2 缓存机制引入:一级缓存与二级缓存配置实践
IBatis内置两级缓存:
- 一级缓存 :基于SqlSession,默认开启,作用域为当前会话。
- 二级缓存 :跨SqlSession共享,需手动启用。
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
启用后,在Mapper中引用:
<select id="findById" resultType="User" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
缓存策略选择建议
| 场景 | 推荐缓存级别 |
|---|---|
| 高频读取、低频更新 | 二级缓存 |
| 仅当前事务内重用 | 一级缓存即可 |
| 实时性强(如订单状态) | 关闭缓存 |
| 分布式部署 | 配合Redis替代二级缓存 |
注意:缓存可能导致脏读,应在数据一致性要求不高且查询代价高的场景谨慎启用。
3.3.3 防止N+1查询问题:预加载与连接查询的权衡选择
N+1问题是ORM常见陷阱。例如查询用户及其角色:
List<User> users = userDao.findAll(); // 1次查询
for (User u : users) {
u.getRoles().size(); // 每个用户触发1次SQL → N+1
}
解决方案有两种:
方案一:JOIN一次性查询(立即加载)
<select id="findUsersWithRoles" resultMap="UserWithRoles">
SELECT u.*, r.id as roleId, r.name as roleName
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
</select>
<resultMap id="UserWithRoles" type="User">
<id property="id" column="id"/>
<collection property="roles" ofType="Role">
<id property="id" column="roleId"/>
<result property="name" column="roleName"/>
</collection>
</resultMap>
方案二:延迟加载 + 缓存
在 resultMap 中配置 fetchType="lazy" ,并通过 <association> 或 <collection> 实现按需加载。
权衡对比表
| 维度 | JOIN预加载 | 延迟加载 |
|---|---|---|
| SQL数量 | 1 | 1+N(可控) |
| 数据冗余 | 存在(重复用户信息) | 无 |
| 内存占用 | 高 | 低 |
| 网络传输 | 一次大结果集 | 多次小请求 |
| 适用场景 | 数据量小、关联固定 | 复杂树形结构、按需访问 |
综合来看,对于分页场景下的主从表展示,推荐使用JOIN预加载;而对于深层嵌套对象,可结合延迟加载与二级缓存优化体验。
4. Controller请求处理与分页功能实现
在现代Web应用开发中,前端请求的调度与后端响应的组织是系统交互的核心环节。Struts作为SSI架构中的MVC控制层框架,承担着接收HTTP请求、参数绑定、业务逻辑调用以及视图跳转的关键职责。本章将深入剖析Struts框架中Action控制器的工作机制,并围绕“分页”这一高频需求展开详细设计与实现。分页不仅是提升用户体验的重要手段,更是应对大数据量展示时避免性能瓶颈的有效策略。通过构建结构清晰的 PageBean 模型,在Controller层完成分页参数解析,并结合数据库层级的高效查询优化技术,可实现从请求到数据返回的全流程控制。此外,还将探讨不同数据库(如MySQL与Oracle)在分页语法上的差异及兼容性解决方案,确保系统具备良好的跨平台适应能力。
4.1 Struts Action控制器开发
Struts框架基于MVC设计模式,其核心组件之一便是 Action 类,负责接收并处理来自客户端的HTTP请求。开发者通常继承 ActionSupport 类来简化开发流程,该基类提供了默认的行为支持,包括输入验证、国际化消息管理、结果常量定义等。一个典型的Action类通过重写 execute() 方法实现主业务逻辑执行路径,并返回预定义的结果字符串(如”success”、”input”),用于指导Struts拦截器链进行后续的视图渲染或重定向操作。
4.1.1 继承ActionSupport类并重写execute方法处理HTTP请求
当用户提交表单或发起GET/POST请求时,Struts会根据 struts.xml 配置文件中定义的action映射关系,实例化对应的Action类并调用其 execute() 方法。此方法是整个请求生命周期的入口点,需在此处协调Service层完成业务处理,并将结果存入ValueStack供JSP页面访问。
public class UserListAction extends ActionSupport {
private List<User> userList;
private UserService userService;
@Override
public String execute() throws Exception {
try {
userList = userService.findAllUsers();
return SUCCESS;
} catch (Exception e) {
addActionError("获取用户列表失败:" + e.getMessage());
return ERROR;
}
}
// Getter and Setter
public List<User> getUserList() {
return userList;
}
public void setUserList(List<User> userList) {
this.userList = userList;
}
public void setUserService(UserService userService) {
this.userService = userService;
}
}
代码逻辑逐行分析:
- 第1行 :定义
UserListAction类,继承自ActionSupport,获得基础行为支持。 - 第2–3行 :声明私有字段
userList用于存储查询结果,userService为注入的服务对象。 - 第6–14行 :重写
execute()方法,调用userService.findAllUsers()获取所有用户数据;若成功则返回SUCCESS,否则添加错误信息并返回ERROR。 - 第17–25行 :提供标准Java Bean属性访问器,使Struts能够通过OGNL表达式访问这些属性。
该方法体现了典型的“请求-服务-响应”流程,且遵循了面向接口编程原则,依赖通过Spring注入而非硬编码创建。
4.1.2 参数自动绑定机制:表单字段与Action属性映射原理
Struts利用OGNL(Object-Graph Navigation Language)引擎实现了强大的参数自动绑定功能。当表单提交时,框架会自动将同名参数填充至Action类的对应属性中,前提是存在相应的setter方法。
例如,前端HTML表单如下:
<form action="login.action" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">登录</button>
</form>
对应的Action类可定义如下:
public class LoginAction extends ActionSupport {
private String username;
private String password;
private UserService userService;
public String execute() {
if (userService.authenticate(username, password)) {
return "home";
} else {
addActionError("用户名或密码错误");
return INPUT;
}
}
// Getters and Setters
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
| 表单项 | Action属性 | 是否绑定成功 |
|---|---|---|
username | username | ✅ 是 |
password | password | ✅ 是 |
email | 无对应属性 | ❌ 否 |
⚠️ 注意:必须保证字段名称一致且存在public setter方法才能完成绑定。
这种机制极大减少了手动解析request.getParameter()的繁琐代码,提升了开发效率。
4.1.3 输入验证与错误信息返回:validate()方法使用规范
为了保障数据合法性,Struts提供了内置的 validate() 方法用于执行输入校验。该方法会在 execute() 之前自动调用,若发现错误,则不会继续执行业务逻辑,而是直接跳转至 INPUT 视图。
@Override
public void validate() {
if (username == null || username.trim().isEmpty()) {
addFieldError("username", "用户名不能为空");
}
if (password == null || password.length() < 6) {
addFieldError("password", "密码至少6位");
}
}
执行流程说明:
graph TD
A[客户端发起请求] --> B{Struts拦截器捕获}
B --> C[调用Action.validate()]
C --> D{是否有addFieldError?}
D -- 是 --> E[跳转到INPUT视图]
D -- 否 --> F[执行execute()方法]
F --> G[返回结果码]
G --> H[渲染JSP页面]
-
addFieldError(field, msg)将错误信息与特定表单字段关联,可在JSP中通过<s:fielderror>标签显示。 - 若未覆盖
validate()方法或无错误添加,则流程继续进入execute()。
该机制实现了前后端联动的表单验证闭环,有助于防止无效请求进入深层业务逻辑,增强系统的健壮性。
4.2 分页逻辑设计与实现
面对海量数据展示场景,一次性加载全部记录不仅消耗大量内存,还会导致页面渲染缓慢甚至超时。因此,分页成为Web系统不可或缺的功能模块。合理的分页设计应包含统一的数据模型封装、边界条件判断、参数传递机制以及与Service层的良好协作。
4.2.1 分页模型PageBean封装:包含当前页、页大小、总记录数等字段
为统一管理分页相关状态,建议创建通用的 PageBean<T> 泛型类,封装关键元数据。
public class PageBean<T> {
private int currentPage; // 当前页码(从1开始)
private int pageSize = 10; // 每页显示条数
private int totalCount; // 总记录数
private int totalPage; // 总页数
private List<T> dataList; // 当前页数据集合
public PageBean(int currentPage, int pageSize) {
this.currentPage = currentPage;
this.pageSize = pageSize;
}
// 计算总页数
public void calculateTotalPage() {
this.totalPage = (int) Math.ceil((double) totalCount / pageSize);
}
// Getter & Setter 省略...
}
参数说明:
| 字段 | 类型 | 描述 |
|---|---|---|
currentPage | int | 用户请求的页码,通常由前端传入,默认为1 |
pageSize | int | 每页显示数量,可配置,默认10 |
totalCount | int | 从数据库查出的符合条件的总记录数 |
totalPage | int | 根据 totalCount 和 pageSize 计算得出 |
dataList | List<T> | 查询出的当前页数据列表 |
该类作为Controller与View之间的桥梁,既可用于接收分页参数,也可携带查询结果返回前端。
4.2.2 总页数计算公式与边界条件判断逻辑编码示例
在设置完 totalCount 后,必须调用 calculateTotalPage() 方法更新 totalPage 值。由于可能存在小数部分,需向上取整:
\text{总页数} = \left\lceil \frac{\text{总记录数}}{\text{每页条数}} \right\rceil
Java中可通过以下方式实现:
this.totalPage = (totalCount % pageSize == 0) ?
totalCount / pageSize :
totalCount / pageSize + 1;
同时需对输入参数做边界校验:
if (currentPage < 1) {
currentPage = 1;
}
if (currentPage > totalPage && totalPage > 0) {
currentPage = totalPage;
}
这确保即使用户篡改URL参数(如page=999),系统仍能返回合理结果,防止越界异常。
4.2.3 Controller接收分页参数并调用Service层获取分页数据
在实际开发中,Controller应负责组装 PageBean 并委托Service执行分页查询。
public class UserPageAction extends ActionSupport {
private PageBean<User> pageBean;
private int currentPage = 1;
private int pageSize = 10;
private UserService userService;
public String execute() {
pageBean = new PageBean<>(currentPage, pageSize);
// 获取总记录数
int totalCount = userService.getUserCount();
pageBean.setTotalCount(totalCount);
pageBean.calculateTotalPage();
// 获取当前页数据
List<User> users = userService.findUsersByPage(currentPage, pageSize);
pageBean.setDataList(users);
return SUCCESS;
}
// Getters and Setters...
}
调用流程示意:
sequenceDiagram
participant C as Client
participant A as UserPageAction
participant S as UserService
participant D as DAO Layer
C->>A: GET /userList.action?page=2
A->>S: getUserCount()
S->>D: SELECT COUNT(*) FROM user
D-->>S: 返回总数
S-->>A: 返回totalCount
A->>S: findUsersByPage(2, 10)
S->>D: SELECT * FROM user LIMIT 10 OFFSET 10
D-->>S: 返回第2页数据
S-->>A: 返回users列表
A->>A: 封装进PageBean
A-->>C: 返回SUCCESS → JSP渲染
上述流程展示了完整的分页请求处理链条,体现了各层职责分离的设计思想。
4.3 SQL层级分页查询优化
尽管Java层可以对结果集进行截取,但真正高效的分页必须下沉至数据库层面,利用原生分页语法减少网络传输与内存占用。不同的数据库管理系统(DBMS)提供了各自的分页机制,理解其差异对于构建高性能、可移植的应用至关重要。
4.3.1 使用LIMIT和OFFSET实现高效数据库分页
MySQL采用 LIMIT offset, size 语法实现分页:
SELECT id, name, email FROM user
ORDER BY id DESC
LIMIT 10 OFFSET 20;
等价于:
SELECT id, name, email FROM user
ORDER BY id DESC
LIMIT 20, 10;
-
OFFSET 20:跳过前20条记录 -
LIMIT 10:最多返回10条
该方式适用于中小偏移量场景,但在大页码下(如第1000页)会产生全表扫描问题,性能急剧下降。
4.3.2 MySQL与Oracle分页语法差异及兼容性处理
Oracle早期版本不支持 LIMIT/OFFSET ,而是使用 ROWNUM 伪列实现分页:
SELECT * FROM (
SELECT rownum rn, t.* FROM (
SELECT id, name, email FROM user ORDER BY id DESC
) t WHERE rownum <= 30
) WHERE rn > 20;
而从Oracle 12c起引入了标准语法:
SELECT id, name, email FROM user
ORDER BY id DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
| 数据库 | 分页语法 | 特点 |
|---|---|---|
| MySQL | LIMIT offset, size | 简洁直观,广泛支持 |
| PostgreSQL | LIMIT size OFFSET offset | 符合SQL标准 |
| Oracle (<12c) | 嵌套ROWNUM子查询 | 复杂且易错 |
| Oracle (≥12c) | OFFSET ... FETCH NEXT | 支持标准SQL:2008 |
为提高兼容性,推荐使用MyBatis动态SQL根据不同方言生成适配语句:
<select id="findUsersByPage" parameterType="map" resultType="User">
SELECT id, name, email FROM user
ORDER BY id DESC
<if test="_databaseId == 'mysql'">
LIMIT #{pageSize} OFFSET #{offset}
</if>
<if test="_databaseId == 'oracle' and oracleVersion < 12">
<![CDATA[
WHERE ROWNUM <= #{endRow}
]]>
AND id NOT IN (
SELECT id FROM (
SELECT id FROM user ORDER BY id DESC
<![CDATA[ WHERE ROWNUM <= #{startRow} ]]>
)
)
</if>
<if test="_databaseId == 'oracle' and oracleVersion >= 12">
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</if>
</select>
💡 提示:可通过
<databaseIdProvider>在MyBatis配置中自动识别数据库类型。
4.3.3 子查询优化大偏移量分页性能:避免全表扫描策略
当 OFFSET 值极大时(如 OFFSET 100000 ),数据库仍需扫描前N条记录,造成I/O浪费。此时可借助索引覆盖+子查询优化:
SELECT u.id, u.name, u.email
FROM user u
INNER JOIN (
SELECT id FROM user
ORDER BY id DESC
LIMIT 10 OFFSET 100000
) tmp ON u.id = tmp.id;
- 内层仅查询主键(轻量级)
- 利用主键索引快速定位位置
- 外层通过JOIN回表获取完整数据
该方案显著降低磁盘I/O,尤其适合以ID排序的分页场景。
综上所述,Controller层不仅要完成请求调度与参数绑定,还需协同DAO层实现高效分页。通过抽象 PageBean 模型、合理运用数据库原生分页语法,并针对大偏移量采取优化策略,可在保障功能完整性的同时大幅提升系统响应速度与稳定性。
5. 视图展示与系统安全性能调优
5.1 JSP页面数据渲染与分页导航展示
在SSI架构中,前端视图层主要由JSP(JavaServer Pages)承担,结合Struts提供的标签库(如 <s:iterator> 、 <s:property> 等),可实现高效的数据绑定与动态内容输出。当Controller将查询结果封装为 PageBean 并存入请求域后,JSP页面即可通过Struts标签进行遍历和渲染。
以下是一个典型的用户列表展示表格代码片段:
<%@ taglib prefix="s" uri="/struts-tags" %>
<table border="1" class="user-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
<s:iterator value="pageBean.list" var="user">
<tr>
<td><s:property value="#user.id"/></td>
<td><s:property value="#user.username"/></td>
<td><s:property value="#user.email"/></td>
<td><s:date name="#user.createTime" format="yyyy-MM-dd HH:mm:ss"/></td>
</tr>
</s:iterator>
</tbody>
</table>
上述代码中:
- <s:iterator> 标签用于遍历 pageBean.list 集合;
- var="user" 定义了当前迭代元素的引用别名;
- <s:property> 输出对象属性值,避免直接使用 <%= %> 脚本表达式,提升安全性;
- <s:date> 提供日期格式化功能,无需在Java层预处理时间字符串。
分页导航栏构建
为提升用户体验,需在页面底部生成标准分页控件。以下为分页链接的实现逻辑:
<div class="pagination">
<s:url id="firstPage" action="user_list">
<s:param name="pageNum">1</s:param>
</s:url>
<s:a href="%{firstPage}">首页</s:a>
<s:if test="pageBean.hasPrevious">
<s:url id="prevPage" action="user_list">
<s:param name="pageNum" value="pageBean.pageNum - 1"/>
</s:url>
<s:a href="%{prevPage}">上一页</s:a>
</s:if>
<!-- 显示当前页附近3个页码 -->
<s:bean name="org.apache.struts2.util.Counter" var="counter">
<s:param name="first" value="pageBean.startPage"/>
<s:param name="last" value="pageBean.endPage"/>
<s:iterator value="#counter">
<s:url id="pageUrl" action="user_list">
<s:param name="pageNum" value="top"/>
</s:url>
<s:a href="%{pageUrl}" cssClass="%{pageBean.pageNum == top ? 'current' : ''}">
<s:property/>
</s:a>
</s:iterator>
</s:bean>
<s:if test="pageBean.hasNext">
<s:url id="nextPage" action="user_list">
<s:param name="pageNum" value="pageBean.pageNum + 1"/>
</s:url>
<s:a href="%{nextPage}">下一页</s:a>
</s:if>
<s:url id="lastPage" action="user_list">
<s:param name="pageNum" value="pageBean.totalPages"/>
</s:url>
<s:a href="%{lastPage}">末页</s:a>
</div>
该分页组件支持:
- 动态生成页码范围(例如显示第3~7页);
- 当前页高亮显示;
- 禁用无效按钮(如无前页时不显示“上一页”);
- 所有链接携带 pageNum 参数,确保状态保持。
此外,可通过JavaScript增强交互体验,例如拦截跳转事件,启用Ajax异步加载:
document.querySelectorAll('.pagination a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const url = this.getAttribute('href');
fetch(url)
.then(response => response.text())
.then(html => {
document.querySelector('#content').innerHTML = html;
});
});
});
此方式减少整页刷新,显著提升响应速度。
5.2 安全性保障机制实施
随着Web应用暴露面扩大,安全性成为不可忽视的核心议题。SSI框架虽未内置完整安全模块,但可通过合理设计弥补短板。
防止SQL注入攻击
IBatis/MyBatis推荐使用 #{} 占位符而非 ${} 拼接SQL,以启用预编译机制。对比示例如下:
<!-- 不安全:字符串拼接 -->
<select id="findUserByNameBad" resultType="User">
SELECT * FROM users WHERE username LIKE '%${name}%'
</select>
<!-- 安全:预编译参数 -->
<select id="findUserByNameGood" parameterType="string" resultType="User">
SELECT * FROM users WHERE username LIKE CONCAT('%', #{name}, '%')
</select>
#{} 会将参数视为PreparedStatement中的?占位符,有效防止恶意输入破坏SQL结构。
权限控制拦截器设计
利用Struts2拦截器实现基于角色的访问控制(RBAC)。定义一个 AuthorizationInterceptor :
public class AuthorizationInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
ActionContext ctx = invocation.getInvocationContext();
Map<String, Object> session = ctx.getSession();
String role = (String) session.get("userRole");
Object action = invocation.getAction();
if (action instanceof AdminAction && !"admin".equals(role)) {
return "forbidden"; // 跳转至403页面
}
return invocation.invoke();
}
}
在 struts.xml 中注册并应用:
<interceptors>
<interceptor name="auth" class="com.example.AuthorizationInterceptor"/>
<intercepter-stack name="secureStack">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="auth"/>
</intercepter-stack>
</interceptors>
<action name="admin_*" class="AdminAction" method="{1}">
<interceptor-ref name="secureStack"/>
</action>
敏感数据脱敏处理
对日志输出与前端展示中的敏感字段(如身份证、手机号)进行掩码处理:
public class SensitiveDataMasker {
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 18) return idCard;
return idCard.substring(0, 6) + "********" + idCard.substring(14);
}
}
在JSP中调用EL函数或自定义标签完成脱敏:
<td>${fn:escapeXml(SensitiveDataMasker.maskPhone(user.phone))}</td>
5.3 系统部署与性能调优建议
Spring配置文件详解
核心 applicationContext.xml 应包含如下关键配置:
| 组件 | 配置项 | 示例 |
|---|---|---|
| 数据源 | DruidDataSource | 连接池初始化/最大连接数 |
| 事务管理器 | DataSourceTransactionManager | 支持@Transactional |
| SqlMapClient | IBatis工厂 | 指向SqlMapConfig.xml |
| DAO注入 | <property name="sqlMapClient" ref="sqlMapClient"/> | 实现解耦 |
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="initialSize" value="5"/>
<property name="maxActive" value="20"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation" value="classpath:SqlMapConfig.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
IDE环境适配步骤
导入项目至IntelliJ IDEA/Eclipse流程:
1. 文件 → 导入 → Maven Project;
2. 定位pom.xml,自动识别依赖;
3. 配置Tomcat Server运行环境;
4. 设置Deployment Artifacts为WAR exploded;
5. 启动调试模式,验证端口8080访问。
JVM与连接池调优参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
-Xms / -Xmx | 2g | 初始与最大堆内存 |
-XX:PermSize | 256m | 元空间大小(JDK8+为MetaspaceSize) |
maxActive | 50 | Druid最大连接数 |
maxWait | 60000 | 获取连接超时时间(毫秒) |
validationQuery | SELECT 1 | 心跳检测SQL |
通过合理设置这些参数,系统在高并发场景下可稳定支撑上千QPS请求。
graph TD
A[HTTP请求] --> B{是否登录?}
B -- 否 --> C[跳转登录页]
B -- 是 --> D[执行权限检查]
D --> E[调用Service业务逻辑]
E --> F[DAO访问数据库]
F --> G[返回PageBean]
G --> H[JSP渲染视图]
H --> I[输出HTML响应]
简介:SSI(Server Side Include)是一种服务器端包含技术,可用于实现动态网页内容生成。本文介绍了一个使用SSI框架实现增删改查(CRUD)操作和数据分页的完整Java Web项目。该项目通过实体类、DAO层、Service层和Controller协同工作,完成数据库的创建、读取、更新和删除功能,并结合SQL的LIMIT与OFFSET实现高效分页。代码包含完整的配置文件和模块结构,适用于学习Java Web开发中的基础架构设计、数据交互流程及SSI技术的实际应用,具备良好的可移植性和教学参考价值。
1万+

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



