背景介绍
jfinal框架有ORM功能,可以根据数据库中表结构生成BaseModel类,极大提高了开发效率.但是jfinal将mysql中datetime
类型映射为java.util.Data
,没有使用Java 8提供的新的日期和时间API,那如何将datetime
映射为java.time.LocalDateTime
呢?jfinal有提供现成的方法吗?答案是没有! jfinal为何不直接支持java.time.LocalDateTime
jfinal是一个扩展性极强的框架,我们能否通过写一些子类来达到我们的目的?有哪些需要注意的问题呢?
问题
- 如何生成时间类型为
java.time.LocalDateTime
的BaseModel类代码? - 如何将ResultSet中的JDBC类型转换为
java.time.LocalDateTime
? - renderJson()时如何正确显示
java.time.LocalDateTime
? - 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
- mysql type:是mysql数据列的类型,如:FLOAT,DECIMAL,TINYINT,DATE, TIME, DATETIME, TIMESTAMP
- 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);
}
}
可以看到,正是由于TypeMapping
将java.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
的转换:
- 自定义
LocalDateTimeConverter
- 注册自定义的
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的源码非常了解了.
已将代码提交到码云上,供大家参考.码云