jfinal-mysql时间类型映射到LocalDateTime

背景介绍

jfinal框架有ORM功能,可以根据数据库中表结构生成BaseModel类,极大提高了开发效率.但是jfinal将mysql中datetime类型映射为java.util.Data,没有使用Java 8提供的新的日期和时间API,那如何将datetime映射为java.time.LocalDateTime呢?jfinal有提供现成的方法吗?答案是没有! jfinal为何不直接支持java.time.LocalDateTime
jfinal是一个扩展性极强的框架,我们能否通过写一些子类来达到我们的目的?有哪些需要注意的问题呢?

问题

  1. 如何生成时间类型为java.time.LocalDateTime的BaseModel类代码?
  2. 如何将ResultSet中的JDBC类型转换为java.time.LocalDateTime?
  3. renderJson()时如何正确显示java.time.LocalDateTime?
  4. getBean()时如何将Http请求中的参数转换为java.time.LocalDateTime?

如何生成时间类型为java.time.LocalDateTime的BaseModel类代码?

jfinal提供了com.jfinal.plugin.activerecord.generator.Generator生成Model类,generate()如下:

    public void generate() {
        if (dialect != null) {
            metaBuilder.setDialect(dialect);
        }

        long start = System.currentTimeMillis();
        //1.生成表元数据List<TableMeta>
        List<TableMeta> tableMetas = metaBuilder.build();
        if (tableMetas.size() == 0) {
            System.out.println("TableMeta 数量为 0,不生成任何文件");
            return ;
        }
        //2.生成baseModel
        baseModelGenerator.generate(tableMetas);
        //3.生成Model
        if (modelGenerator != null) {
            modelGenerator.generate(tableMetas);
        }
        //4.生成_MappingKit
        if (mappingKitGenerator != null) {
            mappingKitGenerator.generate(tableMetas);
        }
        //5.生成DataDictioncry
        if (dataDictionaryGenerator != null && generateDataDictionary) {
            dataDictionaryGenerator.generate(tableMetas);
        }

        long usedTime = (System.currentTimeMillis() - start) / 1000;
        System.out.println("Generate complete in " + usedTime + " seconds.");
    }

我们看下BaseModel是如何被生成的

    public void generate(List<TableMeta> tableMetas) {
        System.out.println("Generate base model ...");
        System.out.println("Base Model Output Dir: " + baseModelOutputDir);
        //1.生成jfinal引擎
        Engine engine = Engine.create("forBaseModel");
        engine.setSourceFactory(new ClassPathSourceFactory());
        engine.addSharedMethod(new StrKit());
        engine.addSharedObject("getterTypeMap", getterTypeMap);
        engine.addSharedObject("javaKeyword", javaKeyword);
        //2.生成BaseModel内容,即TableMeta.baseModelContent
        for (TableMeta tableMeta : tableMetas) {
            genBaseModelContent(tableMeta);
        }
        //3.将TableMeta.baseModelContent写入TableMeta.baseModelOutputDir
        writeToFile(tableMetas);
    }

