JFinal 源码分析 [DB+ActiveRecord]
我记得以前有人跟我说,“面试的时候要看spring的源码,要看ioc、aop的源码"那为什么要看这些开源框架的源码呢,其实很多人都是"应急式"的去读,就像读一篇文章一下,用最快的速度把文章从头到尾读一遍,那结果就是当你读完它,你也不清楚它讲了一个什么故事,想表达什么。
一个优秀的架构的源码我认为就好像一本名著一样,你的“文学”水平越高,你就越能读出作者设计的精妙之处。一篇源码在你不同水平的时候,能读出不同的东西,因此,我觉得优秀的框架的源码是经久不衰的,反复读多少次都不嫌多,直到你能设计出预期并驾齐驱甚至超越它的优美的架构。
读源码起初是一件很痛苦的事儿,想赶紧把它像流水账一样的读完;慢慢实力增强后,会感觉到读源码能够不费力气的读通;再假以时日,就能看出这些精妙的设计模式的组合。我有一个朋友,典型的源码痴狂症,他跟我说他第一次看见spring的源码,感觉特别兴奋,读了一宿没睡觉.......好吧,我还有很长的路需要走~
话说多了,我们赶紧入正题:
JFinal的框架我24号的一篇博文写到过,它优秀的地方在精简代码上,那么有两处源码是我觉得是值得我们要好好解析一下,一处是初始化加载—servlet跳转,另一处是DB+ActiveRecord的映射。
那么DB映射相对比较简单,我们这次就先来看看。
首先我们看看代码,还是之前我写过的 dog与cat的故事。
// 采用DB+ActiveRecord模式
ActiveRecordPlugin arp = new ActiveRecordPlugin(c3p0Plugin);
me.add(arp);
// 进行DB映射
arp.addMapping("animal", AnimalModel.class);
第一步:为ActiveRecordPlugin的 private IDataSourceProvider dataSourceProvider 赋值。这三行代码就是加载DB映射的关键,那么我们复习一下,JFinal的DB映射无需配置文件,无需与DB对应的POJO,只需要写一个类,继承Model<M extends Model>即可。
那么我们先来看看ActiveRecordPlugin的构造器。
public ActiveRecordPlugin(IDataSourceProvider dataSourceProvider) {
this(DbKit.MAIN_CONFIG_NAME, dataSourceProvider);
}
这里重要的是dataSourceProvider,IDataSourceProvider是一个接口,它的运行时类型是
JFinal 源码分析 [DB+ActiveRecord]
那么,可以看到
this(DbKit.MAIN_CONFIG_NAME, dataSourceProvider);
这段代码又继续读取另一个重载的构造器,然后调用了
public ActiveRecordPlugin(String configName, IDataSourceProvider dataSourceProvider, int transactionLevel) {
if (StrKit.isBlank(configName))
throw new IllegalArgumentException("configName can not be blank");
if (dataSourceProvider == null)
throw new IllegalArgumentException("dataSourceProvider can not be null");
this.configName = configName.trim();
this.dataSourceProvider = dataSourceProvider;
this.setTransactionLevel(transactionLevel);
}
最重要的就是这行代码: this.dataSourceProvider = dataSourceProvider;
这时,ActiveRecordPlugin的static变量的dataSourceProvider就已经被赋为C3p0Plugin的实例了。
第二步:定义映射用POJO
public class AnimalModel extends Model<AnimalModel> {...}
这里Model的源码我们一会再看,现在不着急。
然后进行映射
// 进行DB映射
arp.addMapping("animal", AnimalModel.class);
这里我们又回到了ActiveRecordPlugin类里,它实际上有两个addMapping方法,只是参数不同。
public ActiveRecordPlugin addMapping(String tableName, String primaryKey, Class<? extends Model<?>> modelClass) {
tableList.add(new Table(tableName, primaryKey, modelClass));
return this;
} public ActiveRecordPlugin addMapping(String tableName, Class<? extends Model<?>> modelClass) {
tableList.add(new Table(tableName, modelClass));
return this;
}
我们看到,第一个方法多了一个参数 String primaryKey,我的代码里用的是第二个方法。这两个方法实际上都调用了tableList.add(Table tbl)方法,我们看看tableList是什么
private List<Table> tableList = new ArrayList<Table>();
它是ActiveRecordPlugin的一个成员变量,并且是private的,那我们可以猜到,tableList保存了所有的映射关系。(ActiveRecordPlugin真是强大,后面会越来越强大~)。
第三步:创建映射关系
new Table(tableName, primaryKey, modelClass)
new Table(tableName, modelClass)
我们进去看看
public Table(String name, Class<? extends Model<?>> modelClass) {
if (StrKit.isBlank(name))
throw new IllegalArgumentException("Table name can not be blank.");
if (modelClass == null)
throw new IllegalArgumentException("Model class can not be null."); this.name = name.trim();
this.modelClass = modelClass;
} public Table(String name, String primaryKey, Class<? extends Model<?>> modelClass) {
if (StrKit.isBlank(name))
throw new IllegalArgumentException("Table name can not be blank.");
if (StrKit.isBlank(primaryKey))
throw new IllegalArgumentException("Primary key can not be blank.");
if (modelClass == null)
throw new IllegalArgumentException("Model class can not be null."); this.name = name.trim();
setPrimaryKey(primaryKey.trim()); // this.primaryKey = primaryKey.trim();
this.modelClass = modelClass;
}
这两个方法都是为Table里的成员变量赋值,第二个方法,也就是带primaryKey参数的那个多出一行,我们看看这一行干了什么
setPrimaryKey(primaryKey.trim()); // this.primaryKey = primaryKey.trim();
void setPrimaryKey(String primaryKey) {
String[] keyArr = primaryKey.split(",");
if (keyArr.length > ) {
if (StrKit.isBlank(keyArr[]) || StrKit.isBlank(keyArr[]))
throw new IllegalArgumentException("The composite primary key can not be blank.");
this.primaryKey = keyArr[].trim();
this.secondaryKey = keyArr[].trim();
}
else {
this.primaryKey = primaryKey;
}
这样的作用就是为Table下的primaryKey 和 secondaryKey赋值。
第四步:加载ActiveRecordPlugin
那么代码好像跟到这里就完事了,怎么回事?是不是跟丢了?
别忘了,ActiveRecordPlugin是在FinalConfig里的configPlugin方法加载的。那么又有谁来加载FinalConfig呢?
PS:(FinalConfig是我自己定义的类)
这儿涉及到初始化的加载了,我简单的讲一下。
public class FinalConfig extends JFinalConfig
整个JFinal的入口是web.xml的一段配置:
<web-app>
<filter>
<filter-name>jfinal</filter-name>
<filter-class>com.jfinal.core.JFinalFilter</filter-class>
<init-param>
<param-name>configClass</param-name>
<param-value>com.demo.config.FinalConfig</param-value>
</init-param>
</filter>
接着我们看到了关键的累 JFinalFilter,还是点进去看看。
public final class JFinalFilter implements Filter
这个类实现了Filter接口,那就得实现方法init(),doFilter(),destroy()方法。
我们去看init()方法:
public void init(FilterConfig filterConfig) throws ServletException {
createJFinalConfig(filterConfig.getInitParameter("configClass")); if (jfinal.init(jfinalConfig, filterConfig.getServletContext()) == false)
throw new RuntimeException("JFinal init error!"); handler = jfinal.getHandler();
constants = Config.getConstants();
encoding = constants.getEncoding();
jfinalConfig.afterJFinalStart(); String contextPath = filterConfig.getServletContext().getContextPath();
contextPathLength = (contextPath == null || "/".equals(contextPath) ? : contextPath.length());
}
绕过其他的加载,直接看这行
if (jfinal.init(jfinalConfig, filterConfig.getServletContext()) == false)
我们看看jfinal的类型是 private static final JFinal jfinal = JFinal.me();
那么我们去JFinal类里看看它的init方法。
boolean init(JFinalConfig jfinalConfig, ServletContext servletContext) {
this.servletContext = servletContext;
this.contextPath = servletContext.getContextPath(); initPathUtil(); Config.configJFinal(jfinalConfig); // start plugin and init logger factory in this method
constants = Config.getConstants(); initActionMapping();
initHandler();
initRender();
initOreillyCos();
initI18n();
initTokenManager(); return true;
看这行,下面这行主要是通过Config来加载暴露给程序员的核心文件,JFinalConfig的子类FinalConfig。
Config.configJFinal(jfinalConfig); // start plugin and init logger factory in this method
再点进去
* Config order: constant, route, plugin, interceptor, handler
*/
tatic void configJFinal(JFinalConfig jfinalConfig) {
jfinalConfig.configConstant(constants); initLoggerFactory();
jfinalConfig.configRoute(routes);
jfinalConfig.configPlugin(plugins); startPlugins(); // very important!!!
jfinalConfig.configInterceptor(interceptors);
jfinalConfig.configHandler(handlers);
这段代码实际上有个地方特别坑!就是
jfinalConfig.configPlugin(plugins); startPlugins(); // very important!!!
这行代码一共做了两件事,第一件事是jfinalConfig.configPlugin(plugins);来加载插件。还记得我们之前写的FinalConfig里的configPlugin(Plugins me) 方法吗?
/**
* Config plugin
* 配置插件
* JFinal有自己独创的 DB + ActiveRecord模式
* 此处需要导入ActiveRecord插件
*/
@Override
public void configPlugin(Plugins me) {
// 读取db配置文件
loadPropertyFile("db.properties");
// 采用c3p0数据源
C3p0Plugin c3p0Plugin = new C3p0Plugin(getProperty("jdbcUrl"),getProperty("user"), getProperty("password"));
me.add(c3p0Plugin);
// 采用DB+ActiveRecord模式
ActiveRecordPlugin arp = new ActiveRecordPlugin(c3p0Plugin);
me.add(arp);
// 进行DB映射
arp.addMapping("animal", AnimalModel.class);
}
它实际上就是通过me.add来加载插件,通过Config的 private static final Plugins plugins = new Plugins(); 来装载。
第二件事就是 发现没有,后面的startPlugins()不是注释!是一个方法,这块实在太坑了,恰巧,这就是我们要找到的地方。
这个方法的代码有点长,但因为很重要,我不得不都贴出来。
private static void startPlugins() {
List<IPlugin> pluginList = plugins.getPluginList();
if (pluginList != null) {
for (IPlugin plugin : pluginList) {
try {
// process ActiveRecordPlugin devMode
if (plugin instanceof com.jfinal.plugin.activerecord.ActiveRecordPlugin) {
com.jfinal.plugin.activerecord.ActiveRecordPlugin arp =
(com.jfinal.plugin.activerecord.ActiveRecordPlugin)plugin;
if (arp.getDevMode() == null)
arp.setDevMode(constants.getDevMode());
} boolean success = plugin.start();
if (!success) {
String message = "Plugin start error: " + plugin.getClass().getName();
log.error(message);
throw new RuntimeException(message);
}
}
catch (Exception e) {
String message =
"Plugin start error: " + plugin.getClass().getName() + ". \n" + e.getMessage();
log.error(message, e);
throw new RuntimeException(message, e);
}
}
}
}
上面这个方法一共有两个地方要注意一下,
for (IPlugin plugin : pluginList) {
上面这行是循环所有的插件,并且启动插件的start()方法。
那么,我们中有一个插件记不记得是ActiveRecordPlugin的实例?那么
boolean success = plugin.start();
这行代码就会执行ActiveRecordPlugin下的start()代码。终于绕回来了!!红军二万五千里长征,为了证明这个调用,我写了多少字....
那么我们看ActiveRecordPlugin下的start()方法吧,实际上这个start()方法是因为实现了IPlugin接口里的start()方法。
public boolean start() {
if (isStarted)
return true; if (dataSourceProvider != null)
dataSource = dataSourceProvider.getDataSource();
if (dataSource == null)
throw new RuntimeException("ActiveRecord start error:
ActiveRecordPlugin need DataSource or DataSourceProvider"); if (config == null)
config = new Config(configName, dataSource, dialect,
showSql, devMode, transactionLevel, containerFactory, cache);
DbKit.addConfig(config); boolean succeed = TableBuilder.build(tableList, config);
if (succeed) {
Db.init();
isStarted = true;
}
return succeed;
}
我们直接看与DB映射有关的代码,首先是取得dataSource,dataSourceProvider这个忘了没,忘了就翻到最前面,第一步讲的。
config = new Config(configName, dataSource, dialect, showSql, devMode, transactionLevel, containerFactory, cache);
这行代码中的dataSource 在插件里配置的C3P0数据源。这里的Config与前面加载FinalConfig的可不是一个啊,千万别看错了,这个是DB的 com.jfinal.plugin.activerecord.Config。
第五步:TableBuilder
来自ActiveRecordPlugin.java
boolean succeed = TableBuilder.build(tableList, config);
static boolean build(List<Table> tableList, Config config) {
Table temp = null;
Connection conn = null;
try {
conn = config.dataSource.getConnection();
TableMapping tableMapping = TableMapping.me();
for (Table table : tableList) {
temp = table;
doBuild(table, conn, config);
tableMapping.putTable(table);
DbKit.addModelToConfigMapping(table.getModelClass(), config);
}
return true;
} catch (Exception e) {
if (temp != null)
System.err.println("Can not create Table object,
maybe the table " + temp.getName() + " is not exists.");
throw new ActiveRecordException(e);
}
finally {
config.close(conn);
}
}
这里循环所有的tableList,对每个Table对象进行建表。那么我们先看看Table是用什么来存储数据库映射关系的,相信大家都能猜到是Map了。
public class Table { private String name;
private String primaryKey;
private String secondaryKey = null;
private Map<String, Class<?>> columnTypeMap; // config.containerFactory.getAttrsMap(); private Class<? extends Model<?>> modelClass;
columnTypeMap是关键字段,暂且记下来。
下面我们还是回到TableBuilder里的doBuild(table, conn, config);方法。
这个才是DB映射的关键,我其实直接讲这一个类就可以的......这个方法代码实在太多了,我贴部分代码做讲解吧。
那么第六步:doBuild详解。
这块有点类,我直接在代码里写注释吧:
@SuppressWarnings("unchecked")
private static void doBuild(Table table, Connection conn, Config config) throws SQLException { // 初始化 Table 里的columnTypeMap字段。
table.setColumnTypeMap(config.containerFactory.getAttrsMap());
// 取得主键,如果取不到的话,默认设置"id"。
// 记不记得最开始的两个同名不同参的方法 addMapping(...),
在这才体现出后续处理的不同。
if (table.getPrimaryKey() == null)
table.setPrimaryKey(config.dialect.getDefaultPrimaryKey());
// 此处如果没有设置方言,则默认 Dialect dialect = new MysqlDialect(); Mysql的方言。
// sql为"select * from `" + tableName + "` where 1 = 2";
String sql = config.dialect.forTableBuilderDoBuild(table.getName());
Statement stm = conn.createStatement();
ResultSet rs = stm.executeQuery(sql);
//取得个字段的信息
ResultSetMetaData rsmd = rs.getMetaData();
// 匹配映射
for (int i=; i<=rsmd.getColumnCount(); i++) {
String colName = rsmd.getColumnName(i);
String colClassName = rsmd.getColumnClassName(i);
if ("java.lang.String".equals(colClassName)) {
// varchar, char, enum, set, text, tinytext, mediumtext, longtext
table.setColumnType(colName, String.class);
}
else if ("java.lang.Integer".equals(colClassName)) {
// int, integer, tinyint, smallint, mediumint
table.setColumnType(colName, Integer.class);
}
else if ("java.lang.Long".equals(colClassName)) {
// bigint
table.setColumnType(colName, Long.class);
}
// else if ("java.util.Date".equals(colClassName)) {
// java.util.Data can not be returned
// java.sql.Date, java.sql.Time,
java.sql.Timestamp all extends java.util.Data so getDate can return the three types data
// result.addInfo(colName, java.util.Date.class);
// }
else if ("java.sql.Date".equals(colClassName)) {
// date, year
table.setColumnType(colName, java.sql.Date.class);
}
else if ("java.lang.Double".equals(colClassName)) {
// real, double
table.setColumnType(colName, Double.class);
}
else if ("java.lang.Float".equals(colClassName)) {
// float
table.setColumnType(colName, Float.class);
}
else if ("java.lang.Boolean".equals(colClassName)) {
// bit
table.setColumnType(colName, Boolean.class);
}
else if ("java.sql.Time".equals(colClassName)) {
// time
table.setColumnType(colName, java.sql.Time.class);
}
else if ("java.sql.Timestamp".equals(colClassName)) {
// timestamp, datetime
table.setColumnType(colName, java.sql.Timestamp.class);
}
else if ("java.math.BigDecimal".equals(colClassName)) {
// decimal, numeric
table.setColumnType(colName, java.math.BigDecimal.class);
}
else if ("[B".equals(colClassName)) {
// binary, varbinary, tinyblob, blob, mediumblob, longblob
// qjd project: print_info.content varbinary(61800);
table.setColumnType(colName, byte[].class);
}
else {
int type = rsmd.getColumnType(i);
if (type == Types.BLOB) {
table.setColumnType(colName, byte[].class);
}
else if (type == Types.CLOB || type == Types.NCLOB) {
table.setColumnType(colName, String.class);
}
else {
table.setColumnType(colName, String.class);
}
// core.TypeConverter
// throw new RuntimeException
("You've got new type to mapping. Please add code in " + TableBuilder.class.getName()
+ ". The ColumnClassName can't be mapped: " + colClassName);
}
} rs.close();
stm.close();
}
这里巧妙的运用了 where 1=2的无检索条件结果,通过ResultSetMetaData rsmd = rs.getMetaData(); 导出了DB模型,这招确实漂亮。之前我还冥思苦相,他是怎么做的呢,看着此处源码,茅塞顿开。
接着,把编辑好的Table实例,放到TableMapping的成员变量 Model<?>>, Table> modelToTableMap 里去,TableMapping是单例的。
private final Map<Class<? extends Model<?>>, Table> modelToTableMap=
new HashMap<Class<? extends Model<?>>, Table>();
public void putTable(Table table) {
modelToTableMap.put(table.getModelClass(), table);
}
这样,所有的映射关系就都存在TableMapping的modelToTableMap
tableMapping.putTable(table);
再将modelToConfig都放入DbKit.modelToConfig里。
DbKit.addModelToConfigMapping(table.getModelClass(), config);
第七步,使用
Model里的save方法举例:
/**
* Save model.
*/
public boolean save() {
Config config = getConfig();
Table table = getTable(); StringBuilder sql = new StringBuilder();
List<Object> paras = new ArrayList<Object>();
config.dialect.forModelSave(table, attrs, sql, paras);
// if (paras.size() == 0) return false;
// The sql "insert into tableName() values()" works fine, so delete this line // --------
Connection conn = null;
PreparedStatement pst = null;
int result = ;
try {
conn = config.getConnection();
if (config.dialect.isOracle())
pst = conn.prepareStatement(sql.toString(),
new String[]{table.getPrimaryKey()});
else
pst = conn.prepareStatement(sql.toString(),
Statement.RETURN_GENERATED_KEYS); config.dialect.fillStatement(pst, paras);
result = pst.executeUpdate();
getGeneratedKey(pst, table);
getModifyFlag().clear();
return result >= ;
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
config.close(pst, conn);
}
}
Config config = getConfig();
上面这行就是调用DbKit的方法,取得DB配置。
public static Config getConfig(Class<? extends Model> modelClass) {
return modelToConfig.get(modelClass);
}
下面这段代码是去单例的TableMapping里取得表的具体信息。
Table table = getTable();
private Table getTable() {
return TableMapping.me().getTable(getClass());
}
JFinal 源码分析 [DB+ActiveRecord]的更多相关文章
- JFinal源码 分析之 Core包分析
ActionHandler.java 这个类继承了上面 说的Handler类,首先我们 上 几个属性 ,下面几个 属性我们 需要 关心哪些东西 呢?首先 是ActionMapping和RenderMa ...
- 史上最全的JFinal源码分析(不间断更新)
打算 开始 写 这么 一个系列,希望 大家 喜欢,学习 本来就是 一个查漏补缺的过程,希望大家能提出建议.本篇 文章 是整个目录的向导,希望 大家 喜欢.本文 将以 包的形式跟大家做向导. Handl ...
- YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)
YII 框架源码分析 百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- MyBatis源码分析(4)—— Cache构建以及应用
@(MyBatis)[Cache] MyBatis源码分析--Cache构建以及应用 SqlSession使用缓存流程 如果开启了二级缓存,而Executor会使用CachingExecutor来装饰 ...
- jQuery 2.0.3 源码分析 Deferred概念
JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作.这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码 ...
- tornado 学习笔记6 Application 源码分析
Application 是Tornado重要的模块之一,主要是配置访问路由表及其他应用参数的设置. 源代码位于虚拟运行环境文件夹下(我的是env),具体位置为env > lib>sit-p ...
- MySQL源码分析以及目录结构 2
原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...
- MySQL源码分析以及目录结构
原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...
随机推荐
- requireJS心得
最近有幸接触到前端分模块加载JS框架,并且结合avalonJS使用,在此记录学习痕迹: a.实现js文件的异步加载,避免网页失去响应: b.管理模块之间的依赖性,便于代码的编写和维护. (1)requ ...
- js获取数组中的最大值最小值
遍历方法: var tmp = [1,12,8,5]; var max = tmp[0]; for(var i=1;i<tmp.length;i++){ if(max<tmp[i])max ...
- PHP 实现下载文件到本地
只需要在php文件中设置请求头就可以了,创建download.php文件,代码如下: $fileName = $_GET['filename']; //得到文件名 header( "Cont ...
- shell字符串操作详解
shell字符串操作详解的相关资料. 1.shell变量声明的判断 表达式 含义 ${var} 变量var的值, 与$var相同 ${var-DEFAULT} 如果var没有被声明, 那么就以$DE ...
- js方法和原型继承(一)
在js语言规范中并不存在方法这一概念,方便起见,将作为对象属性的函数成为方法this引用的规则a.在最外层代码中,this引用的是全局对象b.在函数内,this引用根据函数调用方式不同而不同函数内部的 ...
- DevExpress BarManager 部分用法
1.创建一个BarManager会默认产生三个菜单.BarManager右键ShowDesignTime enhancements会显示[add]按钮,可对菜单进行编辑. 2.其中比较有用的属性: 选 ...
- 最新中国IP段获取办法与转成ROS导入格式
获取中国IP段办法 1.到APNIC获取亚太最新IP分配 http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest 2 ...
- Javascript 插件统一的实现步骤
步骤: // 1. 定义立即调用的函数 +function($){ "use strict"; //使用严格模式ES5支持 //后续步骤 // 2. xx 插件类及原型方法的定义 ...
- 比较C++中的4种类型转换方式
C++的四种cast操作符的区别并非我的原创-------------------------------------------from:http://blog.csdn.net/hrbeuwhw/ ...
- 10.python中的序列
本来说完字符串.数字.布尔值之后,应该要继续讲元祖.列表之类的.但是元祖和列表都属于序列,所以有必要先讲讲python的序列是什么. 首先,序列是是Python中最基本的数据结构.序列中的每个元素都分 ...