MVC框架,webx 流程解读

  1. 云栖社区>
  2. 博客>
  3. 正文

MVC框架,webx 流程解读

弈扬 2018-07-10 10:48:03 浏览9221
展开阅读全文

写在前面的话

写这篇文章的目的,主要还是站在一个新人的角度做一些沉淀,同样也为了方便后面的人快速熟悉集团web开发常用技术。这篇文章,将分析一个请求发到服务端,经tomcat容器、webx映射直到最后调用controller入口的过程,对java web基础知识结合webx做一个整体回顾。同时,也将简单分析集团web的基本分层方式,以及VO、DTO、DO等数据实例在各层所起的作用。

Web容器

理解web容器,也是理解我们程序的运行平台,同时也是了解servlet处理流程的基础所在。tomcat的主要结构如下图:

tomcat.png

Connector

Connector是tomcat的连接器。Tomcat在监听80端口的时候,一个HTTP请求访问过来,实际上是通过在80端口用socket来输入HTTP报文。Connector通过socket读取报文文本并进行解析,然后将报文内容封装为request实体,并将响应结果利用response进行封装,新起一个线程,并交给container容器进行处理。

Container

container是所有子容器的父接口。Engine主要负责对请求进行分发,而Host就是tomcat虚拟host功能的实体,Context就是我们一个应用服务的完整环境,每次一个请求对应的一个新的servlet,都是封装在Context中的。

Tomcat在处理Connector传来的request和response时,就是通过责任链模式,一层一层的去调用Engine、Host、Context并封装出一个servletWrapper来进行doService操作。

Servlet处理流程及WebX的调用

上面简单介绍了一下Tomcat的容器结构,接下来具体分析处理一个请求的过程。

tomcat容器执行过程

请求到达后,会新起一个线程并由Container进行处理。 Container中的Engine、Host、Context、Wrapper都继承了ValveBase抽象类并实现了其invoke方法。这也类似于webx的pipeline,对请求进行流水线处理。

首先是StandardEngineValve的invoke。engine通过对请求host域名的解析,映射到合适的host容器中去,然后调用host的invoke方法。

                StandardEngine engine = (StandardEngine)this.getContainer();
                Host host = (Host)engine.map(request, true);
                if(host == null) {
                    ((HttpServletResponse)response.getResponse()).sendError(400, sm.getString("standardEngine.noHost", request.getRequest().getServerName()));
                } else {
                    host.invoke(request, response);
                }

在host的invoke中,实际是调用了流水线的invoke方法:

 public void invoke(Request request, Response response) throws IOException, ServletException {
        this.pipeline.invoke(request, response);
    }    

最终又调用到standardPipeline中的invokeNext:

    public void invokeNext(Request request, Response response) throws IOException, ServletException {
        Integer current = (Integer)this.state.get();
        int subscript = current.intValue();
        this.state.set(new Integer(subscript + 1));
        if(subscript < this.valves.length) {
            this.valves[subscript].invoke(request, response, this);
        } else {
            if(subscript != this.valves.length || this.basic == null) {
                throw new ServletException(sm.getString("standardPipeline.noValve"));
            }

            this.basic.invoke(request, response, this);
        }

    }

可以看到流水线的设计模式是将engine、host、context等容器的实例放进一个数组中,并在运行时依次执行。

然后就到了StandardHostValve中,同样实行invoke方法:

        StandardHost host = (StandardHost)this.getContainer();
         Context context = (Context)host.map(request, true);

逻辑也类似,就是找到对应的context,也就是我们的应用并调用invoke方法。

context的invoke主要做了两件事:

1、获取session并绑定

2、找到该url对应的wrapper,并调用wrapper的invoke方法。

这里先略过session的获取绑定过程,直接来到wrapper的调用中去。在servletWrapper中,我们可以看到几个熟悉的对象:

        StandardWrapper wrapper = (StandardWrapper)this.getContainer();
        ServletRequest sreq = request.getRequest();
        ServletResponse sres = response.getResponse();
        Servlet servlet = null;
        HttpServletRequest hreq = null;

servlet会通过wrapper进行分配:

                servlet = wrapper.allocate();

