一篇文章了解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一次请求的时序图吧

一篇文章了解mvc框架工作流程_第1张图片

1依赖注入中心

argo中大量的使用了依赖注入,源码通读下来,你会发现DI(Dependency Injection)的有点,但初始接触会有一种代码不连贯的感觉。
Argo的依赖注入配置中心是ArgoModule这个类,这里面包含了所有的注入规则,

 for (Class clazz : argo.getControllerClasses())
            bind(clazz).in(Singleton.class);

上面代码片段中可以发现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);

        }

    }

初始化完走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);

    }

这里便便是系统的真正的入口,可以看到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();

}

可以看 @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;
    }

这里有一个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();
        }
    }

调用代码中可以看到调用了router.route方法执行路由,根据BeatContext得到请求的url及其请求方式(get or post & eg.)。

接下来看一下DefaultRouter里面的代码

 @Inject
    public DefaultRouter(Argo argo, @ArgoSystem Set> 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());
    }

这是DefaultRouter的构造方法,构造方法中已经注入了controller所有子类的class(不熟悉DI同学看到这个可能有点蒙了,没看到哪里new DefaultRouter啊,如果你熟悉guice的用法,你就不会迷茫了。@Inject这个注解表示构造参数中的参数会自动通过guice给你注入,又有同学问那构造方法中的参数哪里来的,这个同样通过guice注入的啊,还记得开头在guice配置中心提到的所有的controller都是单例实例化的,是的,guice就是相当于给你帮你进行new操作,是不是很方便了)

在构造方法中通过buildActions获得action,这个action所代表的就是服务器上能被访问的资源,包括controller中我们开发的所有接口,所有静态文件。

//DefaultRouter.java
 List buildActions(Argo argo, Set> controllerClasses, Action staticAction) {

        Set controllers = getControllerInstances(argo, controllerClasses);
        return buildActions(controllers, staticAction);
    }

通过所有的controller获得action

//DefaultRouter.java

 List buildActions(Set controllers, Action staticAction) {
        
    	List actions = Lists.newArrayList();
        actions.add(staticAction);

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

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

        }

        return ImmutableList.copyOf(actions);
    }

上面代码就是获得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 httpMethodPair = pickupHttpMethod(controllerInfo, method);
        this.isGet = httpMethodPair.getKey();
        this.isPost = httpMethodPair.getValue();

        annotations = collectAnnotations(controllerInfo, method);

        // 拦截器
        List interceptorInfoList = findInterceptors();
        preInterceptors = getPreInterceptorList(interceptorInfoList);
        postInterceptors = getPostInterceptorList(interceptorInfoList);
    }

ActionInfo的构造方法中对argo使用者编写的controller的所有的注解进行遍历,这里说一下argo的拦截器如何使用,可以看到argo实现了前置拦截器PreInterceptorAnnotation,后置拦截器PostInterceptorAnnotation两个注解及其相关接口,使用者将拦截器类声明相关接口

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

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;
    }

可以看到这里有个for循环,通过我们前面扫描获取的action调用他们的matchAndInvoke方法

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

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

        Map 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);
    }

可以看到先是运行顺序是前置拦截器-controller-后置拦截器,运行完返回路由处理结果RouteResult,如果路由成功(根据url找到对应的controller或者静态资源

//ActionInfo.java
ActionResult invoke(Map 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();
        }
    }

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");

    }

上面是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();
        }
    }

还记得这是开头调用的代码,当获得result之后先判断是否为空,空的话我们看到了我们熟悉的404。

不同的返回类型由不同的ActionResult来实现,总的来说ActionResult#render就是将我们的返回结果交给reponse,servlet来返回处理,呈献给用户。

总结

其实这篇文章也就讲了argo一个流程或者说是大概,很多细节我也没细说,不过我相信大流程搞明白之后,一些小细节上的东西自己在慢慢研究也是没问题的。

你可能感兴趣的:(第三方库/工具使用)