可以看出BaseModel代码的生成是利用TableMeta的数据和jfinal的Engine技术,Engine模板文件/com/jfinal/plugin/activerecord/generator/base_model_template.jf是固定的,能够变化的只有TableMeta的信息,那我们想要修改BaseModel类代码就只能修改TableMeta中的信息了,回过头看下TableMeta是如何生成的?

    public List<TableMeta> build() {
        System.out.println("Build TableMeta ...");
        try {
            conn = dataSource.getConnection();
            dbMeta = conn.getMetaData();

            List<TableMeta> ret = new ArrayList<TableMeta>();
            //1.构造表名
            buildTableNames(ret);
            for (TableMeta tableMeta : ret) {
                //2.构造主键
                buildPrimaryKey(tableMeta);
                //3.构造列元数据
                buildColumnMetas(tableMeta);
            }
            return ret;
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
        finally {
            if (conn != null) {
                try {conn.close();} catch (SQLException e) {throw new RuntimeException(e);}
            }
        }
    }
    /**
     * 文档参考:
     * http://dev.mysql.com/doc/connector-j/en/connector-j-reference-type-conversions.html
     * 
     * JDBC 与时间有关类型转换规则,mysql 类型到 java 类型如下对应关系:
     * DATE             java.sql.Date
     * DATETIME         java.sql.Timestamp
     * TIMESTAMP[(M)]   java.sql.Timestamp
     * TIME             java.sql.Time
     * 
     * 对数据库的 DATE、DATETIME、TIMESTAMP、TIME 四种类型注入 new java.util.Date()对象保存到库以后可以达到“秒精度”
     * 为了便捷性,getter、setter 方法中对上述四种字段类型采用 java.util.Date,可通过定制 TypeMapping 改变此映射规则
     */
    protected void buildColumnMetas(TableMeta tableMeta) throws SQLException {
        ...
        for (int i=1; i<=rsmd.getColumnCount(); i++) {
            ...
            String typeStr = null;
            if (typeStr == null) {
                String colClassName = rsmd.getColumnClassName(i);
                //重点
                typeStr = typeMapping.getType(colClassName);
            }

            if (typeStr == null) {
                int type = rsmd.getColumnType(i);
                if (type == Types.BINARY || type == Types.VARBINARY || type == Types.LONGVARBINARY || type == Types.BLOB) {
                    typeStr = "byte[]";
                } else if (type == Types.CLOB || type == Types.NCLOB) {
                    typeStr = "java.lang.String";
                } else {
                    typeStr = "java.lang.String";
                }
            }
            cm.javaType = typeStr;
            ...
        }
        ...
    }

在分析上述代码之前,要先明确MySQL type和JDBC type的概念.Java, JDBC and MySQL Types

  1. mysql type:是mysql数据列的类型,如:FLOAT,DECIMAL,TINYINT,DATE, TIME, DATETIME, TIMESTAMP
  2. JDBC type:使用JDBC执行SQL语句后得到ResultSet,ResultSet中数据的数据类型都是Java中的类

使用JDBC连接数据库就会涉及到mysql type到JDBC type的转换,是谁来执行这个转换?我们能够自定义转换规则吗?

答案很明显,既然是使用JDBC连接数据库,自然是JDBC的jar包完成这个转换,我们也无法自定义转换规则,jar包maven如下:

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.44</version>
    </dependency>

若mysql type为DATETIME,转换后的JDBC type是java.sql.Timestamp,但是我们发现在BaseModel中从未出现过java.sql.Timestamp,而是只有java.util.Date,这是怎么回事?
这个就是jfinal提供的灵活性了,获取JDBC type后,jfinal进行了JDBC type到BaseModel type的映射,映射类就是com.jfinal.plugin.activerecord.generator.TypeMapping

/**
 * TypeMapping 建立起 ResultSetMetaData.getColumnClassName(i)到 java类型的映射关系
 * 特别注意所有时间型类型全部映射为 java.util.Date,可通过继承扩展该类来调整映射满足特殊需求
 * 
 * 与 com.jfinal.plugin.activerecord.JavaType.java 类型映射不同之处在于将所有
 * 时间型类型全部对应到 java.util.Date
 */
public class TypeMapping {

    @SuppressWarnings("serial")
    protected Map<String, String> map = new HashMap<String, String>() {{
        // date, year
        put("java.sql.Date", "java.util.Date");

        // time
        put("java.sql.Time", "java.util.Date");

        // timestamp, datetime
        put("java.sql.Timestamp", "java.util.Date");
        ...
    }};

    public String getType(String typeString) {
        return map.get(typeString);
    }
}

可以看到,正是由于TypeMappingjava.sql.Timestamp映射到java.util.Date,所以在BaseModel中只出现了java.util.Date,我们可以通过继承TypeMapping自定义映射规则

public class Java8TypeMapping extends TypeMapping {
    {
        map.put("java.sql.Timestamp", "java.time.LocalDateTime");
    }
}

还要在_JFinalDemoGenerator中配置Java8TypeMapping

public class _JFinalDemoGenerator {

    public static void main(String[] args) {
        ...
        //设置mysql type映射为JDBC type后,若何将JDBC type映射为BaseModel type
        generator.setTypeMapping(new Java8TypeMapping());
        // 生成
        generator.generate();
    }
}

至此在生成的BaseModel代码中就可以看到java.time.LocalDateTime了,但是这只是万里长征的第一步,还有更多的工作需要完成.

如何将ResultSet中的JDBC类型转换java.time.LocalDateTime?

jfinal提供了大量帮助执行SQL语句的API,如Model.dao.find(),但是这个方法又是如何工作的呢?进行单步调试后发现,最终停到了Model中的find(java.sql.Connection, java.lang.String, java.lang.Object...)

    /**
     * Find model.
     */
    private List<M> find(Connection conn, String sql, Object... paras) throws Exception {
        Config config = _getConfig();
        PreparedStatement pst = conn.prepareStatement(sql);
        config.dialect.fillStatement(pst, paras);
        //执行SQL语句获取ResultSet
        ResultSet rs = pst.executeQuery();
        //将ResultSet转换为Model
        List<M> result = config.dialect.buildModelList(rs, getUsefulClass());   // ModelBuilder.build(rs, getUsefulClass());
        DbKit.close(rs, pst);
        return result;
    }
/**
 * ModelBuilder.
 */
public class ModelBuilder {

    public static final ModelBuilder me = new ModelBuilder();

    @SuppressWarnings({"rawtypes", "unchecked"})
    public <T> List<T> build(ResultSet rs, Class<? extends Model> modelClass) throws SQLException, InstantiationException, IllegalAccessException {
        List<T> result = new ArrayList<T>();
        ResultSetMetaData rsmd = rs.getMetaData();
        int columnCount = rsmd.getColumnCount();
        String[] labelNames = new String[columnCount + 1];
        int[] types = new int[columnCount + 1];
        buildLabelNamesAndTypes(rsmd, labelNames, types);
        while (rs.next()) {
            Model<?> ar = modelClass.newInstance();
            Map<String, Object> attrs = ar._getAttrs();
            for (int i=1; i<=columnCount; i++) {
                Object value;
                if (types[i] < Types.BLOB)
                    value = rs.getObject(i);
                else if (types[i] == Types.CLOB)
                    value = handleClob(rs.getClob(i));
                else if (types[i] == Types.NCLOB)
                    value = handleClob(rs.getNClob(i));
                else if (types[i] == Types.BLOB)
                    value = handleBlob(rs.getBlob(i));
                else
                    value = rs.getObject(i);

                attrs.put(labelNames[i], value);
            }
            result.add((T)ar);
        }
        return result;
    }
}

可以发现是通过Dialect.buildModelList()–>ModelBuilder.build()完成从ResultSet到List<Model>的转变,由于java.sql.Timestamp不在判断的特殊类型中,所以在相关属性依然是java.sql.Timestamp,当调用blog.getGmtCreate()时便会将java.sql.Timestamp强制类型转换为java.time.LocalDateTime,此时就会抛出异常.
为此需要继承ModelBuilder,将java.sql.Timestamp转换为java.time.LocalDateTime

/**
 * 将java.util.Date转换为Java8的LocalDateTime
 *
 * @author pfjia
 * @since 2018/1/18 20:43
 */
public class Java8ModelBuilder extends ModelBuilder {
    public static final Java8ModelBuilder me = new Java8ModelBuilder();

    @Override
    public <T> List<T> build(ResultSet rs, Class<? extends Model> modelClass) throws SQLException, InstantiationException, IllegalAccessException {
        List<T> result = super.build(rs, modelClass);
        for (T t : result) {
            Model<?> m = (Model<?>) t;
            for (Map.Entry<String, Object> entry : m._getAttrsEntrySet()) {
                Object value = entry.getValue();
                if (value instanceof Timestamp) {
                    entry.setValue((((Timestamp) value).toLocalDateTime()));
                }
            }
        }
        return result;
    }
}

注意:在博客中为了简洁性调用super.build(rs, modelClass)后再进行循环将Timestamp转换为LocalDateTime,但此时由于多进行了一次循环,性能并不是最优.可以将父类ModelBuilder的代码拷贝一份,在while()循环中增加判断数据是否是Timestamp类型的代码,同样可完成此功能,且性能更好,提交在码云中的代码便是如此实现的.

public class DemoConfig extends JFinalConfig {
    @Override
    public void configPlugin(Plugins me) {
        // 配置ActiveRecord插件
        ActiveRecordPlugin arp = new ActiveRecordPlugin(druidPlugin);
        // 返回字段按字母序排序
        arp.setContainerFactory(new OrderedFieldContainerFactory());
        MysqlDialect mysqlDialect = new MysqlDialect();
        //配置自定义ModelBuilder和RecordBuilder
        mysqlDialect.setModelBuilder(Java8ModelBuilder.me);
        mysqlDialect.setRecordBuilder(Java8RecordBuilder.me);
        arp.setDialect(mysqlDialect);
        // 所有映射在 MappingKit 中自动化搞定
        _MappingKit.mapping(arp);
        me.add(arp);
    }
}

上文只展示了ModelBuilder的代码,RecordBuilder代码类似,同样需要配置.

renderJson()时如何正确显示java.time.LocalDateTime?

我做的项目是一个APP的后台,通信协议为json,使用JFinalJson将Object转换成json,JFinalJson虽然效率较高,但实现简单,不会将java.time.LocalDateTime转换成正常的时间格式,而是将java.time.LocalDateTime中的getXXX()当做属性显示.为此,继承JFinalJson,增加一个判断

/**
 * @author pfjia
 * @since 2018/1/18 21:20
 */
public class Java8JFinalJson extends JFinalJson {
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    protected String toJson(Object value, int depth) {
        if (value instanceof LocalDateTime) {
            return "\"" + ((LocalDateTime) value).format(DATE_TIME_FORMATTER) + "\"";
        }
        return super.toJson(value, depth);
    }
}

由于jfinal使用了工厂模式构建Json实现类,同样需要继承IJsonFactory后进行配置

public class DemoConfig extends JFinalConfig {
    /**
     * 配置常量
     */
    @Override
    public void configConstant(Constants me) {
        ...
        //json
        me.setJsonFactory(new Java8JFinalJsonFactory());
    }
}

getBean()如何将Http请求中的参数转为java.time.LocalDateTime?

jfinal3.1之后增加了Action带参功能,jfinal中是如何将request中的属性组合成Bean的呢?
Controller.getModel()–>Injector.injectModel()–>TypeConverter.convert()

    /**
     * 将 String 数据转换为指定的类型
     * @param type 需要转换成为的数据类型
     * @param s 被转换的 String 类型数据,注意: s 参数不接受 null 值,否则会抛出异常
     * @return 转换成功的数据
     */
    public final Object convert(Class<?> type, String s) throws ParseException {
        ...
        // 在已注册的IConverter中查找是否有type类型的IConverter
        IConverter<?> converter = converterMap.get(type);
        if (converter != null) {
            return converter.convert(s);
        }
        ...
    }

TypeConverter.convert()中查找相应类型的IConverter进行转换,我们只需两步即可完成request中参数到java.time.LocalDateTime的转换:

  1. 自定义LocalDateTimeConverter
  2. 注册自定义的LocalDateTimeConverter
/**
 - @author pfjia
 - @since 2018/3/6 21:33
 */
public class LocalDateTimeConverter implements IConverter<LocalDateTime> {
    private static final Converters.DateConverter DATE_CONVERTER = new Converters.DateConverter();

    @Override
    public LocalDateTime convert(String s) throws ParseException {
        Date date = DATE_CONVERTER.convert(s);
        Instant instant = date.toInstant();
        ZoneId zone = ZoneId.systemDefault();
        return LocalDateTime.ofInstant(instant, zone);
    }
}
public class DemoConfig extends JFinalConfig {
    /**
     * 配置常量
     */
    @Override
    public void configConstant(Constants me) {
        ...
        //注册LocalDateTimeConverter
        TypeConverter.me().regist(LocalDateTime.class, new LocalDateTimeConverter());
    }
}

总结

至此,四个问题已全部解决,通过自定义子类完成设想的功能,说明jfinal具有强大的可扩展性.
可以看出子类中的代码非常精简,只需要少许修改即可,但是确定修改哪些代码才能完成相应功能就需要对jfinal的源码非常了解了.
已将代码提交到码云上,供大家参考.码云

参考

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值