澳门新葡萄京娱乐场 3

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

这次提到的Java反射涉及的代码比较多。因为工作中经常用到反射,对代码做了很多抽象以及过滤器。虽然代码量很多,但是简单易用,过滤插件也易修改。

Universal-Image-Loader解析系列

Universal-Image-Loader解析(一)基本介绍与使用
Universal-Image-Loader解析(二)内部缓存原理
Universal-Image-Loader解析(三)源代码解析

前篇文章则跟大家介绍了UIL的一些常用用法。

对于我们所知道的缓存,常用的是内存缓存MemoryCache和硬盘缓存DiscCache。一个读取快容量小,一个读取慢容量大。

对于各自使用哪种缓存,则可以在前面配置ImageLoaderConfiguration进行缓存设置,当然也可以自己自定义适合的缓存。

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)  
        .memoryCache(new WeakMemoryCache())  
        .build();  

对于Universal-Image-Loader来说它的缓存结构也是分为内存缓存MemoryCache和硬盘缓存DiskCache

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

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

下面介绍下工作中哪些地方比较容易用到反射。比如插件或者过滤器,如果抽象的子类比较少,配置成XML等结构也是可以达到同样的效果。如果希望灵活一些,添加了插件或者过滤器代码子类后希望可以直接使用。可能反射会比较好点,通过扫描所有class或者jar文件,得到所有继承的子类。如果每次调用都扫描所有的文件会比较影响性能。所以在实现里面加入反射缓存,对所要获取反射子类时涉及的所有参数作为一个key缓存所有的反射结果。下次如果是同样的key,就不在重新扫描。

一.MemoryCache内存缓存

首先先看个结构图,理解UIL里面内存缓存的结构

MemoryCache

由于空间有限就没画成标准的UML类图形式。
对于基类MemoryCache它则是一个接口,里面定义了put,get图片的方法

public interface MemoryCache {
    ...
    boolean put(String key, Bitmap value);

    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

都是大家比较所熟悉的方法,而对于其他的类

我们一个个看

源码的研究策略

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

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

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

代码示例如下:

LruMemoryCache

这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用。直接实现了MemoryCache方法

public class LruMemoryCache implements MemoryCache {

    private final LinkedHashMap<String, Bitmap> map;
    //最大容量
    private final int maxSize;
    /** 目前缓存的容量大小 */
    private int size;
    public LruMemoryCache(int maxSize) {
        ...
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }
    @Override
    public final Bitmap get(String key) {
        ...
        synchronized (this) {
            return map.get(key);
        }
    }
    @Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

    /**
     * Lru算法,当容量超过最大缓存容量,则移除最久的条目
     */
    private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= sizeOf(key, value);
            }
        }
    }

    @Override
    public final Bitmap remove(String key) {
        ...
        synchronized (this) {
            Bitmap previous = map.remove(key);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
            return previous;
        }
    }
    ...
    //返回图片的字节大小
    private int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }
    ...
}

LruMemoryCache的源码也比较简单,内部有个成员变量LinkedHashMap<String, Bitmap> map这里直接进行保存的话则是强引用的形式。
主要看get,put方法。
对于get方法来说,比较简单,直接根据指定的key返回对应的图片。
而对于put方法来说,则需要考虑容量的问题。

@Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

put方法首先调用了sizeof方法,该方法则是返回指定Bitmap的字节大小,之后size
+=,总缓存量增加,之后调用trimToSize该方法则是进行缓存容量判断的。

private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= sizeOf(key, value);
            }
        }
    }

如果加入后的size 缓存容量 <= maxSize
最大缓存容量,则直接break,不用进行判定处理。
如果大于的话,则直接移除最久未使用的。

大家肯定有疑问,它到底怎么判断最久未使用的?没看到相关代码呀?

相信知道LinkedHashMap的话可能就知道。
LinkedHashMap自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据。大家常见也就是按顺序存储,很少忘了它还可以根据最近未使用的方法。

//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

//LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
//我们要做的就是重写这个方法,当满足一定条件时删除老数据
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
}

回看我们前面LinkedHashMap的创建

  this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

再举个使用例子

例子

就比较明了了。

搭建源码开发环境

