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

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: ## 前言 在上篇文章中,我们讨论了如何调试Jenkins的页面。今天我们将开始研究研究Jenkins的页面与路由机制。因为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/ 可以看到如下的页面
d5299579225a189cdcc50a076342293a.png
按照上一篇文章学习到的知识,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对象的继承实现关系。

dfb2d11c77d59e7fc06c85af738cfb41.png

在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中模块的机制,组件的渲染方式以及异步请求的实现进行分析。

目录
相关文章
|
jenkins Java 持续交付
Learn Jenkins the hard way (1) - 从源码运行Jenkins开始
## 前言 在上一篇文章中,总结了Jenkins的罪与罚。从本文开始,我们将迈入Jenkins的源码学习部分。学习源码的方式有很多种,对于Jenkins而言,网上关于源码的学习非常有限,比较建议大家先阅读官方关于如何成为contributor的文档,了解大体的结构后再逐步深入。
7942 0
|
存储 Java jenkins
Learn Jenkins the hard way (3) - Jenkins的存储模型
一篇文章来看Jenkins的存储模型并讨论高可用的可行性。
4737 0
|
存储 jenkins 持续交付
Learn Jenkins the hard way (0) - Jenkins的罪与罚
## 写在开头 Jenkins是非常流行的开源的持续交付平台,拥有丰富的插件、文档与良好的社区。是国内大多数公司私有持续集成方案CI/CD服务器的选型。开发者可以快速的通过Jenkins搭架符合自己业务场景的pipeline,结合大量的开源插件,可以轻松的满足不同语言、不同平台、不同框架的持续集成场景。
7865 0
|
13天前
|
jenkins Devops Java
DevOps实践:Jenkins在持续集成与持续部署中的价值
【10月更文挑战第27天】在快速发展的软件开发领域,DevOps实践日益重要。Jenkins作为一款流行的开源自动化服务器,在持续集成(CI)和持续部署(CD)中扮演关键角色。本文通过案例分析,探讨Jenkins在Java项目中的应用,展示其自动化构建、测试和部署的能力,提高开发效率和软件质量。
34 2
|
3月前
|
jenkins 持续交付 开发者
自动化部署:使用Jenkins和Docker实现持续集成与交付
【8月更文挑战第31天】本文旨在为读者揭示如何通过Jenkins和Docker实现自动化部署,从而加速软件开发流程。我们将从基础概念讲起,逐步深入到实际操作,确保即使是初学者也能跟上步伐。文章将提供详细的步骤说明和代码示例,帮助读者理解并应用这些工具来优化他们的工作流程。
|
1天前
|
运维 jenkins Java
Jenkins在持续集成与持续部署中的价值
Jenkins在持续集成与持续部署中的价值
|
14天前
|
jenkins Devops 测试技术
DevOps实践:Jenkins在持续集成与持续部署中的价值
【10月更文挑战第26天】随着DevOps理念的普及,Jenkins作为一款开源自动化服务器,在持续集成(CI)与持续部署(CD)中发挥重要作用。本文通过某中型互联网企业的实际案例,展示了Jenkins如何通过自动化构建、持续集成和持续部署,显著提升开发效率、代码质量和软件交付速度,帮助企业解决传统手工操作带来的低效和错误问题。
40 4
|
1月前
|
jenkins Shell 持续交付
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(二)
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(二)
67 0
|
1月前
|
jenkins Shell 持续交付
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(一)
Jenkins持续集成GitLab项目 GitLab提交分支后触发Jenkis任务 持续集成 CI/CD 超级详细 超多图(一)
128 0
|
3月前
|
持续交付 jenkins Devops
WPF与DevOps的完美邂逅:从Jenkins配置到自动化部署,全流程解析持续集成与持续交付的最佳实践
【8月更文挑战第31天】WPF与DevOps的结合开启了软件生命周期管理的新篇章。通过Jenkins等CI/CD工具,实现从代码提交到自动构建、测试及部署的全流程自动化。本文详细介绍了如何配置Jenkins来管理WPF项目的构建任务,确保每次代码提交都能触发自动化流程,提升开发效率和代码质量。这一方法不仅简化了开发流程,还加强了团队协作,是WPF开发者拥抱DevOps文化的理想指南。
82 1