结合提供者模式解析Jenkins源码国际化的实现

关键字:提供者模式,设计模式,github,gerrit,源码学习,jenkins,国际化,maven高级,maven插件

本篇文章的源码展示部分由于长度问题不会全部粘贴展示,或许只是直接提及,需要了解的朋友请fork in github,文中会给出源码地址。

源码的研究策略

从这篇文章开始,陆续要展开一些源码分析的内容,既然确立了这个目标,就要寻找研究源码的策略,经过各方面的取经和自己的总结,接下来我将采取的策略为:

  1. 源码内容:
    • 从最早的release版本开始,任何伟大而复杂的工程可能都源自于“helloworld”,从最初的版本(如果你能找到的话)开始看,可能会降低很多难度,随着工程的不断升级,根据release历史,可以跟踪到每次大的升级更新内容。
  2. 源码库:
    • 采用github。作为世界最大的源码库,github使用非常方便,并且我也在上面有很多自己的repo。可以直接fork官方源码然后加入自己的调试研究过程,可以记录下每一次的更新与变化,我想这也是github除了保存自己的代码以外最为重要的功能之一。
  3. 程序入口:
    • 本地工程运行,按图索骥,编译调试,理解设计模式的一些常见命名方式。
  4. 分析架构:
    • 结合官方手册(注意要与当前源码release版本相一致)get started, API,使用UML,分析核心功能模块的实现。
  5. 造轮子:
    • 修改源码工程,添加自己的注释,增加自己的代码,以支持模拟业务场景。同时要保存自己的github提交历史,这也是学习过程的记录。

在以下文章分析过程中,我会通过这种格式来记录每一个我突发奇想的可以用来实验Jenkins源码的业务需求,这些需求会在未来继续研究源码的文章中进行实现。

搭建源码开发环境

一、github本地配置修改

由于本地存在其他git库的配置并且他们集成了gerrit,所以如果我想在本地配置一套github的开发环境,必须要做些改变。如果你的机器是纯净的,大可不必有此顾虑,直接配置成全局变量即可。

git配置文件

git的默认配置是在用户home目录下的.gitconfig文件,这个文件我是不可以修改的,否则会影响现有库的使用。而在每个git工程中还有.git目录,这下面的config就是该项目的本地Git配置,相当于复写home目录下的.gitconfig文件,home目录下的对应的是global配置,项目本地的对应的是local配置。

gerrit

  • 代码审核服务器,一种免费、开放源代码的代码审查软件,使用网页界面。
  • 同一个团队的软件程序员,可以相互审阅彼此修改后的程序代码,决定是否能够提交,退回或者继续修改。
  • 通过钩子hooks/commit-msg程序,将每次推送的提交附属上唯一的Change-Id,从而转化为一个一个的代码审核任务。
  • 代码审核工作流,完全在网页上面操作,其中涉及到comments,Code-Review,Verified,submit,merge等操作。
  • gerrit同时也是一个git的版本库,一般用于维护项目的主干分支,各开发者可以将本地库与其进行pull,merge等操作。

本地git配置文件修改

1.删除hooks

目标确定为git工程下的.git目录,首先删除其中的hooks文件夹(hooks默认为空,如果安装了gerrit,每次clone时会同步下载hooks/commit-msg钩子程序),要知道gerrit的集成主要就靠这个钩子,这个钩子的作用就是每次在你提交代码时,默认附属上一串Change-Id,这样一来就将你的每一次提交建立了一个主键,通过这个主键去review,merge等

2.配置用户名和邮件

在git工程下直接配置上git config user. name 和user.email即可使用当前配置而不是用户目录下的.gitconfig的默认配置。请参考Setting your username in Git

3.git提供ssh和http两种交互方式

这里采用http的方式,它可以绕过防火墙和网络代理,很方便,但是每次与远端库交互的时候都要验证账户和密码。请参考remote url method

  • ssh

ssh的方式要在远端库中配置上本地的id_rsa.pub,从而实现免密认证。

  • http

http的话,直接使用credential.helper store来存储用户名密码,可避免日后必须始终输入账号密码的麻烦。具体操作如下:

evsward@lwbsPC:~/work/github/mainbase$ git config credential.helper store
evsward@lwbsPC:~/work/github/mainbase$ git push origin master 
Username for 'https://github.com': evsward
Password for 'https://evsward@github.com': 
Everything up-to-date
evsward@lwbsPC:~/work/github/mainbase$ git push origin master 
Everything up-to-date

http修改存储密码的方式以上方式会在根目录下建立一个.git-credentials的文件明文存储密码。虽然可以指定该文件的访问权限,我仍然觉得很不安全,所以采用另外一种方式——存储于缓存。请参考Caching your GitHub password in Git,延长默认缓存时间从15分钟改为1小时。如下方式执行以后,会在用户根目录下生成一个文件夹.git-credential-cache,里面存储一个socket的设备文件,用于缓存用户名密码,通常手段无法读取这个文件,采取缓存用户名密码的方式比起上一种直接存储的方式要安全一些。(注意:当你的系统仍需连接其他git库的时候,参数不要使用global,全部设置为local即默认)另外,同一个github下的不同项目只要存储过一次账号密码以后,任何项目在其本地执行