从allocate方法中,我们可以看到servlet实际是通过类加载器记载的,并且当servlet加载完毕后,会调用初始化方法init。

     classClass = classLoader.loadClass(actualClass);
     ...
     servlet = (Servlet)classClass.newInstance();
     ...
     servlet.init(this.facade);

有了servlet以后,就开始出现另一个重要角色:ApplicationFilterChain。FilterChain会传入request和response并对servlet进行过滤:

filterChain.doFilter(sreq, sres);

在doFilter中,会调用到internalDoFilter 方法。该方法对filters进行遍历调用:

this.iterator = this.filters.iterator();
...
filter.doFilter(request, response, this);

可以看到,FilterChain 采用链式模式,通过多次调用chain.doFilter(request, response)方法会多次调用internalDoFilter方法,从而通过Iterator调用所有注册的Filter。

Webx执行流程

以一个简单的接口为例子,来看一看webX最终是如何调用到我们的execute方法的。

public class DemoScreen extends BmsBaseModule {
    public void execute(@Param('param') String param) {
      ...
    }
}

首先,继续上一部分的Filter开始看。Filter是servlet的标准过滤器,而上部分最后提到的filters则封装了web.xml文件中关于Filter的配置:

    <filter>
        <filter-name>webx</filter-name>
        <filter-class>com.alibaba.citrus.webx.servlet.WebxFrameworkFilter</filter-class>
        <init-param>
            <param-name>excludes</param-name>
            <param-value>*/checkpreload.htm<!-- 需要被“排除”的URL路径,以逗号分隔,如/static, *.jpg。适合于映射静态页面、图片。 --></param-value>
        </init-param>
        <init-param>
            <param-name>passthru</param-name>
            <param-value><!-- 需要被“略过”的URL路径,以逗号分隔,如/myservlet, *.jsp。适用于映射servlet、filter。
                对于passthru请求,webx的request-contexts服务、错误处理、开发模式等服务仍然可用。 --></param-value>
        </init-param>
    </filter>

上面是一个典型的webx配置,可以看到,设置的过滤器WebXFrameworkFilter会被ApplicationFilterChain执行。来看其中的doFilter方法:

if (isExcluded(path)) {
            log.debug("Excluded request: {}", path);
            chain.doFilter(request, response);
            return;
        }

校验我们设置的excluded参数,如果需要排除,那么通过doFilter的internalDoFilter来链式调用下一个过滤器。

关键语句在此:

getWebxComponents().getWebxRootController().service(request, response, chain);

调用webx RootController的service方法。service中首先对HttpServletRequest 和 HttpServletResponse 实例封装进RequestContext中。后续会调用 WebxRootControllerImpl 的 handleRequest 方法:

然后,会根据路径找到WebxComponent:

WebxComponent component = getComponents().findMatchedComponent(path);

在findMatchedComponent中会依据url来进行匹配:

            for (WebxComponent component : this) {
                if (component == defaultComponent) {
                    continue;
                }
                String componentPath = component.getComponentPath();
                if (!path.startsWith(componentPath)) {
                    continue;
                }
                // path刚好等于componentPath,或者path以componentPath/为前缀
                if (path.length() == componentPath.length() || path.charAt(componentPath.length()) == '/') {
                    matched = component;
                    break;
                }
            }

webx在Spring启动的时候,通过对文件目录的扫描,装配进符合规则的组件Component,比如xxx.module.screen下的类,来和url做匹配。找到对应的component后:

served = component.getWebxController().service(requestContext);

然后就到了webx的pipeline处理流程:

    public boolean service(RequestContext requestContext) throws Exception {
        PipelineInvocationHandle handle = pipeline.newInvocation();

        handle.invoke();

        // 假如pipeline被中断,则视作请求未被处理。filter将转入chain中继续处理请求。
        return !handle.isBroken();
    }

pipeline 同样是链式模式,会依次调用注册的valve。常用的valve可能包括url检查、登录检查、csrfToken校验等,同样pipeline中的一些条件语句比如Choose、Loop等同样也是一个valve,调用其invoke方法其实也是生成了一个新的pipeline并执行invoke。比如这样一个when-otherwise语句中就会有如下代码:

        if (!satisfied && otherwiseBlock != null) {
            otherwiseBlock.newInvocation(pipelineContext).invoke();
        }

