淺談JFinal原始碼,短小精悍之道

習慣了不習慣發表於2018-12-22

前言

這篇文章的前提是對JFnial有了大致的瞭解,不熟悉的話可以看一下官網文件https://www.jfinal.com/doc,非常簡單的一個框架,回到原題,老大說有一個JFinal的專案以後有可能讓我維護,於是讓我熟悉一下JFinal的原理,看了JFinal的官網文件和原始碼才發現JFinal真是短小精悍,言簡意賅。因為之前只用過Spring的mvc和公司之前封裝的MVC,話不多說,上原始碼...

一.啟動

JFinal.start("src/main/resources", 9527, "/");
複製程式碼

這時候JFinal就啟動了,指定埠,這時候會載入resources目錄下的WEB-INF/的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.jf.Config</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>jfinal</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
複製程式碼

可以看到JFinal是使用過濾器的原理攔截所有請求這也是和SpringMVC的不同之一,同樣可以看到我們需要制定init-param,這相當於JFinal的配置檔案,這也是需要我們自己去實現的。

二.配置檔案


import com.jfinal.config.*;
import com.jfinal.template.Engine;

public class Config extends JFinalConfig {
    @Override
    public void configConstant(Constants me) {
        //配置全域性常量
    }

    @Override
    public void configRoute(Routes me) {
        //配置路由,所有的Controller需要在這裡配置,這也是我們今天所講的主題
        me.add("/user", UserController.class);
    }

    @Override
    public void configEngine(Engine me) {
        //配置模板
    }

    @Override
    public void configPlugin(Plugins me) {
        //配置外掛
        //druid 資料庫連線池外掛
        DruidPlugin druidPlugin = new DruidPlugin("url", "username", "password");
        me.add(druidPlugin);

        //配置ActiveRecord外掛
        ActiveRecordPlugin arp = new ActiveRecordPlugin(druidPlugin);
        _MappingKit.mapping(arp);//這裡是將資料庫裡的實體類指定連線池(_MappingKit這裡是自動生成的實體類所對應對映類)
        me.add(arp);
    }

    @Override
    public void configInterceptor(Interceptors me) {
        //這裡配置攔截器
    }

    @Override
    public void configHandler(Handlers me) {
        //這裡是配置自定義handler,因為JFnial是鏈式呼叫,所以允許我們自定義handler(Handler是選擇並呼叫Controller,後面會講)
    }
}

複製程式碼

三.路由配置

public void configRoute(Routes me) {
    //配置路由,所有的Controller需要在這裡配置,這也是我們今天所講的主題
    me.add("/user", UserController.class);
}
複製程式碼

我們點開me.add方法看下怎麼封裝的

public Routes add(String controllerKey, Class<? extends Controller> controllerClass) {
    return add(controllerKey, controllerClass, controllerKey);
}
複製程式碼

繼續點進去

public Routes add(String controllerKey, Class<? extends Controller> controllerClass, String viewPath) {
    routeItemList.add(new Route(controllerKey, controllerClass, viewPath));
    return this;
}
複製程式碼

可以看到controllerKey和viewPath就是我們之前配置的"/user"路徑,controllerClass就是我們傳入的UserController.class物件,這裡封裝成Route物件並集合在routeItemList集合裡,好,我們看一下routeItemList會在什麼時候被呼叫。

protected void buildActionMapping() {
		mapping.clear();
		Set<String> excludedMethodName = buildExcludedMethodName();
		InterceptorManager interMan = InterceptorManager.me();
		for (Routes routes : getRoutesList()) {
		for (Route route : routes.getRouteItemList()) {
			Class<? extends Controller> controllerClass = route.getControllerClass();
			Interceptor[] controllerInters = interMan.createControllerInterceptor(controllerClass);
			
			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[] actionInters = interMan.buildControllerActionInterceptor(routes.getInterceptors(), controllerInters, controllerClass, method);
				String controllerKey = route.getControllerKey();
				
				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 action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));
				if (mapping.put(actionKey, action) != null) {
					throw new RuntimeException(buildMsg(actionKey, controllerClass, method));
				}
			}
		}
		}
		routes.clear();
		
		// support url = controllerKey + urlParas with "/" of controllerKey
		Action action = mapping.get("/");
		if (action != null) {
			mapping.put("", action);
		}
	}
複製程式碼

在ActionMapping我們找到了routes.getRouteItemList()方法,這個封裝的方法有點長,我們慢慢來講. 首先將mapping物件清空,找到excludedMethodName不需要執行的方法(這裡是Controller的方法,我們不需要對它的方法進行快取,因為我們需要快取的是它子類的方法),這裡會找到所配置的攔截器管理InterceptorManager(在這裡就不詳細解析了)。