git config credential.helper 'cache --timeout=3600'

不必初始化存入密码,即可立即免密使用,因为同一个github账户下的项目访问时的账户密码是相同的,默认都是从用户根目录下的.git-credential-cache去读取,因此,同一个github账户初始化过程只需要一次即可。当然了,超过了我们设定的缓存时限1个小时,就需要重新输入了。下面是具体操作方式:

evsward@lwbsPC:~/work/github/mainbase$ git config credential.helper 'cache --timeout=3600'
evsward@lwbsPC:~/work/github/mainbase$ git push origin master 
Username for 'https://github.com': evsward
Password for 'https://evsward@github.com': 
Everything up-to-date
evsward@lwbsPC:~/work/github/mainbase$ git push origin master 
Everything up-to-date   

git修改历史提交记录

一般来说是直接reset + commitId,然后git push -f <remote> <branch>到远程库直接删除commitId以后的所有提交历史,请参考git如何修改已提交的commit

二、Jenkins项目源码

1.首先fork Jenkins源码到自己的账户,并下载到本地。
2.同步更新,Configuring a remote for a fork -> Syncing a fork

Jenkins 业务构想之一:监控Jenkins 源代码,如果有任何更新,则fetch到本地,然后同步推送至我的github库。

3.开始检查jenkins 的release版本,找到第一个发布在github上的release版本1.312,可惜的是这个历史版本因为太古老只留下了zip的下载方式,直接下载下来,jenkins-1.312.zip。
4.github网页端新建一个repo起名为jenkins-1.312,将这个空项目clone到本地,然后导入前面下载的jenkins-1.312.zip解压出来的文件。
5.注意新clone下来的github项目一定要先删除hooks,配置好user. name,email以及credential,然后push到github远端。
6.eclipse通过检测pom文件将jenkins1.312以maven项目导入。

三、Maven构建源码工程

本文就细细地将研究过程中遇到的所有可记录的知识点都写下来。

配置Maven

1.去Maven下载一个zip包,我下载的是Maven3.5.2
2.解压缩,打开conf/setting.xml,修改localRepository到你预设的本地Maven资源库。
3.修改mirror,添加阿里云maven库

<mirrors>
    <mirror>
      <id>nexus-aliyun</id>
      <mirrorOf>*</mirrorOf>
      <name>Nexus aliyun</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public</url>
    </mirror>
  </mirrors>

4.在eclipse中配置上刚刚下载并修改好的maven地址,同时别忘记更改user-setting。
5.linux下配置maven环境变量(Windows的配置这里不再赘述),在用户根目录下打开.profile,增加export MAVEN_HOME=/home/CORPUSERS/evsward/work/apache-maven-3.5.2,并将$MAVEN_HOME/bin添加到PATH中去。
6.terminal下输入mvn -v测试。

开始构建

eclipse中直接使用clean project来触发maven重构工程,但是发生错误,我们刚配置的阿里云的maven库似乎连接不上,我按图索骥,使用浏览器对该url路径进行了检查,确定了这个文件确实是存在于阿里云上面的。

下面我在terminal中,定位到项目路径下,使用命令去测试mvn install(安装artifacts,compile是编译工程代码,package是为现有工程打包并上传到maven库),错误仍旧是那样。所以目前的问题是浏览器可以访问,但是terminal和eclipse无法访问。

我又尝试了在terminal中直接wget,仍然是好使的,我将vpn配发的proxy路径配置到$MAVEN_HOME/conf/setting.xml中以后,开始工作了!

<proxies>
    <proxy>
      <id>A</id>
      <active>true</active>
      <protocol>http</protocol>
      <username>evsward</username>
      <password>xxxxxxx</password>
      <host>proxy.xxxx.net</host>
      <port>8080</port>
    </proxy>
    <proxy>
      <id>B</id>
      <active>true</active>
      <protocol>https</protocol>
      <username>evsward</username>
      <password>xxxxxxx</password>
      <host>proxy.xxxxxx.net</host>
      <port>8080</port>
    </proxy>
  </proxies>

阿里云的Maven库还是非常全的!

我们先terminal本地install一下,最终Maven安装artifacts结果如下:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] Hudson main module ................................. SUCCESS [04:48 min]
[INFO] Hudson remoting layer .............................. SUCCESS [01:58 min]
[INFO] Hudson CLI ......................................... SUCCESS [02:31 min]
[INFO] Hudson core ........................................ FAILURE [05:33 min]
[INFO] Hudson Maven PluginManager interceptor ............. SKIPPED
[INFO] Hudson Maven CLI agent ............................. SKIPPED
[INFO] Maven Integration plugin ........................... SKIPPED
[INFO] Hudson war ......................................... SKIPPED
[INFO] Test harness for Hudson and plugins ................ SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22:49 min
[INFO] Finished at: 2017-11-22T17:12:58+08:00
[INFO] Final Memory: 30M/164M
[INFO] ------------------------------------------------------------------------

总共耗时近23分钟,只有一项Hudson core编译失败了,其他均成功了。

Jenkins 业务构想之二:每次的源码更新,本地要自动执行mvn install去编译,这样就为我们真正的开发节省了很多时间。

现在去查看一下我们的repo目录:

