1. 项目概述
在Java Web开发中,Spring+MyBatis框架组合已经成为主流选择。随着项目规模扩大,实体类数量增加,开发者不得不面对大量重复的CRUD代码编写工作。每次新增一个实体类,都需要手动创建对应的Mapper接口、Query查询类、Service接口和ServiceImpl实现类,这种重复劳动不仅效率低下,还容易引入人为错误。
我最近在一个音乐管理系统中就遇到了这个问题。系统包含Mtype(音乐类型)、Album(专辑)、Songer(歌手)等20多个实体类,手动编写这些基础代码花费了我近3天时间。更糟糕的是,团队中不同成员编写的代码风格不一致,给后期维护带来了很大困扰。
为了解决这个问题,我开发了一套基于模板驱动的自定义代码生成器。它通过简单的IO流操作,实现了分层代码的一键生成,将原本需要数小时的工作缩短到几秒钟。下面我将详细介绍这个工具的设计思路和实现细节。
2. 核心设计思路
2.1 模板驱动原理
代码生成器的核心思想是"模板+替换"。我们预先编写好标准的代码模板文件,其中使用占位符(如"Demo")代表实体类名。生成代码时,只需要将模板中的占位符替换为实际的实体类名,就能快速生成目标代码。
这种方式的优势在于:
- 灵活性高:修改模板即可适应不同项目结构
- 学习成本低:不需要掌握复杂的代码生成框架
- 可控性强:生成的代码完全符合项目规范
2.2 分层架构设计
代码生成器按照标准的Java Web分层架构设计,主要生成以下四层代码:
- 持久层(Mapper):继承BaseDao的基础CRUD接口
- 查询层(Query):扩展实体类的查询条件封装
- 服务接口层(Service):定义业务方法接口
- 服务实现层(ServiceImpl):实现Service接口并注入Mapper
这种分层设计确保了代码结构的清晰性和可维护性,同时也方便团队成员之间的协作开发。
3. 环境准备
3.1 技术依赖
在开始实现代码生成器之前,需要确保项目已经具备以下基础环境:
- Java开发环境:JDK 1.8+
- 构建工具:Maven 3.5+
- 框架整合:已完成Spring+MyBatis的整合配置
- 基础类准备:
- BaseDao:通用的Mapper接口
- BaseService:通用的Service接口
- BaseServiceImpl:通用的Service实现类
3.2 项目目录结构
合理的目录结构是代码生成器正常工作的前提。建议采用标准的Maven项目结构,并在test/resources下创建模板目录:
code复制project-root/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── dao/ # Mapper接口
│ │ │ ├── model/ # 实体类
│ │ │ ├── query/ # 查询类
│ │ │ ├── service/ # Service接口
│ │ │ │ └── impl/ # Service实现类
│ │ │ └── util/ # 代码生成器
│ │ └── resources/ # 配置文件
│ └── test/
│ └── resources/
│ └── tpl/ # 模板文件
│ ├── DemoMapper.tpl
│ ├── DemoQuery.tpl
│ ├── DemoService.tpl
│ └── DemoServiceImpl.tpl
4. 模板文件实现
4.1 Mapper接口模板
Mapper接口模板定义了持久层的基础结构,继承自BaseDao:
java复制package com.example.dao;
import com.example.model.Demo;
import com.example.query.DemoQuery;
public interface DemoMapper extends BaseDao<DemoQuery, Demo> {
}
关键点说明:
- 包路径需要根据实际项目调整
- BaseDao的泛型参数为<Query, Entity>
- 占位符"Demo"将在生成时被替换
4.2 Query查询类模板
Query类扩展了实体类,用于封装查询条件:
java复制package com.example.query;
import com.example.model.Demo;
public class DemoQuery extends Demo {
}
4.3 Service接口模板
Service接口定义了业务层契约:
java复制package com.example.service;
import com.example.model.Demo;
import com.example.query.DemoQuery;
public interface DemoService extends BaseService<DemoQuery, Demo> {
}
4.4 Service实现类模板
ServiceImpl实现了Service接口并注入Mapper:
java复制package com.example.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.dao.DemoMapper;
import com.example.model.Demo;
import com.example.query.DemoQuery;
import com.example.service.DemoService;
@Service
public class DemoServiceImpl extends BaseServiceImpl<DemoQuery, Demo> implements DemoService {
private DemoMapper demoDao;
@Autowired
public void setDemoDao(DemoMapper demoDao) {
this.demoDao = demoDao;
this.baseDao = demoDao;
}
}
注意事项:
- 需要同时替换大写的"Demo"和小写的"demo"
- @Service注解确保类能被Spring扫描
- setter方法实现了依赖注入
5. 代码生成器实现
5.1 核心执行类
代码生成器的核心是SourceGenerator类,主要完成以下功能:
- 读取模板文件
- 替换占位符
- 写入目标文件
java复制package com.example.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
public class SourceGenerator {
public static void main(String[] args) throws Exception {
generatorSource("Product"); // 生成Product相关代码
}
public static void generatorSource(String objName) throws Exception {
generateQuery(objName);
generateDao(objName);
generateService(objName);
generateServiceImpl(objName);
System.out.println("✅ " + objName + "代码生成完成");
}
private static void generateQuery(String objName) throws Exception {
String tplPath = "src/test/resources/tpl/DemoQuery.tpl";
String targetPath = "src/main/java/com/example/query/"+objName+"Query.java";
writeFile(tplPath, targetPath, objName, null);
}
// 其他generate方法类似...
private static void writeFile(String tplPath, String targetPath,
String upperObjName, String lowerObjName) throws Exception {
// 确保目录存在
File targetFile = new File(targetPath);
File parentDir = targetFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
try (BufferedReader br = new BufferedReader(new FileReader(tplPath));
BufferedWriter bw = new BufferedWriter(new FileWriter(targetPath))) {
String line;
while ((line = br.readLine()) != null) {
line = line.replace("Demo", upperObjName);
if (lowerObjName != null) {
line = line.replace("demo", lowerObjName);
}
bw.write(line);
bw.newLine();
}
}
}
}
5.2 关键技术点
- IO流操作:使用BufferedReader/BufferedWriter提高读写效率
- 资源管理:使用try-with-resources确保流正确关闭
- 目录处理:自动创建不存在的目录
- 占位符替换:同时处理大小写形式的类名
6. 使用与集成
6.1 生成代码步骤
- 准备好实体类(如Product.java)
- 在SourceGenerator中设置目标类名
- 运行main方法
- 检查生成的代码文件
6.2 生成的代码示例
以Product为例,生成的ServiceImpl如下:
java复制package com.example.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.dao.ProductMapper;
import com.example.model.Product;
import com.example.query.ProductQuery;
import com.example.service.ProductService;
@Service
public class ProductServiceImpl extends BaseServiceImpl<ProductQuery, Product>
implements ProductService {
private ProductMapper productDao;
@Autowired
public void setProductDao(ProductMapper productDao) {
this.productDao = productDao;
this.baseDao = productDao;
}
}
6.3 集成到Spring
生成的代码可以直接集成到Spring环境中:
- 配置Mapper扫描:
xml复制<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao"/>
</bean>
- 确保组件扫描包含service包:
xml复制<context:component-scan base-package="com.example.service"/>
7. 扩展与定制
7.1 添加自定义方法
在生成的代码基础上,可以方便地添加业务方法:
- 在Mapper接口中添加:
java复制List<Product> findExpensiveProducts(ProductQuery query);
- 在Service接口中添加:
java复制List<Product> getExpensiveProducts();
- 在ServiceImpl中实现:
java复制@Override
public List<Product> getExpensiveProducts() {
ProductQuery query = new ProductQuery();
query.setMinPrice(1000.0);
return productDao.findExpensiveProducts(query);
}
7.2 模板增强
可以在模板中添加更多功能:
- 添加注释模板:
java复制/**
* ${objName} Service实现类
* @author ${user}
* @date ${date}
*/
- 添加日志支持:
java复制private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);
8. 常见问题解决
8.1 文件编码问题
解决方案:明确指定UTF-8编码
java复制BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(tplPath), "UTF-8"));
8.2 目录不存在问题
解决方案:在writeFile方法中添加目录检查:
java复制File parentDir = new File(targetPath).getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
8.3 依赖注入失败
可能原因:
- Mapper未扫描
- Service未扫描
- 包路径不一致
解决方案检查:
- MapperScannerConfigurer配置
- 组件扫描范围
- 模板中的包路径
9. 高级优化建议
9.1 支持命令行参数
改造main方法支持命令行传参:
java复制public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("Usage: java SourceGenerator <EntityName>");
return;
}
generatorSource(args[0]);
}
9.2 集成逆向工程
将代码生成器与MyBatis Generator结合:
- 先用MyBatis Generator生成实体类
- 再用本工具生成分层代码
9.3 添加模板变量
支持更多模板变量:
java复制line = line.replace("${date}", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
line = line.replace("${author}", System.getProperty("user.name"));
9.4 批量生成模式
支持一次生成多个实体类的代码:
java复制public static void batchGenerate(String... entityNames) throws Exception {
for (String name : entityNames) {
generatorSource(name);
}
}
10. 实际应用效果
在我参与的音乐管理系统项目中,使用这套代码生成器带来了显著的效果提升:
- 效率提升:新增实体类的时间从30分钟缩短到10秒
- 代码质量:生成的代码风格统一,减少了人为错误
- 团队协作:新人能够快速上手,降低了沟通成本
- 维护便利:基础代码结构一致,便于后期维护
特别是在项目初期有大量实体类需要创建时,代码生成器的优势更加明显。原本需要1周时间完成的基础编码工作,现在只需要半天就能完成。
11. 注意事项
- 模板维护:模板文件是核心资产,需要妥善保管和版本控制
- 代码审查:虽然生成的代码质量高,但仍需进行必要的审查
- 适度使用:不适合所有场景,复杂业务逻辑仍需手动实现
- 团队规范:确保团队成员都了解生成器的使用方式和约定
12. 总结与展望
这套基于模板驱动的代码生成器虽然简单,但在实际项目中发挥了巨大作用。它的主要优势在于:
- 轻量级:不依赖任何第三方框架
- 灵活性:通过修改模板适应不同需求
- 易用性:学习成本低,使用简单
未来可以考虑的改进方向包括:
- 支持更多代码层的生成(如Controller)
- 添加GUI界面提升易用性
- 支持模板的动态加载
- 集成到IDE插件中
对于任何使用Spring+MyBatis技术栈的项目,我都强烈推荐使用类似的代码生成方案。它不仅能提高开发效率,还能保证代码质量,是团队开发的利器。