動機
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一個流程或者說是大概,很多細節我也沒細說,不過我相信大流程搞明白之後,一些小細節上的東西自己在慢慢研究也是沒問題的。