JFinal 启动、请求源码简析(上篇)

花了一天时间看了下JFinal的源码,发现设计非常精巧。代码简洁明了。是一个很不错的MVC框架!

1、初始JFinal

1、MVC架构,设计精巧,使用简单

2、遵循COC原则,零配置,无xml

3、独创Db + Record模式,灵活便利

4、ActiveRecord支持,使数据库开发极致快速

5、自动加载修改后的java文件,开发过程中无需重启web server

6、AOP支持,拦截器配置灵活,功能强大

7、Plugin体系结构,扩展性强

8、多视图支持,支持FreeMarker、JSP、Velocity

9、强大的Validator后端校验功能

10、功能齐全,拥有struts2的绝大部分功能

11、体积小仅218K,且无第三方依赖

2、JFinal使用入门

     JFinal提倡无配置,可全部通过编码实现。如果想在项目中使用它,以下两步就可以完成:
1、核心配置类(必须继承 JFinalConfig
public class CoreConfig extends JFinalConfig {

	/** 常量配置 **/
	@Override
	public void configConstant(Constants me) {
		loadPropertyFile("system_config_info.txt");
		// 设置开发模式
		me.setDevMode(getPropertyToBoolean("devMode", true));
		me.setUrlParaSeparator(Const.SYMBOLAMPERSAND);
	}

	/** 处理器配置 接管所有web请求,可在此层做功能性的拓展 **/
	@Override
	public void configHandler(Handlers me) {
		DruidStatViewHandler dvh =  new DruidStatViewHandler("/druid");
		me.add(dvh);
		
	}

	/** 拦截器配置 类似与struts2拦截器,处理action **/
	@Override
	public void configInterceptor(Interceptors me) {

		me.add(new TxByMethods("find","update","delete","save")); //声明式事物管理
		
	}

	/** 插件配置 JFinal集成了很多插件:redis,druid,quartz... **/
	@Override
	public void configPlugin(Plugins me) {

		/** 数据库监控druid **/
		DruidPlugin dp = new DruidPlugin(getProperty("jdbcUrl"),
				getProperty("user"), getProperty("password"));
		dp.addFilter(new StatFilter()); //sql监控
		dp.addFilter(new WallFilter()); //防止sql注入
		//dp.addFilter(new WebStatFilter());
		WallFilter wall = new WallFilter();
		wall.setDbType("mysql");  //mysql
		dp.addFilter(wall);
		me.add(dp);
		
		ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
		me.add(arp);
		
		//表映射关系
		arp.addMapping(BlogConst.BLOGTABLE, "id", Blog.class);
		
		/** redis缓存支持根据不同模块使用缓存,目前我创建一个关于blog的缓存块 **/
		RedisPlugin blogRedis = new RedisPlugin(BlogConst.BLOGTABLE,
				"localhost");
		me.add(blogRedis);

		/** 定时任务插件 目前配置了一个每5秒钟执行一次的定时脚本**/
		QuartzPlugin quartzPlugin =  new QuartzPlugin("job.properies");
		me.add(quartzPlugin);

	}

	/** 路由配置:1.如下配置;2.写个类如:BlogRoute继承Routes,配置,me.add(new BlogRoute());也可 **/
	/** 路径设置:1.第三个参数;2.可通过注解@ActionKey("/index")方式 **/
	@Override
	public void configRoute(Routes me) {

		me.add("/", IndexController.class, "/index");
		/** controller配置路径 **/
		me.add("/blog", BlogController.class);

	}
	
	/*插件关闭之前【方法可选择性使用】**/	
	public void beforeJFinalStop(){
		System.out.println("插件关闭"); //这里我们可以加一些业务监控代码
	};
	
	/*插件加载完后【方法可选择性使用】**/
	public void afterJFinalStart(){
		System.out.println("加载完毕"); //这里我们可以加一些业务监控代码
};
2、配置web.xml
  就像我们使用Spring初始化数据(始化ContextLoaderListener)一样,JFinal需要配置filter来加载数据(com.jfinal.core.JFinalFilter)。 
        <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.jfinal.config.CoreConfig</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>jfinal</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

3、JFinal启动过程简析

  这里通过分析JFinal源码,具体来看看web容器启动加载filter的时候,JFinal做了什么事情!
1、启动初始化数据, JFinalFilter的init源码如下:
		createJFinalConfig(filterConfig.getInitParameter("configClass")); //这里获取web.xml配置的class,实例化JFinalConfig
		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());
	

一步步看:

1.1
private void createJFinalConfig(String configClass) {
		if (configClass == null)
			throw new RuntimeException("Please set configClass parameter of JFinalFilter in web.xml");
		
		Object temp = null;
		try {
			temp = Class.forName(configClass).newInstance();
		} catch (Exception e) {
			throw new RuntimeException("Can not create instance of class: " + configClass, e);
		}
		
		if (temp instanceof JFinalConfig)
			jfinalConfig = (JFinalConfig)temp;
		else
			throw new RuntimeException("Can not create instance of class: " + configClass + ". Please check the config in web.xml");
	}
实例化JFinalConfig的子类CoreConfig(我们业务需要配置的信息)
1.2 JFinal.init方法是初始化数据的核心。
boolean init(JFinalConfig jfinalConfig, ServletContext servletContext) {
		this.servletContext = servletContext; //获取servletContext
		this.contextPath = servletContext.getContextPath(); //获取contextPath
		
		initPathUtil(); //初始化根路径
		
		Config.configJFinal(jfinalConfig);	// start plugin and init logger factory in this method
		constants = Config.getConstants();
		
		initActionMapping();
		initHandler();
		initRender();
		initOreillyCos();
		initTokenManager();
		
		return true;
	}
1.2.1 看一下 Config.configJFinal(jfinalConfig);
这个方法的执行(父类( JFinalConfig)对象指向子类引用(我们业务配置的CoreConfig实例))就是加载设置的一些信息
static void configJFinal(JFinalConfig jfinalConfig) {
		jfinalConfig.configConstant(constants);				initLoggerFactory();
		jfinalConfig.configRoute(routes);
		jfinalConfig.configPlugin(plugins);					startPlugins();	// very important!!!
		jfinalConfig.configInterceptor(interceptors);
		jfinalConfig.configHandler(handlers);
	}
	
JFinal源码config包里面有5个Final修饰的类,是数据初始化的核心内容
Constants:框架的一些常量
Routes:保存的路由信息,该类有两个属性Map<String, Class<? extends Controller>> map,Map<String, String> viewPathMap就是在这时候初始化的。
Plugins:JFinal集成的一些插件,该类有一个属性List<IPlugin> pluginList,具体插件有:ActiveRecorePlugin,C3p0Plugin,ConfigPlugin,DruidPlugin等等,可以根据自己的喜好添加
Interceptors:该类有个属性List<Interceptor> globalActionInterceptor,保存了配置的所有拦截器,到后面ActionHandler的时候会依次根据配置调用Intercept的intercept方法。这里我只配置了TxByMethods事务拦截器。
Handlers:该类有一个属性List<Handler> handlerList,会保存所有的Handler,后面会依次把所有的Handler组成单向链表的结构。我只配置了DruidStatViewHandler,还有若干Handler可以根据需要配置。 Handler会处理所有请求,包括静态资源等。JFinal的请求过程也是先走Handler再走Action,后面会讲到。
Routes:
这里做一些参数化检测。
public Routes add(String controllerKey, Class controllerClass, String viewPath) {
		if (controllerKey == null)
			throw new IllegalArgumentException("The controllerKey can not be null");
		// if (controllerKey.indexOf(".") != -1)
			// throw new IllegalArgumentException("The controllerKey can not contain dot character: \".\"");
		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);
		
		if (viewPath == null || "".equals(viewPath.trim()))	// view path is controllerKey by default
			viewPath = controllerKey;
		
		viewPath = viewPath.trim();
		if (!viewPath.startsWith("/"))					// "/" added to prefix
			viewPath = "/" + viewPath;
		
		if (!viewPath.endsWith("/"))					// "/" added to postfix
			viewPath = viewPath + "/";
		
		if (baseViewPath != null)						// support baseViewPath
			viewPath = baseViewPath + viewPath;
		
		viewPathMap.put(controllerKey, viewPath);
		return this;
	}