public static void main(String[] args) {
        //设置扫描范围,可以是class文件所在位置例如bin下或者是mysql开头或者mysql结尾的jar,
        //设置为""为全部都扫描,这种比较耗时
        ReflectUtils.createSharedReflections("classes", "bin", "mysql");
        try {
            //调试阶段可以设置每次都全扫描
            //Beans.setDesignTime(true);
            final Collection<String> subTypes = ReflectUtils.listSubClass(IA.class);//
            for (final String subType : subTypes) {
                //这里获取的是所有继承IA的子类
                System.out.println(subType);
                final IA impl = ReflectUtils.initClass(subType, IA.class);
                if (null == impl)
                    continue;
                //通过该方式,可以统一做操作,
                impl.print();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

BaseMemoryCache

BaseMemoryCache同样也是实现了MemoryCache方法,不过它还是一个抽象类。
它是一个内存缓存的基类,实现了内存缓存中常用的方法,只不过它里面提供了一个非强引用的Reference作为扩展,方便GC的回收,避免OOM.

public abstract class BaseMemoryCache implements MemoryCache {

    /** Stores not strong references to objects */
    private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());

    @Override
    public Bitmap get(String key) {
        Bitmap result = null;
        Reference<Bitmap> reference = softMap.get(key);
        if (reference != null) {
            result = reference.get();
        }
        return result;
    }

    @Override
    public boolean put(String key, Bitmap value) {
        softMap.put(key, createReference(value));
        return true;
    }

    @Override
    public Bitmap remove(String key) {
        Reference<Bitmap> bmpRef = softMap.remove(key);
        return bmpRef == null ? null : bmpRef.get();
    }

    /** Creates {@linkplain Reference not strong} reference of value */
    protected abstract Reference<Bitmap> createReference(Bitmap value);
}

代码也比较简单,内存持有一个Map<String, Reference<Bitmap>> softMap来保存非强引用对象,具体的引用类型则看它实现的抽象方法createReference

一、github本地配置修改

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

代码执行结果:

WeakMemoryCache

我们看它的一个子类WeakMemoryCache则是继承与BaseMemory,实现createReference

public class WeakMemoryCache extends BaseMemoryCache {
    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

很明显是来保存弱引用对象的。

git配置文件

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

//缓存文件,避免每次调用反射都重新扫描
//如果删除该文件,再次调用反射时,会重新扫描,一般会在代码里面有添加子类的时候会删除该文件
XmlUtils.readXml failure:.configuration.REF (系统找不到指定的文件。)
net.simple.reflect.test.B
net.simple.reflect.test.B
net.simple.reflect.test.D
net.simple.reflect.test.V

LimitedMemoryCache

我们看它的另外一个子类LimitedMemoryCache,但它并没有实现BaseMemoryCache里的createReference方法,它也是一个抽象类,在BaseMemoryCache基础上封装了个抽象方法
protected abstract Bitmap removeNext();用来处理当缓存容量不足时的情况。

public abstract class LimitedMemoryCache extends BaseMemoryCache {
    ...
    //当前保存的Bitmap,用来统计缓存数
    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // Add value to soft cache
        super.put(key, value);
        return putSuccessfully;
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            if (hardCache.remove(value)) {
                cacheSize.addAndGet(-getSize(value));
            }
        }
        return super.remove(key);
    }
   ...
    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

可以看到在
LimitedMemoryCache里面又有一个List<Bitmap>保存的是强引用,而在BaseMemoryCache里面也有个Map<String, Reference<Bitmap>> softMap来保存Bitmap,为什么要这样。

这主要是因为在BaseMemoryCache里面并没有做缓存限制处理,它只是封装实现了基本的Bitmap的put,get。而当面对缓存容量有限的情况下,则需要交给子类去处理。

我们看下这里的put方法,关键在

while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }

当超过容量时,调用抽象方法removeNext由子类自行实现,之后hardCache移除,但此时并没有调用softMap的移除。

也就是对于List<Bitmap>来说,当它的缓存容量超过的时候,它会移除第一个对象来缓解容量,但是保存在Map<String, Reference<Bitmap>> softMap里面的Bitmap并没有被移除。
如果这样下去softMap岂不是会无限大?

这是因为在Map<String, Reference<Bitmap>> softMap里面保存的Bitmap是弱引用的存在,而在List<Bitmap>里面保存的是强引用,当内存不足的时候,GC则会先清除softMap里面的对象。

gerrit

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

具体的类里面如何实现的大家就看下源码吧,这里贴出两个核心类的代码。源码地址:

FIFOLimitedMemoryCache

我们看下LimitedMemoryCache的一个子类FIFOLimitedMemoryCache,看到FIFO也就是先进先出了。

public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            queue.add(value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            queue.remove(value);
        }
        return super.remove(key);
    }
    ...
    @Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

