鑒于 Oracle 最近對 Java 許可模式的變更,了解在您的環境中運行 Oracle Java 的風險和暴露至關重要。
譯自 How To Protect Your Java Against Licensing Liability Risks,作者 Jay Patel。
因此,您已從您的環境中刪除了所有需要昂貴的 Oracle Java SE 通用許可證的 Oracle JDK,該許可證與 Oracle 主協議 (OMA) 相關。但是,現在您需要阻止 Oracle Java 偷偷潛入并觸發對 Oracle 的新許可義務。以下介紹了禁用 JavaUpdater 和其他當前和常見方法、它們的有效性和缺點,以及提供了一些供您考慮的其他機制。
2023 年 1 月,Oracle 將 Java 的許可要求更改為“所有員工指標”。具體內容應在他們的實際協議中進行審查,但為了討論的目的,讓我們將該協議改寫為:如果您安裝了一個或多個 Oracle Java SE 實例,無論安裝位置如何,都屬于 Oracle 技術網絡 (OTN) 許可證,您的組織可能需要遵守“所有員工”許可要求,無論有多少員工使用 Java 甚至擁有或使用計算機。您組織可能面臨的重大財務風險需要持續警惕。
Oracle Java 可以通過多種方式安裝或更新。其中一些方法僅適用于桌面;一些適用于服務器。應考慮這兩種環境,因為它們都受相同的 Oracle 許可要求約束。Oracle Java 可以通過多種方式安裝或更新。
以下是針對上述重點內容的一些注意事項:
Java 已經存在了近 30 年。在此期間,它經歷了許多迭代,包括從免費到付費,具體取決于提供商。由于 Java 的普及以及 Oracle Java 在許多用例中免費提供的事實,Oracle Java 通過許多網絡渠道、分銷合作伙伴和學術機構提供,其中許多機構沒有費心維護或刪除公開可用的 JDK 和 JRE 的完整功能副本。這些較舊的二進制文件從許可的角度來看通常不會造成麻煩(安全性是另一回事),但附帶的 Java 更新程序和 Java 控制面板小程序使用戶很容易無意中將其安裝更新到需要 Oracle 許可證的版本。
我們將討論適用于最常見版本(Java 6-21)的策略,但您應該知道,Oracle Java 的某些特定版本需要許可證。這些版本是 8u211+、11、12、13、14、15、16。此外,Oracle 將“商業功能”的使用視為其許可要求的另一個觸發因素,例如使用其 MSI (Microsoft Windows 安裝程序) 或使用 JFR(Java Flight Recorder)。
注意:2024 年 9 月,Oracle Java 17 也將需要付費的 OTN 許可證才能用于商業用途。
做:
不要做:
Oracle Java 包含一個默認情況下啟用的后臺(TSR 或終止并駐留)進程,用于檢查 Java 的新版本。
如果不影響已安裝 Java 的當前工作狀態,這些項是可以禁用的。Azul 建議在注冊表級別將其移除或至少禁用這些項,并禁止用戶使用或重新啟用這些項。某些早期的 Oracle JVM/JRE/JDK 版本允許卸載更新器;較新的內部版本和版本不分離該組件。請注意,修改或移除 JDK 的任何部分都是 Oracle 條款的違規行為,然而,限制訪問任何文件并不違規。
Oracle Java JRE 中包含的控制面板小程序(Windows 上的 javacpl.exe)提供了一個圖形界面來控制 Oracle JVM 的許多方面。我將在這里提到 WebStart,即使它沒有與 Oracle JVM 分開,因為控制面板小程序與 WebStart 關系密切,并且等效功能由開源社區(在 IcedTea-Web 開源項目中)單獨實現。控制面板小程序包含一個選項卡,允許用戶通過以下幾種方式更新他們的 Java:
通過設置自動化或單擊“立即更新”按鈕,用戶可以啟動下載其 JRE/JDK 的最新版本。
做:
不要做:
許多企業使用黃金映像來實例化虛擬機,用于各種用例,從快速現場測試和敏捷開發到生產級機器的標準化。這些映像通常包含通用或企業強制執行的軟件,并且可能包含用于服務器的 JRE 或用于開發映像的 JDK。由于這些映像在實例化之前一直處于脫機狀態,磁盤處于存檔狀態,因此它們可能處于休眠狀態,不會被正常的掃描/觀察機制捕獲。如果它們仍然存在于您的環境中,如果它們在生產場景中被實例化和使用,它們確實會構成風險。
做:
不要做:
隨著時間的推移,備份等將不再是一個問題;它們會自然地老化。但在遷移后的這段時間內,應特別注意跟蹤調用任何這些恢復機制的系統。應注意備份本身;據觀察,Oracle 在某些情況下已表明備份文件仍然可以輕松獲得并輕松啟用,因此構成許可證風險違規。從備份恢復的系統將受您現有的軟件執行策略的約束,因此請確保盡快捕獲并修復它們。修復后,應進行新的備份/快照,如果可能,應將以前的備份移至脫機狀態并使其“不可輕松獲得”。
做:
不要做:
如果用戶在命令提示符下鍵入“java”或應用程序在未初始化(沒有 Java)的 MacOS 系統上請求 Java,您將從操作系統收到一個響應,告訴您訪問 java.com:
做:
不要做: - 訪問 java.com 并安裝其中提到的任何版本。
這種情況需要謹慎處理,并可能需要與軟件供應商進行溝通。有些應用程序在安裝過程中會合法地包含 Java JRE 或 JDK,例如使用 Java Compact Profile 的低功耗/物聯網設備、在網絡受限環境中使用的虛擬機映像等。在這些情況下,務必正確理解應用程序的支持要求和許可責任。
在大多數情況下,供應商現在會支持其應用程序在 TCK(技術兼容性工具包)認證的 OpenJDK 構建 上運行。如果供應商確實將他們的軟件認證為 OpenJDK 兼容,那么選擇任何你想要的 OpenJDK 發行版就足夠了。供應商可能會認證特定發行版,但這可能是由于方便或他們缺乏了解。供應商應該將他們的測試視為針對 OpenJDK 規范,這意味著任何經過認證的 OpenJDK 都足夠。如果供應商使用特定于發行版的類,他們應該專門記錄該要求。
在一些罕見的情況下,供應商會認為使用任何 JRE 而不是規定的 Oracle JRE 將使他們的支持合同失效。在這種情況下,你應該要求提供上述要求的書面文件,以及一份聲明,說明在你的環境中出于此目的使用 Oracle Java 不會使你的組織受到 Oracle 全員許可模式的約束。
做:
不要做:
只需一行簡單的腳本就可以創建一個 HTML 按鈕,讓用戶只需單擊一下即可下載 Oracle JDK,并創建一個新的許可責任。雖然這些情況通常超出了你的控制范圍,但你可以采取一些主動措施。
做:
不要做:
防止持續的 Oracle Java 許可風險是可行的,但現在應該清楚的是,僅靠邊界策略不是最佳方法。它需要與緩解策略和合理的警惕性相結合。
由于 Java 的普遍性和 Oracle Java 許可模式的最新變化,如果你繼續允許 Oracle Java 在你的環境中運行,了解你的風險和暴露非常重要。
避免代價高昂的許可責任需要持續的警惕和分類。以上指南作為“盡力而為”的方法提供,我相信還有其他機制可以將 Oracle Java 重新引入你的環境。
請隨時發表評論并添加您認為可能影響他人的任何發現,我們將盡力提供有關如何進行分類的建議。如果您需要更多詳細信息或有任何不適合在公開論壇上分享的疑慮,請聯系 migration@azul.com,我們將直接回復您。
前兩篇文章我介紹了AtomicInteger和AtomicIntegerArray,現在總結一下兩個類的特點。
1:AtomicInteger是對單個變量進行原子操作的。 2:AtomicIntegerArray是對數組中的元素進行原子操作的,而不是數組本身。
本篇文章介紹第三類原子操作:AtomicIntegerFieldUpdater.
1:關于private/protected/public訪問控制符 2:AtomicIntegerFieldUpdater的一些限制 3:AtomicIntegerFieldUpdater實例 4:AtomicIntegerFieldUpdater源碼解析
由于下面要用到Java這個知識點,所以我們在復習一下關于Java的訪問控制符,在Java中主要定義三個訪問控制符,還有一個默認的(沒有被任何訪問控制符修飾default),一共四個訪問控制符
1:private:私有的,只能自己能訪問,其他都訪問不到 2:默認的(default,如果沒有被任何訪問控制符修飾):同一類或者同一個包中能夠訪問到,其他訪問不到。 3:protected:同一個類、子類、同一個包能放到,其他訪問不到。 4:public:所有類都能訪問到。
他們的訪問范圍如圖:
通過一個表格更加清晰的看一下:
這個知識點大家都非常熟悉了,理解了訪問控制符,AtomicIntegerFieldUpdater的一些知識點就好理解了。
接下來的文章內容如果沒有特殊說明,AtomicIntegerFieldUpdater更新的字段簡稱為被操作的字段。
1:被操作的字段必須被volatile修飾 2:被操作的字段必須是int類型,不能是包裝類型,如Integer 3:被操作的字段只能是實例變量,不能是類變量,就是不能被static修飾 4:被操作的字段只能是可變變量,不能被final修飾,其實final和volatile不能同時存在,否則編譯不通過。 5:如果調用方無法直接訪問到被操作字段,則會拋出異常
關于第5點就是用到了上面所說的訪問控制權限:
1:如果調用方和被操作字段所在的類是同一類,那么被操作字段可以被private/defalut/protected/public修飾。 2:如果調用方和被操作字段所在的類是同一個包,那么被操作字段可以被defalut/protected/public修飾 3:如果調用方和被操作字段所在的類屬于子類,那么被操作字段可以被protected/public修飾 4:如果調用方和被操作字段所在的類不在同一個包,也不屬于子類關系,那么只能被public修飾。
上面提出了它的限制后,下面我們通過實例去一個一個的證明:
1:首先證明AtomicIntegerFieldUpdater可以原子更新一個對象中被volatile修飾的int類型的字段
下面的實例調用對象AtomicIntegerFieldUpdater和被操作字段所在的類在同一個包中
調用對象:AtomicIntegerFieldUpdater
package com.stqsht.juc.atomic; public class AtomicIntegerFieldUpdaterTest { //定義一個線程池 private static ThreadPoolExecutor pool; static { pool=new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000)); } public static void main(String[] args) throws InterruptedException { Obj obj=new Obj(0, 0); AtomicIntegerFieldUpdater<Obj> afu=AtomicIntegerFieldUpdater.newUpdater(Obj.class, "age"); for (int i=0; i < 5; i++) { pool.execute(() -> { for (int j=0; j < 10000; j++) { obj.setId(obj.getId() + 1); afu.incrementAndGet(obj); } }); } pool.awaitTermination(5, TimeUnit.SECONDS); System.out.println("id=" + obj.getId()); System.out.println("age=" + obj.getAge()); pool.shutdown(); } }
被操作字段所在的類Obj中有兩個字段,一個字段沒有被volatile修飾,一個字段被volatile修飾
package com.stqsht.juc.atomic; class Obj { private int id; volatile int age; public Obj(int id, int age) { this.id=id; this.age=age; } //省略getter和setter方法 }
通過運行多次后,得出一個結論,每一次id的結果都不相同,而age得到的結果相同且是正確的,運行結果如下:
從上述案例可以證明,AtomicIntegerFieldUpdater可以原子的更新一個對象中的字段。
2:如果被操作的字段沒有關鍵字volatile,則會拋出異常:Must be volatile type
class Obj { private int id; int age; public Obj(int id, int age) { this.id=id; this.age=age; } //setter和getter省略 }
上面的被修飾的字段age并沒有關鍵字volatile,運行代碼結果如下:
從運行結果可以看出,如果被操作的字段沒有被volatile修飾,那么它直接拋出異常,因為多線程下volatile具有可見性和有序性。
3:被操作的字段只能是int類型,不能是包裝類型Integer
class Obj { private int id; volatile Integer age; public Obj(int id, int age) { this.id=id; this.age=age; } //setter和getter省略 }
上面的被操作字段是一個包裝類Integer,運行結果如下圖:
4:被操作的字段只能是實例變量,不能是類變量,也就是被操作字段不能被static修飾
class Obj { private int id; static volatile int age; public Obj(int id, int age) { this.id=id; this.age=age; } //setter和getter省略 }
運行結果如下圖:
5:被操作的字段只能是可變變量,不能被final修飾,其實final和volatile不能同時存在,否則編譯不通過。
6:如果調用方無法直接訪問到被操作字段,則會拋出異常
6.1:如果調用方和被操作字段所在的類是同一個類,被操作字段可以被private/defalut/protected/public修飾
public class User { public static void main(String[] args) { AtomicIntegerFieldUpdater<User>ai=AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); User user=new User(0); ai.incrementAndGet(user); System.out.println(user.getAge()); } //可以被private/defalut/protected/public修飾 private volatile int age; public User(int age) { this.age=age; } //省略setter和gettter方法 }
大家可以親自測試以下上面的demo,無論成員變量age被什么訪問控制符修飾,都是可以的。
6.2:如果調用方和被操作字段所在的類在同一個包中,被操作字段可以被defalut/protected/public修飾,用private則會拋出異常
調用方:Caller
package com.stqsht.juc.other; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; //調用方 public class Caller { public static void main(String[] args) { User user=new User(0); AtomicIntegerFieldUpdater<User> ai=AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); ai.incrementAndGet(user); System.out.println(user.getAge()); } }
被操作字段所在的類:User
package com.stqsht.juc.other; //調用方 public class User { //可以被default/protected/public修飾 volatile int age; public User(int age) { this.age=age; } //省略setter和getter方法 }
如果上面的例子如果被private修飾,則會拋出如圖的異常:
6.3:如果調用方和被操作字段所在的類屬于子類關系,被操作字段可以被protected/public修飾
調用方:com.stqsht.juc.other.Caller
package com.stqsht.juc.other; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import com.stqsht.juc.common.User; //調用方 public class Caller extends User { public static void main(String[] args) { Caller user=new Caller(0); AtomicIntegerFieldUpdater<User> ai=AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); ai.incrementAndGet(user); System.out.println(user.getAge()); } public Caller(int age) { super(age); } }
被操作字段所在的類:com.stqsht.juc.common.User
package com.stqsht.juc.common; //被操作字段所在的類,可以被protect/public修飾 public class User { volatile int age; public User(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age=age; } }
如果上面的例子被private/defalut修飾,則會拋出以下異常。
6.4:如果調用方和被操作字段所在的類不在同一個包,也不屬于子類關系,那么只能被public修飾。
調用方:com.stqsht.juc.other.Caller
package com.stqsht.juc.other; import com.stqsht.juc.common.User; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; //調用方 public class Caller { public static void main(String[] args) { User user=new User(0); AtomicIntegerFieldUpdater<User> ai=AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); ai.incrementAndGet(user); System.out.println(user.getAge()); } }
被操作字段所在的類:com.stqsht.juc.common.User
package com.stqsht.juc.common; public class User { protected volatile int age; public User(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age=age; } }
如果上面的代碼被private/defalut/protected修飾,則會拋出如下異常
上面列出了AtomicIntegerFieldUpdater的一些限制,我們知道了怎么使用AtomicIntegerFieldUpdaterle ,那從源碼角度,它是怎么有這些限制呢,我們進入它的源碼一探究竟。
當我們跟進源碼中看到它是一個抽象類,它只有一個方法能夠獲取一個實例對象。
public abstract class AtomicIntegerFieldUpdater<T> { @CallerSensitive public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) { return new AtomicIntegerFieldUpdaterImpl<U> (tclass, fieldName, Reflection.getCallerClass()); } //省略其他代碼 } 參數1:tclass:實例的Class對象,例如上面例子User.class 參數2:fieldName:被操作的字段的名稱,例如上面例子成員變量age
通過一個newUpdater()方法獲取一個實例對象,從這個方法中可以看出他的實現類是AtomicIntegerFieldUpdaterImpl,這個實現類在它的內部,我們一起跟進它的實現類。
private static final class AtomicIntegerFieldUpdaterImpl<T> extends AtomicIntegerFieldUpdater<T> { //原子操作封裝對象 private static final sun.misc.Unsafe U=sun.misc.Unsafe.getUnsafe(); private final long offset; //調用方的Class對象,如上面例子Caller.class private final Class<?> cclass; //被操作字段所在的類的Class對象,如上面例子User.class private final Class<T> tclass;
上面是子類的幾個重要的成員變量,下面我們看一下它的核心構造函數
//參數一:tclass:被操作字段所在類的Class對象 //參數二:被操作字段的名稱 //參數三:調用方的Class對象 AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller) { final Field field; final int modifiers; try { //通過反射獲取被操作字段 field=AccessController.doPrivileged( new PrivilegedExceptionAction<Field>() { public Field run() throws NoSuchFieldException { return tclass.getDeclaredField(fieldName); } }); //獲取被操作字段修飾符 modifiers=field.getModifiers(); //判斷訪問控制符的權限 sun.reflect.misc.ReflectUtil.ensureMemberAccess( caller, tclass, null, modifiers); ClassLoader cl=tclass.getClassLoader(); ClassLoader ccl=caller.getClassLoader(); if ((ccl !=null) && (ccl !=cl) && ((cl==null) || !isAncestor(cl, ccl))) { sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass); } } catch (PrivilegedActionException pae) { throw new RuntimeException(pae.getException()); } catch (Exception ex) { //$5:如果調用方不能直接訪問被操作字段,則拋出異常 throw new RuntimeException(ex); } if (field.getType() !=int.class) //$2:如果不是int類型,則會拋出異常 throw new IllegalArgumentException("Must be integer type"); if (!Modifier.isVolatile(modifiers)) //$1:如果沒有被volatile修飾,則拋出異常 throw new IllegalArgumentException("Must be volatile type"); this.cclass=(Modifier.isProtected(modifiers) && tclass.isAssignableFrom(caller) && !isSamePackage(tclass, caller)) ? caller : tclass; this.tclass=tclass; //$3:objectFieldOffset()方法的作用:獲取某個字段相對Java對象的起始地址的偏移地址 this.offset=U.objectFieldOffset(field); }
這個構造函數涵蓋了AtomicIntegerFieldUpdater的所有限制,所以從源碼角度也證明了上面總結的限制。這個構造函數的流程圖如下:
AtomicIntegerFieldUpdater的核心方法和AtomicInteger類似,功能也是一樣的:原子更新一個被volatile修飾的變量,這里就不在介紹了,如果不太明白這些方法的用法,請進入我的主頁查看
接下來介紹幾個特殊的方法:判斷調用方和被操作字段所在的類是否在同一個包,是否是子類關系。
第一個方法:isAncestor:判斷是否屬于子類關系
private static boolean isAncestor(ClassLoader first, ClassLoader second) { ClassLoader acl=first; do { acl=acl.getParent(); if (second==acl) { //屬于子類,返回true return true; } } while (acl !=null); return false; }
第二個方法:isSamePackage:判斷是否在同一個包