1. 为什么选择SSM框架开发房屋租赁系统?
作为一名有5年Java全栈开发经验的工程师,我经手过3个不同规模的房屋租赁管理系统项目。第一次接触这个领域时,我也曾纠结于技术选型问题。SSM(Spring+SpringMVC+MyBatis)这套经典组合之所以成为我的首选,主要基于以下几个实际考量:
首先从业务适配性来看,房屋租赁管理系统本质上是典型的中小型企业级应用。它需要处理的核心业务包括房源信息管理(CRUD)、租约周期管理(状态机)、财务流水(事务处理)等。SSM框架中:
- Spring的IoC容器完美解决业务组件依赖管理
- SpringMVC的注解驱动开发模式特别适合RESTful接口设计
- MyBatis的SQL映射机制能灵活应对复杂查询场景
对比Spring Boot全家桶,SSM在项目初期需要更多配置工作,但这也带来两个优势:
- 更清晰的架构分层(DAO/Service/Controller)
- 更精细的SQL优化控制
去年我参与的一个长租公寓项目就遇到典型性能问题:在同时查询2000+房源时,JPA生成的SQL出现N+1查询。改用MyBatis手动编写联表查询后,响应时间从1800ms降至230ms。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与项目初始化
2.1 基础环境准备清单
以下是经过多个项目验证的稳定版本组合(避免踩坑新版兼容性问题):
- JDK 1.8(注意配置JAVA_HOME环境变量)
- Apache Maven 3.6.3
- MySQL 5.7(必须开启InnoDB引擎)
- Tomcat 8.5(配置server.xml的URIEncoding为UTF-8)
重要提示:千万不要使用JDK11+与MyBatis 3.4.x的组合,我们曾因此遭遇过TypeHandler解析异常。
2.2 项目骨架搭建实操
使用maven-archetype-webapp创建项目后,需要改造的标准目录结构如下:
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── rental/
│ │ ├── config/ # Spring配置类
│ │ ├── controller/ # MVC控制器
│ │ ├── dao/ # MyBatis接口
│ │ ├── entity/ # 实体类
│ │ ├── service/ # 业务逻辑
│ │ └── util/ # 工具类
│ ├── resources/
│ │ ├── mapper/ # MyBatis映射文件
│ │ ├── spring/ # XML配置
│ │ └── jdbc.properties
│ └── webapp/
│ ├── WEB-INF/
│ └── static/ # 静态资源
关键pom.xml依赖(注意锁定版本号):
xml复制<properties>
<spring.version>5.2.8.RELEASE</spring.version>
<mybatis.version>3.5.6</mybatis.version>
</properties>
<dependencies>
<!-- Spring核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- MyBatis整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
3. 数据库设计与MyBatis优化实践
3.1 租赁系统核心表结构
经过多个项目迭代,我总结出最稳定的表设计方案:
sql复制CREATE TABLE `house` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '房源ID',
`title` varchar(100) NOT NULL COMMENT '房源标题',
`address` varchar(200) NOT NULL COMMENT '详细地址',
`rental_price` decimal(10,2) NOT NULL COMMENT '月租金',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态(0可租1已租2维护)',
`landlord_id` bigint(20) NOT NULL COMMENT '房东ID',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_landlord` (`landlord_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `contract` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`house_id` bigint(20) NOT NULL,
`tenant_id` bigint(20) NOT NULL,
`start_date` date NOT NULL,
`end_date` date NOT NULL,
`payment_cycle` tinyint(4) NOT NULL COMMENT '付款周期(1月付3季付6半年付)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_house` (`house_id`,`start_date`) # 防止重复出租
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 MyBatis高级映射技巧
在房源查询场景中,我们经常需要联表查询房东信息。以下是经过优化的ResultMap配置:
xml复制<resultMap id="HouseDetailMap" type="com.rental.entity.HouseVO">
<id column="id" property="id"/>
<result column="title" property="title"/>
<!-- 其他基础字段 -->
<!-- 关联房东信息 -->
<association property="landlord" javaType="com.rental.entity.Landlord">
<id column="landlord_id" property="id"/>
<result column="landlord_name" property="name"/>
<result column="landlord_phone" property="phone"/>
</association>
<!-- 集合映射合同历史 -->
<collection property="contracts" ofType="com.rental.entity.Contract">
<id column="contract_id" property="id"/>
<result column="start_date" property="startDate"/>
</collection>
</resultMap>
配合动态SQL实现多条件查询:
xml复制<select id="selectByCondition" resultMap="HouseDetailMap">
SELECT h.*, l.name AS landlord_name,
l.phone AS landlord_phone
FROM house h
LEFT JOIN landlord l ON h.landlord_id = l.id
<where>
<if test="minPrice != null">
AND h.rental_price >= #{minPrice}
</if>
<if test="statusList != null and statusList.size() > 0">
AND h.status IN
<foreach collection="statusList" item="status"
open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
ORDER BY h.id DESC
LIMIT #{offset}, #{pageSize}
</select>
4. 业务逻辑层设计与事务控制
4.1 租约状态机实现
房屋租赁最复杂的业务逻辑在于租约状态流转。我们采用状态模式进行封装:
java复制public interface LeaseState {
void signContract(LeaseContext context);
void terminateContract(LeaseContext context);
void renewContract(LeaseContext context);
}
@Service
@Transactional
public class LeaseServiceImpl implements LeaseService {
private Map<Integer, LeaseState> stateMap = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
stateMap.put(LeaseStatus.AVAILABLE.getCode(), new AvailableState());
stateMap.put(LeaseStatus.RENTED.getCode(), new RentedState());
// 其他状态...
}
@Override
public void handleContractAction(Long houseId, LeaseAction action) {
House house = houseDao.selectById(houseId);
LeaseState state = stateMap.get(house.getStatus());
switch(action) {
case SIGN:
state.signContract(new LeaseContext(house));
break;
// 其他动作处理...
}
}
}
4.2 分布式事务实践
在支付押金场景中,需要同时更新财务记录和合同状态。我们采用本地事务+消息队列的最终一致性方案:
java复制@Service
public class PaymentServiceImpl implements PaymentService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
public void processDeposit(PaymentDTO dto) {
// 第一阶段:本地事务
Boolean result = transactionTemplate.execute(status -> {
try {
// 1. 创建支付记录
paymentDao.insert(dto);
// 2. 冻结押金金额
accountDao.freezeAmount(dto.getTenantId(), dto.getAmount());
// 3. 发送准备消息
rocketMQTemplate.sendInTransaction(
"deposit-topic",
MessageBuilder.withPayload(dto).build(),
null
);
return true;
} catch(Exception e) {
status.setRollbackOnly();
return false;
}
});
if(!result) {
throw new BusinessException("押金支付失败");
}
}
}
5. 前端交互与性能优化
5.1 房源列表分页优化
面对大量房源数据时,传统LIMIT分页会出现性能瓶颈。我们采用"游标分页"方案:
java复制@GetMapping("/houses")
public PageResult<HouseVO> listHouses(
@RequestParam(required = false) Long lastId,
@RequestParam(defaultValue = "10") Integer size) {
// 使用ID作为游标
List<HouseVO> list = houseService.selectAfterId(lastId, size);
// 构建下一页游标
Long nextLastId = list.isEmpty() ? null : list.get(list.size()-1).getId();
return new PageResult<>(list, nextLastId);
}
对应的SQL优化:
sql复制SELECT * FROM house
WHERE id < #{lastId} -- 游标条件
ORDER BY id DESC
LIMIT #{size}
5.2 静态资源缓存策略
对于房源图片等静态资源,我们配置Nginx实现强缓存:
nginx复制location ~* \.(jpg|png|gif)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
# 指纹策略解决更新问题
if ($request_uri ~* "^(.+)\.\w{8}\.(jpg|png|gif)$") {
rewrite "^(.+)\.\w{8}\.(jpg|png|gif)$" $1.$2 break;
}
}
前端采用内容哈希命名文件:
html复制<img src="/static/images/house-abc12345.jpg">
<!-- 实际文件名为house.jpg -->
6. 项目部署与监控
6.1 Tomcat生产级配置
在server.xml中优化线程池:
xml复制<Connector port="8080" protocol="HTTP/1.1"
maxThreads="200"
minSpareThreads="20"
acceptCount="100"
connectionTimeout="20000"
URIEncoding="UTF-8"
compression="on"
compressableMimeType="text/html,text/xml,text/css,application/json"/>
6.2 日志收集方案
使用Logback+ELK实现集中式日志管理:
xml复制<!-- logback-spring.xml -->
<appender name="ELK" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>192.168.1.100:5000</destination>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<pattern>
<pattern>
{
"app": "rental-system",
"env": "${spring.profiles.active}",
"traceId": "%mdc{traceId}"
}
</pattern>
</pattern>
<message/>
<loggerName/>
<threadName/>
<logLevel/>
<stackTrace/>
</providers>
</encoder>
</appender>
7. 典型问题排查实录
7.1 MyBatis缓存导致的数据不一致
现象:更新房东信息后,查询仍返回旧数据。根本原因是MyBatis二级缓存作用域配置错误。
解决方案:
xml复制<!-- 在mapper.xml中明确关闭缓存 -->
<mapper namespace="com.rental.dao.LandlordMapper">
<cache-ref namespace=""/> <!-- 禁用二级缓存 -->
<select id="selectById" useCache="false" flushCache="true">
SELECT * FROM landlord WHERE id=#{id}
</select>
</mapper>
7.2 日期类型序列化异常
前端传递的JSON日期格式与Java LocalDate不兼容时,需要自定义消息转换器:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
8. 项目扩展与演进建议
当系统需要扩展为微服务架构时,建议按功能垂直拆分:
- 房源服务(House-Service)
- 合同服务(Contract-Service)
- 支付服务(Payment-Service)
每个服务独立数据库,通过Dubbo或Spring Cloud实现服务调用。我曾在一个2000+房源的分布式系统中采用这种架构,TPS从150提升到1200+。
对于初期项目,建议保持单体架构,但做好模块化分割。这是我用SSM开发租赁系统5年来最重要的经验:不要过早优化,但要为扩展留好接口。