此外,比较重要的valve就是负责处理表单的PerformActionVavle、负责执行screen的PerformScreenValve。

因为screen在应用中经常是一个接口的入口,并最终调用到业务逻辑部分的代码。所以这里我们分析一下PerformScreenValve这个阀门是如何调用到我们的业务逻辑代码的。screenValve的invoke最终调用了performScreenModule函数。显示依据规则找到了我们的module,然后执行了execute方法:

Module module = finder.getScreenModule();
...
module.execute();

然后调用了DataBindingAdapter的execute方法:

private final MethodInvoker executeMethod;
...
executeMethod.invoke(moduleObject, log);

在MethodInvoker这个类里面,我们可以看到装配execute方法参数的过程。

首先,在扫描类中注解的时候,webX会为每一个execute方法中有@Param注解的参数生成一个DataResolver。MethodInvoker中会有如下代码获取参数,最终会调用FastMethod的invoke方法。

Object[] args = new Object[resolvers.length];
for (int i = 0; i < args.length; i++) {
     Object value;
      value = resolvers[i].resolve();
 }
...
fastMethod.invoke(moduleObject, args);

最后就是反射调用的过程了,我们的接口object即为moduleObject,而args即为带有@Param注解的参数。最终的整体调用流程如下图:

flow.png

项目分层

关于web层、service层、manager层与DAO层

执行到我们发布的接口后,再来简单的说一下web项目常见分层手段。

在我们的项目中,常见的是依次调用web层,service层,manager,dao层。数据实体在VO、DTO、DO之间流转。

因为在执行某个业务的时候,可能需要查询多个表的数据并进行拼接。比如现在需要这样一条数据D,由A,B,C组成。如果仅仅提供一个方法查询A、B、C并拼接为D返回,那么下次如果又有一个需求为E(A+B)的数据,无疑又要重新写查询A和B的代码并拼接。

这样,我们把代码分层,使各部分职责独立。其中service负责拼接,manager负责查询,这样不同的service可以对manager进行复用。同时,由于DTO和DO不是完全对应,查询的时候往往要把DTO转换成DO查询条件去调用DAO,而DAO返回的结果DO又和web层所需要的数据格式不一致,所以常常要转换成DTO。 manager的作用,就是处理DTO到DO,然后DAO生成DO再到DTO的过程。如下就是一个manager:

        BmsPurchaseOrderQuery purchaseOrderQuery = new BmsPurchaseOrderQuery();
        purchaseOrderQuery.setPurchaseCode(purchaseCode);
        List<BmsPurchaseOrderDO> orderDOList = bmsPurchaseOrderDAO.select(purchaseOrderQuery);
        Assert.notEmpty(orderDOList);
        return orderDOList.get(0);

假设查询了订单的数据。然后再来一个manger查询订单明细的数据:

    @Override
    public List<BmsPurchaseOrderTransOutInfoDO> getTranOutInfoListByCode(String subscribeCode) {
        //查询子单明细
        BmsPurchaseOrderTransOutInfoDO transOutInfoQuery = new BmsPurchaseOrderTransOutInfoDO();
        transOutInfoQuery.setSubscribeCode(subscribeCode);
        List<BmsPurchaseOrderTransOutInfoDO> transOutInfoDOList =  bmsPurchaseOrderTransOutInfoDAO.select(transOutInfoQuery);
        Assert.notNull(transOutInfoDOList);
        return transOutInfoDOList;
    }

然后,在service对订单和明细进行拼接。

总而言之,分层如下:

web层(也就是webX中的screen):处理表单,调用服务,返回结果

service层:业务逻辑,数据拼接

manager层:粒度比较细的查询

dao层:sql语句映射

分层可能会导致代码量增大,但是可以提高程序的可复用性,方便程序维护。

总结

至此为止,关于webx服务的请求处理的来龙去脉简单的分析了一遍。了解大致处理流程还是比较有必要的,这样当我们的程序出现异常,可以及时的定位问题。当打断点时,面对层层的调用栈也不会显得不知所措。当然,在这里也是对自己经常接触到的技术做一个简单的沉淀。

网友评论

登录后评论
0/500
评论
弈扬
+ 关注