MVC觀察者框架

動機

argo是58同城開源出來的一個基於java的輕量級mvc框架。這個框架是其13年開源出來源代碼,但接下來就沒有維護了,但58內部好像還一直維護沿用wf(argo內部稱呼)。
但閱讀這款輕量級框架源碼對於我們理解mvc框架運行原理還是有很大裨益的。其代碼量不是很大,這也是我讀的第一個開源框架源碼。同時argo跟springmvc在思想上有很多相似之處,相信讀過這個源碼,對以後閱讀springmvc有會很有幫助。

0.知識要求

熟悉google的依賴注入框架guice。最好熟悉java servlet。對tomcat的servlet容器瞭解 \^_^

UML類圖時序圖整理
首先我整理了argo的uml類圖下載地址,該資源用rational rose打開即可查看。把argo核心類都整理了一遍。

先放一張Argo一次請求的時序圖吧

這裏寫圖片描述

1依賴注入中心

argo中大量的使用了依賴注入,源碼通讀下來,你會發現DI(Dependency Injection)的有點,但初始接觸會有一種代碼不連貫的感覺。
Argo的依賴注入配置中心是ArgoModule這個類,這裏麪包含了所有的注入規則,

 for (Class<? extends ArgoController> clazz : argo.getControllerClasses())
            bind(clazz).in(Singleton.class);
  • 1
  • 2

上面代碼片段中可以發現argo所有controller都是單例實現的。

2框架入口在哪?

這是我要說的第一個問題,servlet容器啓動後,又是怎麼進入我們這個框架,又是怎樣運行我們寫的業務邏輯代碼的。

拿tomcat來說在其web.xml配置文件中有一個load-on-startup配置項,如果其值\<0 表示tomcat在在啓動時不會加載該資源(拿servlet舉例,你可以發現web.xml的文件中包括servlet,jsp,defaultServlet這三個配置項且其值大於0),tomcat會根據其值的從小到大進行加載。

ArgoFilter就是真個argo處理請求的源頭,其實現了Filter接口,當瀏覽器請求落到web容器上(本文中就是tomcat)。可以看到ArgoFilter#init()方法中實例化了 用於處理請求分發的ArgoDispatcher對象,並且初始化Argo.class

 ArgoFilter.java

 public void init(FilterConfig filterConfig) throws ServletException {


        ServletContext servletContext = filterConfig.getServletContext();

        try {
            dispatcher = ArgoDispatcherFactory.create(servletContext);//該方法裏又初始化了Argo
            dispatcher.init();
        } catch (Exception e) {

            servletContext.log("failed to argo initialize, system exit!!!", e);
            System.exit(1);

        }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

初始化完走ArgoFilter#doFilter方法

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest) request;
        HttpServletResponse httpResp = (HttpServletResponse) response;

        dispatcher.service(httpReq, httpResp);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這裏便便是系統的真正的入口,可以看到dispatcher對其進行處理,ArgoDispatcher是一個接口

@ImplementedBy(DefaultArgoDispatcher.class)
public interface ArgoDispatcher {

    void init();

    void service(HttpServletRequest request, HttpServletResponse response);

    void destroy();

    public HttpServletRequest currentRequest();

    public HttpServletResponse currentResponse();

    BeatContext currentBeatContext();

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

可以看 @ImplementedBy(DefaultArgoDispatcher.class)這個註解,這是Guice的註解,作用是指該接口的默認實現是DefaultArgoDispatcher,這個實現過程就交流guice實現了,所以在讀這個代碼瞭解guice這個依賴注入框架是非常必要的。
在DefaultArgoDispatcher#service方法中綁定了request,response,context等參數

DefaultArgoDispatcher.java