evsward@lwbsPC:~/work/maven-repo$ ls
ant                       commons-digester    geronimo-spec  net
antlr                     commons-discovery   httpunit       org
aopalliance               commons-el          javanettasks   oro
args4j                    commons-fileupload  javax          plexus
asm                       commons-httpclient  jaxen          qdox
avalon-framework          commons-io          jdom           slide
backport-util-concurrent  commons-jelly       jfree          stax
ch                        commons-lang        jline          velocity
classworlds               commons-logging     jtidy          xalan
com                       commons-pool        junit          xerces
commons-beanutils         commons-validator   log4j          xml-apis
commons-cli               de                  logkit         xom
commons-codec             dom4j               mx4j           xpp3
commons-collections       doxia               nekohtml


evsward@lwbsPC:~/work/maven-repo$ du -sh
115M

可以看到,原来空空如也的本地repo已经被填入了115M的不同的依赖包,这些都是从之前我们配置的mirror——阿里云下载过来的。

下面我们转战到IDE,刷新一下项目,工程在Maven的帮助下自动进入安装阶段。

  • 失败一次

可惜最终还是没有build成功,报错信息显示有些依赖包在阿里云上面无法找到,看来阿里云还是不够全啊。

  • 失败二次

于是我将conf/setting.xml中的mirror内容注释掉了,重新运行mvn package从maven中央库下载,build又开始工作了!(之前加的mirror不是当时无法download的根源问题,根源问题已解决,是proxy的问题)

  • 失败N次

失败已经持续了10个小时,转去翻官方文档。

重新出发

由于没有依据官方文档,自己在摸索中构建导致了很多问题,无法顺利构建成功,这一次依据官方文档,Build Jenkins,我来尝试follow一下。第一个改变就是我们丢弃了jenkins-1.312版本,直接使用jenkin最新版本,这是因为最新版本的文档和代码都是非常齐全,适合我们分析与研究。嫌麻烦的同学不用担心,我会将所有的构建步骤贴在下面。
1.构建准备

使用jdk7+,maven3

2.环境变量配置

alias jdk7='export JAVA_HOME=/home/CORPUSERS/evsward/work/java/jdk1.7.0_80_x64 ; export PATH=$JAVA_HOME/bin:$PATH'

3.maven 基础构建

$ cd jenkins
$ mvn -Plight-test install

4.找到作者的github

If you want simply to have the jenkins.war as fast as possible (without test
execution), run:

    mvn clean install -pl war -am -DskipTests

The WAR file will be in war/target/jenkins.war (you can play with it)
You can deactivate test-harness execution with -Dskip-test-harness

最终,terminal build成功!

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] Jenkins main module ................................ SUCCESS [  0.888 s]
[INFO] Jenkins cli ........................................ SUCCESS [ 12.555 s]
[INFO] Jenkins core ....................................... SUCCESS [01:31 min]
[INFO] Jenkins war ........................................ SUCCESS [01:09 min]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 02:54 min
[INFO] Finished at: 2017-11-23T13:13:35+08:00
[INFO] Final Memory: 86M/488M
[INFO] ------------------------------------------------------------------------

在目录war/target/jenkins.war中已经在本地成功生成了jenkins.war包。但是环境依然有问题,有很多红叉在项目里面。

localizer

打开整个jenkins工程,感觉乱七八糟头有点大,不知道从何开始研究,本地跑起来刚刚生成的war包没问题,但是调试起来还有很多障碍,我们就先从这些红叉叉开始研究吧。localizer也是由kohsuke(Hudson&Jenkins的作者)写的一个属性文件本地化工具。先来介绍一下它的功能,它可以将属性文件*.properties按照国际化语言设定规则转成一个常量类文件,可以直接在其他类中调用。我们把jenkins.war包解压缩,找到cli-2.92-SNAPSHOT.war,继续解压缩,进入到cli-2.92-SNAPSHOT/hudson/cli/client目录下,可以发现:

Messages_bg.properties  Messages_es.properties  Messages.properties
Messages.class          Messages_fr.properties  Messages_pt_BR.properties
Messages_da.properties  Messages_it.properties  Messages_zh_TW.properties
Messages_de.properties  Messages_ja.properties

这里面除了我们编写的各个国家地区的语言属性文件,还有一个Message.class并不是我们写的,而是Maven生成的,这非常方便,因为属性文件作为静态文件,并不是类需要动态编译,所以常量类文件可以完全被属性文件取代,同时又能拥有常量类文件的调用便携性。下面我们来分析一下这个工具。

1.首先去kohsuke的github库中下载该项目
每次下载都要执行以下操作(这仅针对于我的环境):

evsward@lwbsPC:~/work/github/localizer/.git$ rm -rf hooks
evsward@lwbsPC:~/work/github/localizer/.git$ vi config
evsward@lwbsPC:~/work/github/localizer/.git$ cd..
evsward@lwbsPC:~/work/github/localizer$ git config credential.helper 'cache --timeout=3600'
evsward@lwbsPC:~/work/github/localizer$ git config user.name "evsward"
evsward@lwbsPC:~/work/github/localizer$ git config user.email "xxxxx@xxx.com"

