我記得以前有人跟我說,“面試的時候要看spring的源碼,要看ioc、aop的源碼"那為什麼要看這些開源架構的源碼呢,其實很多人都是"應急式"的去讀,就像讀一篇文章一下,用最快的速度把文章從頭到尾讀一遍,那結果就是當你讀完它,你也不清楚它講了一個什麼故事,想表達什麼。
一個優秀的架構的源碼我認為就好像一本名著一樣,你的“文學”水準越高,你就越能讀出作者設計的精妙之處。一篇源碼在你不同水準的時候,能讀出不同的東西,是以,我覺得優秀的架構的源碼是經久不衰的,反複讀多少次都不嫌多,直到你能設計出預期并駕齊驅甚至超越它的優美的架構。
讀源碼起初是一件很痛苦的事兒,想趕緊把它像流水賬一樣的讀完;慢慢實力增強後,會感覺到讀源碼能夠不費力氣的讀通;再假以時日,就能看出這些精妙的設計模式的組合。我有一個朋友,典型的源碼癡狂症,他跟我說他第一次看見spring的源碼,感覺特别興奮,讀了一宿沒睡覺.......好吧,我還有很長的路需要走~
話說多了,我們趕緊入正題:
JFinal的架構我24号的一篇博文寫到過,它優秀的地方在精簡代碼上,那麼有兩處源碼是我覺得是值得我們要好好解析一下,一處是初始化加載—servlet跳轉,另一處是DB+ActiveRecord的映射。
那麼DB映射相對比較簡單,我們這次就先來看看。
首先我們看看代碼,還是之前我寫過的 dog與cat的故事。
來自FinalConfig.java
// 采用DB+ActiveRecord模式
ActiveRecordPlugin arp = new ActiveRecordPlugin(c3p0Plugin);
me.add(arp);
// 進行DB映射
arp.addMapping("animal", AnimalModel.class);
這三行代碼就是加載DB映射的關鍵,那麼我們複習一下,JFinal的DB映射無需配置檔案,無需與DB對應的POJO,隻需要寫一個類,繼承Model<M extends Model>即可。
第一步:為ActiveRecordPlugin的 private IDataSourceProvider dataSourceProvider 指派。
那麼我們先來看看ActiveRecordPlugin的構造器。
來自ActiveRecordPlugin.java
public ActiveRecordPlugin(IDataSourceProvider dataSourceProvider) {
this(DbKit.MAIN_CONFIG_NAME, dataSourceProvider);
}
這裡重要的是dataSourceProvider,IDataSourceProvider是一個接口,它的運作時類型是
來自C3p0Plugin.java
public class C3p0Plugin implements IPlugin, IDataSourceProvider{...}
那麼,可以看到
來自ActiveRecordPlugin.java
this(DbKit.MAIN_CONFIG_NAME, dataSourceProvider);
這段代碼又繼續讀取另一個重載的構造器,然後調用了
來自ActiveRecordPlugin.java
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
來自AnimalModel.java
public class AnimalModel extends Model<AnimalModel> {...}
這裡Model的源碼我們一會再看,現在不着急。
然後進行映射
來自FinalConfig.java
// 進行DB映射
arp.addMapping("animal", AnimalModel.class);
這裡我們又回到了ActiveRecordPlugin類裡,它實際上有兩個addMapping方法,隻是參數不同。
來自ActiveRecordPlugin.java
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是什麼
來自ActiveRecordPlugin.java
private List<Table> tableList = new ArrayList<Table>();
它是ActiveRecordPlugin的一個成員變量,并且是private的,那我們可以猜到,tableList儲存了所有的映射關系。(ActiveRecordPlugin真是強大,後面會越來越強大~)。
第三步:建立映射關系
來自ActiveRecordPlugin.java
new Table(tableName, primaryKey, modelClass)
new Table(tableName, modelClass)
我們進去看看
來自Table.java
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參數的那個多出一行,我們看看這一行幹了什麼
來自Table.java
setPrimaryKey(primaryKey.trim()); // this.primaryKey = primaryKey.trim();
void setPrimaryKey(String primaryKey) {
String[] keyArr = primaryKey.split(",");
if (keyArr.length > 1) {
if (StrKit.isBlank(keyArr[0]) || StrKit.isBlank(keyArr[1]))
throw new IllegalArgumentException("The composite primary key can not be blank.");
this.primaryKey = keyArr[0].trim();
this.secondaryKey = keyArr[1].trim();
}
else {
this.primaryKey = primaryKey;
}
}
這樣的作用就是為Table下的primaryKey 和 secondaryKey指派。
第四步:加載ActiveRecordPlugin
那麼代碼好像跟到這裡就完事了,怎麼回事?是不是跟丢了?
别忘了,ActiveRecordPlugin是在FinalConfig裡的configPlugin方法加載的。那麼又有誰來加載FinalConfig呢?
PS:(FinalConfig是我自己定義的類)
public class FinalConfig extends JFinalConfig
這兒涉及到初始化的加載了,我簡單的講一下。
整個JFinal的入口是web.xml的一段配置:
來自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()方法:
來自JFinalFilter.java
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) ? 0 : contextPath.length());
}
繞過其他的加載,直接看這行
來自JFinalFilter.java
if (jfinal.init(jfinalConfig, filterConfig.getServletContext()) == false)
我們看看jfinal的類型是 private static final JFinal jfinal = JFinal.me();
那麼我們去JFinal類裡看看它的init方法。
來自JFinal.java
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。
來自JFinal.java
Config.configJFinal(jfinalConfig); // start plugin and init logger factory in this method
再點進去
來自com.jfinal.core.Config.java
/*
* Config order: constant, route, plugin, interceptor, handler
*/
static void configJFinal(JFinalConfig jfinalConfig) {
jfinalConfig.configConstant(constants); initLoggerFactory();
jfinalConfig.configRoute(routes);
jfinalConfig.configPlugin(plugins); startPlugins(); // very important!!!
jfinalConfig.configInterceptor(interceptors);
jfinalConfig.configHandler(handlers);
}
這段代碼實際上有個地方特别坑!就是
來自com.jfinal.core.Config.java
jfinalConfig.configPlugin(plugins); startPlugins(); // very important!!!
這行代碼一共做了兩件事,第一件事是jfinalConfig.configPlugin(plugins);來加載插件。還記得我們之前寫的FinalConfig裡的configPlugin(Plugins me) 方法嗎?
來自FinalConfig.java
/**
* 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()不是注釋!是一個方法,這塊實在太坑了,恰巧,這就是我們要找到的地方。
這個方法的代碼有點長,但因為很重要,我不得不都貼出來。
來自ActiveRecordPlugin.java
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);
}
}
}
}
上面這個方法一共有兩個地方要注意一下,
來自ActiveRecordPlugin.java
上面這行是循環所有的插件,并且啟動插件的start()方法。
那麼,我們中有一個插件記不記得是ActiveRecordPlugin的執行個體?那麼
來自ActiveRecordPlugin.java
boolean success = plugin.start();
這行代碼就會執行ActiveRecordPlugin下的start()代碼。終于繞回來了!!紅軍二萬五千裡長征,為了證明這個調用,我寫了多少字....
那麼我們看ActiveRecordPlugin下的start()方法吧,實際上這個start()方法是因為實作了IPlugin接口裡的start()方法。
來自ActiveRecordPlugin.java
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這個忘了沒,忘了就翻到最前面,第一步講的。
來自ActiveRecordPlugin.java
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);
來自TableBuilder.java
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了。
來自Table.java
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詳解。
這塊有點類,我直接在代碼裡寫注釋吧:
來自TableBuilder.java
@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=1; 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是單例的。
來自TableMapping.java
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
來自TableBuilder.java
tableMapping.putTable(table);
,再将modelToConfig都放入DbKit.modelToConfig裡。
來自TableBuilder.java
DbKit.addModelToConfigMapping(table.getModelClass(), config);
第七步,使用
Model裡的save方法舉例:
來自Model.java
/**
* 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 = 0;
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 >= 1;
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
config.close(pst, conn);
}
}
Config config = getConfig();
上面這行就是調用DbKit的方法,取得DB配置。
來自Model.java
public static Config getConfig(Class<? extends Model> modelClass) {
return modelToConfig.get(modelClass);
}
下面這段代碼是去單例的TableMapping裡取得表的具體資訊。
來自Model.java
Table table = getTable();
private Table getTable() {
return TableMapping.me().getTable(getClass());
}
以上,就是DB+ActiveRecord的核心調用流程,下次我會帶來初始化流程,不過這是個大活,估計要分開三章來寫吧。