加载后的Routes数据:
map:{/=class com.demo.jfinal.controller.index.IndexController, /blog=class com.demo.jfinal.controller.blog.BlogController}

viewPathMap{/=/index/, /blog=/blog/}
Plugins:
添加Plugin 
public Plugins add(IPlugin plugin) {
		if (plugin != null)
			this.pluginList.add(plugin);
		return this;
	}
加载后的Plugins数据:

[com.jfinal.plugin.druid.DruidPlugin@79efbd29, com.jfinal.plugin.activerecord.ActiveRecordPlugin@6a6c0d49, com.jfinal.plugin.redis.RedisPlugin@5bdc6a34, com.jfinal.ext.plugin.quartz.QuartzPlugin@63c0ab09]

可以看出这些就是我们配置的一些插件信息。 
Interceptors: 
	/**
	 * The same as addGlobalActionInterceptor. It is used to compatible with earlier version of jfinal
	 */
	public Interceptors add(Interceptor globalActionInterceptor) {
		if (globalActionInterceptor != null)
			this.globalActionInterceptor.add(globalActionInterceptor);
		return this;
	}
加载后的Interceptors数据:
[com.jfinal.plugin.activerecord.tx.TxByMethods@1438c5fa]
Handlers:
public Handlers add(Handler handler) {
		if (handler != null)
			handlerList.add(handler);
		return this;
	}
