前言
在实际的项目开发中,根据数据库表创建实体、service、controller等结构是一件非常繁琐的事。所以我们经常需要使用到各种代码生成器,例如mybatis-plus,若依等框架都有自己的代码生成器和生成逻辑。本篇文章我们就从0开始,手写一个简单的代码生成器。 源码github地址
项目依赖
本项目是基于springboot+javapoet+dom4j实现的。
<dependencies>
<!-- DOM4J依赖 -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.squareup/javapoet -->
<dependency>
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
<version>1.13.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.0</version>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.8</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
整体思路
代码生成的逻辑主要是基于以下两个sql语句实现的:
show tables //查询所有数据库表
SHOW FULL COLUMNS FROM User" // 查询指定表中的所有字段信息
查询出来的表字段信息如下图所示:
我们遍历这些表,根据每个表生成对应的实体类,再根据这些表的字段信息生成不同的属性和变量。整体架构图如下:
核心类
在主程序使用代码生成器的时候将Datasource传入给Generate 类,Generate 对象根据数据源使用jdbcTemplate查询数据库表信息。并根据配置设置各项需求(我没有写,例如是否使用lombok、controllerPath等设置,读者可以自己尝试完成)
/**
* 生成器最核心的类,保存了实体类、service、mapper的输出路径以及数据库的链接信息
* @author 黎勇炫
* @date 2022年08月19日 11:17
*/
@Data
@Accessors(chain = true)
public class Generate {
private static final String SHOWTABLES = "show tables";
/** jdbc */
private JdbcTemplate jdbcTemplate;
/** 实体类 */
private String domainPath;
/** service */
private String servicePath;
/** dao */
private String daoPath;
/** controller */
private String controllerPath;
/** 是否使用lombok */
private boolean lombok;
/** 所有表名 */
private List<String> table;
/** 表前缀 */
private String tablePrefix;
/**
* 包路径(entity,controller,service包的父包)
*/
private String pkgRootPath;
/**
* 资源路径
*/
private String sourcePath;
private BuilderChain chain;
public Generate(DataSource dataSource,String pkgRootPath,String sourcePath) {
this.pkgRootPath = pkgRootPath;
this.sourcePath = sourcePath;
jdbcTemplate = new JdbcTemplate(dataSource);
}
/**
* 开始生成代码
*/
public void doCreate(){
// 1.在数据库中查询所有的表
table = jdbcTemplate.queryForList(SHOWTABLES,String.class);
// 处理表名
table = table.stream().map(item -> {
String entityName = buildEntityName(item);
// 从新建实体类开始
chain = createChain(jdbcTemplate,entityName,item,pkgRootPath,sourcePath);
chain.build(jdbcTemplate, entityName, item, pkgRootPath, sourcePath);
return entityName;
}).collect(Collectors.toList());
}
/**
* 创建生成类的调用链 实体类-dao-service-controller
*/
private BuilderChain createChain(JdbcTemplate jdbcTemplate, String entityName, String item, String pkgRootPath, String sourcePath) {
EntityBuilder chain = new EntityBuilder();
chain.appendNext(new DaoBuilder())
.appendNext(new ServiceBuilder())
.appendNext(new ControllerBuilder());
return chain;
}
/**
* 构建实体类名称
*/
private String buildEntityName(String name) {
// 替换前缀
if (!StringUtils.isEmpty(tablePrefix)) {
name = name.replaceFirst(tablePrefix,"");
}
// 首字母大写
// 驼峰命名
String[] hump = name.split("_");
StringBuilder builder = new StringBuilder();
for (String s : hump) {
builder.append(s.substring(0,1).toUpperCase()+s.substring(1));
}
return builder.toString();
}
}
类构建者
因为实体类、Dao、Service、Controller之间有调用关系,例如Dao需要实体类作为泛型,Service又需要注入Dao层的类。所以我们需要保证整个创建者链的调用顺序,使用责任链模式实现。
/**
* @author 黎勇炫
* @date 2022年08月22日 16:03
*/
public class EntityBuilder extends BuilderChain{
/**
*
*/
public static final String ENTITY_PKG_SUFFID = ".domain";
@Override
public BuilderChain appendNext(BuilderChain next){
this.next = next;
return next;
}
@Override
public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {
// 查询表中所有的字段
List<Columns> columns = jdbcTemplate.query("SHOW FULL COLUMNS FROM " + tableName, new BeanPropertyRowMapper<Columns>(Columns.class));
// 开始创建实体类
TypeSpec.Builder builder = TypeSpec.classBuilder(entityName)
// 关联表
.addAnnotation(AnnotationSpec.builder(TableName.class).addMember("value","\""+tableName+"\"").build())
// lombok注解
.addAnnotation(AnnotationSpec.builder(Data.class).build())
// 修饰符
.addModifiers(Modifier.PUBLIC)
.addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
for (Columns column : columns) {
System.out.println(column);
FieldSpec.Builder fieldBuilder = FieldSpec.builder((TypeName) FieldRel.getJavaType(column.getType()), buildFieldName(column.getField()), Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(Schema.class).addMember("title", "\""+column.getComment()+"\"").build());
// 如果是主键就加上@TableId
if(!StringUtils.isEmpty(column.getKey())){
fieldBuilder.addAnnotation(AnnotationSpec.builder(TableId.class).build());
}
builder.addField(fieldBuilder.build());
}
genJavaSourceFile(pkgRootPath+ENTITY_PKG_SUFFID,sourcePath,builder);
// 创建实体类
if(null != next){
next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
}
}
/**
* 转换变量名称-小写/驼峰
*/
private String buildFieldName(String field) {
field = field.toLowerCase();
String[] segment = field.split("_");
StringBuilder str = new StringBuilder();
for (int i = 0; i < segment.length; i++) {
if(i==0){
str.append(segment[i]);
}else {
str.append(segment[i].substring(0,1).toUpperCase()+segment[i].substring(1));
}
}
return str.toString();
}
}
/**
* @author 黎勇炫
* @date 2022年08月23日 11:25
*/
public class DaoBuilder extends BuilderChain{
/**
* edit
*/
public static final String DAO_SUFFIX = "Dao";
/**
* edit
*/
public static final String DAO_PKG_SUFFIX = ".dao";
@Override
public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {
Class<BaseMapper> clazz = BaseMapper.class;
// 创建接口
TypeSpec.Builder builder = TypeSpec.interfaceBuilder(entityName + DAO_SUFFIX)
.addAnnotation(Mapper.class)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(clazz), ClassName.get(pkgRootPath + EntityBuilder.ENTITY_PKG_SUFFID, entityName)))
.addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
genJavaSourceFile(pkgRootPath+DAO_PKG_SUFFIX,sourcePath,builder);
// 生成mapper文件
createMapperFile(pkgRootPath+DAO_PKG_SUFFIX+"."+entityName,entityName+"Dao");
if(null != this.next){
this.next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
}
}
}
/**
* @author 黎勇炫
* @date 2022年08月23日 15:37
*/
public class ServiceBuilder extends BuilderChain{
/**
* service后缀
*/
public static final String SERVICE_SUFFIX = "Service";
/**
* serviceimpl后缀
*/
public static final String SERVICEIMPL_SUFFIX = "ServiceImpl";
/**
* service包后缀
*/
public static final String SERVICE_PKG_SUFFIX = ".service";
/**
* serviceimpl包后缀
*/
public static final String SERVICEIMPL_PKG_SUFFIX = ".service.impl";
@Override
public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {
Class<IService> service = IService.class;
Class<ServiceImpl> serviceImpl = ServiceImpl.class;
// 创建inteface
TypeSpec.Builder builder = TypeSpec.interfaceBuilder(entityName + SERVICE_SUFFIX)
// 继承iservice接口
.addSuperinterface(ParameterizedTypeName.get(ClassName.get(service), ClassName.get(pkgRootPath+EntityBuilder.ENTITY_PKG_SUFFID,entityName)))
.addModifiers(Modifier.PUBLIC)
.addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 生成service接口
genJavaSourceFile(pkgRootPath+SERVICE_PKG_SUFFIX, sourcePath, builder);
// 加载刚才生成的service接口,在生成实现类的时候继承这个接口
// 创建impl
TypeSpec.Builder impl = null;
impl = TypeSpec.classBuilder(entityName + SERVICEIMPL_SUFFIX)
.superclass(ParameterizedTypeName.get(ClassName.get(serviceImpl), ClassName.get(pkgRootPath+DaoBuilder.DAO_PKG_SUFFIX,entityName+DaoBuilder.DAO_SUFFIX), ClassName.get(pkgRootPath+EntityBuilder.ENTITY_PKG_SUFFID,entityName)))
.addSuperinterface(ClassName.get(pkgRootPath+SERVICE_PKG_SUFFIX,entityName+SERVICE_SUFFIX))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Service.class)
.addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 生成实现类
genJavaSourceFile(pkgRootPath+SERVICEIMPL_PKG_SUFFIX + ".impl", sourcePath, impl);
// todo 生成mapper文件
if(null != this.next){
this.next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
}
}
}
/**
* 生成controller
* @author 黎勇炫
* @date 2022年08月24日 13:51
*/
public class ControllerBuilder extends BuilderChain{
public static final String CONTROLLER_SUFFIX = "Controller";
public static final String CONTROLLER_OKG_SUFFIX = ".controller";
@Override
public void build(JdbcTemplate jdbcTemplate, String entityName, String item, String pkgRootPath, String sourcePath) {
/**
* 1.先创建controller类,添加相关注解和权限标识
* 2.创建属性,注入service类
* 3.创建api接口
*/
TypeSpec.Builder builder = TypeSpec.classBuilder(entityName + CONTROLLER_SUFFIX)
// 添加@RestController注解
.addAnnotation(AnnotationSpec.builder(RestController.class).build())
// 添加RequestMapping注解
.addAnnotation(AnnotationSpec.builder(RequestMapping.class).addMember("value", "\"/" + entityName.toLowerCase()+"\"").build())
// 导入实体类
// 权限修饰符
.addModifiers(Modifier.PUBLIC);
// 添加Service注入
builder.addField(FieldSpec.builder(ClassName.get(pkgRootPath+ServiceBuilder.SERVICE_PKG_SUFFIX,entityName+ServiceBuilder.SERVICE_SUFFIX),entityName.substring(0,1).toLowerCase()+entityName.substring(1)+ServiceBuilder.SERVICE_SUFFIX)
.addModifiers(Modifier.PRIVATE)
.addAnnotation(AnnotationSpec.builder(Autowired.class).build())
.build());
genJavaSourceFile(pkgRootPath+CONTROLLER_OKG_SUFFIX,sourcePath,builder);
}
}
测试代码生成器
到这里,实体类、Dao层、Service层以及Controller层的类基本都已经创建了。在需要代码生成的项目中导入代码生成器,在测试类中调用
@Test
void contextLoads() {
DataSource dataSource = context.getBean(DataSource.class);
Generate generate = new Generate(dataSource,"gencode.demo","src/main/java");
generate.setTablePrefix("l_")
.setDaoPath("ss");
generate.doCreate();
}
测试前项目结构:
测试后项目结构:
简单的代码生成器已经完成了,还有很多功能因为我太懒了没开发,感兴趣的可以自己可以尝试拓展。这个代码生成器当然是不够完善的,主要是尝试理解代码生成器的设计思路。