可以看到同样的这里也有个List<Bitmap> queue来保存记录,而在removeNext那里,返回的正是队列的第一个元素,符合FIFO。

本地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   
package net.simple.reflect;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import net.simple.reflect.filter.IPathURLFilter;
import net.simple.reflect.filter.ISubTypeFilter;
import net.simple.reflect.filter.ITypeFilter;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * 
 * @author 李岩飞
 * @email eliyanfei@126.com 
 * 2016年11月2日 下午3:23:49
 *
 */
public final class Reflections {
    private final Collection<URL> pathUrls;
    private final Collection<IPathURLFilter> pathURLfilters;
    private final Collection<ITypeFilter> typeFilters;
    private ISubTypeFilter subTypeFilter;

    public Reflections() {
        typeFilters = new ArrayList<ITypeFilter>();
        pathURLfilters = new ArrayList<IPathURLFilter>();
        this.pathUrls = ClasspathHelper.getUrlsForCurrentClasspath();
    }

    public Reflections(final Collection<URL> pathUrls) {
        this.pathUrls = pathUrls;
        typeFilters = new ArrayList<ITypeFilter>();
        pathURLfilters = new ArrayList<IPathURLFilter>();
    }

    /**
     * @param subTypeFilter
     *            the subTypeFilter to set
     */
    public void setSubTypeFilter(final ISubTypeFilter subTypeFilter) {
        this.subTypeFilter = subTypeFilter;
    }

    /**
     * @return the subTypeFilter
     */
    public ISubTypeFilter getSubTypeFilter() {
        return subTypeFilter;
    }

    public Reflections addPathURLFilter(final IPathURLFilter pathURLFilter) {
        if (null == pathURLFilter)
            return this;
        if (!this.pathURLfilters.contains(pathURLFilter))
            this.pathURLfilters.add(pathURLFilter);
        return this;
    }

    public Reflections addTypeFilter(final ITypeFilter typeFilter) {
        if (null == typeFilter)
            return this;
        if (!this.typeFilters.contains(typeFilter))
            this.typeFilters.add(typeFilter);
        return this;
    }

    private static final String histFile = "./configuration.REF";
    private Document histDom;

    public Collection<String> getSubTypesFast(final Class<?> baseType) {//, final String... typeNames
        //首先过滤出当前允许扫描的路径
        final StringBuilder bufPathsId = new StringBuilder(32);
        final Map<File, URL> fileUrls = new LinkedHashMap<File, URL>(8);
        for (final URL pathUrl : pathUrls) {
            if (!acceptPathUrl(pathUrl))
                continue;
            File file = null;
            try {
                file = new File(URLDecoder.decode(pathUrl.getFile(), "UTF-8"));
            } catch (final Exception e) {
                file = new File(pathUrl.getFile());
            }
            fileUrls.put(file, pathUrl);
            if (!file.exists())//is url file?ignore
                continue;
            bufPathsId.append(file.getName()).append(file.lastModified());
        }
        final String domId = MD5.getHashString(bufPathsId.toString());
        if (null == histDom)
            histDom = W3cUtils.readXml(histFile);
        if (null == histDom)
            histDom = W3cUtils.newDom("R");
        Element rootEle = histDom.getDocumentElement();
        if (null == rootEle)
            histDom.appendChild(rootEle = histDom.createElement("R"));
        if (!domId.equals(rootEle.getAttribute("id"))) {
            rootEle.getParentNode().removeChild(rootEle);
            histDom.appendChild(rootEle = histDom.createElement("R"));
            rootEle.setAttribute("id", domId);
        }
        final String baseTypeId = MD5.getHashString(baseType.getName());
        Element refEle = W3cUtils.firstChildElement(rootEle, "E", "id", baseTypeId);
        if (null != refEle) {
            final List<Element> valueEles = W3cUtils.childElementList(refEle, "F");
            final Collection<String> result = new ArrayList<String>(valueEles.size());
            for (final Element valueEle : valueEles) {
                result.add(new String(Base64.decodeFast(valueEle.getAttribute("id"))));
            }
            return result;
        }
        final ThreadPool<ListSubTypes> pool = new ThreadPool<ListSubTypes>();
        for (final File fileKey : fileUrls.keySet()) {
            pool.execute(new ListSubTypes(baseType, fileKey, fileUrls.get(fileKey)));
        }
        try {
            pool.shutdown(3, TimeUnit.MINUTES);
        } catch (final InterruptedException e) {
            e.printStackTrace();//for debug
        }
        final Collection<String> result = new ArrayList<String>();
        for (final ListSubTypes task : pool.getThreadRunables()) {
            result.addAll(task.result);
        }
        refEle = W3cUtils.addEle(rootEle, "E");
        refEle.setAttribute("id", baseTypeId);
        for (final String itm : result) {
            W3cUtils.addEle(refEle, "F").setAttribute("id", Base64.encodeToString(itm.getBytes(), false));
        }
        try {
            W3cUtils.writeXmlDocument(histFile, histDom);
        } catch (final Exception e) {
        }
        return result;
    }