2.导入项目到eclipse中去
查看核心类ResourceBundleHolder类,可以看到上面的holder.format方法:

   /**
     * Formats a resource specified by the given key by using the default locale
     */
    public String format(String key, Object... args) {
        return MessageFormat.format(get(LocaleProvider.getLocale()).getString(key), args);
    }

其中get(LocaleProvider.getLocale())方法在类的上方也给出了。

Jenkins业务构想之三:开发一个处理国际语言本地化的工具系统

每种语言都有一个文件后缀,例如汉语是zh,这里是全世界关于这个语言后缀的列表。取其中ISO 639-1的值。

源码相关知识,对象的软引用SoftReference,弱引用WeakReference。弱引用HashMap:WeakHashMap。强引用也是类加载器引用a classloader refernce

3.介绍一种缓存的使用方法

    private static final Map<Class<?>, WeakReference<ResourceBundleHolder>> cache =
            new WeakHashMap<Class<?>, WeakReference<ResourceBundleHolder>> ();

    public synchronized static ResourceBundleHolder get(Class<?> clazz) {
        WeakReference<ResourceBundleHolder> entry = cache.get(clazz);
        if (entry != null) {
            ResourceBundleHolder rbh = entry.get();
            if (rbh != null)    return rbh;
        }
        
        ResourceBundleHolder rbh = new ResourceBundleHolder(clazz);
        cache.put(clazz, new WeakReference<ResourceBundleHolder>(rbh));
        return rbh;
    }

