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