Learn Jenkins the hard way (2) - Stapler与Jelly

前言

在上篇文章中,我们讨论了如何调试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中模块的机制,组件的渲染方式以及异步请求的实现进行分析。

时间: 2024-11-01 05:20:23

Learn Jenkins the hard way (2) - Stapler与Jelly的相关文章

Learn Jenkins the hard way (1) - 从源码运行Jenkins开始

前言 在上一篇文章中,总结了Jenkins的罪与罚.从本文开始,我们将迈入Jenkins的源码学习部分.学习源码的方式有很多种,对于Jenkins而言,网上关于源码的学习非常有限,比较建议大家先阅读官方关于如何成为contributor的文档,了解大体的结构后再逐步深入. 从源码本地运行Jenkins 学习任何源码前,首先要做的事情是将源码跑起来.克隆Jenkins的源码: git clone https://github.com/jenkinsci/jenkins.git //可以切换到当前的

Learn Jenkins the hard way (3) - Jenkins的存储模型

前言 在上篇文章中我们主要讲解了Jenkins的页面与路由,在本章中我们要讲解下Jenkins的数据持久化机制.在Jenkins中数据的持久化是通过文件进行存储的,大家平时使用Hibernate进行持久化的时候,我们只需要关心哪些地方是需要存储的,哪些位置是不需要储存的,并且在不需要存储的位置添加transient关键字即可,持久化的框架会自动帮我做好Java Object与数据库存储之间的序列化与反序列化的过程,而在Jenkins中由于数据的存储都是通过文件的方式进行存储的,有必要让大家了解下

Jenkins知识地图

转自:http://blog.csdn.net/feiniao1221/article/details/10259449    这篇文章大概写于三个月前,当时写了个大纲列表,但是在CSDN上传资源实在不方便,有时上传了莫名审核不通过,如果以前有人上传过,也会导致上传失败.现在把之前工作中找到的好东西和各位分享.现在不搞这些了,也算是个归档吧.内容主要涉及Hudson/Jenkins的使用,维护,以及插件开发,开发的东西更多些吧. 首先说下Jenkins能干什么?说下两个典型的应用场景. 1. G

【转载】Jenkins + Git + Maven + tomcat集成环境搭建

本文转载自http://shift-alt-ctrl.iteye.com/blog/2208786   折腾了好几天,终于吧Jenkins + Git + Maven + tomcat集成环境搭建起来了,最终主要实现"自动构建.部署"web应用.   1.安装环境     操作系统:Centos 6.5     JDK:1.7.x     Maven:3.1.x     Git: 1.7.1,自建GitLab平台     tomcat:7.x       上述宿主机器2台:192.16

持续集成windows下jenkins的常见问题

所谓主从,主要应用的场景例如多种环境(windows/linux,.net/java/php)需要不同的构建基础,而我们又不想都将一系列的步骤和环境混杂在一台构建服务器上,所以类似于go中的代理,jenkins也提供了slave节点的概念,大家可以把不同类别的项目的构建部署在分类的节点服务器上.节点服务器不需要安装完整的jenkins包,构建事件的分发由master端来执行. 这里需要注意的就是主从节点之间的通信,我这里选择是将从节点以windows service的方式启动,而我碰到的坑就是环

Learn WCF (3)--开发WCF客户程序

前篇Learn WCF (2)--开发WCF服务介绍了开发服务程序,这篇开发一个客户程序,主要有三种方案: 添加一个Web引用 使用svcutil.exe工具 编程方案 1.添加一个Web引用 这个和添加引用Web服务的方法基本一致,在添加引用的对话框中输入URL:http://localhost:39113/WCFServiceText/WCFStudentText.svc 为WCF起个名字,点击添加引用按钮将会完成如下的任务: (1)从指定的URL为学生管理服务下载WSDL文件 (2)生成代

远程部署-使用jenkins发布maven项目到远程的jetty容器中

问题描述 使用jenkins发布maven项目到远程的jetty容器中 27C 如题(就这点币了)使用jenkins发布maven项目到远程的jetty容器中 解决方案 jenkins只是打包吧! 还能远程部署到jetty里吗? 这个没试过还真不知道.

一步一步用jenkins,ansible,supervisor打造一个web构建发布系统

新blog地址:http://hengyunabc.github.io/deploy-system-build-with-jenkins-ansible-supervisor/ 一步一步用jenkins,ansible,supervisor打造一个web构建发布系统. 本来应该还有gitlab这一环节的,但是感觉加上,内容会增加很多.所以直接用github上的spring-mvc-showcase项目来做演示. https://github.com/spring-projects/spring-

Jenkins持久化集成使用

1.概述 Jenkins是基于Java开发的一种持续集成工具,用于监控持续重复的工作,功能包括: 持续的软件版本发布/测试项目 监控外部调用执行的工作 2.搭建 2.1环境准备 首先我们要准备搭建的环境,配置如下: 操作系统:CentOS 6.x JDK版本:JDK1.7 2.2安装Jenkins 执行如下命令: sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo sud