31
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Jenkinsコードリーディング - JDK自動インストールの謎を追え

Last updated at Posted at 2015-06-07

TL;DR

  • 自動インストールでインストールされるJDKのアーキテクチャは、各Nodeを登録するために起動するslave.jarのJREのアーキテクチャだよ
  • Jenkinsのソースコードは構造のバランスがいい
  • Jenkinsは週次リリースできるソフトウェアのアーキテクチャを学ぶ題材として最適

きっかけ

Jenkinsの便利な機能に、JDKの自動インストールがある。プロジェクトに利用するJDKを設定すれば、実行するノードに自動でJDKをインストールしてくれる、という素晴らしい機能だ。WindowsやLinuxだと32bit、64bitのJDKが配布されているので、どうやってインストールするJDKを判定してるんだろう、と思ってコードを読み始めた。

ゴール

  • 自動インストールを実装しているクラスを見つけ、どのように実装されているか理解すること

実装クラスを見つける

実装を見つけるときは、ログを出力して実際にその操作をすると、どのクラスで実行されているかすぐにわかる。ただ、今回はテスト用のJenkinsを立ち上げるのは面倒だったので、ソースコードリポジトリの中を見てみることにした。

JenkinsのリポジトリはGitHubで公開されている。リポジトリが公開されているので、リポジトリの中を手当たり次第パッケージをたどってパッケージから全体の構造を推測してみようと試みた。しかし、hudson時代のパッケージもあったりしてどこを見たらいいのかわからない。hudson時代のパッケージが残るのは、APIの互換性維持のためだと思うので、仕方ないことだとおもう。

あきらめて、JDKのインストールなので、JDKでリポジトリを検索してみた。するとhudson.tools.JDKInstallerというそのものズバリが見つかった。

関連するクラスとか見ると、親クラスがToolInstaller とかなので、このクラスだろう。

で、どんな実装だった?

インストール処理をしている部分を抜き出す。

    public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
...
            Platform p = Platform.of(node);
            URL url = locate(log, p, CPU.of(node));

            FilePath file = expectedLocation.child(p.bundleFileName);
            file.copyFrom(url);

NodeのPlatformとCPUアーキテクチャを調べ、そのURLを取得し、ダウンロードする。では、このURLを計算するlocate()では何をやっているのだろう?

        JDKFamilyList families = JDKList.all().get(JDKList.class).toList();
        if (families.isEmpty())
            throw new IOException("JDK data is empty.");

        JDKRelease release = families.getRelease(id);
        if (release==null)
            throw new IOException("Unable to find JDK with ID="+id);

        JDKFile primary=null,secondary=null;
        for (JDKFile f : release.files) {
            String vcap = f.name.toUpperCase(Locale.ENGLISH);

            // JDK files have either 'windows', 'linux', or 'solaris' in its name, so that allows us to throw
            // away unapplicable stuff right away
            if(!platform.is(vcap))
                continue;

            switch (cpu.accept(vcap)) {
            case PRIMARY:   primary = f;break;
            case SECONDARY: secondary=f;break;
            case UNACCEPTABLE:  break;
            }
        }

JDKListからJDKFileを取り出し、JDKFile毎にPlatformとCPUがOKとするかチェックしている。CPU#accept()PRIMARYSECONDARYがあるのは、64bitアーキテクチャのJREが動いているNodeであれば、32bitでもOKだからだろう。このJDKListはどうやって取得しているのか?

    public static final class JDKList extends Downloadable
        public JDKList() {
            super(JDKInstaller.class);
        }

        public JDKFamilyList toList() throws IOException {
            JSONObject d = getData();
            if(d==null) return new JDKFamilyList();
            return (JDKFamilyList)JSONObject.toBean(d,JDKFamilyList.class);
        }
    }

Downloadableってなんだろう?import文を見てみると、

import hudson.model.DownloadService.Downloadable;

となっているので、DownloadableDownloadServiceの中に定義されたクラスのようだ。DownloadServiceを検索し、Downloadableの定義周辺を見てみる。

    /**
     * Represents a periodically updated JSON data file obtained from a remote URL.
     *
     * <p>
     * This mechanism is one of the basis of the update center, which involves fetching
     * up-to-date data file.
     *
     * @since 1.305
     */
    public static class Downloadable implements ExtensionPoint {
    ...
        /**
         *
         * @param url
         *      URL relative to {@link UpdateCenter#getDefaultBaseUrl()}.
         *      So if this string is "foo.json", the ultimate URL will be
         *      something like "http://updates.jenkins-ci.org/updates/foo.json"
         *
         *      For security and privacy reasons, we don't allow the retrieval
         *      from random locations.
         */
        public Downloadable(String id, String url, long interval) {
            this.id = id;
            this.url = url;
            this.interval = interval;
        }

        public Downloadable() {
            this.id = getClass().getName().replace('$','.');
            this.url = this.id+".json";
            this.interval = DEFAULT_INTERVAL;
        }
        
        ...
        public Downloadable(Class id) {
            this(id.getName().replace('$','.'));
        }
        ...
        public String getUrl() {
            return Jenkins.getInstance().getUpdateCenter().getDefaultBaseUrl()+"updates/"+url;
        }        
    }

JavaDocにちゃんと書いてある。DownloadableのパラメータurlURL relative to {@link UpdateCenter#getDefaultBaseUrl()}.と書かれている。urlはUpdateCenter#getDefaultBaseUrl()からの相対パスです。もしfoo.jsonという文字列であれば、アクセスするURLはhttp://updates.jenkins-ci.org/updates/foo.jsonのようになる。

さっきのJDKListのコンストラクタはthis(JDKInstaller.class)なので、下記のURLからJSONを取得する。

JSONP形式のJSONが取得できるはずだ。

Q.UpdateCenter#getDefaultBaseUrl()ってほんとにhttp://updates.jenkins-ci.org/なの?

そういう時はJenkinsのスクリプトコンソールを開いて試してみればいい。スクリプトコンソールはJenkinsの管理を開こう。Groovyはスクリプト環境はJavaのコードを解釈できる。UpdateCenterはJenkinsのインスタンスから取得する。下記のように書くといい。

Kobito.gJ6ZXz.png

JDKInstallerの中のドメインモデル

ところで、JDKInstallerの中でPlatformCPUというクラスが定義されている。Jenkinsはクラスの定義がモデルと対応していて、感銘を受けた。JDKInstaller#JDKListの周辺クラスは下記のとおりだ。

Kobito.XFpVPe.png

個々のクラスが小さいとファイル管理が大変になる。しかし、ネストクラスとして定義することでファイル管理の大変さを解決しつつ、概念をうまくモデル化し、コードに落としこんでいる。

そういう意味でも大変勉強になった。

31
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?