mapping.clear();
Set<String> excludedMethodName = buildExcludedMethodName();
InterceptorManager interMan = InterceptorManager.me();
複製程式碼

遍歷routes.getRouteItemList(),就是我們之前看到的將UserController封裝成Route讓後放入的list裡,route.getControllerClass()獲取我們配置的Controller物件,這裡會獲取我們配置的Controller方法,如果是Controller子類則獲取它宣告的方法,反之獲取所有的方法,然後遍歷。

Class<? extends Controller> controllerClass = route.getControllerClass();
Interceptor[] controllerInters = interMan.createControllerInterceptor(controllerClass);
boolean sonOfController = (controllerClass.getSuperclass() == Controller.class);
Method[] methods = (sonOfController ? controllerClass.getDeclaredMethods() : controllerClass.getMethods());
複製程式碼

下面這段程式碼做的就是匹配攔截器和將方法的key提取出來,預設的是我們之前傳入的controllerKey加methodName方法的名字或者可以用註解ActionKey指定名字

	String methodName = method.getName();
	if (excludedMethodName.contains(methodName) || method.getParameterTypes().length != 0)
		continue ;
	if (sonOfController && !Modifier.isPublic(method.getModifiers()))
		continue ;
				
	Interceptor[] actionInters = interMan.buildControllerActionInterceptor(routes.getInterceptors(), controllerInters, controllerClass, method);
	String controllerKey = route.getControllerKey();
				
	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物件,並放入map裡。

Action action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));
	if (mapping.put(actionKey, action) != null) {
		throw new RuntimeException(buildMsg(actionKey, controllerClass, method));
	}
複製程式碼

這裡做的事情其實就是將Controller的方法封裝Action物件一個map裡,這裡的程式碼先告一段落,也許到了這裡大家會有一些思路了,和springmvc類似,springmvc使用註解將方法注入進去。

四.JFinalFilter過濾請求

相信大家對filter都不陌生了,這裡我們直接進入handler.handle方法,這裡預設的實現是ActionHandler,我們直接進入

handler.handle(target, request, response, isHandled);
複製程式碼

進入handle方法,我們又看到了一個熟悉的物件actionMapping,我們根據請求的url獲取到Action(封裝了我們請求的方法),

Action action = actionMapping.getAction(target, urlPara);
複製程式碼

我們可以看到,我們得到了Controller 的class物件,點進去getController方法可以看到controllerClass.newInstance(),也就是說我們每次請求都會建立一個Controller物件,這也就是說Controller物件必須是無參構造,init對Controller進行初始化,將request, response對Controller進行繫結。

Controller controller = null;
  // Controller controller = action.getControllerClass().newInstance();
  controller = controllerFactory.getController(action.getControllerClass());
  controller.init(action, request, response, urlPara[0]);
複製程式碼

下面這個Controller例子可以看到,getRequest()可以獲得request物件,並從request物件讀取資訊,這也就解釋了Controller為什麼方法都是沒有引數的,同理返回資料也是通過我們傳入的response物件。

public class UserController extends Controller {

    public void getById() {
        String param = HttpKit.readData(getRequest());
        JSONObject json = JSON.parseObject(param);

        Integer id = json.getInteger("id");

        User user = getUserById(id);//這裡不做業務處理了
        renderJson(user);
    }

    private User getUserById(Integer id) {
        return new User();
    }
}

複製程式碼

下面就會執行Controller裡我們指定的Method方法了new Invocation(action, controller).invoke();點進去可以看到會先執行過濾器,過濾器執行完才會執行Controller方法,最後可以看到Render render = controller.getRender(),我們得到Render物件,然後對請求是進行轉發還是給客戶端返回資料。

五.淺談

首先ActionHandler是鏈式呼叫的,我們可以自己實現一個ActionHandler,這也是一個擴充套件的方向。我們發現我們每次的請求都是建立一個新的Controller物件,並繫結request和response物件,這裡我也是看了其他大佬的部落格,這裡這麼做是典型的空間換取時間,這樣會加快每次的請求速度,但同時會佔用較大的記憶體。其實這裡還有一個有意思的模組是ActiveRecordPlugin,大家有興趣的話可以自己看看,非常簡單易懂,操作起來也非常簡單,但同時對於複雜的sql不太友好,這也是ActiveRecordPlugin的弊端。

相關文章