我记得以前有人跟我说,“面试的时候要看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]的更多相关文章

  1. JFinal源码 分析之 Core包分析

    ActionHandler.java 这个类继承了上面 说的Handler类,首先我们 上 几个属性 ,下面几个 属性我们 需要 关心哪些东西 呢?首先 是ActionMapping和RenderMa ...

  2. 史上最全的JFinal源码分析(不间断更新)

    打算 开始 写 这么 一个系列,希望 大家 喜欢,学习 本来就是 一个查漏补缺的过程,希望大家能提出建议.本篇 文章 是整个目录的向导,希望 大家 喜欢.本文 将以 包的形式跟大家做向导. Handl ...

  3. YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

           YII 框架源码分析    百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...

  4. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  5. MyBatis源码分析(4)—— Cache构建以及应用

    @(MyBatis)[Cache] MyBatis源码分析--Cache构建以及应用 SqlSession使用缓存流程 如果开启了二级缓存,而Executor会使用CachingExecutor来装饰 ...

  6. jQuery 2.0.3 源码分析 Deferred概念

    JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作.这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码 ...

  7. tornado 学习笔记6 Application 源码分析

    Application 是Tornado重要的模块之一,主要是配置访问路由表及其他应用参数的设置. 源代码位于虚拟运行环境文件夹下(我的是env),具体位置为env > lib>sit-p ...

  8. MySQL源码分析以及目录结构 2

    原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...

  9. MySQL源码分析以及目录结构

    原文地址:MySQL源码分析以及目录结构作者:jacky民工 主要模块及数据流经过多年的发展,mysql的主要模块已经稳定,基本不会有大的修改.本文将对MySQL的整体架构及重要目录进行讲述. 源码结 ...

随机推荐

  1. Mir2源码详解之服务端-登录网关(LoginGate)

    传奇这款游戏,一直对我的影响很大.当年为了玩传奇,逃课,被老师叫过N次家长.言归正传,网上有很多源码,当然了,都是delphi的.并且很多源码还不全, 由于一直学习的c.c++.delphi还真不懂. ...

  2. 在信号处理函数中调用longjmp

    错误情况及原因分析 前两天看APUE的时候,有个程序要自己制作一个sleep程序,结果在这个程序中就出现了在信号处理函数中调用longjmp函数的情况,结果就出现了错误,具体错误是啥呢,请参见下面这段 ...

  3. ADO.NET中ExcuteReader读取存储过程获取的多行数据

    DLL层调用: List<BookInfo> tupleList = new List<BookInfo>(); using (IDataReader reader = thi ...

  4. IOS绘图——简单三角形

    #import <UIKit/UIKit.h> @interface MyView : UIView @end #import "MyView.h" @implemen ...

  5. Run JavaScript on your PeopleSoft pages conditionally

    Here, PeopleCode sets the logic that determines when the JavaScript code will run. This is not as si ...

  6. Python pass 语句使用示例

    Python pass 语句的使用方法示例.Python pass是空语句,pass语句什么也不做,一般作为占位符或者创建占位程序,是为了保持程序结构的完整性,pass语句不会执行任何操作,比如: P ...

  7. MySQL: ON DUPLICATE KEY UPDATE 用法 避免重复插入数据

    INSERT INTO osc_visit_stats(stat_date,type,id,view_count) VALUES (?,?,?,?) ON DUPLICATEKEY UPDATE vi ...

  8. (转)Android L Ripple的使用

    声明:Demo并不是有本人所写,本人只是总结在这里 工程源码: RippleDemo.zip ---------------------------------------------------- ...

  9. Python 2.7的安装(64位win10)

    Python 2.7.12 下载地址:https://www.python.org/downloads/release/python-2712/ 安装路径D:\Program Files\Python ...

  10. Hive深入浅出

    1.  Hive是什么 1) Hive是什么? 这里引用 Hive wiki 上的介绍: Hive is a data warehouse infrastructure built on top of ...