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一个流程或者说是大概,很多细节我也没细说,不过我相信大流程搞明白之后,一些小细节上的东西自己在慢慢研究也是没问题的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值