前言
在上篇文章中,我们讨论了如何调试Jenkins的页面。今天我们将开始研究研究Jenkins的页面与路由机制。因为Stapler与Jelly的内容比较繁琐和复杂,暂定通过两篇文章来探讨。
Jelly与Stapler
Jelly 是一种基于Java 技术和 XML 的脚本编制和处理引擎。Jelly 的特点是有许多基于JSTL (JSP 标准标记库,JSP Standard Tag Library)、Ant、Velocity 及其它众多工具的可执行标记。Jelly 还支持 Jexl(Java 表达式语言,Java Expression Language,Jexl 是JSTL表达式语言的扩展版本。
Stapler 是一个将应用程序对象和 URL 装订在一起的 lib 库,使编写web应用程序更加方便。Stapler 的核心思想是自动为应用程序对象绑定 URL,并创建直观的 URL 层次结构。
上面两段并没有没有什么实际的意义,唯一需要的知道的是下面的一句话,Jelly与Stapler都是由Kohsuke Kawaguchi开发,而这个大哥就是Jenkins的maintainer。换言之,除了Jenkins基本没有什么其他的项目在使用Jelly与Stapler,这也增加了大家学习Jenkins的难度。在上篇文章中,我们初步的学习了一下Stapler的原理,下面我们用一个实例的来剖析它。
基础知识
Jelly文件的位置是一种约定大于配置的方式,类可以在Resources下的同名位置创建Jelly目录,并可以使用该目录下的资源。Jelly文件直接绑定到了对应的类上,即Jelly页面可以调用类中的函数。为了引用Jelly页面绑定的类,Jelly文件使用it对象代表当前绑定的类。app对象表示Jekins实例,instance表示正在被配置的对象,descriptor表示对应于instance的Descriptor对象,h表示hudson.Functions的实例。更具体的内容可以参考官方文档Jelly docs
首页渲染剖析
先在本地通过源码的方式将Jenkins跑起来,打开浏览器输入 localhost:8080/jenkins/ 可以看到如下的页面
按照上一篇文章学习到的知识,hudson.model.Hudson是绑定在路由根对象(/)的,而Jenkins本身设置的WebAppContext是jenkins。因此此时Hudson的路由根对象即为(/jenkins)。下面分析下这个页面是如何渲染的,打开hudson.model.Hudson文件,可以发现Hudson对象大部分的方法都被注解标注为了Deprecated.这表明在Hudson逐渐迁移到Jenkins的时候,为了API的兼容性,保留了原来Hudson的接口,而将主体的功能都移到了jenkins.model.Jenkins中。
public class Hudson extends Jenkins {
//some Deprecated functions
}
Stapler会将对象绑定到路由,而对象的方法转变为处理的action,那么对于首页而言,他的action为空,在这种场景下Jenkins是如何处理的呢。这就需要向上来查找Hudson的父类来继续探索,下面是Hudson对象的继承实现关系。
在Hudson的父类Jenkins中实现了StaplerProxy,StaplerFallback两个接口
public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLevelItemGroup, StaplerProxy, StaplerFallback,
ModifiableViewGroup, AccessControlled, DescriptorByNameOwner,
ModelObjectWithContextMenu, ModelObjectWithChildren, OnMaster {
// something else
}
在此需要重点讲解下StaplerProxy和StaplerFallback的作用。
public interface StaplerProxy {
/**
* Returns the object that is responsible for processing web requests.
*
* @return
* If null is returned, it generates 404.
* If {@code this} object is returned, no further
* {@link StaplerProxy} look-up is done and {@code this} object
* processes the request.
*/
Object getTarget();
}
public interface StaplerFallback {
/**
* Returns the object that is further searched for processing web requests.
*
* @return
* If null or {@code this} is returned, stapler behaves as if the object
* didn't implement this interface (which means the request processing
* failes with 404.)
*/
Object getStaplerFallback();
}
上面是这两个接口的定义,这两个接口是Stapler中非常重要的部分,在Jenkins的路由机制中,通常发现找不到对应的Jelly或者路径比较奇怪的时候,都是因为StaplerProxy与StaplerFallback的作用,在Kohsuke的文档中是这样描述的:
- If an object delegates all its UI processing to another object, it can implement this interface and return the designated object from the getTarget() method.
Compared to StaplerFallback, stapler handles this interface at the very beginning, whereas StaplerFallback is handled at the very end.
By returning this from the getTarget() method, StaplerProxy can be also used just as an interception hook (for example to perform authorization.)
- An object can fall back to another object for a part of its UI processing, by implementing this interface and designating another object from getStaplerFallback().
Compared to StaplerProxy, stapler handles this interface at the very end, whereas StaplerProxy is handled at the very beginning.
简而言之,StaplerProxy是在路由处理之前将一个路由转发或者代理给另一个对象,而StaplerFallback是在路由处理之后进行处理。在Jenkins对象中既实现了StaplerProxy也实现了StaplerFallback,下面是Jenkins实现的源码
public Object getTarget() {
try {
checkPermission(READ);
} catch (AccessDeniedException e) {
String rest = Stapler.getCurrentRequest().getRestOfPath();
for (String name : ALWAYS_READABLE_PATHS) {
if (rest.startsWith(name)) {
return this;
}
}
for (String name : getUnprotectedRootActions()) {
if (rest.startsWith("/" + name + "/") || rest.equals("/" + name)) {
return this;
}
}
// TODO SlaveComputer.doSlaveAgentJnlp; there should be an annotation to request unprotected access
if (rest.matches("/computer/[^/]+/slave-agent[.]jnlp")
&& "true".equals(Stapler.getCurrentRequest().getParameter("encrypt"))) {
return this;
}
throw e;
}
return this;
}
public View getStaplerFallback() {
return getPrimaryView();
}
那么根据上面的代码可知,链路是这样的,(/jenkins/)传递到Hudson,Hudson继承了Jenkins并且实现了StaplerProxy和StaplerFallback,首先将Stapler拦截到路由,然后将路由的对象代理到对象本身,然后没有与之对应的action处理,最后由StaplerFallback进行处理,然后渲染了getPrimaryView()。按照Stapler的原理,这个getPrimaryView()应该返回的是一个对象,然后在这个对象对应的resource中会存在一个index.jelly或者被再次转发,进一步追踪源码。
// jenkins.model.Jenkins
public View getPrimaryView() {
return viewGroupMixIn.getPrimaryView();
}
// hudson.model.ViewGroupMixIn
@Exported
public View getPrimaryView() {
View v = getView(primaryView());
if(v==null) // fallback
v = views().get(0);
return v;
}
也就是说最终返回的是一个Hudson.model.View对象,因此最终我们在Reources下的Hudson.model.View找到了index.jelly,这个Jelly文件就是Hudson的首页渲染模板,如下:
<?jelly escape-by-default='true'?>
<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${it.class.name=='hudson.model.AllView' ? '%Dashboard' : it.viewName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}" norefresh="${!it.automaticRefreshEnabled}">
<j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
<st:include page="sidepanel.jelly" />
<l:main-panel>
<st:include page="view-index-top.jelly" it="${it.owner}" optional="true">
<!-- allow the owner to take over the top section, but we also need the default to be backward compatible -->
<div id="view-message">
<div id="systemmessage">
<j:out value="${app.systemMessage!=null ? app.markupFormatter.translate(app.systemMessage) : ''}" />
</div>
<t:editableDescription permission="${it.CONFIGURE}"/>
</div>
</st:include>
<j:set var="items" value="${it.items}"/>
<st:include page="main.jelly" />
</l:main-panel>
<l:header>
<!-- for screen resolution detection -->
<l:yui module="cookie" />
<script>
YAHOO.util.Cookie.set("screenResolution", screen.width+"x"+screen.height);
</script>
</l:header>
</l:layout>
</st:compress>
再次需要首先掌握几个重要的知识 xmlns:j, xmlns:st 这些代表着不同的命名空间,比如xmlns:l="/lib/layout"表示l代表/lib/layout空间,同理st代表jelly:stapler空间,那么 按照字面意思来讲就是要加载一个模板到这个Jelly中,但是这个文档的位置,是由namespace决定的,st表示的是jelly:stapler命名空间,那么就表示需要根据当前的路由绑定对象的namespace进行查找,在本例中即为同名路径下的sidepannel.jelly.而表示需要到t的命名空间即/lib/hudson下查找editableDescription.jelly,来扩展这个标签,即使用Resources/lib/hudson/editableDescription.jelly来替换当前的标签。
最后
Jenkins的Stapler与Jelly机制本质上还是使用了传统的模板机制,通过约定大于配置的方式将页面和模型进行绑定,将路由和方法结合。这种方式也是Jenkins一种内置的机制,在后面的文章中,我们会发现Jenkins在内存中维持了一个巨大的对象,而这个对象在Jenkins的整个生命周期中负责了巨大的职责。本篇文章剖析了一下Jenkins的首页的渲染,通过管中窥豹的方式先尝试了,在下一篇文章中,Jenkins中模块的机制,组件的渲染方式以及异步请求的实现进行分析。