加载后的Handlers数据: 
[com.jfinal.plugin.druid.DruidStatViewHandler@488d4100]
基础数据都已经加载出来了,下面就是组装数据结构了。
1.2.2 initActionMapping方法
private void initActionMapping() {
		actionMapping = new ActionMapping(Config.getRoutes(), Config.getInterceptors());
		actionMapping.buildActionMapping();
	}
重头戏在 buildActionMapping方法上面,这里会组装ActionMapping的Map<String, Action> mapping参数。组装的基本规则为:第一个for:遍历Routes map中所有的 Controller,第二个for:遍历Controller中的所有method,根据配置的controllerKey和method作为mapping的key,对应数据封装成Action,Action类里面封装了请求的一些基本信息(配置的controllerKey,方法,拦截器),有了这些。最后当执行ActionHandler时候,会匹配controllerKey,返回对应的Action,最后通过反射执行对应Controller的method方法。获取数据,Render出去。看一下 buildActionMapping方法具体做了什么事情吧。 
void buildActionMapping() {
		mapping.clear();
		Set excludedMethodName = buildExcludedMethodName(); //获取Controller里面所有无参数的方法,主要为了区分自定义Controller的方法
		ActionInterceptorBuilder interceptorBuilder = new ActionInterceptorBuilder();
		Interceptor[] globalInters = interceptors.getGlobalActionInterceptor();
		interceptorBuilder.addToInterceptorsMap(globalInters); //获取全局拦截器
		for (Entry> entry : routes.getEntrySet()) {
			Class controllerClass = entry.getValue();
			Interceptor[] controllerInters = interceptorBuilder.buildControllerInterceptors(controllerClass);//获取注解拦截器,这里支持@Before形式的Controller级别
			
			boolean sonOfController = (controllerClass.getSuperclass() == Controller.class);
			Method[] methods = (sonOfController ? controllerClass.getDeclaredMethods() : controllerClass.getMethods());
			for (Method method : methods) {
				String methodName = method.getName();
				if (excludedMethodName.contains(methodName) || method.getParameterTypes().length != 0)
					continue ;
				if (sonOfController && !Modifier.isPublic(method.getModifiers()))
					continue ;
				
				Interceptor[] methodInters = interceptorBuilder.buildMethodInterceptors(method); //方法级别拦截器
				Interceptor[] actionInters = interceptorBuilder.buildActionInterceptors(globalInters, controllerInters, methodInters, method);//最终都会放在一个拦截器数组里面(每个method拦截器数组可能不一样)
				String controllerKey = entry.getKey();
				
				ActionKey ak = method.getAnnotation(ActionKey.class);
				String actionKey;
				if (ak != null) {
					actionKey = ak.value().trim();
					if ("".equals(actionKey))
						throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank.");
					
					if (!actionKey.startsWith(SLASH))
						actionKey = SLASH + actionKey;
				}
				else if (methodName.equals("index")) {
					actionKey = controllerKey;
				}
				else {
					actionKey = controllerKey.equals(SLASH) ? SLASH + methodName : controllerKey + SLASH + methodName;
				}
				//封装Action数据,每个Controller的method对应一个Action。 
				Action action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));
				if (mapping.put(actionKey, action) != null)
					throw new RuntimeException(buildMsg(actionKey, controllerClass, method));
			}
		}
		
		// support url = controllerKey + urlParas with "/" of controllerKey
		Action actoin = mapping.get("/");
		if (actoin != null)
			mapping.put("", actoin);
	}