    /**
     * @see {@link ReflectUtils#createSharedReflections(String...)}
     * @see {@link ReflectUtils#setSharedReflections(Reflections)}
     * @see {@link ReflectUtils#listSubClass(Class)}
     * @param baseType
     * @return
     */
    public Collection<String> getSubTypes(final Class<?> baseType, final String... typeNames) {//
        final ThreadPool<ListSubTypes> pool = new ThreadPool<ListSubTypes>();
        for (final URL pathUrl : pathUrls) {
            if (!acceptPathUrl(pathUrl))
                continue;
            File file = null;
            try {
                file = new File(URLDecoder.decode(pathUrl.getFile(), "UTF-8"));
            } catch (final Exception e) {
                file = new File(pathUrl.getFile());
            }
            pool.execute(new ListSubTypes(baseType, file, pathUrl, typeNames));
        }
        try {
            pool.shutdown(3, TimeUnit.MINUTES);
        } catch (final InterruptedException e) {
            e.printStackTrace();//for debug
        }
        final Collection<String> result = new ArrayList<String>();
        for (final ListSubTypes task : pool.getThreadRunables()) {
            result.addAll(task.result);
        }
        return result;
    }

    class ListSubTypes implements Runnable {
        final File file;
        final Class<?> baseType;
        final URL pathUrl;
        final String[] typeNames;

        public ListSubTypes(final Class<?> baseType, final File file, final URL pathUrl, final String... typeNames) {
            this.baseType = baseType;
            this.file = file;
            this.pathUrl = pathUrl;
            this.typeNames = typeNames;
        }

        Collection<String> result = new ArrayList<String>(4);

        @Override
        public void run() {
            if (file.isDirectory()) {
                listSubTypesFromDirectory(file, baseType, pathUrl, file, result, typeNames);
            } else
                listSubTypesFromJar(baseType, pathUrl, result, typeNames);
        }
    }

