写在前面的话
写这篇文章的目的,主要还是站在一个新人的角度做一些沉淀,同样也为了方便后面的人快速熟悉集团web开发常用技术。这篇文章,将分析一个请求发到服务端,经tomcat容器、webx映射直到最后调用controller入口的过程,对java web基础知识结合webx做一个整体回顾。同时,也将简单分析集团web的基本分层方式,以及VO、DTO、DO等数据实例在各层所起的作用。
Web容器
理解web容器,也是理解我们程序的运行平台,同时也是了解servlet处理流程的基础所在。tomcat的主要结构如下图:
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注解的参数。最终的整体调用流程如下图:
项目分层
关于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服务的请求处理的来龙去脉简单的分析了一遍。了解大致处理流程还是比较有必要的,这样当我们的程序出现异常,可以及时的定位问题。当打断点时,面对层层的调用栈也不会显得不知所措。当然,在这里也是对自己经常接触到的技术做一个简单的沉淀。