myzhan

jenkins源码阅读:启动和请求处理

· myzhan

jenkins 编译后,会生成一个 jenkins.war 文件,阅读 jenkins 源码,我们需要研究的一切,都在这个文件里面了。按照习惯,先寻找入口点,然后顺藤摸瓜,才能进入源码树中。

如果 jenkins 独立运行,启动命令是:

$ java -jar jenkins.war

所以 jenkins.war 其实是一个普通的 jar 包。java 命令运行一个 jar 文件,会在 jenkens.war/META-INF/ 目录下,找 MANIFEST.MF 文件。这个文件里面有入口类,在这里,是 Main,即是 jenkins.war/Main.class 文件。

jenkins.war/META-INF/MANIFEST.MF

$ cat MANIFEST.MF
Manifest-Version: 1.0
Jenkins-Version: 2.13-SNAPSHOT
Implementation-Version: 2.13-SNAPSHOT
Built-By: myzhan
Build-Jdk: 1.7.0_79
Hudson-Version: 1.395
Created-By: Apache Maven 3.3.3
Main-Class: Main
Archiver-Version: Plexus Archiver

反编译一下 Main.class 文件,一个标准的 Main 文件,属于 default 包。但是这个 Main 文件,并不在 jenkins 的源码里面,即不在 jenkins core 里面。过了一遍反编译的源码,发现它先将 jenkins.war 解压到 $JENKINS_HOME/war 目录下,再启动一个 winstone 容器。鉴于jenkins 本身是一个 Servlet 程序,我猜测,在这个入口,是先启动一个 Servlet 容器,再加载 jenkins 本身。

通过查看 $JENKINS_SRC/war/pom.xml 文件,发现这个 Main 文件,在 org.jenkins-ci:executable-war 这个依赖中,验证了我的猜测。

既然是一个 Servlet 程序,那我们再去 $JENKINS_HOME/war/WEB-INF/web.xml 找入口点。与 Servlet 有关的配置,在这里。

$JENKINS_HOME/war/WEB-INF/web.xml

<servlet>
    <servlet-name>Stapler</servlet-name>
    <servlet-class>org.kohsuke.stapler.Stapler</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Stapler</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

在这里,jenkins 将所有 URL 请求的处理,都交给了 Stapler。根据 Stapler 的文档,了解到它提供了 URL 到对象的映射。即所有请求,都先交给 Stapler 处理,然后 Stapler 通过反射的方式,找到对应的处理类及方法。

顺着 web.xml 往下看,终于找到了与 jenkins core 有关的配置。

$JENKINS_HOME/war/WEB-INF/web.xml

<listener>
    <listener-class>hudson.WebAppMain</listener-class>
</listener>

WebAppMain.java 文件,在 $JENKINS_SRC/core/src/main/java/hudson 目录下面,文件开头的注释,解释了它的身份。

$JENKINS_SRC/core/src/main/java/hudson/WebAppMain.java

/**
* Entry point when Hudson is used as a webapp.
*
* @author Kohsuke Kawaguchi
*/

这个类的代码大约 400 行,大多是一些初始化工作,下面这段代码引起了我的注意。

$JENKINS_SRC/core/src/main/java/hudson/WebAppMain.java

context.setAttribute(APP, new HudsonIsLoading());
initThread = new Thread("Jenkins initialization thread") {
    @Override
    public void run() {
        Jenkins instance = new Hudson(_home, context);
        context.setAttribute(APP, instance);
    }
};
initThread.start();

熟悉 jenkins 的同学,到了这里有没有一点似曾相识的感觉? jenkins 在刚启动的时候,会显示一个 loading 页面,启动完成后,才切到主页,就是靠这段代码实现的,HudsonIsLoading 就是 loading 页面,Hudson 就是主页。

现在看来 context.setAttribute(APP, xxxx) 这个方法调用,能把 xxxx 对象挂到 “/” 这个 URL 下面。不过 context.setAttribute 方法,在 Servlet 编程里面,只是保存了一个变量而已,为什么能挂到 “/” 下面呢?答案一定在 Stapler 的源码里!从 web.xml 配置我们可以知道,入口在 org.kohsuke.stapler.Stapler。继续查看 Stapler 的源码,我找到了答案。

/org/kohsuke/stapler/Stapler.java

protected @Override void service(HttpServletRequest req, HttpServletResponse rsp) {
    Object root = webApp.getApp();
    if(root==null)
        throw new ServletException("there's no \"app\" attribute in the application context.");
    invoke( req, rsp, root, servletPath);
}

/org/kohsuke/stapler/WebApp.java

/**
* Returns the 'app' object, which is the user-specified object that
* sits at the root of the URL hierarchy and handles the request to '/'.
*/
public Object getApp() {
    return context.getAttribute("app");
}

跟踪到这里,jenkins 的启动,和处理请求的过程,已经清楚。总结一下,主要是下面这几步。

  1. 启动 winstone 容器,加载 jenkins。
  2. Stapler 负责处理请求。
  3. WebAppMain 负责初始化。

最后,写一个 Hello World 庆祝一下,在 Jenkins.java 中,加入以下代码。

$JENKINS_SRC/core/src/main/java/jenkins/model/Jenkins.java

public void doHelloWorld( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
    rsp.getWriter().println("Hello World");
}

重新编译生成 jenkins.war,运行后用浏览器打开 http://localhost:8080/helloWorld 即可看到 Hello World。

注意这里的 URL 是大小写敏感的。

如果想了解 Stapler 的基本用法,可以查看下面这几篇文档。