    /**
     * @param baseType
     * @param pathUrl
     * @param result
     */
    public void listSubTypesFromDirectory(final File baseDirectory, final Class<?> baseType, final URL pathUrl, final File directory,
            final Collection<String> result, final String... typeNames) {
        File[] files = directory.listFiles();
        if (null == files)
            files = new File[] {};
        String clazzPath;
        final int baseDirLen = baseDirectory.getAbsolutePath().length() + 1;
        for (final File file : files) {
            if (file.isDirectory()) {
                listSubTypesFromDirectory(baseDirectory, baseType, pathUrl, file, result, typeNames);
            } else {
                clazzPath = file.getAbsolutePath().substring(baseDirLen);
                clazzPath = clazzPath.replace('\', '/');
                doTypesFilter(baseType, pathUrl, result, clazzPath, typeNames);
            }
        }
    }

    /**
     * @param baseType
     * @param pathUrl
     * @param result
     */
    public void listSubTypesFromJar(final Class<?> baseType, URL pathUrl, final Collection<String> result, final String... typeNames) {
        try {
            // It does not work with the filesystem: we must
            // be in the case of a package contained in a jar file.
            JarFile jarFile = null;
            try {
                if ("file".equals(pathUrl.getProtocol()))
                    pathUrl = new URL("jar:" + pathUrl.toExternalForm() + "!/");
                jarFile = ((JarURLConnection) pathUrl.openConnection()).getJarFile();
            } catch (final Exception e) {
                final String filePath = pathUrl.getFile();
                // if on win platform
                if (filePath.indexOf(':') != -1) {
                    if (pathUrl.getFile().charAt(0) == '/')
                        jarFile = new JarFile(filePath.substring(1));
                }
                if (null == jarFile)
                    jarFile = new JarFile(filePath);
            }
            final Enumeration<JarEntry> e = jarFile.entries();
            ZipEntry entry;
            while (e.hasMoreElements()) {
                entry = e.nextElement();
                doTypesFilter(baseType, pathUrl, result, entry.getName(), typeNames);
            }
        } catch (final IOException ioex) {
        }
    }

    private void doTypesFilter(final Class<?> baseType, final URL pathUrl, final Collection<String> result, final String clazzPath,
            final String... typeNames) {
        if (!clazzPath.endsWith(".class"))
            return;
        final int lastDotIdx = clazzPath.lastIndexOf('.');
        if (-1 == lastDotIdx)
            return;
        final String typeDef = clazzPath.substring(0, lastDotIdx).replace('/', '.');
        if (null != typeNames && typeNames.length > 0) {
            final int lastDot = typeDef.lastIndexOf('.');
            if (lastDot == -1)
                return;
            final String typeName = typeDef.substring(lastDot + 1);
            boolean withLiked = false;
            for (final String tmpTypeName : typeNames) {
                if (!typeName.contains(tmpTypeName))
                    continue;
                withLiked = true;
                break;
            }
            if (withLiked == false)
                return;
        }
        if (this.typeFilters.isEmpty()) {
            if (null == this.subTypeFilter || this.subTypeFilter.accept(baseType, pathUrl, clazzPath))
                result.add(typeDef);
        } else {
            for (final ITypeFilter typeFilter : this.typeFilters) {
                if (!typeFilter.accept(clazzPath))
                    continue;
                if (null == this.subTypeFilter || this.subTypeFilter.accept(baseType, pathUrl, clazzPath))
                    result.add(typeDef);
            }
        }
    }

    /**
     * @param pathUrl
     * @return
     */
    private boolean acceptPathUrl(final URL pathUrl) {
        if (this.pathURLfilters.isEmpty())
            return true;
        for (final IPathURLFilter pathURLFilter : this.pathURLfilters) {
            if (pathURLFilter.accept(pathUrl))
                return true;
        }
        return false;
    }
}

package net.simple.reflect;

import java.beans.Beans;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import net.simple.reflect.filter.PathURLFilter;
import net.simple.reflect.filter.SampleSubInstanceFilter;
import net.simple.reflect.filter.TypeFilter;

/**
 * 
 * @author 李岩飞
 * @email eliyanfei@126.com 
 * 2016年11月2日 下午3:24:02
 *
 */
public final class ReflectUtils {
    public static final String VAR_START_FLAG = "${";
    public static final String VAR_END_FLAG = "}";

    private static Reflections sharedReflections;
    static final Collection<String> EMP_COLL = Collections.emptyList();

    public static final void createSharedReflections(final String... filterExts) {
        final Reflections refs = new Reflections();
        refs.addPathURLFilter(new PathURLFilter(filterExts));//
        refs.addTypeFilter(TypeFilter.DEFAULT);
        refs.setSubTypeFilter(SampleSubInstanceFilter.DEFAULT);
        ReflectUtils.setSharedReflections(refs);
    }

    /**
     * 此方法用于绑定一个通用的共享类型遍列工具.
     * @param sharedReflections
     */
    public static final void setSharedReflections(final Reflections sharedReflections) {
        ReflectUtils.sharedReflections = sharedReflections;
    }

