• JFinal 的源代码超具体的分析DB+ActiveRecord


           我记得有人告诉我。“面试一下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() + ". 
    " + e.getMessage();
    					log.error(message, e);
    					throw new RuntimeException(message, e);
    				}
    			}
    		}
    	}
    上面这种方法一共同拥有两个地方要注意一下。

    来自ActiveRecordPlugin.java

    			for (IPlugin plugin : pluginList) {
    上面这行是循环全部的插件,而且启动插件的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是keyword段。暂且记下来。

    以下我们还是回到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的核心调用流程,下次我会带来初始化流程,只是这是个大活的,预计到单独的章节来写。


    版权声明:本文博主原创文章,博客,未经同意不得转载。

  • 相关阅读:
    使用一行Python代码从图像读取文本
    FastAI 简介
    OpenCV-Python 轮廓:更多属性 | 二十四
    Numpy和OpenCV中的图像几何变换
    从云计算到边缘计算
    OpenCV-Python 轮廓属性 | 二十三
    c# GetType()和typeof()的区别
    C# 子类父类方法同名,三种处理方式
    C# string 与 String的区别
    decimal
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/4848868.html
Copyright © 2020-2023  润新知