最后可以看一下mapping里面的保存的数据:(业务里面我定义了两个Controller,IndexController(只有index方法)和BlogController(只有find方法))
{/blog/find=com.jfinal.core.Action@7bbecc3c, /=com.jfinal.core.Action@2b52b69c}
action信息如下: 
Action@7bbecc3c
Action@2b52b69c 
1.2.3 initHandler方法
JFinal执行本质就是执行一个Handler链表。具体如下:
private void initHandler() {
		Handler actionHandler = new ActionHandler(actionMapping, constants);
		handler = HandlerFactory.getHandler(Config.getHandlers().getHandlerList(), actionHandler);
	}

这里组成一个Handler链表,链表最后一个元素为之前初始化的ActionMapping,由此可以看到,Hander调用的优先级是高于ActionHander的。
/**
	 * Build handler chain
	 */
	public static Handler getHandler(List handlerList, Handler actionHandler) {
		Handler result = actionHandler;
		
		for (int i=handlerList.size()-1; i>=0; i--) {
			Handler temp = handlerList.get(i);
			temp.nextHandler = result;
			result = temp;
		}
		
		return result;
	}

本次配置的链表结构为:
DruidStatViewHandler--->ActionHandler 
1.2.4 initRender方法
private void initRender() {
		RenderFactory renderFactory = RenderFactory.me(); //单例类RenderFactory,JFinal通过该类返回数据
		renderFactory.init(constants, servletContext);
	}
public void init(Constants constants, ServletContext servletContext) {
		this.constants = constants;
		RenderFactory.servletContext = servletContext;
		
		// init Render
		Render.init(constants.getEncoding(), constants.getDevMode());
		initFreeMarkerRender(servletContext); //初始化freeMarkerRender
		initVelocityRender(servletContext); //初始化VelocityRender
		initJspRender(servletContext); //初始化 JspRender
		initFileRender(servletContext); //初始化FileRender
		
		// create mainRenderFactory
		if (mainRenderFactory == null) {
			ViewType defaultViewType = constants.getViewType();
			if (defaultViewType == ViewType.FREE_MARKER)
				mainRenderFactory = new FreeMarkerRenderFactory();
			else if (defaultViewType == ViewType.JSP)
				mainRenderFactory = new JspRenderFactory();
			else if (defaultViewType == ViewType.VELOCITY)
				mainRenderFactory = new VelocityRenderFactory();
			else
				throw new RuntimeException("View Type can not be null.");
		}
		
		// create errorRenderFactory
		if (errorRenderFactory == null) {
			errorRenderFactory = new ErrorRenderFactory();
		}
	}
1.2.5 initOreillyCos方法
private void initOreillyCos() { //初始化参数
		OreillyCos.init(constants.getUploadedFileSaveDirectory(), constants.getMaxPostSize(), constants.getEncoding());
	}
1.2.6 initTokenManager方法 
private void initTokenManager() {
		ITokenCache tokenCache = constants.getTokenCache(); //初始化token信息
		if (tokenCache != null)
			TokenManager.init(tokenCache);
	}
1.3 获取参数
这里获取初始化的一些信息,给doFilter方法使用。
                handler = jfinal.getHandler();
		constants = Config.getConstants();
		encoding = constants.getEncoding();
		jfinalConfig.afterJFinalStart();
		
		String contextPath = filterConfig.getServletContext().getContextPath();
		contextPathLength = (contextPath == null || "/".equals(contextPath) ? 0 : contextPath.length());

相关阅读:JFinal 启动、请求源码简析(下篇)

本文来自网易实践者社区,经作者田躲躲授权发布。