    /**
     * 调用此方法之前必须先设置共享的类型遍列工具,参考:{@link #setSharedReflections(Reflections)},
     * 此方法主要使更方便的遍列给定类的实现,
     */
    public static final Collection<String> listSubClass(final Class<?> baseType, final String... typeNames) {//
        if (null == sharedReflections)
            return EMP_COLL;
        //调用阶段由于可能增加新的子类实现,需要每次都重新扫描,只有在发布的产品时使用保存记录的方法以提高启动速度.
        return Beans.isDesignTime() ? sharedReflections.getSubTypes(baseType, typeNames) : sharedReflections.getSubTypesFast(baseType);
    }

    public static List<Class<?>> listClassOfPackage(final Class<?> cType, final String extenion) {
        final List<Class<?>> result = new ArrayList<Class<?>>();
        final List<String> cPath = ReflectUtils.listClassCanonicalNameOfPackage(cType, extenion);
        for (final String path : cPath) {
            try {
                result.add(Class.forName(path, false, Thread.currentThread().getContextClassLoader()));
            } catch (final Exception e) {
                // ignore
            }
        }
        return result;
    }

    public static List<String> listClassCanonicalNameOfPackage(final Class<?> clazz, final String extenion) {
        return ReflectUtils.listNameOfPackage(clazz, extenion, true);
    }

    public static List<String> listClassNameOfPackage(final Class<?> clazz, final String extenion) {
        return ReflectUtils.listNameOfPackage(clazz, extenion, false);
    }

    public static List<String> listNameOfPackage(final Class<?> clazz, final String extenion, final boolean fullPkgName) {
        return ReflectUtils.listNameOfPackage(clazz.getName().replace('.', '/') + ".class", extenion, fullPkgName);
    }

    public static List<String> listNameOfPackage(final String clazzPkg, final String extenion, final boolean fullPkgName) {
        final List<String> result = new ArrayList<String>();

        final StringBuffer pkgBuf = new StringBuffer(clazzPkg);

        if (pkgBuf.charAt(0) != '/')
            pkgBuf.insert(0, '/');

        final URL urlPath = ReflectUtils.class.getResource(pkgBuf.toString());

        if (null == urlPath)
            return result;

        String checkedExtenion = extenion;
        if (!extenion.endsWith(".class"))
            checkedExtenion = extenion + ".class";

        if (pkgBuf.toString().endsWith(".class"))
            pkgBuf.delete(pkgBuf.lastIndexOf("/"), pkgBuf.length());

        pkgBuf.deleteCharAt(0);

        final StringBuffer fileUrl = new StringBuffer();
        try {
            fileUrl.append(URLDecoder.decode(urlPath.toExternalForm(), "UTF-8"));
        } catch (final UnsupportedEncodingException e1) {
            fileUrl.append(urlPath.toExternalForm());
        }

        if (fileUrl.toString().startsWith("file:")) {
            fileUrl.delete(0, 5);// delete file: flag
            if (fileUrl.indexOf(":") != -1)
                fileUrl.deleteCharAt(0);// delete flag
            final String baseDir = fileUrl.substring(0, fileUrl.lastIndexOf("classes") + 8);
            ReflectUtils.doListNameOfPackageInDirectory(new File(baseDir), new File(baseDir), result, pkgBuf.toString(), checkedExtenion, fullPkgName);
        } else {
            ReflectUtils.doListNameOfPackageInJar(urlPath, urlPath, result, pkgBuf.toString(), checkedExtenion, fullPkgName);
        }

        return result;
    }

    /**
     */
    private static void doListNameOfPackageInJar(final URL baseUrl, final URL urlPath, final List<String> result, final String clazzPkg, final String extenion, final boolean fullPkgName) {
        try {
            // It does not work with the filesystem: we must
            // be in the case of a package contained in a jar file.
            final JarURLConnection conn = (JarURLConnection) urlPath.openConnection();
            final JarFile jfile = conn.getJarFile();
            final Enumeration<JarEntry> e = jfile.entries();

            ZipEntry entry;
            String entryname;

            while (e.hasMoreElements()) {
                entry = e.nextElement();
                entryname = entry.getName();

                if (entryname.startsWith(clazzPkg) && entryname.endsWith(extenion)) {
                    if (fullPkgName)
                        result.add(entryname.substring(0, entryname.lastIndexOf('.')).replace('/', '.'));
                    else
                        result.add(entryname.substring(entryname.lastIndexOf('/') + 1, entryname.lastIndexOf('.')));
                }
            }
        } catch (final IOException ioex) {
        }
    }

