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);  

這三行代碼就是加載DB映射的關鍵,那麼我們複習一下,JFinal的DB映射無需配置文件,無需與DB對應的POJO,只需要寫一個類,繼承Model<M extends Model>即可。

第一步:爲ActiveRecordPlugin的 private IDataSourceProvider dataSourceProvider 賦值。

那麼我們先來看看ActiveRecordPlugin的構造器。

public ActiveRecordPlugin(IDataSourceProvider dataSourceProvider) {  
    this(DbKit.MAIN_CONFIG_NAME, dataSourceProvider);  
}  

這裏重要的是dataSourceProvider,IDataSourceProvider是一個接口,它的運行時類型是

public class C3p0Plugin implements IPlugin, IDataSourceProvider{...}  

那麼,可以看到

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 > 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-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) ? 0 : 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=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是單例的。

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 = 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配置。

public static Config getConfig(Class<? extends Model> modelClass) {  
    return modelToConfig.get(modelClass);  
}  

下面這段代碼是去單例的TableMapping裏取得表的具體信息。

Table table = getTable();  

private Table getTable() {  
    return TableMapping.me().getTable(getClass());  
}  

以上,就是DB+ActiveRecord的核心調用流程,下次我會帶來初始化流程,不過這是個大活,估計要分開三章來寫吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章