分析一波:

  • 这个缓存cache的类型是WeakHashMap,即弱引用的哈希Map,并且它的key为Class类,值为弱引用的ResourceBundleHolder对象。这种缓存的定义就决定了它在垃圾回收器需要的时候可以随时自动清除针对此对象的所有弱引用。
  • 我们来看下面的get(Class)方法,首先它是上了锁的,这是必须的,因为要避免缓存同时被多线程操作造成内部数据混乱的结果。然后,它也是static的,所以这个方法是属于类的而不是对象的,比起对象属性,它的作用域更加广,也保证了缓存的唯一性。
  • 另外,这个缓存在系统资源紧张的时候会被随时清理,就会出现你去按key查找却查不到值的情况。看进去方法体,先获得key为某Class的值,判断其是否为空,不为空则取出,为空则说明要么是该key从来没被存入过,要么是被垃圾回收器清理掉了,无论哪种情况,我们再存入一遍即可。注意以该Class为key的值,就是ResourceBundleHolder的弱引用对象。(所以构造器ResourceBundleHolder(Class o)虽然被丢弃,但内部仍要使用。
    /**
     * @param owner
     *      The name of the generated resource bundle class.
     * @deprecated
     *      Use {@link #get(Class)}
     */
    public ResourceBundleHolder(Class<?> owner) {
        this.owner = owner;
    }

4.本地化文件的集合容器

private transient final Map<Locale,ResourceBundle> bundles = new ConcurrentHashMap<Locale,ResourceBundle>();

java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

ConcurrentHashMap 是支持并发的HashMap。Locale和ResourceBundle都是Jdk中的关于国际化的类。

  1. ResourceBundle,资源包包含特定于语言环境的对象。当程序需要一个特定于语言环境的资源时(如 String),程序可以从适合当前用户语言环境的资源包中加载它。
  2. Locale,Locale 对象表示了特定的地理、政治和文化地区。上面提到的众多国家地区语言都可以从这个类中取到。

下面是ResourceBundleHolder最核心的方法get(Locale locale),

public ResourceBundle get(Locale locale) {
        ResourceBundle rb = bundles.get(locale);// 根据地区情况获取其资源包
        if (rb != null)
            return rb;

        synchronized (this) {// 如果未获取到locale对应的资源包,则为其上锁并创建资源包
            rb = bundles.get(locale);
            if (rb != null)
                return rb;// 进锁以后再查一遍。还是没有则继续
            Locale next = getBaseLocale(locale);// 扩大一级搜索范围

            String s = locale.toString();
            // 这个owner就是Message类,拼串以后就是例如Message_zh.properties,getResource查找带有给定名称的资源。
            URL res = owner.getResource(owner.getSimpleName() + (s.length() > 0 ? '_' + s : "") + ".properties");
            if (res != null) {
                // 找到对应属性文件
                try {
                    URLConnection uc = res.openConnection();
                    uc.setUseCaches(false);
                    InputStream is = uc.getInputStream();
                    // ResourceBundleImpl是自己实现的一个可根据 InputStream
                    // 创建属性资源包。构造器是继承实现
                    ResourceBundleImpl bundle = new ResourceBundleImpl(is);
                    is.close();
                    rb = bundle;
                    if (next != null)// 多线程操作,当涉及事务操作的时候要先做检查
                        // ResourceBundle类可以根据parent属性找到相近的属性文件,而不是查不到就直接返回null.
                        bundle.setParent(get(next));
                    bundles.put(locale, bundle);
                } catch (IOException e) {
                    MissingResourceException x = new MissingResourceException("Unable to load resource " + res,
                            owner.getName(), null);
                    x.initCause(e);
                    throw x;
                }
            } else {
                if (next != null)
                    bundles.put(locale, rb = get(next));
                else
                    throw new MissingResourceException("No resource was found for " + owner.getName(), owner.getName(),
                            null);
            }

        }
        return rb;
    }

总结一下ResourceBundleHolder类,就是它是一个序列化的本地化资源数据的缓存,缓存中存储了多个key为类,值为该类为owner的ResourceBundleHolder类的键值对。而每个ResourceBundleHolder对象会维护一个不序列化且外部不可修改的成员属性二级缓存Map,该Map会存储每次查询过的本地化文件数据,如果没有则会新插入数据。在插入新数据时,要根据本地化参数Locale去查找相近的属性文件,然后将该文件存入资源包作为前面说的那个Map的值。

5.Message类
按图索骥,下面来分析上面提到的那个由Maven自动生成的Message类,我们将它反编译看一下:

package hudson.cli.client;

import org.jvnet.localizer.Localizable;
import org.jvnet.localizer.ResourceBundleHolder;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Restricted({ NoExternalUse.class })
public class Messages {
    private static final ResourceBundleHolder holder = ResourceBundleHolder.get(Messages.class);

    public Messages() {
    }

    public static String CLI_Usage() {
        return holder.format("CLI.Usage", new Object[0]);
    }

    public static Localizable _CLI_Usage() {
        return new Localizable(holder, "CLI.Usage", new Object[0]);
    }

    public static String CLI_NoURL() {
        return holder.format("CLI.NoURL", new Object[0]);
    }

    public static Localizable _CLI_NoURL() {
        return new Localizable(holder, "CLI.NoURL", new Object[0]);
    }

    public static String CLI_NoSuchFileExists(Object arg0) {
        return holder.format("CLI.NoSuchFileExists", new Object[] { arg0 });
    }

    public static Localizable _CLI_NoSuchFileExists(Object arg0) {
        return new Localizable(holder, "CLI.NoSuchFileExists", new Object[] { arg0 });
    }

    public static String CLI_VersionMismatch() {
        return holder.format("CLI.VersionMismatch", new Object[0]);
    }

    public static Localizable _CLI_VersionMismatch() {
        return new Localizable(holder, "CLI.VersionMismatch", new Object[0]);
    }
}

这个类是直接使用我们刚刚分析过的ResourceBundleHolder类,其中还调用到了Localizable类,下面来分析一下Localizable类,然后再绕回来继续分析它。

6.Localizable类
Localizable类就是一个针对本地资源包国家地区数据的一个封装类,该类有一个ResourceBundleHolder的私有成员对象。然后比较重要的就是它的toString(Locale locale)方法:

    public String toString(Locale locale) {
        try {
            return MessageFormat.format(holder.get(locale).getString(key),(Object[])args);
        } catch (MissingResourceException e) {
            throw new RuntimeException("Failed to localize key="+key+",args="+ asList(args),e);
        }
    }

这里面调用到了我们分析ResourceBundleHolder类中的核心方法get(Locale),也即通过地区条件Locale查找对应的属性文件。这里有个"key",代表的是属性文件内部的数据的key(属性文件内部数据结构也是key-value)。

7.最终目的
最终目的就是在资源包中找到属性文件,然后在该文件中找到key为"CLI.VersionMismatch"的值,用参数args内容通过MessageFormat.format替换掉值里面的占位符。

综上分析,Message类的

return new Localizable(holder, "CLI.VersionMismatch", new Object[0]);

返回的即是包含上面toString需要的三个参数的Localizable对象,当在Message类的更外部调用的时候,会让这个对象toString输出,而

    public String toString() {
        return toString(LocaleProvider.getLocale());
    }

所以就调用回了toString(Locale locale)方法,最终实现了我们刚才说的最终目的。

而LocaleProvider.getLocale()就是一个缓存存储的就是当前本地化数据,如语言、国家地区等。

    public static final LocaleProvider DEFAULT = new LocaleProvider() {
        public Locale get() {
            return Locale.getDefault();
        }
    };

一步步跟进去到jdk的Locale类中,找到对应方法:

private static Locale initDefault() {
        String language, region, script, country, variant;
        language = AccessController.doPrivileged(
            new GetPropertyAction("user.language", "en"));
        // for compatibility, check for old user.region property
        region = AccessController.doPrivileged(
            new GetPropertyAction("user.region"));
        if (region != null) {
            // region can be of form country, country_variant, or _variant
            int i = region.indexOf('_');
            if (i >= 0) {
                country = region.substring(0, i);
                variant = region.substring(i + 1);
            } else {
                country = region;
                variant = "";
            }
            script = "";
        } else {
            script = AccessController.doPrivileged(
                new GetPropertyAction("user.script", ""));
            country = AccessController.doPrivileged(
                new GetPropertyAction("user.country", ""));
            variant = AccessController.doPrivileged(
                new GetPropertyAction("user.variant", ""));
        }

        return getInstance(language, script, country, variant, null);
    }

以上的方法通过本地方法(native method)获取机器的语言,国家地区等信息。

提供者模式

首先展示一下上面localizer的类图,localizer就使用到了提供者模式,因为我们看到了LocaleProvider,我们通过它的类图来研究和学习提供者模式。

image

以上LocaleProvider的DEFAULT属性为LocaleProvider本身的匿名内部类,这里可以再次重申

继承了抽象类的类必须实现其抽象方法。

提供者模式并非一个全新的主意,它主要从流行的策略模式发展而来。快速浏览下策略模式是个不错的想法。

提供者模式是由.net2.0提出的,虽然语言与java不同,但是设计模式是跨语言的。有了提供者模式,很多时候可以用它来代替策略模式,他们的角色也是非常类似的。

  • 角色
    • provider类,用于统筹管理具体provider对象,是一个抽象类
    • XXXProvider类,继承自Provider,是具体的provide类,有自己的方法实现

通过与策略模式对比,我们可以发现LocaleProvider很神奇,有点与策略模式相类似但又不太一样,下面具体分析,

image

  1. LocaleProvider的setProvider(LocaleProvider p)方法与策略模式中的Context类的setStrategy(Strategy strategy)基本思想是一致的。
  2. 同时,LocaleProvider又可以成为策略模式中的Strategy类,
    1. 他们本身都是抽象类
    2. 都定义了一个冲抽象方法,策略模式中的是abstract void algorithm(),LocaleProvider定义的是abstract Locale get()
    3. 他们都有自己的实现类,策略模式继承与Strategy类的有BSTAdapter和RedBlackBSTAdapter,LocaleProvider虽然没有直接的子类,但是它内部定义的DEFAULT是一个继承了LocaleProvider本身的匿名内部类,它实现了那个抽象方法Locale get(),且返回的是Locale.getDefault()。(其实setProvider方法也是注入了一个继承于LocaleProvider的实现了Locale get方法的类,可以是匿名内部类,会更加方便。)

所以结论是什么?LocaleProvider类将策略模式中的Context类和Strategy类合并了起来。最终所有的模式其实都汇聚到这一个类中,然而这并非不符合“单一指责原则”,因为LocaleProvider类的职责自始至终都是一个,那就是决定Locale对象。

以后如果遇到这种情况,我们也可以使用这种模式创建我们的Provider,决定(服务?)某个类的对象。

Message.java

Message.java是整个localizer包的出口。它的功能是:

  1. 允许用户创建一系列按照某名字KeyName附加国家地区的两位字母的国际化文件(例如KeyName_zh, KeyName_en)。
  2. 创建一个名字为KeyName的类,包含一个ResourceBundleHolder的成员对象,该对象是传入了KeyName类获得的资源包持有器。
  3. KeyName类创建了一系列方便客户端调用属性文件中的数据的方法,一般是以属性文件中内容的key为方法名,传入的是替换属性文件中该key的值的占位符。
  4. 方法实现分为两种:
    1. holder.format("属性文件中key", new Object[] { arg0 });
    2. new Localizable(holder, "属性文件中key", new Object[] { arg0 });
  5. 以上两种方式的内部实现分别为

MessageFormat.format(get(LocaleProvider.getLocale()).getString(key),args);

MessageFormat.format(holder.get(locale).getString(key),(Object[])args);

这两种实现基本是一致,只是调用方式稍有不同,他们均可以实现按照语言本地化的方式调用字符串,并用参数替换占位符,格式化该字符串的功能。

但是我们发现有意思的是,如果你是第一次下载下来localizer的源码,会发现并不存在这个Message.java的文件。经过分析才知道,该类文件是通过Maven的插件自动创建的。

maven-localizer-plugin

这个Maven插件也属于localizer包的一部分,它的功能就一个:自动创建上面提到的那个Message.java类文件。

首先先来思考,这个Message.java的类文件(也就是上面提及的KeyName类)如此重要,是外部调用的接口,为什么要自动生成?

原因很简单,因为太麻烦。我们定义属性文件的时候,基本已经把所有的数据按照key-value的形式写入,同时又创建了多个相同结构,不同翻译版本的value的地区语言属性文件。Message类文件需要按照属性文件内部的key来生成对应的方法,这个过程就是复制粘贴还容易出错的工作量很大的枯燥的工程,因此,通过插件去读取这些属性文件然后自动生成是比较好的选择。

下面针对Maven如何创建一个插件来另开一个章节仔细介绍。

Maven插件

Maven本身只是提供了一个执行环境,所有的具体操作包括打包、单元测试、代码检查、版本规则等等都是通过Maven插件完成的。为了让Maven完成不同的任务,我们要为它配置各种插件。

archetype:generate:从archetype创建一个Maven项目

首先Archetype也是Maven的一个插件。

创建一个新的Maven工程,我们需要用到Maven的archetype,archetype是一个模板工具包,定义了一类项目的基本架构。

  • Maven通过不同的archetype为程序员提供了创建Maven项目的模板,同时也可以根据已有的Maven项目生成自己团队使用的参数化模板。
  • 通过archetype,开发人员可以很方便的复用一类项目的最佳实现架构到自己的项目中去。
  • 在一个Maven项目中,开发人员可以通过archetype提供的范例快速入门并了解该项目的结构与特点。

Maven的Archetype包含:

  • maven-archetype-plugin: Archetype插件。通过该插件,开发者可以在Maven中使用Archetype。它主要有两个goal:
    • archetype:generate:从archetype 中创建一个Maven项目。
    • archetype:create-from-project:从已有的项目中生成archetype。
  • archetype-packaging:用于描述Archetype的生命周期与构建项目软件包。
  • archetype-models:用于描述类和引用。
  • archetype-common:核心类
  • archetype-test:用于测试Maven archetype的内部组件。

下面利用Archetype具体创建一个Maven项目,这里使用命令行的方式,IDE只是集成了这些功能,最终仍旧是转化成命令行的方式,所以理解了命令行操作,IDE的操作也就直接掌握了。

  • 执行mvn archetype:generate,终端会显示开始下载很多archetype,最终稳定在一个让你输入一个编号的界面。这个编号有个默认的1082,对应的是maven archetype quickstart。如果直接回车则默认选择该quickstart的archetype为你构建一个Maven项目。回车以后会让你选择一个quickstart的版本,默认是最近稳定版。继续回车会让你默认输入
Define value for property 'groupId':
Define value for property 'artifactId':
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' : :
  • 按照上面傻瓜式的输入,就创建了一个完整的Maven工程,我们将其导入eclipse,然后观察它的目录结构。可以发现src/main/java和src/test/java已经成为了source folder,其中也包含例子程序,并且该项目也引用了jdk,mavne默认加了一个junit的依赖。

  • pom.xml
    最后主要内容为查看该项目的pom文件。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.evsward</groupId>
  <artifactId>testforOne</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>testforOne</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

非常简洁,只有一个junit的依赖,其他的都是常见的属性信息字段。那么问题是刚刚讲过的Maven的其他强大的功能所依赖的那些插件在哪里定义的呢?
实际上,我们项目中的pom文件是继承于一个Super Pom,我们在该项目目录下的终端里输入

mvn help:effective-pom

就会展示一个完整的包含其super pom内容的pom文件,完整的pom文件太长了,就不展示在这里了,核心思想就是我们项目中的pom文件是继承一个super pom的,所以项目内的pom可以仅关注于本业务的依赖定义即可,Maven默认的功能插件支持在super pom中都会默认帮你配置好。

archetype:create-from-project:从已有的项目中生成archetype

在上面通过archetype生成了Maven工程以后,我们对其进行一个针对我们组内开发需求,加入依赖包,创建示例程序等,抽象出来一个我们自己的maven项目构建模板。然后在项目根目录终端在中输入:

mvn archetype:create-from-project

执行完以上命令以后,就可以在target/generated-sources/archetype目录下生成一个archetype目录,进去这个目录,然后mvn install就可以将该archetype安装到本地仓库,如果要共享到组内,则可以使用mvn deploy安装到nexus等公共仓库。非常方便。

创建一个自己的maven插件

学习了以上maven archetype的知识,我们要通过archetype创建一个自定义的maven插件开发工程,archetype选择maven-archetype-mojo。然后按照上面讲过的内容将该Maven工程创建成功。然后我们来观察这个项目的结构和内容,

  • pom.xml文件中的packaging字段的值为maven-plugin,这与我们其他的maven项目不同,其他的项目可能是jar,war,hpi(Jenkins插件安装包)等。
  • 示例程序中,我们发现了一个Mojo结尾的类,这里我们可以转到 maven-localizer-plugin,可以看到GeneratorMojo,它继承自org.apache.maven.plugin.AbstractMojo。它的类注解有两个新东西:
  1. @goal generate 每个maven插件都对应着一个goal,这个goal会在使用该插件的项目的pom中定义,我们去jenkins-CLI的pom文件中查找。
      <plugin>
        <groupId>org.jvnet.localizer</groupId>
        <artifactId>maven-localizer-plugin</artifactId>
        <version>1.9</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <fileMask>Messages.properties</fileMask>
              <outputDirectory>target/generated-sources/localizer</outputDirectory>
            </configuration>
          </execution>
        </executions>
      </plugin>

可以发现goal字段的generate对应的就是GeneratorMojo的注解@goal generate,这是为查找插件使用的。
2. @phase generate-sources
这个注解定义了插件在Maven的哪一个生命周期中运行。Maven构建的生命周期,以下通过一个表格来展示。

生命周期阶段 描述
validate 检查工程配置是否正确,完成构建过程的所有必要信息是否能够获取到。
initialize 初始化构建状态,例如设置属性。
generate-sources 生成编译阶段需要包含的任何源码文件。
process-sources 处理源代码,例如,过滤任何值(filter any value)。
generate-resources 生成工程包中需要包含的资源文件。
process-resources 拷贝和处理资源文件到目的目录中,为打包阶段做准备。
compile 编译工程源码。
process-classes 处理编译生成的文件,例如 Java Class 字节码的加强和优化。
generate-test-sources 生成编译阶段需要包含的任何测试源代码。
process-test-sources 处理测试源代码,例如,过滤任何值(filter any values)。
test-compile 编译测试源代码到测试目的目录。
process-test-classes 处理测试代码文件编译后生成的文件。
test 使用适当的单元测试框架(例如JUnit)运行测试。
prepare-package 在真正打包之前,为准备打包执行任何必要的操作。
package 获取编译后的代码,并按照可发布的格式进行打包,例如 JAR、WAR 或者 EAR 文件。
pre-integration-test 在集成测试执行之前,执行所需的操作。例如,设置所需的环境变量。
integration-test 处理和部署必须的工程包到集成测试能够运行的环境中。
post-integration-test 在集成测试被执行后执行必要的操作。例如,清理环境。
verify 运行检查操作来验证工程包是有效的,并满足质量要求。
install 安装工程包到本地仓库中,该仓库可以作为本地其他工程的依赖。
deploy 拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程。

所以该注解定义了maven-localizer-plugin插件的执行时间是在generate-sources阶段,也就是在生成工程包中需要包含的资源文件的阶段,会将Message.java生成。

  • MavenProject属性
    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

GeneratorMojo类包含一个MavenProject的对象属性,该属性并未赋值,它可以在插件运行时通过@parameter expression="${project}"将maven项目注入(Maven自己的IoC容器Plexus)到该属性对象中去。在使用MavenProject类时,要在pom中加入依赖

 <dependency>  
           <groupId>org.apache.maven</groupId>  
           <artifactId>maven-project</artifactId>  
           <version>2.2.1</version>  
 </dependency> 

即可使用该类。

  • execute方法
    继续研究GeneratorMojo类,它实现了AbstractMojo类以后,就会默认必须实现一个execute方法。这个方法就是该插件功能的核心实现。回到我们的Maven插件开发项目中去,简单编写execute的内容,最终我们的测试Mojo类的完整内容如下:
package com.evsward.test_maven_plugin;

+import org.apache.maven.model.Build;..

/**
 * 
 * @author Evsward
 * @goal evswardtest
 * @phase pre-integration-test
 */
public class EvswardTestMojo extends AbstractMojo {

    /**
     * @parameter expression="${project}"
     * @readonly
     */
    private MavenProject project;

    public void execute() throws MojoExecutionException, MojoFailureException {
        Build build = project.getBuild();
        getLog().info("\n=========test here=================\n");
        getLog().info("build: " + build.getDefaultGoal() + build.getDirectory() + build.getFinalName());
        getLog().info("=======================");
    }

}

然后对整个Maven项目执行mvn clean install
build success以后执行mvn com.evsward:test-maven-plugin:0.0.1-SNAPSHOT:evswardtest
输出内容如下:

[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building test-maven-plugin Maven Plugin 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- test-maven-plugin:0.0.1-SNAPSHOT:evswardtest (default-cli) @ test-maven-plugin ---
[INFO] 
=========test here=================

[INFO] build: nullE:\Evsward\git\test-maven-plugin\targettest-maven-plugin-0.0.1-SNAPSHOT
[INFO] =======================
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.368 s
[INFO] Finished at: 2017-11-25T13:04:49+08:00
[INFO] Final Memory: 8M/245M
[INFO] ------------------------------------------------------------------------
  • 改善插件goal的命令

mvn com.evsward:test-maven-plugin:0.0.1-SNAPSHOT:evswardtest

这个命令实在是太长很麻烦,不像我们之前执行的mvn install等,因此我们要针对我们的命令进行改善,这就需要使用别名的方式代替冗长的命令,有两点要求:

  • 插件工程的命名规则必须是xxx-maven-plugin或者maven-xxx-plugin,我们的工程是test-maven-plugin,已经满足了这个命名规则。(经过尝试这一条并不应验)
  • Maven默认搜索插件只会在org.apache.maven.plugins和org.codehaus.mojo两个groupId下搜索,我们要让它也来搜索我们自己的groupId,就要在Maven的setting.xml中加入
<pluginGroups>
    <!-- pluginGroup
     | Specifies a further group identifier to use for plugin lookup.
    <pluginGroup>com.your.plugins</pluginGroup>
    -->
    <pluginGroup>com.evsward</pluginGroup>  
  </pluginGroups>

所以最终命令执行

mvn test-maven-plugin:evswardtest

即可。

GeneratorMojo的execute方法

GeneratorMojo除了注入project属性以外,还通过@parameter注入了outputDirectory,fileMask,outputEncoding,keyPattern,generatorClass,strictTypes,accessModifierAnnotations,他们分别都是maven build过程中的一些属性内容。

// packaging 方式为pom的跳过
String pkg = project.getPackaging();
if(pkg!=null && pkg.equals("pom"))
    return;

execute方法首先要确认packaging方式,如果是pom方式则不处理。
下面则是一系列java io相关的文件写入工作,文件过滤器FileFilter可以搜索属性文件或结尾包含"_xx"的文件,将他们通过一系列处理最终调用ClassGenerator的build方法完成写入工作。

TODO: java io 方面具体的深入研究请关注我即将发布的文章。

下面是maven-localizer-plugin插件中涉及类生成工作的类图。

image

本文总结

通过本文的研究,我们深入学习了:

  • Maven的配置使用,模板架构,工程创建,插件开发,部署等高级使用方法。这部分源码地址在test-maven-plugin
  • Jenkins源码中所有涉及属性文件的操作工具localizer以及其开发的maven-localizer-plugin插件,并完全研究了localizer的源码
  • 通过研究localizer源码,我们复习了设计模式中的策略模式,同时也学习了新型的提供者模式。
  • 最后也是本文的初衷,涉及Jenkins源码部分,我们仅是完成了对其国际化工具的实现,这对于整套源码来讲只是冰山一角,之后会随着越加深入而展开更多的Jenkins源码研究课题。
posted @ 2017-11-25 16:14  一面千人  阅读(2035)  评论(0编辑  收藏  举报