    private static void doListNameOfPackageInDirectory(final File baseDirectory, final File directory, final List<String> result, final String clazzPkg, final String extenion,
            final boolean fullPkgName) {
        File[] files = directory.listFiles();
        if (null == files)
            files = new File[] {};
        String clazzPath;
        final int baseDirLen = baseDirectory.getAbsolutePath().length() + 1;
        for (final File file : files) {
            if (file.isDirectory()) {
                ReflectUtils.doListNameOfPackageInDirectory(baseDirectory, file, result, clazzPkg, extenion, fullPkgName);
            } else {
                if (!file.getName().endsWith(extenion))
                    continue;

                if (fullPkgName) {
                    clazzPath = file.getAbsolutePath().substring(baseDirLen);
                    clazzPath = clazzPath.substring(0, clazzPath.length() - 6);
                    result.add(clazzPath.replace(File.separatorChar, '.'));
                } else {
                    result.add(file.getName().substring(0, file.getName().length() - 6));
                }
            }
        }
    }

    public static final <T> T initClass(final String implClass, final Class<T> tType) {
        return ReflectUtils.initClass(implClass, tType, true);
    }

    public static final <T> T initClass(final String implClass, final Class<T> tType, final boolean doInit) {
        try {
            final Object object = Class.forName(implClass, doInit, Thread.currentThread().getContextClassLoader()).newInstance();
            return tType.cast(object);
        } catch (final Throwable e) {
            return null;
        }
    }
}

LRULimitedMemoryCache

再来看一个另外一个子类LRULimitedMemoryCache也就是最近未使用删除。

public class LRULimitedMemoryCache extends LimitedMemoryCache {

    /** Cache providing Least-Recently-Used logic */
    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
   ...
    @Override
    protected Bitmap removeNext() {
        Bitmap mostLongUsedValue = null;
        synchronized (lruCache) {
            Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
            if (it.hasNext()) {
                Entry<String, Bitmap> entry = it.next();
                mostLongUsedValue = entry.getValue();
                it.remove();
            }
        }
        return mostLongUsedValue;
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

可以看到,这里的LRU处理则是使用LinkedHashMap,在它的构造方法中第三个参数为true表示使用LRU,之后再removeNext返回那个Bitmap。

同理其他子类也如下,就不一一列举。

git修改历史提交记录

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

MemoryCache小结

  1. 只使用的是强引用缓存
  • LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用)

2.使用强引用和弱引用相结合的缓存有

  • UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)

  • LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)

  • FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)

  • LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)

  • LimitedAgeMemoryCache(当
    bitmap加入缓存中的时间超过我们设定的值,将其删除)

3.只使用弱引用缓存

  • WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

二、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项目导入。

二.DiskCache硬盘缓存

同样先来看个结构

DiskCache

DiskCache的设计其实和MemoryCache一样,对于基类DiskCache,它同样是一个接口

public interface DiskCache {
    //返回硬盘缓存的根目录
    File getDirectory();

    File get(String imageUri);

    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

    boolean save(String imageUri, Bitmap bitmap) throws IOException;

    boolean remove(String imageUri);

    void close();

    void clear();
}

同样一个个看

三、Maven构建源码工程

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

LruDiskCache

LruDiskCache则是直接实现了DiskCache接口,采用LRU算法来进行缓存处理。
再理解LruDiskCache前,先理解另一个类DiskLruCache

final class DiskLruCache implements Closeable {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TEMP = "journal.tmp";
    static final String JOURNAL_FILE_BACKUP = "journal.bkp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    ...
    private final LinkedHashMap<String, Entry> lruEntries =
            new LinkedHashMap<String, Entry>(0, 0.75f, true);
    ...
    final ThreadPoolExecutor executorService =
            new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    ...
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
            throws IOException {
        ...
    }
    ...
    public synchronized Snapshot get(String key) throws IOException {
        ...
    }
    ...
    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
    }
    /** A snapshot of the values for an entry. */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private File[] files;
        private final InputStream[] ins;
        private final long[] lengths;
        ...
    }
    ...
    public final class Editor {
        private final Entry entry;
        private final boolean[] written;
        private boolean hasErrors;
        private boolean committed;
        ...
    }
    ...
    private final class Entry {
        private final String key;

        private final long[] lengths;

        private boolean readable;

        private Editor currentEditor;

        private long sequenceNumber;
        ...
    }

