• 代码生成器那点事儿


    代码生成器那点事儿

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

    界面一览

    勾选表

    首页

    代码结构

    代码

    通用代码

    通用代码

    目标

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

    前提

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

    关于开源

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

  • 相关阅读:
    原型设计 + 用户规格说明书
    第三次作业
    MathExam第二次作业
    第一次随笔
    冲鸭第一的合作
    功能规格说明书
    测试与优化
    结对编程
    高分小学计算器
    现实与梦
  • 原文地址:https://www.cnblogs.com/bener/p/12157247.html
Copyright © 2020-2023  润新知