代码生成器那点事儿

代码生成器那点事儿

谈谈代码生成器那点事儿,一些技术方案,细节

界面一览

勾选表

首页

代码结构

代码

通用代码

通用代码

目标

为了简化代码,生成模板代码,因此有了代码生成器。

前提

代码生成器的前提是已经有一些模板化,标准化的代码。比如通用的 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 信息中

关于开源

感觉没有开源的必要,各位道友何不自己撸一个?

posted @ 2020-01-06 16:59  bener  阅读(459)  评论(0编辑  收藏  举报