这个DiskLruCache比较长也比较复杂,它是LruDiskCache的一个文件工具类。这里的缓存数据存储在文件系统上的一个目录。
同时也注意到这里的一个成员变量
private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
可以知道这是用来处理LRU的。

同时这里的value则是EntryEntry则是封装了当前文件的编辑情况Ediotr以及key
而这里Editor封装了文件的写入情况OutputStreamSnapshot封装了文件的读取情况InputStream

回头看回LruDiskCache

public class LruDiskCache implements DiskCache {
    protected DiskLruCache cache;
    private File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;
    ...
    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        ...
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
        ...
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        ...
    }
    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }
    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        ...
        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        }
        ...
        return savedSuccessfully;
    }

首先LruDiskCache内部成员变量带有DiskLruCache还有文件的保存目录等,在它的构造方法中调用DiskLruCache.open方法创建了DiskLruCache对象,而在它的open方法里,则根据文件的目录情况创建了对应的文件系统。

再看它的save方法,先调用getKey方法将uri转换为对应的key,而在cache,edit中

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // Snapshot is stale.
        }
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null; // Another edit is in progress.
        }

        Editor editor = new Editor(entry);
        entry.currentEditor = editor;
        ...
        return editor;
    }

则是根据指定的key先判断缓存文件中有没有相应的key,如果没有则创建一个Entry对象持有它,之后保存在lruEntries之后,创建一个当前Entry的编辑对象Editor,以便之后写入到文件中。

s之后调用了

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);

editor.newOutputStream则是根据当前目录和key创建出一个文件,之后打开这个文件的一个输出流情况,获取到之后就进行Bitmap的写入。

同理,看下LruDiskCache的get方法

@Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }

调用了cache,get

public synchronized Snapshot get(String key) throws IOException {
        。。。
        Entry entry = lruEntries.get(key);
        ...
        File[] files = new File[valueCount];
        InputStream[] ins = new InputStream[valueCount];
        try {
            File file;
            for (int i = 0; i < valueCount; i++) {
                file = entry.getCleanFile(i);
                files[i] = file;
                ins[i] = new FileInputStream(file);
            }
        } 
        ...
        return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
    }

在get方法中,先根据key拿到对应的Entry,再拿到对应的文件打开输入流,之后传入到Snapshot
而在snapshot.getFile

/** Returns file with the value for {@code index}. */
        public File getFile(int index) {
            return files[index];
        }

返回的则是对应的文件。

配置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测试。

BaseDiskCache

BaseDiskCache同样也是直接实现了DiskCache方法,实现的方法也比较简单

public abstract class BaseDiskCache implements DiskCache {
    ...
    protected final File cacheDir;
    protected final File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;

    public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        ...
        this.cacheDir = cacheDir;
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
    }

    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
            if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
                savedSuccessfully = false;
            }
            if (!savedSuccessfully) {
                tmpFile.delete();
            }
        }
        bitmap.recycle();
        return savedSuccessfully;
    }

    @Override
    public File get(String imageUri) {
        return getFile(imageUri);
    }

    protected File getFile(String imageUri) {
        String fileName = fileNameGenerator.generate(imageUri);
        File dir = cacheDir;
        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                dir = reserveCacheDir;
            }
        }
        return new File(dir, fileName);
    }

比较简单,根据对应的文件去打开获取。它的两个子类LimitedAgeDiskCacheUnlimitedDiskCache也都不一一扩展开了。

开始构建

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,我们通过它的类图来研究和学习提供者模式。

澳门新葡萄京娱乐场 1

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

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

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

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

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

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

澳门新葡萄京娱乐场 2

  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:Evswardgittest-maven-plugintargettest-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插件中涉及类生成工作的类图。

澳门新葡萄京娱乐场 3

本文总结

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注