jenkins源码阅读:启动和请求处理
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 的启动,和处理请求的过程,已经清楚。总结一下,主要是下面这几步。
- 启动 winstone 容器,加载 jenkins。
- Stapler 负责处理请求。
- 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 的基本用法,可以查看下面这几篇文档。