Jenkins源碼閱讀指北,一文看懂Jenkins用到的java技術點

  • 2019 年 10 月 4 日
  • 筆記

引言:

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技術點。