   private BeatContext bindBeatContext(HttpServletRequest request, HttpServletResponse response) {
        Context context = new Context(request, response);
        localContext.set(context);

        BeatContext beat = argo.injector().getInstance(defaultBeatContextKey);
        // 增加默認參數到model
        beat.getModel().add("__beat", beat);
        context.setBeat(beat);
        return beat;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

這裏有一個ThreadLocal localContext變量,他會爲每一個線程創建一個Context的副本,等線程結束該副本便銷燬,BeatContext也是通過guice注入的。

3分發路由

在請求進來後,根據請求url找到我們實際的controller並且並且運行又是一個關鍵點

DefaultArgoDispatcher.java

 private void route(BeatContext beat) {
        try {
            ActionResult result = router.route(beat);

            if (ActionResult.NULL == result)
                result = statusCodeActionResult.getSc404();

            result.render(beat);

        } catch (Exception e) {

            statusCodeActionResult.render405(beat);

            e.printStackTrace();

            logger.error(String.format("fail to route. url:%s", beat.getClient().getRelativeUrl()), e);

            //TODO: catch any exceptions.

        } finally {
            localContext.remove();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

調用代碼中可以看到調用了router.route方法執行路由,根據BeatContext得到請求的url及其請求方式(get or post & eg.)。

接下來看一下DefaultRouter裏面的代碼

 @Inject
    public DefaultRouter(Argo argo, @ArgoSystem Set<Class<? extends ArgoController>> controllerClasses, @StaticActionAnnotation Action staticAction) {

        this.argo = argo;

        argo.getLogger().info("initializing a %s(implements Router)", this.getClass());

        this.actions = buildActions(argo, controllerClasses, staticAction);

        argo.getLogger().info("%s(implements Router) constructed.", this.getClass());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

這是DefaultRouter的構造方法,構造方法中已經注入了controller所有子類的class(不熟悉DI同學看到這個可能有點蒙了,沒看到哪裏new DefaultRouter啊,如果你熟悉guice的用法,你就不會迷茫了。@Inject這個註解表示構造參數中的參數會自動通過guice給你注入,又有同學問那構造方法中的參數哪裏來的,這個同樣通過guice注入的啊,還記得開頭在guice配置中心提到的所有的controller都是單例實例化的,是的,guice就是相當於給你幫你進行new操作,是不是很方便了)

在構造方法中通過buildActions獲得action,這個action所代表的就是服務器上能被訪問的資源,包括controller中我們開發的所有接口,所有靜態文件。

//DefaultRouter.java
 List<Action> buildActions(Argo argo, Set<Class<? extends ArgoController>> controllerClasses, Action staticAction) {

        Set<ArgoController> controllers = getControllerInstances(argo, controllerClasses);
        return buildActions(controllers, staticAction);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

通過所有的controller獲得action

//DefaultRouter.java

 List<Action> buildActions(Set<ArgoController> controllers, Action staticAction) {

        List<Action> actions = Lists.newArrayList();
        actions.add(staticAction);

        for (ArgoController controller : controllers) {
            ControllerInfo controllerInfo = new ControllerInfo(controller);
            List<ActionInfo> subActions = controllerInfo.analyze();

            for(ActionInfo newAction : subActions)
                merge(actions, MethodAction.create(newAction));

        }

        return ImmutableList.copyOf(actions);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面代碼就是獲得controller中所有的方法。

關於argo自己的攔截器

這裏特別摘出來說一下

//ActionInfo.java

 public ActionInfo(ControllerInfo controllerInfo, Method method, Argo argo) {
        this.controllerInfo = controllerInfo;
        this.method = method;
        this.argo = argo;

        Path path = AnnotationUtils.findAnnotation(method, Path.class);
        this.order = path.order();

        this.pathPattern = simplyPathPattern(controllerInfo, path);

        this.paramTypes = ImmutableList.copyOf(method.getParameterTypes());
        this.paramNames = ImmutableList.copyOf(ClassUtils.getMethodParamNames(controllerInfo.getClazz(), method));

        // 計算匹配的優先級,精確匹配還是模版匹配
        isPattern = pathMatcher.isPattern(pathPattern)
                || paramTypes.size() > 0;

        Pair<Boolean, Boolean> httpMethodPair = pickupHttpMethod(controllerInfo, method);
        this.isGet = httpMethodPair.getKey();
        this.isPost = httpMethodPair.getValue();

        annotations = collectAnnotations(controllerInfo, method);

        // 攔截器
        List<InterceptorInfo> interceptorInfoList = findInterceptors();
        preInterceptors = getPreInterceptorList(interceptorInfoList);
        postInterceptors = getPostInterceptorList(interceptorInfoList);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

ActionInfo的構造方法中對argo使用者編寫的controller的所有的註解進行遍歷,這裏說一下argo的攔截器如何使用,可以看到argo實現了前置攔截器PreInterceptorAnnotation,後置攔截器PostInterceptorAnnotation兩個註解及其相關接口,使用者將攔截器類聲明相關接口

@Target({ElementType.TYPE, ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@PreInterceptorAnnotation( value =MyI.class)  
public @interface MyInterceptorAnnotation {  

}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

MyI.java是我自己實現的一個攔截器類,通過PreInterceptorAnnotation/PostInterceptorAnnotation註解關聯。看源碼好像argo自己的攔截器只能通過這個方式實現,通過ActionInfo類就可以發現其獲取攔截器的方法,掃描controller上所有的註解,得到攔截器相關並轉爲action。

4 controller代碼運行

終於要將到開發者在controller寫的代碼怎麼運行的了。

在DefaultRouter類的route方法中

public ActionResult route(BeatContext beat) {

        RouteBag bag = RouteBag.create(beat);

        for(Action action : actions) {
            RouteResult routeResult = action.matchAndInvoke(bag);
            if (routeResult.isSuccess())
                return routeResult.getResult();
        }

        return ActionResult.NULL;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到這裏有個for循環,通過我們前面掃描獲取的action調用他們的matchAndInvoke方法

//MethodAction.java
@Override
    public RouteResult matchAndInvoke(RouteBag bag) {

        if (!actionInfo.matchHttpMethod(bag))
            return RouteResult.unMatch();

        Map<String, String> uriTemplateVariables = Maps.newHashMap();

        boolean match = actionInfo.match(bag, uriTemplateVariables);
        if (!match)
            return RouteResult.unMatch();

        // PreIntercept
        for(PreInterceptor preInterceptor : actionInfo.getPreInterceptors()) {
            ActionResult actionResult = preInterceptor.preExecute(bag.getBeat());
            if (ActionResult.NULL != actionResult)
                return RouteResult.invoked(actionResult);
        }

        ActionResult actionResult = actionInfo.invoke(uriTemplateVariables);

        // PostIntercept
        for(PostInterceptor postInterceptor : actionInfo.getPostInterceptors()) {
            actionResult = postInterceptor.postExecute(bag.getBeat(), actionResult);
        }

        return RouteResult.invoked(actionResult);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

可以看到先是運行順序是前置攔截器-controller-後置攔截器,運行完返回路由處理結果RouteResult,如果路由成功(根據url找到對應的controller或者靜態資源

//ActionInfo.java
ActionResult invoke(Map<String, String> urlParams) {
        Object[] param = new Object[getParamTypes().size()];
        for(int index = 0; index < getParamNames().size(); index++){
            String paramName = getParamNames().get(index);
            Class<?> clazz = getParamTypes().get(index);

            String v = urlParams.get(paramName);

            if (v == null)
                throw ArgoException.newBuilder("Invoke exception:")
                        .addContextVariable(paramName, "null")
                        .build();

            // fixMe: move to init
            if(!getConverter().canConvert(clazz))
                throw ArgoException.newBuilder("Invoke cannot convert parameter.")
                        .addContextVariable(paramName, "expect " + clazz.getName() + " but value is " + v)
                        .build();

            param[index] = getConverter().convert(clazz, v);
        }

        try {
            Object result = method().invoke(controller(), param);
            return ActionResult.class.cast(result);
        } catch (Exception e) {
            throw ArgoException.newBuilder("invoke exception.", e)
                    .addContextVariables(urlParams)
                    .build();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

ActionInfo#invoke方法中通過反射調用controller中對應的方法,執行相應的代碼。並且返回ActionResult,接着將其放入RouterResult中。

在這個運行結果其實就是開發者寫在controller裏的代碼運行的結果。
我們可以通過Argo的demo中可以看到

//HomeController.java
@Path("{phoneNumber:\\d+}")
    public ActionResult helloView(int phoneNumber) {
        BeatContext beatContext = beat();

        beatContext
                .getModel()
                .add("title", "phone")
                .add("phoneNumber", phoneNumber);

        return view("hello");

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面是demo的代碼片段,最後調用AbstractController#view()方法返回的是ActionResult,然後將其set到RouterResult中。

這裏提一下我們經常將傳遞給前端(velocity)的數據放到beat中。這個beat是存在Argo.java中,上面代碼通過beat()方法在argo中獲取BeatContext,雖然Argo是單例的,但beat是會爲每一線程創建一個副本的,所有每個請求會保存自己的值。

5. 交由Response返回

當這些分發路由controller運行完,根據其返回結果ActionResult進行相應的處理

//DefautlArgoDispatcher.java

 private void route(BeatContext beat) {
        try {
            ActionResult result = router.route(beat);

            if (ActionResult.NULL == result)
                result = statusCodeActionResult.getSc404();

            result.render(beat);

        } catch (Exception e) {

            statusCodeActionResult.render405(beat);

            e.printStackTrace();

            logger.error(String.format("fail to route. url:%s", beat.getClient().getRelativeUrl()), e);

            //TODO: catch any exceptions.

        } finally {
            localContext.remove();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

還記得這是開頭調用的代碼,當獲得result之後先判斷是否爲空,空的話我們看到了我們熟悉的404。

不同的返回類型由不同的ActionResult來實現,總的來說ActionResult#render就是將我們的返回結果交給reponse,servlet來返回處理,呈獻給用戶。

總結

其實這篇文章也就講了argo一個流程或者說是大概,很多細節我也沒細說,不過我相信大流程搞明白之後,一些小細節上的東西自己在慢慢研究也是沒問題的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章