zoukankan      html  css  js  c++  java
  • 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 > 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() + ". 
    " + 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());  
    }  
  • 相关阅读:
    Atitit 软件知识点分类体系 分类 按照书籍的分类 学科分类 体系与基础部分 计算机体系结构 硬件接口技术(usb,agp,pci,div,hdmi) os操作系统 中间件 语言部分
    Atitit spring注解事务的demo与代码说明 目录 1.1. Spring框架中,要如何实现事务?有一个注解,@EnableTransactionManagement 1 1.2. 事务管理
    Atitit springboot mybatis spring 集成 Springboot1.4 mybatis3.4.6 /springbootMybatis 目录 1.1. 设置map
    Atitit 计算机系统结构 计算机系统结构 Cpu 存储 cache 指令系统 目录 Line 56: 第2章指令系统设计 指令格式 寻址方式 1 Line 64: 第3章CPU及其实现
    Atitit 微服务 分布式 区别 微服务的判断标准 目录 1.1. 区别 微服务侧重于微小服务进程隔离级别,分布式侧重于机器隔离 1 2. 微服务是一种架构, 。多微才叫微? 1 2.1. 微服务
    Atitit spirngboot 访问 html文件总结 自设计web服务器原理与实现 Url路由压力,读取url,获得项目更路径绝对路径,拼接为文件路径。读取文建内容输出即可 目录路径 u
    Atitit 现代信息检索 Atitit 重要章节 息检索建模 检索评价 第8章 文本分类 Line 210: 第9章 索引和搜索 第11章 Web检索 第13章 结构化文本检索 目录 L
    Atitit 定时器timer 总结 目录 1.1. Js定时器 window.setInterval 1 2. Java定时器 timer 1 1.1.Js定时器 window.setInter
    Atitit spring5 集成 mybatis 注解班
    Atitit 微服务的一些理论 目录 1. 微服务的4个设计原则和19个解决方案 1 2. 微服务应用4个设计原则 1 2.1. AKF拆分原则 2 2.2. 前后端分离 2 2.3. 无状态服务
  • 原文地址:https://www.cnblogs.com/visec479/p/4087021.html
Copyright © 2011-2022 走看看