Jenkins源码阅读指北,一文看懂Jenkins用到的java技术点

Jenkins源码阅读指北,一文看懂Jenkins用到的java技术点
Jenkins是一个基于Java开发的,用于持续集成的工具。Jenkins的前身是Sun 公司的Hudson,第一个版本于2005年发布,在2010年11月期间,因为Oracle对Sun的收购带来了Hudson的所有权问题。2011年1月29日,该建议得到社区投票的批准,创建了Jenkins项目。

本文在学习Jenkins源码的同时,也会分析Jenkins对于java技术的运用,并对相关技术进行简单介绍。Jenkins使用的是Stapler框架,国内使用较少,这里不对该框架进行分析,而是根据场景直接分析源码。

**1 doCheckJobName方法**

我们在Jenkins创建一个Item的时候,当在Enteran item name输入一个名称,其实会请求checkJobName这个api,触发的方法在doCheckJobName,方法定义如下:

public FormValidation doCheckJobName(@QueryParameter String value) {
// this method can be used to check if a file existsanywhere in the file system,
// so it should be protected.
getOwner().checkPermission(Item.CREATE);

if (Util.fixEmpty(value) == null) {
return FormValidation.ok();
}

try {
Jenkins.checkGoodName(value);
value = value.trim(); // why trim *after* checkGoodName? not sure, butItemGroupMixIn.createTopLevelItem does the same
Jenkins.get().getProjectNamingStrategy().checkName(value);
} catch (Failure e){
return FormValidation.error(e.getMessage());
}

if (getOwner().getItemGroup().getItem(value)!= null) {
return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

// looks good
return FormValidation.ok();
}

比较重要的几行代码

getOwner().checkPermission(Item.CREATE);是对当前用户的权限进程检测

Jenkins.checkGoodName(value);对名称的合法性进行检测

if (getOwner().getItemGroup().getItem(value)!= null) {
return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

查看是否名称已经存在。

我们以getOwner().getItemGroup().getItem(value)为例,着重讲解一下。

**2 getOwner方法和ViewGroup**

getOwner()这个方法会返回一个ViewGroup。

ViewGroup接口:

public interface ViewGroup extendsSaveable, ModelObject,AccessControlled

在注释中描述为Containerof Views.

AccessControlled这个接口有四个方法:

@Nonnull ACL getACL();
default void checkPermission(@Nonnull Permission permission) throws AccessDeniedException {
getACL().checkPermission(permission);
}

default boolean hasPermission(@Nonnull Permission permission) {
return getACL().hasPermission(permission);
}

default boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) {
if (a == ACL.SYSTEM) {
return true;
}
return getACL().hasPermission(a, permission);
}

需要注意的是这个接口有三个方法带有default关键字,并且还带有具体的实现。自从jdk1.8以后接口中可以定义方法的具体实现,这个特性解决了interface扩展必须修改实现类的问题。

对于getOwner方法返回的实际上是个Hudson的实例,这个方法位于View抽象类中,因为AllView继承了View。Hudson继承Jenkins类,Jenkins类继承了AbstractCIBase抽象类,而AbstractCIBase抽象类实现了ViewGroup接口。

前面说到ViewGroup在注释中描述为View的容器。所以这个接口提供了Collection<View>getViews();这个获取View集合的方法。在Jenkins类中实现了这个方法:

@Exported
public Collection<View>getViews() {
return viewGroupMixIn.getViews();
}

这个方法的实现是返回ViewGroupMixIn类型的对象viewGroupMixIn执行getViews方法的返回结果。

下面我们从ViewGroupMixIn类型开始分析。

**3 ViewGroupMixIn类型**

首先我们看一下viewGroupMixIn是什么:

privatetransient final ViewGroupMixIn viewGroupMixIn = new ViewGroupMixIn(this) {
protected List<View> views() { return views; }
protected String primaryView() { return primaryView; }
protected void primaryView(String name) { primaryView=name; }
};

可以发现ViewGroupMixIn是一个抽象类,而viewGroupMixIn变量以内部类的形式实现了该类。我们再来看ViewGroupMixIn中getViews方法的实现

public Collection<View> getViews() {
List<View>orig = views();
List<View>copy = new ArrayList<>(orig.size());
for (View v : orig) {
if (v.hasPermission(View.READ))
copy.add(v);
}
copy.sort(View.SORTER);
return copy;
}

逻辑大概是:首先会调用views方法,这个方法在Jenkins类中的viewGroupMixIn内部类的实现中已经定义了。

protected List<View>views() { return views; }
views方法会返回Jenkins中的views变量,那么views变量是什么呢:

private final CopyOnWriteArrayList<View>views = new CopyOnWriteArrayList<>();
原来views是一个CopyOnWriteArrayList<View>。

**4 java.util.concurrent中的CopyOnWriteArrayList**

CopyOnWriteArrayList是java.util.concurrent包中的一个并发集合,可以保证线程安全,在读多写少的场景下拥有更好的性能。

假设我们要设计一个线程安全的List,首相想到的是:为了保证线程安全对于List的操作(读,写)时候需要加锁。但是如果无论读写都加锁势必对性能造成很大的浪费。毕竟读与读之间不会对数据进行修改,所以读读之间可以不考虑锁。这样只有在读写之间,写写之间需要同步等待。显然这种方式减少了读读情况下不必要的锁。比较与所有操作都需要锁已经有了性能的提升。

而CopyOnWriteArrayList使用了cow技术。详细来说就是当这个List在修改时候会复制一份新的副本来修改,而修改后对原数据进行替换。

我们来看看关键的源码:

public E get(int index) {
return get(getArray(), index);
}

可以看到CopyOnWriteArrayList在读取时候不会有锁操作。写操作就复杂一些:

publicboolean add(E e) {
final ReentrantLocklock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

首先需要使用ReentrantLock加锁,
Object[]newElements = Arrays.copyOf(elements, len + 1);
复制数组为一个长度加1的数组。
setArray(newElements);将数组赋值给原引用。

而我们所操作的数据private transient volatile Object[] array;array定义是一个volatile变量。

volatile关键字相当于声明这个被修饰的变量是一个需要频繁更新使用的变量,需要及时的修改。因为在没有同步的情况下,编译器处理器等可能对操作的执行顺序进行一些调整。

java内存模型允许编译器对操作顺序重新进行排序,并且将值暂时缓存到寄存器中。对于cpu操作顺序进行了重新排序。

当声明为volatile时候,这个变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取该变量时候总是最新的。

所以对于array的写操作,即使有多个线程在读取,也不会出现可见性的问题。

好了,刚才我们以及了解到ViewGroup是View的container,并且了解到Jenkins类对于View的管理方式和getViews的实现方式。

我们继续来看:

if(getOwner().getItemGroup().getItem(value) != null) {
这里我们已经知道getOwner方法返回的ViewGroup是Hudson对象,在Hudson类中没有实现getItemGroup方法,那么对于Hudson对象调用getItemGroup方法,实际是执行它继承的类中的方法或者实现的接口中默认的方法。

首先在ViewGroup接口中

default ItemGroup<?extends TopLevelItem> getItemGroup() {
return Jenkins.get();
}

但是在Jenkins类中也有定义

public Jenkins getItemGroup() {
return this;
}

也就是说getItemGroup方法返回的是对象本身。同时AbstractCIBase也实现了ItemGroup。最后getItem方法取出一个Item。Jenkins对于Item的解释是“Basic configuration unit in Hudson.”。

在Jenkins类中对于getItem的实现:

@Override
public TopLevelItemgetItem(String name) throws AccessDeniedException {
if (name==null) return null;
TopLevelItem item = items.get(name);
if (item==null)
return null;
if (!item.hasPermission(Item.READ)) {
if (item.hasPermission(Item.DISCOVER)) {
throw new AccessDeniedException(“Please login to access job ” + name);
}
return null;
}
return item;
}

我们发现items声明是一个Map<String,TopLevelItem>

transient final Map<String,TopLevelItem> items = new CopyOnWriteMap.Tree<>(CaseInsensitiveComparator.INSTANCE);

CopyOnWriteMap.Tree是一个在hudson.util包中CopyOnWriteMap类中的内部类。

publicstatic final class Tree<K,V> extends CopyOnWriteMap<K,V>{
……
}

Tree继承了CopyOnWriteMap类,所以我们首先来看一下CopyOnWriteMap这个类。

publicabstract class CopyOnWriteMap<K,V> implements Map<K,V> {
……
}

**5 CopyOnWriteMap**

CopyOnWriteMap是一个实现了Map接口的抽象类。

CopyOnWriteMap首先声明了两个Map
protectedvolatile Map<K,V> core;
privatevolatile Map<K,V> view;

 

其中view是不可修改的map视图,这个会在后面介绍。

这两个变量使用了volatile关键字来修饰,volatile的作用以及可见性相关的内容在前边的CopyOnWriteArrayList已经介绍过了,这里不再介绍了。

CopyOnWriteMap有两个构造方法

protected CopyOnWriteMap(Map<K,V> core) {
update(core);
}

protected CopyOnWriteMap() {
update(Collections.emptyMap());
}

我们在前边看到实例化Tree的时候使用的是Tree这个构造方法

public Tree(Comparator<K> comparator) {
super(new TreeMap<>(comparator));
this.comparator =comparator;
}

其中super(new TreeMap<>(comparator));会调用protected CopyOnWriteMap(Map<K,V> core)并且传入一个TreeMap的实例,这里简单介绍一下TreeMap。

TreeMap是Map接口的实现类,它继承自AbstractMap抽象类,并且实现了NavigableMap接口。NavigableMap接口继承了SortedMap接口。SortedMap接口是有序Map的实现接口。

NavigableMap接口则是可导航Map接口(如小于指定值的最大值),比如NavigableMap接口中的方法:
K lowerKey(K key);
是Returns the greatest key strictly less than the givenkey, or if there is no such key.
K higherKey(K key);
Returns theleast key strictly greater than the given key, or null if there is no such key.

由此可知TreeMap是个有序并且是可导航的map,而TreeMap是基于红黑树实现的。对于红黑树,每次修改(以及增删)都可能破坏红黑树。所以对于put和remove需要更复杂的逻辑。TreeMap的Entry拥有6个属性:

K key;
V value;
Entry<K,V> left; 左节点
Entry<K,V> right; 右节点
Entry<K,V> parent; 父亲节点
boolean color = BLACK;颜色设置

TreeMap的put方法中

do {
parent =t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t =t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);

 

这里的do-while会从树根开始迭代寻找key的所在位置。

Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);

 

在插入新Entry以后,通过调用fixAfterInsertion方法来修正红黑树,因为此时红黑树可能已经遭到破坏:

private void fixAfterInsertion(Entry<K,V> x) {

//将节点设为红色
x.color = RED;

//循环条件x不为null,非根节点,红色节点
while (x != null && x != root && x.parent.color == RED) {

//如果x节点的父亲节点是x的爷爷节点的左子节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))){

//获取x爷爷节点的右孩子(x父亲节点的兄节点(x的大爷节点))
Entry<K,V> y = rightOf(parentOf(parentOf(x)));

//如果x大爷节点的颜色是红色
if (colorOf(y) == RED) {

//设置x父亲节点为黑色
setColor(parentOf(x), BLACK);

//设置x的大爷节点为黑色
setColor(y,BLACK);

//设置x的爷爷节点为红色(红黑树红色节点的孩子必须是黑色)
setColor(parentOf(parentOf(x)), RED);

//x设置为x的爷爷节点
x = parentOf(parentOf(x));

//如果x的大爷节点不为红色(注意如果x的大爷为null,那么colorOf方法也会返回黑色)
} else {

//如果x是父亲节点的右子节点
if (x == rightOf(parentOf(x))) {

//x设置为x的父亲节点
x= parentOf(x);

//以x为轴左旋操作
rotateLeft(x);
}

//设置x节点的父亲节点为黑色
setColor(parentOf(x), BLACK);

//设置x节点的爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);

//以x的爷爷节点为轴右旋操作
rotateRight(parentOf(parentOf(x)));
}

//如果x的父亲节点是x爷爷节点的右子节点
} else {

//y为x的爷爷节点的左孩子,也就是x的叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));

//如果x叔叔节点为红色
if (colorOf(y) == RED) {

//设置x的父亲节点为黑色
setColor(parentOf(x), BLACK);

//设置x的叔叔节点为黑色
setColor(y, BLACK);

//设置x的爷爷节点为红色
setColor(parentOf(parentOf(x)), RED);

//设置x为x的爷爷节点
x = parentOf(parentOf(x));

//x的叔叔节点为黑色(null)
} else {

//如果x是x父亲节点的左子节点
if (x == leftOf(parentOf(x))) {

//x设置为父亲节点
x= parentOf(x);

//以x为轴右旋
rotateRight(x);
}

//设置x的父亲为黑色
setColor(parentOf(x), BLACK);

//设置x的父亲为红色
setColor(parentOf(parentOf(x)), RED);

//以x的爷爷节点为轴左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}

//设置根节点为黑色
root.color = BLACK;
}

putAll方法中使用了一个buildFromSorted方法,这个方法的作用是将一个SortedMap构造成一个TreeMap。这个方法是一个递归的方法。这个方法实现的算法的逻辑是:首先以一组数据的中间元素作为根(此处中间的坐标取整int mid = (lo + hi) >>> 1;).
递归的构建左子树:

 

 

Entry<K,V> left = null;
if (lo < mid)
left =buildFromSorted(level+1, lo, mid- 1, redLevel,
it, str,defaultVal);

然后再构建右子树。

if (mid <hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
it, str, defaultVal);
middle.right = right;
right.parent = middle;
}

简单的了解了红黑树以及TreeMap以后,我们来继续介绍CopyOnWriteMap以及CopyOnWriteMap.Tree。

既然是cow机制,基本与前面介绍的CopyOnWriteArrayList的形式相似。get方法(读操作没有锁操作),也没有使用视图。

public V get(Objectkey) {
return core.get(key);
}

下面着重说一下put方法(写操作)

publicsynchronized V put(K key, V value) {
Map<K,V> m = copy();
V r = m.put(key,value);
update(m);

return r;
}

Jenkins中这个put实现没有像java.util.concurrent包中的CopyOnWriteArrayList那样使用ReentrantLock,而是使用了synchronized关键字,在1.6以后对synchronized的性能有了大幅度的优化,与ReentrantLock的性能差距已经非常小了,在大部分场景这种差距可以忽略。

在synchronized修饰的方法内首先调用了copy方法,这个方法在Tree中实现:

protected Map<K,V> copy() {
TreeMap<K,V>m = new TreeMap<>(comparator);
m.putAll(core);
return m;
}

实现的逻辑非常的简单,调用了TreeMap的putAll方法将core赋予新的TreeMap m。然后在put方法将新KV put 到m中。最后执行update方法:

protectedvoid update(Map<K,V> m) {
core = m;
view = Collections.unmodifiableMap(core);
}

可以看到这个方法将新的Map赋予给了core。
view =Collections.unmodifiableMap(core);则创建了一个core的不可修改的副本。不可修改主要体现在比如UnmodifiableMap的put方法:

public V put(K key, V value) {
throw new UnsupportedOperationException();
}

在上述介绍完Jenkins中hudson.util包实现Cowmap(CopyOnWriteMap)以后,我们基本介绍完了checkJobName方法的逻辑以及关联到的java技术点。

K8S中文社区微信公众号

评论 抢沙发

登录后评论

立即登录