代码生成器那点事儿
谈谈代码生成器那点事儿,一些技术方案,细节
界面一览
勾选表
代码结构
通用代码
目标
为了简化代码,生成模板代码,因此有了代码生成器。
前提
代码生成器的前提是已经有一些模板化,标准化的代码。比如通用的 DAO 层、Service 层、甚至
Controller 层。
技术手段
最基本的功能
- 选取一个熟悉的模版引擎,比如 freemark ,读取数据库字段信息,根据模板生成相关代码。
- 提供一个友好的界面,能勾选需要的表,支持搜索
优化体验
- 表支持自定义实体映射
- 支持外部数据源
- 在线编辑模板文件
技术实现
我这里根据公司技术栈,基于 spring boot 和 jetbrick 模板引擎开发的一款 Java 代码生成器。
支持
- 在线预览表,勾选需要的表
- 指定包前缀、表前缀等自动生成代码,支持实体名自定义
- 内嵌 sqlite 数据库,无需额外的配置,直接启动本项目。
- 连接外部数据源(多数据源自动切换),生成模板代码
- 使用 lombok 简化模板代码
其中,比较有亮点的就是内嵌 sqlite 数据库,维护源信息。读取外部数据源,数据源自动切换。其他功能都比较常规,这里不做赘述。
外部数据源
首先得维护外部数据源的连接信息,然后如果实现比较矬的话,通过 jdbcTemplate 等读取外部数据源表结构信息,最后根据模板渲染即可。
但是,这样不炫酷呀!
数据源切换
能不能写好读取表结构信息的代码后,自动根据前端选择的某个数据源信息自动切换连接呢?
可以的!
spring jdbc 的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
给了我灵感。熟悉它的道友应该知道如何操作了,不知道到的网上一大堆。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
// 省略掉其他代码
}
但是有一个问题就是,需要事先在配置文件中、或者程序上定义好各个数据源呐!还是不够动态,无法在界面上配置维护。因此,我定义了一个动态数据源:
/**
* 动态数据源
*
* @author 奔波儿灞
* @since 1.0
*/
@Slf4j
public class DynamicDataSource extends AbstractDataSource implements DataSourceManager {
/**
* 维护数据源ID与数据源对应关系
*/
private final ConcurrentMap<Long, DataSource> dataSources;
/**
* 默认数据源,也就是内嵌的 sqlite 数据源
*/
private DataSource defaultDataSource;
public DynamicDataSource() {
dataSources = new ConcurrentHashMap<>();
}
public void setDefaultTargetDataSource(DataSource defaultTargetDataSource) {
this.defaultDataSource = defaultTargetDataSource;
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this)) {
return (T) this;
}
return determineTargetDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
}
/**
* 主要就是根据此方法,决定获取哪个数据源
* 通过 ThreadLocal 放入数据源ID,再从 map 中获取到数据源
*/
private DataSource determineTargetDataSource() {
Long lookupKey = DataSourceContextHolder.getKey();
DataSource dataSource = Optional.ofNullable(lookupKey)
.map(dataSources::get)
.orElse(defaultDataSource);
if (dataSource == null) {
throw new IllegalStateException("Cannot determine DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* 界面更新或新增数据源时,更新内部 map 维护信息
*/
@Override
public void put(Long id, DataSource dataSource) {
log.info("put datasource: {}", id);
dataSources.put(id, dataSource);
}
@Override
public DataSource get(Long id) {
return dataSources.get(id);
}
/**
* 界面删除数据源时,删除内部 map 维护信息
*/
@Override
public void remove(Long id) {
log.warn("remove datasource: {}", id);
dataSources.remove(id);
}
}
看到这里,各位道友应该虎躯一震,知道如何动态了。
/**
* 数据源key上下文管理
*
* @author 奔波儿灞
* @since 1.0
*/
public class DataSourceContextHolder {
private static final ThreadLocal<Long> THREAD_LOCAL = new ThreadLocal<>();
private DataSourceContextHolder() {
throw new IllegalStateException("Utils");
}
public static synchronized void setKey(Long key) {
THREAD_LOCAL.set(key);
}
public static Long getKey() {
return THREAD_LOCAL.get();
}
public static void clearKey() {
THREAD_LOCAL.remove();
}
}
下面是代码中,切换数据源:
@Override
public PageInfo<Table> getTables(Long dataSourceId, String database, String table, IPage page) {
try {
// 将页面上选取的数据源ID,设置到 ThreadLocal
DataSourceContextHolder.setKey(dataSourceId);
// 下面就是分页查询数据源的表信息了,这里使用的是 mybatis pagehelper
PageHelper.startPage(page);
return PageInfo.of(tableRepository.getTables(database, table));
} finally {
// 最后再将 ThreadLocal 释放,切记切记,防止影响默认数据源,哈哈
DataSourceContextHolder.clearKey();
}
}
当然,这里再写秀一点,可以通过 AOP 方式将 ThreadLocal 部分代码通用化。
思考:
- 程序启动后,需要将数据库中定义的数据源维护到 map 信息中
关于开源
感觉没有开源的必要,各位道友何不自己撸一个?