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

    xiaoxiao2026-06-16  12

    前言

    在上篇文章中,我们讨论了如何调试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' ? '
    转载请注明原文地址: https://yun.8miu.com/read-148090.html
    最新回复(0)