源码解读
启动项目
/**
* 建议使用 JFinal 手册推荐的方式启动项目
* 运行此 main 方法可以启动项目,此main方法可以放置在任意的Class类定义中,不一定要放于此
*/
public static void main(String[] args) {
JFinal.start("WebRoot", 81, "/", 5);
}
web.xml 文件
<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.common.config.DemoConfig</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>jfinal</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
启动项目后,
1)首先,它会根据web.xml。文件中的配置找到JFinal的过滤器
com.jfinal.core.JFinalFilter。
2)之后,就到了该文件中的init()初始化方法中。
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());
createJFinalConfig方法会利用反射创建在web.xml配置中的自定义配
置类com.demo.common.config.DemoConfig的实例。
3)之后,调用`jfinal.init(jfinalConfig,filterConfig.getServletContext())`
该方法两个参数,一个是刚刚初始化的config实例,
另一个是过滤器`filterConfig的上下文`。
4)我们在进入jfinal.init方法里看看
boolean init(JFinalConfig jfinalConfig, ServletContext servletContext) {
this.servletContext = servletContext;
this.contextPath = servletContext.getContextPath();
initPathUtil();
Config.configJFinal(jfinalConfig); // start plugin and init log factory in this method
constants = Config.getConstants();
initActionMapping();
initHandler();
initRender();
initOreillyCos();
initTokenManager();
return true;
}
这时我们就到了jfinal核心包中的jFinal类中init方法。在该类中我们(要特别注意,`private static final JFinal me = new JFinal();`
这里的me就是JFinal的实例。)init方法中:
①initPathUtil()
private void initPathUtil() {
String path = servletContext.getRealPath("/");
PathKit.setWebRootPath(path);
}
这里的path拿到的是`J:\javaproject\workspace\jfinal_demo\WebRoot`,
也就是我们项目的根目录。PathKit.setWebRootPath(path);
就是设置项目的根路径(给PathKit.webRootPath赋值)并且是去掉末尾
的/。
5)Config.configJFinal(jfinalConfig);官方的解释是:`start plugin and init log factory in this method`。源码是
static void configJFinal(JFinalConfig jfinalConfig) {
jfinalConfig.configConstant(constants);
initLogFactory();
jfinalConfig.configRoute(routes);
jfinalConfig.configPlugin(plugins);
startPlugins(); // very important!!!
jfinalConfig.configInterceptor(interceptors);
jfinalConfig.configHandler(handlers);
}
这里
①首先是初始化jfinal的常量:`jfinalConfig.configConstant(constants)`,
这里要知道jfinalConfig就是web.xml中的DemoConfig类。
也就是我们自定义的配置文件,该类继承了JFinalConfig。
而这里调用的configCoonstant方法就是我们在DemoConfig.java类中的方法。
在jfinal给的demo中的代码
// 加载少量必要配置,随后可用PropKit.get(...)获取值
PropKit.use("a_little_config.txt");
me.setDevMode(PropKit.getBoolean("devMode", false));
这里PropKit.use()这个方法会加载a_little_config.txt这个文件。
它的底层代码是:
public static Prop use(String fileName, String encoding) {
Prop result = map.get(fileName);
if (result == null) {
result = new Prop(fileName, encoding);
map.put(fileName, result);
if (PropKit.prop == null)
PropKit.prop = result;
}
return result;
}
看到result = new Prop(fileName, encoding);
其中Prop()源码:
public Prop(String fileName, String encoding) {
InputStream inputStream = null;
try {
inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName); // properties.load(Prop.class.getResourceAsStream(fileName));
if (inputStream == null)
throw new IllegalArgumentException("Properties file not found in classpath: " + fileName);
properties = new Properties();
properties.load(new InputStreamReader(inputStream, encoding));
} catch (IOException e) {
throw new RuntimeException("Error loading properties file.", e);
}
finally {
if (inputStream != null) try {inputStream.close();} catch (IOException e) {LogKit.error(e.getMessage(), e);}
}
}
上面这段源码中使用了`Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)`把a_little_config.txt文件加载进来!
其中`Thread.currentThread().getContextClassLoader()`是获得当前线程的类加载器ClassLoader。
这里要说下Thread.currentThread().getContextClassLoader() 和 Class.getClassLoader()区别
参考地址:
http://blog.youkuaiyun.com/AlbertFly/article/details/52162253
大概的意思是Class.getClassLoader得到的类加载器是静态的,表明类的载入者是谁。
而另一个Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。
对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。
回到正题:把txt文件加载进来为输入流后,再调用java的`properties.load(new InputStreamReader(inputStream, encoding));`来读取文件。
之后回到PropKit.use()方法中`map.put(fileName, result);`把结果保存到map集合中。
这里的map是ConcurrentHashMap。ConcurrentHashMap优势是有利于高并发并且线程安全,内部使用的是数组和链表。具体详见:
ConcurrentHashMap详解>
http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
最后把result赋值给`PropKit.prop = result`,至此,配置文件就读
取完毕。
me.setDevMode(PropKit.getBoolean("devMode", false));
这句话,就是先从刚刚读取的配置文件a_little_config.txt中,获取devMode,默认false。
之后在设置给Constants对象。至此常量设置完成,并且完成了读取配置文件。
②调用initLogFactory方法初始化日志
private static void initLogFactory() {
LogManager.me().init();
log = Log.getLog(Config.class);
JFinalFilter.initLog();
}
static void init() {
if (defaultLogFactory == null) {
try {
Class.forName("org.apache.log4j.Logger");
Class<?> log4jLogFactoryClass = Class.forName("com.jfinal.log.Log4jLogFactory");
defaultLogFactory = (ILogFactory)log4jLogFactoryClass.newInstance(); // return new Log4jLogFactory();
} catch (Exception e) {
defaultLogFactory = new JdkLogFactory();
}
}
}
虽然这里是jfinal自己封装的类,但是它自己底层依然是使用的是apache的log4j包。
`log = Log.getLog(Config.class);`获取config日志记录器,如果没有就创建一个,名字就为类名。
`JFinalFilter.initLog();`获取jfinal日志记录器。关于日志记录器参考链接:
jfinalConfig.configRoute(routes);
讲解
这里回去调DemoConfig中的configRoute(Routes me)方法。
其中参数routes是在Config类中new出来的:
private static final Routes routes = new Routes(){public void config() {}};
在configRoute方法中有两个方法
me.add("/", IndexController.class, "/index");
me.add("/blog", BlogController.class);
它们这里add的方法调用的是Routes中的,源码如下:
public Routes add(String controllerKey, Class<? extends Controller> controllerClass, String viewPath) {
if (controllerKey == null)
throw new IllegalArgumentException("The controllerKey can not be null");
controllerKey = controllerKey.trim();
if ("".equals(controllerKey))
throw new IllegalArgumentException("The controllerKey can not be blank");
if (controllerClass == null)
throw new IllegalArgumentException("The controllerClass can not be null");
if (!controllerKey.startsWith("/"))
controllerKey = "/" + controllerKey;
if (map.containsKey(controllerKey))
throw new IllegalArgumentException("The controllerKey already exists: " + controllerKey);
map.put(controllerKey, controllerClass);
// view path is controllerKey by default
if (viewPath == null || "".equals(viewPath.trim()))
viewPath = controllerKey;
viewPath = viewPath.trim();
// "/" added to prefix
if (!viewPath.startsWith("/"))
viewPath = "/" + viewPath;
// "/" added to postfix
if (!viewPath.endsWith("/"))
viewPath = viewPath + "/";
// support baseViewPath
if (baseViewPath != null)
viewPath = baseViewPath + viewPath;
viewPathMap.put(controllerKey, viewPath);
return this;
}
上面这段源码的作用就是把访问资源的路径controllerKey和相应的controllerClass保存到map中去,该map是Routes类的私有变量。
并且泛型是Map<String, Class<? extends Controller>>
。
这里的?是因为不知道类的名称,只知道他继承了Controller类。
这个add方法还对资源路径进行了处理。假设我们在配置路由是没有 写/,如me.add("blog", BlogController.class);
,它会自动帮你 加上,即保存时controllerKey为/blog,这里我们没有写第三个参数,它也会帮你处理,默认等于controllerKey,还会帮你加上/,也就是viewPath为/blog/,最终viewPathMap.put("/blog", "/blog/")
map.put("/blog", "class com.demo.blog.BlogController");
这样Routes初始化完毕。注意:这里Routes定义了baseViewPath,也就是它允许你对访问路径前再加上一个基础路径。
- jfinalConfig.configPlugin(plugins);详解
它会调用DemoConfig类中的configPlugin(Plugins me)方法。
public void configPlugin(Plugins me) {
// 配置C3p0数据库连接池插件
C3p0Plugin C3p0Plugin = createC3p0Plugin();
me.add(C3p0Plugin);
// 配置ActiveRecord插件
ActiveRecordPlugin arp = new ActiveRecordPlugin(C3p0Plugin);
me.add(arp);
// 所有配置在 MappingKit 中搞定
_MappingKit.mapping(arp);
}
这里用到的成c3p0创建时jfinal集合的,
createC3p0Plugin()
方法就是创建C3p0Plugin对象,并且初始化好了jdbcurl,user,password.该对象对应的类实现了IPlugin接口。me.add(C3p0Plugin);
该方法就是把刚刚创建好的C3p0Plugin对象放到一个pluginList数组里面去,该数组是Plugins类的一个私有属性。Plugins类时被final修饰的。不能被继承类似于String。
ActiveRecordPlugin arp = new ActiveRecordPlugin(C3p0Plugin);
底层嵌套掉好好几次。最终结果就是给ActiveRecordPlugin类的三个属性(configName、dataSourceProvider、transactionLevel)赋值。configName赋的默认值“main”,dataSourceProvider是传进来的c3p0,transactionLevel事务级别为4。
注意:transaction level define in java.sql.Connection
目前我自己有待研究;到了这里我们也就知道ActiveRecordPlugin是用来和数据库打交道的。me.add(arp);
会把ActiveRecordPlugin也加入到pluginList
数组中。
_MappingKit.mapping(arp);
该方法是在model包里写的方法。目的是为了建立数据库表字段与model的对应关系。而又由于jfinal使用ActiveRecordPlugin来和数据库打交道。所以传递的参数也是该类的实例。里面源码:
arp.addMapping("blog", "id", Blog.class);
可以看出,它有调用了ActiveRecordPlugin类的addMapping方法。继续看addMapping源码:
public ActiveRecordPlugin addMapping(String tableName,
String primaryKey,
Class<? extends Model<?>> modelClass) {
tableList.add(new Table(tableName,
primaryKey,
modelClass));
return this;
}
可以看出先创建了一个table类并初始化了表名、主键、模型Class,关于table类,官方的解释是
Table save the table meta info like column name and column type.
意思就是说,Table是存储数据库表的元信息,比如字段名和字段类型。
之后再把该Table添加到tableList里面。至此创建配置完毕。
startPlugins()
启用插件
startPlugins()方法里就是去遍历之前的pluginList。然后去调取他们的start()方法。其中如果是ActiveRecordPlugin插件,它会特别处理。
我们先来看看start方法。start方法是各个插件自己实现类的方法。比如c3p0插件的方法源码:
public boolean start() {
if (isStarted)//默认为false
return true;
dataSource = new ComboPooledDataSource();
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setUser(user);
dataSource.setPassword(password);
try {dataSource.setDriverClass(driverClass);}
catch (PropertyVetoException e) {dataSource = null; System.err.println("C3p0Plugin start error"); throw new RuntimeException(e);}
dataSource.setMaxPoolSize(maxPoolSize);
dataSource.setMinPoolSize(minPoolSize);
dataSource.setInitialPoolSize(initialPoolSize);
dataSource.setMaxIdleTime(maxIdleTime);
dataSource.setAcquireIncrement(acquireIncrement);
isStarted = true;
return true;
}
从源码中可以看出,插件默认是没有开启的。
dataSource = new ComboPooledDataSource();
拿到了数据库连接池的对象。dataSource.setJdbcUrl(jdbcUrl);
给连接池配置jdbcurl和账号密码。
dataSource.setUser(user);
dataSource.setPassword(password);dataSource.setDriverClass(driverClass);
这里是配置连接数据库驱动,默认是com.mysql.jdbc.Driver
。其他的一些配置dataSource.setMaxPoolSize(maxPoolSize);//最大连接数
dataSource.setMinPoolSize(minPoolSize);//最小连接数
dataSource.setInitialPoolSize(initialPoolSize);//初始化大小
dataSource.setMaxIdleTime(maxIdleTime);//连接存活时间
dataSource.setAcquireIncrement(acquireIncrement);//如果使用的连接数已经达到了maxPoolSize,c3p0会立即建立新的连接。
参考地址>http://hanqunfeng.iteye.com/blog/1671412
至此,插件启动完毕。我们也可以看出,所谓启动插件就是创建相应的对象,并且配置好基本参数,以供使用。
注意:在ActiveRecordPlugin
中官方有这么一段注释。ActiveRecord plugin not support mysql type year, you can use int instead of year.
意思是在使用mysql数据库时,建议字段不要使用year类型,因为 jdbc 对于 mysql 的 year 类型处理比较诡异。详见:
jfinal作者解释> http://www.oschina.net/question/924074_2176656
- ActiveRecordPlugin的start()源码:
public boolean start() {
if (isStarted) {
return true;
}
if (configName == null) {
configName = DbKit.MAIN_CONFIG_NAME;
}
if (dataSource == null && 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);
}
if (dialect != null) {
config.dialect = dialect;
}
if (showSql != null) {
config.showSql = showSql;
}
if (devMode != null) {
config.devMode = devMode;
}
if (transactionLevel != null) {
config.transactionLevel = transactionLevel;
}
if (containerFactory != null) {
config.containerFactory = containerFactory;
}
if (cache != null) {
config.cache = cache;
}
new TableBuilder().build(tableList, config);
DbKit.addConfig(config);
Db.init();
isStarted = true;
return true;
}
> 因为之前都配置好了configName、dataSource。这里我们直接看到`config = new Config(configName, dataSource);` 这个方法嵌套调用了很多方法。我们慢慢分析:①嵌套进入:
public Config(String name, DataSource dataSource) {
this(name, dataSource, new MysqlDialect());
}
这里new 了一个MysqlDialect对象。是jfinal自己定义的。其中构造方法是java默认的构造方法,之后我们再进入this()方法:
public Config(String name, DataSource dataSource, Dialect dialect) {
this(name, dataSource, dialect, false, false, DbKit.DEFAULT_TRANSACTION_LEVEL, IContainerFactory.defaultContainerFactory, new EhCache());
}
这里除了之前的三个参数(configname,datasource,dialect),加入了事务隔离级别、容器工厂、缓存。这里缓存EhCache底层其实用的是redis中的java版jedis。最后他们调用的是Config的一个构造方法。用来初始化事务、容器工厂、缓存等参数。回到start方法。虽然config = new Config(configName, dataSource);之后大部分都为null,但是在new Config这一步已经把它们都初始化过了。它之所以写很多if判断,是出于我们可以人工配置,不使用默认的 这种情况考虑的。
- new TableBuilder().build(tableList, config);
void build(List<Table> tableList, Config config) {
if (tableList.size() == 0) {
return ;
}
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);
}
} 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的值 是model包下的_MappingKit中`arp.addMapping("blog", "id", Blog.class);`方法会给tableList进行赋值。回到build方法。这里回去遍历tableList。在for循环里面还有`doBuild(table, conn, config);` 方法。源码:
private void doBuild(Table table, Connection conn, Config config) throws SQLException {
table.setColumnTypeMap(config.containerFactory.getAttrsMap());
if (table.getPrimaryKey() == null) {
table.setPrimaryKey(config.dialect.getDefaultPrimaryKey());
}
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);
Class<?> clazz = javaType.getType(colClassName);
if (clazz != null) {
table.setColumnType(colName, clazz);
}
else {
int type = rsmd.getColumnType(i);
if (type == Types.BINARY || type == Types.VARBINARY || 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();
}
}
这里首先获取到一个空map`table.setColumnTypeMap(config.containerFactory.getAttrsMap());`之后是设个表的主键,要是没有指定默认为id。
之后`config.dialect.forTableBuilderDoBuild(table.getName());` 是为了获取表结构,返回的是sql语句。里面的select语句:`"select * from `" + tableName + "` where 1 = 2";` 其中`where 1=2` 是人为的设置了一个假的条件,使得结果集为了。这条sql最后得到的就是表的元数据。也就是表的字段。
之后`Statement stm = conn.createStatement();
ResultSet rs = stm.executeQuery(sql);`
这是jdbc的基础,先拿到声明在去执行,rs就是结果集。
之后`ResultSetMetaData rsmd = rs.getMetaData();`
这是得到了得到结果集(rs)的结构信息,比如字段数、字段名等。
getMetaData> https://zhidao.baidu.com/question/25316052.html
接上面之后就是for循环,这个循环的目的就是为了使得表中字段与java类型进行对应,对应关系存储在map里面。其中`rsmd.getColumnCount()` 获得字段总数。`rsmd.getColumnName(i)`获得相应的字段名。`getColumnName` 获得相应的数据库里的字段类型。
这里完成的是table表字段名与java类型的对应关系。
我们现在回到build方法中的`tableMapping.putTable(table);`它的源码:
public void putTable(Table table) {
modelToTableMap.put(table.getModelClass(), table);
}
这里把java类中相应表的model的Class与表table对应关系保存在TableMapping中的modelToTableMap中。
该modelToTableMap结构为`Map<Class<? extends Model<?>>, Table>`。
接着回到build中`DbKit.addModelToConfigMapping(table.getModelClass(), config);` 源码:
static void addModelToConfigMapping(Class<? extends Model> modelClass, Config config) {
modelToConfig.put(modelClass, config);
}
这里是把java类中相应表的model的Class和相应的config配置关系保存
在DbKit中的modelToConfig中。`modelToConfig`的结果是`Map<Class<? extends Model>, Config>` 。
我们在回到build中,循环执行完毕后,会执行finally中的`config.close(conn);` 而源码:
public final void close(Connection conn) {
// in transaction if conn in threadlocal
if (threadLocal.get() == null)
if (conn != null)
try {conn.close();} catch (SQLException e) {throw new ActiveRecordException(e);}
}
这里我们注意到`if (threadLocal.get() == null)` 官方也写了注释。
意思是如果threadlocal在事务中还有conn连接。
至此,`new TableBuilder().build(tableList, config);`方法执行完毕。
我们再回到ActiveRecordPlugin中的start方法。`DbKit.addConfig(config);`
该方法目的是为了把config与configName的对应关系保存到DbKit中的`configNameToConfig`。
其结构为`Map<String, Config>`
我们再回到ActiveRecordPlugin中的start方法。
Db.init();
源码是:
static void init() {
dbPro = DbPro.use();
}
其中use方法里面调用的是`use(DbKit.config.name);`而该方法就是把,configName与DbPro对应关系存入到map中。该map是DbPro中全局变量,结构为`Map<String, DbPro>`,而创建DbPro时,回去DbKit中去获取ActiveRecordPlugin的config,这样configName与activerecord的DbPro对应关系也联系起来了。
至此,ActiveRecordPlugin的start方法执行完毕。
我们再回到Config类中的startPlugins方法的for循环中,在pluginList数组中的最后一个ActiveRecordPlugin遍历完毕后,该方法也就执行完毕。
jfinalConfig.configInterceptor(interceptors);
这里会调用DemoConfig.java配置文件中的configInterceptor方法。 由于这里没有配置自定义的拦截器。所以暂不深究。
jfinalConfig.configHandler(handlers);
这里会调用DemoConfig.java配置文件中的configInterceptor方法。 由于这里没有配置自定义的处理器。所以暂不深究。
至此,Config类中的configJFinal方法执行完毕。
我们回到JFianl类中,接着执行
constants = Config.getConstants();
方法。
该方法就是为了的之前初始化好的常量。接着看到下面代码:
initActionMapping();
initHandler();
initRender();
initOreillyCos();
initTokenManager();
- initActionMapping();方法源码:
private void initActionMapping() {
actionMapping = new ActionMapping(Config.getRoutes(), Config.getInterceptors());
actionMapping.buildActionMapping();
Config.getRoutes().clear();
}
上面代码中先`new`一个ActionMapping.执行的构造方法就是给`ActionMapping` 类的routes属性初始化。虽然也传入了`Interceptors`但是源码注释了相关的赋值语句。
接着执行`actionMapping.buildActionMapping();`方法。
由于篇幅原因,我另写一篇,接着写。