.NET Core前後端分離快速開發框架(Core.3.0+AntdVue)

.NET Core前後端分離快速開發框架(Core.3.0+AntdVue)

目錄

引言

時間真快,轉眼今年又要過去了。回想今年,依次開源發布了Colder.Fx.Net.AdminLTE(254Star)、Colder.Fx.Core.AdminLTE(335Star)、DotNettySocket(82Star)、IdHelper(47Star),這些框架及組件都是本着以實際出發,實事求是的態度,力求提高開發效率(我自己都是第一個使用者),目前來看反響不錯。但是隨着前端和後端技術的不斷變革,尤其是前端,目前大環境已經是前後端完全分離為主的開發模式,在這樣的大環境和必然趨勢之下,傳統的MVC就顯得有些落伍了。在這樣的背景下,一款前後端分離的.NET開發框架就顯得尤為必要,由此便定了框架的升級目標:前後端分離。

首先後端技術的選擇,從目前的數據來看,.NET Core的發展遠遠快於.NET Framework,最簡單的分析就是Colder.Fx.Core.AdminLTE發布比Colder.Fx.Net.AdminLTE晚,但是星星卻後來居上而且比前者多30%,並且這個差距在不斷擴大,由點及面的分析可以看出我們廣大.NET開發人員學習的熱情和积極向上的態度,並不是某些人所認為的那麼不堪(走自己的路,讓別人說去吧)。大環境上微軟积極擁抱開源,大力發展.NET Core, 可以說前途一片光明。因此後端決定採用 .NET Core3.0 ,不再浪費精力去支持.NET Framework。

然後是前端技術選擇,首選是三大js框架選擇,也是從實際出發,Vue相對其它而言更加容易上手,並且功能也毫不遜色,深得各種大小公司喜歡,如果偏要說缺點的話,那就是對TS支持不行,但是即將發布Vue3.0肯定會改變這一缺陷。選擇了Vue之後,然後就是UI框架的選擇了,這裏的選擇更多了,我選擇了Ant Design Vue,理由便是簡潔方便,十分符合我的設計理念。

技術選型完畢之後便進入研發,由於鄙人前端比較菜,因此需要從頭學Vue2.x全家桶,從開始到現在差不多經歷3個月,在預期之內。其實學習並使用前端的Vue2.x全家桶並不難,還是比較容易上手的,所以在此給沒有前後端分離開發經驗的老哥打一記預防針,不要退縮,要知難而上,學習永無止境。

某些老哥可能比較直接粗暴,嫌我BBB嘮叨,下面直接上地址
代碼(GitHub):
文檔(GitHub):
代碼(碼雲鏡像):
文檔(碼雲鏡像):
在線預覽地址:
由於服務器是阿里雲的86服務器,帶寬1M小水管,因此將前端部署到碼雲上了,在此多謝碼雲,後端部署在阿里雲86服務器CentOS7上。整個技術棧使用.NET Core + PostggreSQL+ Ant Design Vue + CentOS7+Nginx+Dokcer+jenkins,囊括了從快速開發到自動化部署一條龍,開源免費並具有高性能、高移植性、高拓展性(小公司創業選型+個人接單利器)

簡介

本框架為.NET Core3.0+Ant Design Vue版

本框架旨在極大的提高開發效率

使用技術棧:
後端:採用.NET Core平台,ASP.NET Core3.0,C#語言(使用反射等高級技術),Entity FrameworkCore(數據庫ORM框架)。
使用數據倉儲模式,抽象化數據庫操作(CRUD等)、支持事務處理以及分佈式事務處理(跨庫)
支持數據庫讀寫分離、分庫分表及事務(僅支持單表操作,不支持多表) 全面採用Autofac作為IOC容器,面向接口編程,全面解耦
集成多種工具類庫以及操作拓展
數據庫:支持SqlServer,PostgreSQL,MySQL,Oracle(框架使用簡單工廠,工廠方法,抽象工廠,可輕鬆更換數據庫),Redis作為分佈式緩存
前端:Vue2.x全家桶+Ant Design Vue,其中集成常用組件,力求方便項目開發。

具體技術實施:
項目採用前後端完全分離模式,並採用嚴格分層模式,極大的增加聚合度,降低耦合度,
提高代碼的健壯性,可維護性。
前後端通過JWT進行身份驗證,通過數據接口操作數據,統一使用JSON作為數據格式,並使用默認接口簽名算法保證接口的安全性。

功能架構部分詳解:
快速開發:此功能為框架的核心,通過選擇數據庫中的表,就能自動生成對應的實體層、業務邏輯層、控制器、前端頁面Vue文件,無需編寫代碼即可生成基本的CRUD操作。
接口密鑰管理:管理接口的密鑰,若開啟接口簽名的規則,則前端需要通過給接口簽名才能夠正常訪問後台接口。
權限管理:使用基本的RBAC權限控制,支持操作權限、接口權限以及數據權限

框架主要功能及特色如下

功能 詳細描述
用戶登錄 用戶登錄、密碼修改
系統用戶管理 系統用戶管理
角1色管理 角色管理
部門管理 部門管理、天然多級
權限管理 使用RBAC權限體系,基於角色的權限管理,支持菜單權限、操作權限(按鈕權限)、接口權限、數據權限
系統日誌 支持多彩控制台、文件、數據庫和ElasticSearch記錄日誌,框架全局異常自動捕捉,多級別記錄
快速開發 通過數據庫直接生成實體層、業務邏輯層、控制器層以及前端頁面Vue代碼,無需編碼即可實現CURD
數據庫操作封裝 使用基於EF的倉儲模式、封裝常用的CURD,支持單庫事物和分佈式事物
多數據庫支持 使用基於EF的倉儲模式,支持各大主流關係型數據庫(SQLServer、MySQL、PostgreSQL和Oracle)
超強移植性 使用抽象工廠封裝數據庫操作層,切換數據庫0代碼修改
支持軟刪除 一鍵切換刪除模式,支持物理刪除和軟刪除,對業務操作透明
緩存支持 支持系統自帶緩存和Redis緩存、封裝操作接口、簡單易用
前後端完全分離 前端使用Vue2.x全家桶+Ant Design Vue,界面簡潔美觀,組件化開發
集成JWT驗證 框架使用JWT作為身份驗證,擺脫Cookie苦海,分佈式拓展毫無壓力
集成對外接口簽名算法 框架對外接口可以開啟超強嚴格簽名校驗(防抵賴、防偽造、防重複調用),保障系統安全性
頁面響應式 前端為單頁應用,無iframe,各大主流瀏覽器支持友好
拓展 其它各種幫助類庫及插件

其相關版本請看下錶:

.NET版本 前端UI 地址
Core3.0 Ant Design Vue
Core2.2 AdminLTE
.NET4.72 AdminLTE
.NET4.52 Easyui
Core2.1 Easyui
.NET4.0 Easyui

後台效果展示如下:

環境搭建

開發環境要求:

操作系統:Windows 10
後端開發工具:Visual Studio 2019+
前端開發工具:Visual Studio Code,安裝nodejs,yarn
SDK:安裝.NET Core SKD 3.0 及以上
數據庫:SQLServer2008 R2及以上

基礎數據庫構建:

  • 數據庫創建
    目錄”/docs/初始化文件”中有所需的數據庫資料

    先手動創建數據庫,然後執行對應的SQL腳本即可

  • 連接字符串配置
    打開src目錄下Colder.Admin.AntdVue的解決方案,如下圖

如下圖所示依次展開05.Coldairarrow.Api/appsettings.json,配置數據庫連接字符串,name不用修改,connectionString改為上述創建的數據庫(若不清楚數據庫連接字符串請自行百度搜索教程)

數據庫設計規範

由於本框架支持自動生成代碼的核心功能,此功能是根據數據庫的表結構來生成代碼的
因此規定每張表都有一個主鍵,列名為Id,類型為字符串,實際添加數據時默認使用SnowflakeId(雪花Id,Twitter設計的分佈式趨勢自增Id,若不清楚請自行百度相關資料),表中的每個列都需要有描述信息(建議這樣操作,若不按照這個標準則需要一些額外的改動才能夠成功運行)。每張表都需要Id、CreateTime、CreatorId、CreatorRealName這幾個必帶字段

運行

後端:打開解決方案=>還原nuget包=>配置數據庫=>運行(05.Coldairarrow.Api設為啟動項目)
後台成功運行後會自動打開swagger

前端: 確保已安裝nodejs和yarn
用VS Code 打開文件夾src\Coldairarrow.Web

輸入命令:yarn
輸入命令:yarn run serve
成功運行后即可打開登錄頁面

輸入賬號:Admin,密碼:123456進入後台主頁

使用教程

全局配置

在01.Coldairarrow.Util中的GlobalSwitch類中,設置了各個參數,其中RunModel與DatabaseType需要重點關注一下,其它參數請看註釋。

快速開發

此功能為本框架的核心功能,能夠自動生產完整的可運行代碼,具體使用如下:

  • 配置數據源

首選需要有數據庫源,因為代碼生成是根據數據庫表來生成的。
菜單:開發=>數據庫連接

若列表中沒有目標數據源,則添加數據庫連接

數據連接名、連接字符串、數據庫類型即可。添加完成后即可看到連接字符串信息。

  • 生成代碼

有了數據庫連接之後,即可進行代碼生成。
菜單:開發=>代碼生成

選擇數據庫,然後勾選需要生成代碼的數據庫表,點擊生成代碼會彈出生成選項(這裏演示勾選 Base_BuildTest,其餘表全是系統基礎表,不要勾選,否則會被覆蓋,導致異常,請勾選自己的業務表進行生成):

生成選項中可以選擇需要生成的類型,默認生成全部:實體層、業務層、接口層(即控制器)和頁面(Vue前端頁面代碼)。
生成區域請輸入業務模塊名(例如系統叫Base_Manage),請按具體業務填入(必填)
這裏示例填寫TestManage,點擊生成按鈕,即可完成代碼生成。生成后的代碼在項目解決方案中,將代碼文件包括進入項目

默認生成后的文件會被自動包括到解決方案中,若沒有則需要點擊显示所有文件按鈕,即可看到生成后的新文件

生成的實體層、業務邏輯層、控制器層代碼:

前端生成的代碼:

後端代碼添加后需要重新編譯下(F7),編譯后好可以看到有新的接口

前端生成代碼後會自動保存並編譯(別的文件修改保存也會自動編譯,畢竟編譯一次挺慢的),不放心可以Ctrl+C停止,然後再yarn run serve重新運行

代碼生成完畢,但是要展示到菜單上需要進行配置,並且所有業務菜單都是動態的(權限控制)
打開菜單:系統管理=>權限管理=>新建

菜單名:即需要显示的菜單名
上級菜單:菜單是多級樹狀的,可以放在某個菜單下面
類型:可以選擇菜單或頁面,這裡是具體的頁面,所以選頁面
路徑:頁面的路徑,菜單可以不配置,這裏配置為/TestManage/Base_BuildTest/List,這裏設置規則為:views文件夾為根路徑(即”/”),向下展開依次為/TestManage/Base_BuildTest/List,最後真正的列表頁為List.vue文件,表單頁為EditForm.vue文件
是否需要權限:若為“否”,則此頁面不限制權限,即所有人都能看
圖標:菜單显示圖標,具體到開發=>圖標選擇頁進行選擇
排序:若需要需要對菜單進行排序則設置,默認0,類型為int
頁面權限:頁面擁有的權限(權限值唯一,作為操作權限即接口權限的依據),詳見權限管理

確認保存,然後刷新整個頁面(F5),即可看到全新生成的菜單“生成測試”

整個代碼生成過程,無需編寫代碼即可完成一張表的CRUD,當然需要根據具體業務中進行相應的修改,本次示例中字段比較少,但是當一張表的字段很多時,那麼此功能能夠將開發效率提高几個檔次。

管理員登錄

默認超級管理員賬號為:Admin
密碼為:123456

系統用戶管理

管理系統登錄的用戶
菜單:系統=>用戶管理
點擊右側設置權限

系統角色管理

管理系統角色,角色是權限的載體,合理分配角色有利於權限管理
菜單:系統=>角色管理
操作中可以設置角色的權限,詳情見 模塊

權限管理

一般情況下,後台管理系統多少會涉及權限管理,因此本框架提供了一個靈活、高效、簡潔的權限管理系統。

首先,權限分為兩種,即操作權限和數據權限,其中操作權限主要包括菜單權限和按鈕權限,在角色編輯表單中,給角色勾選需要的權限即可,用戶可以分配多個角色,繼承所有角色的權限。

如上圖,按需勾選,有人會疑問為什麼只有增、改和刪的權限而沒有查的權限,其實這裏考慮到了當勾選“用戶管理”時,就已經默認擁有“查”的權限了,這裏再設置“查”權限豈不是多此一舉,可以根據實際業務需求添加諸如申請、審核等權限,靈活應用。
這裏的樹狀是和菜單一一對應的,勾選哪些菜單就擁有哪些菜單

下面介紹下最常用的按鈕權限

如上圖,在需要控制權限的按鈕使用v-if=”hasPerm(‘Base_User.Add’)”,表示只有當用戶擁有權限值Base_User.Add才显示此按鈕,權限值就是權限表單中配置的權限值

這裏的按鈕級權限控制只能夠在前端控制操作入口,若系統對安全性要求較高,則需要控制接口權限

如上圖,在需要權限控制的接口加上ApiPermission特性即可,參數也為權限值,只有擁有此權限才能訪問此接口

數據權限比較複雜,若採用純SQL方式,那麼會更加複雜,本框架全程採用EF作為ORM框架,通過對IQueryable< T >進行過濾,即可完成數據權限控制,詳細使用方式見用戶管理。

更詳細的使用方式,請參考源代碼中的用戶管理模塊(菜單權限、操作權限、數據權限、聯表查詢、業務層AOP等使用方式均可參考此模塊)。

接口秘鑰管理

作為對外接口簽名的AppId和AppSecret管理
菜單:系統=>接口秘鑰管理

系統日誌

菜單:系統=>系統日誌

單庫事務

使用方式如下:

BaseBusiness<Base_UnitTest> _baseBus  = new BaseBusiness<Base_UnitTest>();
/*RunTransaction傳入執行具體數據庫操作的Action,操作中若出現異常會自動回滾事務,業務只需根據返回的transActionRes進行處理,返回類型為元組(bool Success, Exception ex),Success表示事務是否成功,ex為事務失敗異常信息*/
var transActionRes = _baseBus.RunTransaction(() =>
{
    var newData = _newData.DeepClone();
    newData.Id = Guid.NewGuid().ToString();
    newData.UserId = IdHelper.GetId();
    newData.UserName = IdHelper.GetId();
    _baseBus.Insert(_newData);
    _baseBus.Insert(newData);
});

跨庫事務

使用方式如下:

//第一個數據庫
IRepository _bus1 = DbFactory.GetRepository();
//另一個數據庫
IRepository _bus2 = DbFactory.GetRepository("BaseDb_Test");
_bus1.DeleteAll<Base_UnitTest>();
_bus2.DeleteAll<Base_UnitTest>();
Base_UnitTest data1 = new Base_UnitTest
{
    Id = Guid.NewGuid().ToString(),
    UserId = "1",
    UserName = Guid.NewGuid().ToString()
};
Base_UnitTest data2 = new Base_UnitTest
{
    Id = data1.Id,
    UserId = "1",
    UserName = Guid.NewGuid().ToString()
};
Base_UnitTest data3 = new Base_UnitTest
{
    Id = Guid.NewGuid().ToString(),
    UserId = "2",
    UserName = Guid.NewGuid().ToString()
};

new Action(() =>
{
    //創建並執行事務,使用方式與單庫一致
    var succcess = DistributedTransactionFactory.GetDistributedTransaction(_bus1, _bus2)
        .RunTransaction(() =>
        {
            _bus1.ExecuteSql("insert into Base_UnitTest(Id) values('10') ");
            _bus1.Insert(data1);
            _bus1.Insert(data2);
            _bus2.Insert(data1);
            _bus2.Insert(data3);
        });
    Assert.AreEqual(succcess.Success, false);
    Assert.AreEqual(_bus1.GetIQueryable<Base_UnitTest>().Count(), 0);
    Assert.AreEqual(_bus2.GetIQueryable<Base_UnitTest>().Count(), 0);
})();

讀寫分離分庫分表

本框架支持數據庫讀寫分離分庫分表(即Sharding),並且支持主流關係型數據庫(SQLServer、Oracle、MySQL、PostgreSQL),理論上只要EF支持那麼本框架支持。
由於技術原因以及結合實際情況,目前本框架僅支持單表的Sharding,即支持單表的CRUD、分頁、統計(數量、最大值、最小值、平均值),支持跨庫(表分散在不同的數據庫中,不同類型數據庫也支持)。具體如何使用如下:

  • Sharding配置
    首先、要進行分庫分表操作,那麼必要的配置必不可少。配置代碼如下:
ShardingConfigBootstrapper.Bootstrap()
    //添加數據源
    .AddDataSource("BaseDb", DatabaseType.SqlServer, dbBuilder =>
    {
        //添加物理數據庫
        dbBuilder.AddPhsicDb("BaseDb", ReadWriteType.ReadAndWrite);
    })
    //添加抽象數據庫
    .AddAbsDb("BaseDb", absTableBuilder =>
    {
        //添加抽象數據表
        absTableBuilder.AddAbsTable("Base_UnitTest", tableBuilder =>
        {
            //添加物理數據表
            tableBuilder.AddPhsicTable("Base_UnitTest_0", "BaseDb");
            tableBuilder.AddPhsicTable("Base_UnitTest_1", "BaseDb");
            tableBuilder.AddPhsicTable("Base_UnitTest_2", "BaseDb");
        }, new ModShardingRule("Base_UnitTest", "Id", 3));
    });

上述代碼中完成了Sharding的配置:
ShardingConfigBootstrapper.Bootstrap()在一個項目中只能執行一次,所以建議放到Application_Start中(ASP.NET Core中的Startup)
AddDataSource是指添加數據源,數據源可以看做抽象數據庫,一個數據源包含了一組同類型的物理數據庫,即實際的數據庫。一個數據源至少包含一個物理數據庫,多個物理數據庫需要開啟主從複製或主主複製,通過ReadWriteType(寫、讀、寫和讀)參數來指定數據庫的操作類型,通常將寫庫作為主庫,讀庫作為從庫。同一個數據源中的物理數據庫類型相同,表結構也相同。
配置好數據源后就可以通過AddAbsDb來添加抽象數據庫,抽象數據庫中需要添加抽象數據表。如上抽象表Base_UnitTest對應的物理表就是Base_UnitTest_0、Base_UnitTest_1與Base_UnitTest_2,並且這三張表都屬於數據源BaseDb。分表配置當然需要分表規則(即通過一種規則找到具體數據在哪張表中)。
上述代碼中使用了最簡單的取模分片規則,
源碼如下:

可以看到其使用方式及優缺點。
另外還有一致性HASH分片規則

雪花Id的mod分片規則

上述的分片規則各有優劣,都實現IShardingRule接口,實際上只需要實現FindTable方法即可實現自定義分片規則。
實際使用中個人推薦使用雪花Id的mod分片規,這也是為什麼前面數據庫設計規範中默認使用雪花Id作為數據庫主鍵的原因(PS,之前版本使用GUID作為主鍵被各種嫌棄,這次看你們怎麼說)

  • 使用方式
    配置完成,下面開始使用,使用方式非常簡單,與平常使用基本一致
    首先獲取分片倉儲接口IShardingRepository
IShardingRepository _db = DbFactory.GetRepository().ToSharding();

然後即可進行數據操作:

Base_UnitTest _newData  = new Base_UnitTest
{
    Id = Guid.NewGuid().ToString(),
    UserId = "Admin",
    UserName = "超級管理員",
    Age = 22
};
List<Base_UnitTest> _insertList = new List<Base_UnitTest>
{
    new Base_UnitTest
    {
        Id = Guid.NewGuid().ToString(),
        UserId = "Admin1",
        UserName = "超級管理員1",
        Age = 22
    },
    new Base_UnitTest
    {
        Id = Guid.NewGuid().ToString(),
        UserId = "Admin2",
        UserName = "超級管理員2",
        Age = 22
    }
};
//添加單條數據
_db.Insert(_newData);
//添加多條數據
_db.Insert(_insertList);
//清空表
_db.DeleteAll<Base_UnitTest>();
//刪除單條數據
_db.Delete(_newData);
//刪除多條數據
_db.Delete(_insertList);
//刪除指定數據
_db.Delete<Base_UnitTest>(x => x.UserId == "Admin2");
//更新單條數據
_db.Update(_newData);
//更新多條數據
_db.Update(_insertList);
//更新單條數據指定屬性
_db.UpdateAny(_newData, new List<string> { "UserName", "Age" });
//更新多條數據指定屬性
_db.UpdateAny(_insertList, new List<string> { "UserName", "Age" });
//更新指定條件數據
_db.UpdateWhere<Base_UnitTest>(x => x.UserId == "Admin", x =>
{
    x.UserId = "Admin2";
});
//GetList獲取表的所有數據
var list=_db.GetList<Base_UnitTest>();
//GetIQPagination獲取分頁后的數據
var list=_db.GetIShardingQueryable<Base_UnitTest>().GetPagination(pagination);
//Max
var max=_db.GetIShardingQueryable<Base_UnitTest>().Max(x => x.Age);
//Min
var min=_db.GetIShardingQueryable<Base_UnitTest>().Min(x => x.Age);
//Average
var min=_db.GetIShardingQueryable<Base_UnitTest>().Average(x => x.Age);
//Count
var min=_db.GetIShardingQueryable<Base_UnitTest>().Count();
//事務,使用方式與普通事務一致
bool succcess = _db.RunTransaction(() =>
{
    _db.Insert(_newData);
    var newData2 = _newData.DeepClone();
    _db.Insert(newData2);
}).Success;
Assert.AreEqual(succcess, false);

上述操作中表面上是操作Base_UnitTest表,實際上卻在按照一定規則使用Base_UnitTest_0~2三張表,使分片對業務操作透明,極大提高開發效率
具體使用方式請參考單元測試源碼:
“\src\Coldairarrow.UnitTests\DataRepository\ShardingTest.cs”

常見疑問

如何進行聯表查詢

框架使用EF+LINQ進行聯表操作,核心在於對IQueryable< T >的使用,另可網上搜EF+LINQ的相關教程。

示例如下:
Base_User表左連接Base_Department表獲取DepartmentName屬性

//定義數據模型類
public class Base_UserTestDTO : Base_User
{
    public string DepartmentName { get; set; }
}

//即BaseBusiness中的Service
var db = DbFactory.GetRepository();
Expression<Func<Base_User, Base_Department, Base_UserTestDTO>> select = (a, b) => new Base_UserTestDTO
{
    DepartmentName = b.Name
};
select = select.BuildExtendSelectExpre();
var q = from a in db.GetIQueryable<Base_User>().AsExpandable()
        join b in db.GetIQueryable<Base_Department>() on a.DepartmentId equals b.Id into ab
        from b in ab.DefaultIfEmpty()
        select @select.Invoke(a, b);
//篩選
var where = LinqHelper.True<Base_UserTestDTO>();
where = where.And(x => x.UserName == "Admin");

//篩選並獲取分頁數據
var list = q.Where(where).GetPagination(new Pagination()).ToList();

源碼可參考Base_UserBusiness.GetDataList

如何切換數據庫類型

在01.Coldairarrow.Util項目中的GlobalSwitch,將DatabaseType改為需要的即可,對應的數據庫連接字符串當然也要改為對應數據庫的

如何使用多個數據庫

在具體的Business類中重寫父類BaseBusiness的構造函數即可,按照自己的需求重寫對應的構造函數,同時需要確保數據庫連接字符串已添加

若需要同時使用多個數據庫,或者需要多線程操作數據庫,需要使用

結語

歡迎使用本框架,若覺得不錯,請比心

Github歡迎星星:

博客園歡迎點贊:

QQ群2:579202910
個人QQ:862520575(歡迎技術支持及商務合作,提供.NET Core + Linux + Nginx+ jenkins + git整套持續集成快速開發平台)

本人將會對這個快速開發框架不斷完善與維護,希望能夠幫助到各位

若遇到任何問題或需要技術支持,請聯繫我。

——學習永無止境,技術永無上限,代碼就是藝術——

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

SSE圖像算法優化系列三十:GIMP中的Noise Reduction算法原理及快速實現。

  GIMP源代碼鏈接:

  GEGL相關代碼鏈接:

  最近因為要研究下色溫算法,順便下載了最新的GIMP軟件,色溫算法倒是找到了(有空單獨來講下),也順便看看GIMP都有些什麼更新,嗯,更新還是蠻多的,界面UI上有很多改動,有些已經改的面目全非了。隨便瞄了一下Enhance菜單,發現裏面有一個Nosie Reduction算法,試了下,還有點效果。於是在github上下載了GIMP的源代碼,可是在源代碼里搜索相關的關鍵詞確沒有發現任何的相關代碼,後來才發現很多東西都有個GEGL關鍵詞,結果一百度,原來他是一個單獨的軟件包,於是有下載了GEGL的源代碼,終於在gegl-master\operations\common\裏面看到了noise-reduction.c文件。

  其核心的代碼如下:

static void
noise_reduction (float *src_buf,     /* source buffer, one pixel to the left
                                        and up from the starting pixel */
                 int    src_stride,  /* stridewidth of buffer in pixels */
                 float *dst_buf,     /* destination buffer */
                 int    dst_width,   /* width to render */
                 int    dst_height,  /* height to render */
                 int    dst_stride)  /* stride of target buffer */
{
  int c;
  int x,y;
  int dst_offset;

#define NEIGHBOURS 8
#define AXES       (NEIGHBOURS/2)

#define POW2(a) ((a)*(a))
/* core code/formulas to be tweaked for the tuning the implementation */
#define GEN_METRIC(before, center, after) \
                   POW2((center) * 2 - (before) - (after))

/* Condition used to bail diffusion from a direction */
#define BAIL_CONDITION(new,original) ((new) > (original))

#define SYMMETRY(a)  (NEIGHBOURS - (a) - 1) /* point-symmetric neighbour pixel */

#define O(u,v) (((u)+((v) * src_stride)) * 4)
  int   offsets[NEIGHBOURS] = {  /* array of the relative distance i float
                                  * pointers to each of neighbours
                                  * in source buffer, allows quick referencing.
                                  */
              O( -1, -1), O(0, -1), O(1, -1),
              O( -1,  0),           O(1,  0),
              O( -1,  1), O(0, 1),  O(1,  1)};
#undef O

  dst_offset = 0;
  for (y=0; y<dst_height; y++)
    {
      float *center_pix = src_buf + ((y+1) * src_stride + 1) * 4;
      dst_offset = dst_stride * y;
      for (x=0; x<dst_width; x++)
        {
          for (c=0; c<3; c++) /* do each color component individually */
            {
              float  metric_reference[AXES];
              int    axis;
              int    direction;
              float  sum;
              int    count;

              for (axis = 0; axis < AXES; axis++)
                { /* initialize original metrics for the horizontal, vertical
                     and 2 diagonal metrics */
                  float *before_pix  = center_pix + offsets[axis];
                  float *after_pix   = center_pix + offsets[SYMMETRY(axis)];

                  metric_reference[axis] =
                    GEN_METRIC (before_pix[c], center_pix[c], after_pix[c]);
                }

              sum   = center_pix[c];
              count = 1;

              /* try smearing in data from all neighbours */
              for (direction = 0; direction < NEIGHBOURS; direction++)
                {
                  float *pix   = center_pix + offsets[direction];
                  float  value = pix[c] * 0.5 + center_pix[c] * 0.5;
                  int    axis;
                  int    valid;

                  /* check if the non-smoothing operating check is true if
                   * smearing from this direction for any of the axes */
                  valid = 1; /* assume it will be valid */
                  for (axis = 0; axis < AXES; axis++)
                    {
                      float *before_pix = center_pix + offsets[axis];
                      float *after_pix  = center_pix + offsets[SYMMETRY(axis)];
                      float  metric_new =
                             GEN_METRIC (before_pix[c], value, after_pix[c]);

                      if (BAIL_CONDITION(metric_new, metric_reference[axis]))
                        {
                          valid = 0; /* mark as not a valid smoothing, and .. */
                          break;     /* .. break out of loop */
                        }
                    }
                  if (valid) /* we were still smooth in all axes */
                    {        /* add up contribution to final result  */
                      sum += value;
                      count ++;
                    }
                }
              dst_buf[dst_offset*4+c] = sum / count;
            }
          dst_buf[dst_offset*4+3] = center_pix[3]; /* copy alpha unmodified */
          dst_offset++;
          center_pix += 4;
        }
    }
}

   這個代碼看上去比較混亂,沒辦法,大型軟件沒有哪一個代碼看上去能讓人省心的,而且也不怎麼講究效率,我測試了一個3K*2K的彩色圖,在GIMP里大概在4S左右處理完成,屬於很慢的了,看這個代碼,也知道大概有width * height * 3 * 8 * 4 * Iter個循環,計算量確實是相當的大。

  我試着嘗試優化這個算法。

  優化的第一步是弄明白算法的原理,在GIMP的UI界面上可當鼠標停留在Noise Reduction菜單上時,會出現Anisotroic smoothing operation字樣,所以初步分析他是屬於各項異性擴散類的算法。稍微分析下代碼,也確實是。明顯這屬於一個領域濾波器,對每一個像素,求取其3*3領域內的累加值,但是3*3領域的權重並不是平均分佈或者是高斯分佈,而是和領域的值有關的,如果領域的值是一個邊緣點,他將不參与到累加中,權重為0,否則權重為1。

  具體一點,對於領域里的任何一點,我們先求取其和中心點的平均值,對應 float value = pix[c] * 0.5 + center_pix[c] * 0.5; 這條語句,然後計算這個值在2個45度對角線及水平和垂直方向的梯度(4個梯度值)是否比中心點在四個方向的梯度都小,如果都小,說明這個領域點不屬於邊緣點,可以往這個方向擴散,把他計入到統計值中,如果有任何一個方向小了,則不參与最終的計算。

  上面的過程可以看成是標準的各項異性擴散的特殊在特殊處理,他具有各項異性擴散的特性,也具有一些特殊性。

  下一步,稍微分析下最簡單的優化方法。第一,我們知道,在大循環里一般不建議嵌套入小的循環,這樣是很低效的。我們觀察到上面代碼里的

      for (c=0; c<3; c++) /* do each color component individually */

  這個語句主要是為了方便表達3通道的處理的方便,但是其實三通道之間的處理時沒有任何聯繫的,對於這樣的算法,很明顯,我們可以一次性當然處理R G B R G B R G B ,而不需要像GIMP這個代碼這樣按照 RRR  GGG  BBB這樣的順序來寫,GIMP這種寫法浪費了很多CPU的CACHE,畢竟R和G和B在內存類分佈本來就是連續的。這樣就減少了一個小循環。

  第二個優化的點是,對於普通的圖像數據,我們可以考慮不用浮點數來處理,畢竟上述計算里只有*0.5這樣的浮點操作,我們考慮將原先的圖像數據放大一定的倍數,然後用整形來玩,在處理完后,在縮小到原來的範圍,比如使用short類型應該就足夠了,我把數據放大16倍或者32倍,甚至8倍應該都能獲得足夠的精度。

  第三個優化點,程序中是使用的Pow來判斷梯度的大小的,其實可以不用,直接使用絕對值的結果和Pow是完全一樣的,而絕對值的計算量比pow要小很多,對於整數則更為如此(還可以不考慮pow數據類型的改變,比如short的絕對值還是short類型,但是其pow可能就需要用int來表示了,這在SIMD優化會產生不同的結果)。

  第四個優化點是 for (axis = 0; axis < AXES; axis++)這個小循環我們應該把它直接展開。

  第五點,我們還可以考慮我在其他文章里提到的支持Inplace操作的方式,這樣noise_reduction這個函數的輸入和輸出就可以是同一個內存。

  第六點還有小點上的算法改進,比如一些中間計算沒必要重複進行,有些可以提到外部來等。

  綜合上面的描述,我整理除了一個優化的C語言版本的程序,如下所示:

void IM_AnisotropicDiffusion3X3(short *Src, short *Dest, int Width, int Height, int SplitPos, int Stride)
{
    int Channel = Stride / Width;

    short *RowCopy = (short *)malloc((Width + 2) * 3 * Channel * sizeof(short));
    short *First = RowCopy;
    short *Second = RowCopy + (Width + 2) * Channel;
    short *Third = RowCopy + (Width + 2) * 2 * Channel;
    memcpy(Second, Src, Channel * sizeof(short));
    memcpy(Second + Channel, Src, Width * Channel * sizeof(short));                                                    //    拷貝數據到中間位置
    memcpy(Second + (Width + 1) * Channel, Src + (Width - 1) * Channel, Channel * sizeof(short));

    memcpy(First, Second, (Width + 2) * Channel * sizeof(short));                                                    //    第一行和第二行一樣

    memcpy(Third, Src + Stride, Channel * sizeof(short));                                                            //    拷貝第二行數據
    memcpy(Third + Channel, Src + Stride, Width * Channel* sizeof(short));
    memcpy(Third + (Width + 1) * Channel, Src + Stride + (Width - 1) * Channel, Channel* sizeof(short));

    for (int Y = 0; Y < Height; Y++)
    {
        short *LinePD = Dest + Y * Stride;
        if (Y != 0)
        {
            short *Temp = First; First = Second; Second = Third; Third = Temp;
        }
        if (Y == Height - 1)
        {
            memcpy(Third, Second, (Width + 2) * Channel * sizeof(short));
        }
        else
        {
            memcpy(Third, Src + (Y + 1) * Stride, Channel * sizeof(short));
            memcpy(Third + Channel, Src + (Y + 1) * Stride, Width * Channel * sizeof(short));                            //    由於備份了前面一行的數據,這裏即使Src和Dest相同也是沒有問題的
            memcpy(Third + (Width + 1) * Channel, Src + (Y + 1) * Stride + (Width - 1) * Channel, Channel * sizeof(short));
        }
        for (int X = 0; X < SplitPos * Channel; X++)
        {
            short LT = First[X], T = First[X + Channel], RT = First[X + 2 * Channel];
            short L = Second[X], C = Second[X + Channel], R = Second[X + 2 * Channel];
            short LB = Third[X], B = Third[X + Channel], RB = Third[X + 2 * Channel];
            short LT_RB = LT + RB,    RT_LB = RT + LB;
            short T_B = T + B,        L_R = L + R,        C_C = C + C;
            short Dist1 = IM_Abs(C_C - LT_RB),        Dist2 = IM_Abs(C_C - T_B);
            short Dist3 = IM_Abs(C_C - RT_LB),        Dist4 = IM_Abs(C_C - L_R);
            
            int Sum = C_C, Amount = 2;

            short LT_C = LT + C;
            if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
            {
                Sum += LT_C;
                Amount += 2;
            }
            short T_C = T + C;
            if ((IM_Abs(T_C - LT_RB) < Dist1) && (IM_Abs(T_C - T_B) < Dist2) && (IM_Abs(T_C - RT_LB) < Dist3) && (IM_Abs(T_C - L_R) < Dist4))
            {
                Sum += T_C;
                Amount += 2;
            }
            short RT_C = RT + C;
            if ((IM_Abs(RT_C - LT_RB) < Dist1) && (IM_Abs(RT_C - T_B) < Dist2) && (IM_Abs(RT_C - RT_LB) < Dist3) && (IM_Abs(RT_C - L_R) < Dist4))
            {
                Sum += RT_C;
                Amount += 2;
            }
            short L_C = L + C;
            if ((IM_Abs(L_C - LT_RB) < Dist1) && (IM_Abs(L_C - T_B) < Dist2) && (IM_Abs(L_C - RT_LB) < Dist3) && (IM_Abs(L_C - L_R) < Dist4))
            {
                Sum += L_C;
                Amount += 2;
            }
            short R_C = R + C;
            if ((IM_Abs(R_C - LT_RB) < Dist1) && (IM_Abs(R_C - T_B) < Dist2) && (IM_Abs(R_C - RT_LB) < Dist3) && (IM_Abs(R_C - L_R) < Dist4))
            {
                Sum += R_C;
                Amount += 2;
            }
            short LB_C = LB + C;
            if ((IM_Abs(LB_C - LT_RB) < Dist1) && (IM_Abs(LB_C - T_B) < Dist2) && (IM_Abs(LB_C - RT_LB) < Dist3) && (IM_Abs(LB_C - L_R) < Dist4))
            {
                Sum += LB_C;
                Amount += 2;
            }
            short B_C = B + C;
            if ((IM_Abs(B_C - LT_RB) < Dist1) && (IM_Abs(B_C - T_B) < Dist2) && (IM_Abs(B_C - RT_LB) < Dist3) && (IM_Abs(B_C - L_R) < Dist4))
            {
                Sum += B_C;
                Amount += 2;
            }
            short RB_C = RB + C;
            if ((IM_Abs(RB_C - LT_RB) < Dist1) && (IM_Abs(RB_C - T_B) < Dist2) && (IM_Abs(RB_C - RT_LB) < Dist3) && (IM_Abs(RB_C - L_R) < Dist4))
            {
                Sum += RB_C;
                Amount += 2;
            }
            LinePD[X] = Sum / Amount;
        }
    }
    free(RowCopy);
}

  調用函數

int IM_ReduceNoise(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int SplitPos,  int Strength)
{
    int Channel = Stride / Width;
    if ((Src == NULL) || (Dest == NULL))                        return IM_STATUS_NULLREFRENCE;
    if ((Width <= 0) || (Height <= 0))                            return IM_STATUS_INVALIDPARAMETER;
    if ((Channel != 1) && (Channel != 3))                         return IM_STATUS_INVALIDPARAMETER;

    Strength = IM_ClampI(Strength, 1, 10);
    SplitPos = IM_ClampI(SplitPos, 0, Width);
    int Status = IM_STATUS_OK;
    short *Temp = (short *)malloc(Height * Stride * sizeof(short));
    if (Temp == NULL)    return IM_STATUS_OUTOFMEMORY;
    for (int Y = 0; Y < Height * Stride; Y++)
    {
        Temp[Y] = Src[Y] << 3;
    }
    for (int Y = 0; Y < Strength; Y++)
    {
        IM_AnisotropicDiffusion3X3(Temp, Temp, Width, Height, SplitPos, Stride);
    }
    for (int Y = 0; Y < Height * Stride; Y++)
    {
        Dest[Y] = Temp[Y] >> 3;
    }
    free(Temp);
    return IM_STATUS_OK;
}

  是不是看起來比上面的GIMP得要舒服些,而且中間也大概只要原始圖像2倍的一個臨時內存了。在速度和內存佔用方面都前進了很多。

  我測試前面提到的那副3K*2K的圖像,耗時要7S多,但是我測試表面GIMP用了多核的,如果論單核,我這裏的速度要比他快2倍多。

  很明顯,這個速度是不可以接受的,我們需要繼續優化。

      我還是老套路,使用SIMD指令做處理,看到上面的代碼,其實真的覺得好容易改成SIMD的。

     short LT_RB = LT + RB,    RT_LB = RT + LB;
     short T_B = T + B,        L_R = L + R,        C_C = C + C;
     short Dist1 = IM_Abs(C_C - LT_RB),        Dist2 = IM_Abs(C_C - T_B);
     short Dist3 = IM_Abs(C_C - RT_LB),        Dist4 = IM_Abs(C_C - L_R);
 
這些加減絕對值都有完全對應的SSE指令。 _mm_add_epi16、 _mm_sub_epi16、_mm_abs_epi16,基本上就是照着寫。
  稍微複雜一點就是這裏:
  if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
   {
     Sum += LT_C;
    Amount += 2;
   }
  在C語言里,這裏判斷會進行短路計算,即如果前一個條件已經不滿足了,後續的計算就不會進行。但是在SIMD指令里,是沒有這樣的機制的。我們只能全部計算,然後在通過某一種條件組合。
  在合理,要實現符合條件就進行累加,不符合條件就不做處理的需求,我們需要稍作修改,即不符合條件不是不做處理,而是加0,加0對結果沒有影響的。主要藉助下面的_mm_blendv_epi8來實現。
    __m128i LT_C = _mm_add_epi16(LT, C);
    Flag1 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, LT_RB)), Dist1);        //    只能全部都計算,但還是能提速
    Flag2 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, T_B)), Dist2);
    Flag3 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, RT_LB)), Dist3);
    Flag4 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, L_R)), Dist4);
    Flag = _mm_and_si128(_mm_and_si128(Flag1, Flag2), _mm_and_si128(Flag3, Flag4));
    Sum = _mm_adds_epu16(Sum, _mm_blendv_epi8(Zero, LT_C, Flag));
    Amount = _mm_adds_epu16(Amount, _mm_blendv_epi8(Zero, Two, Flag));

  注意到我們這裏用到了_mm_adds_epu16,無符號的16位加法,這是因為我需要盡量的提速,因此需要減少類型轉換的次數。同時,我們看到在統計累加值時,我們並沒有求平均值,而是直接用的累加值,這樣理論上最大的累加值就是 255 * n * (8 + 1) * 2 < 65535, 這樣n最大能取15,但是15不是個好數據,在放大和縮小都不能用移位來實現,所以我最後取得放大係數為8。

  另外,在最後還有個16位的整數的除法問題,這個沒有辦法,SSE指令沒有提供整數的除法計算方法,還只能轉換到浮點后,再次轉換回來。

  這樣用SSE處理后,還是同一幅測試圖像,在同一台PC上速度能提升到400ms(4次迭代),比之前的普通的C語言提高了約17倍的速度。

  在現代CPU中,具有AVX2指令集已經是很普遍的了,單AVX2能同時處理256字節的數據,比SSE還要多一倍,我也常使用AVX2進行優化處理,速度能達到250ms,相當於普通C語言的28倍之多(但是AVX編程里有很多坑,這些坑都拜AVX不是完全的按照SSE的線性擴展導致的,這個後續有時間我單獨提出)。

  經過測試,1080P的圖像使用4次迭代大約需要80ms,3次迭代55ms,2次迭代月40ms,也就是說前面的一些方法和縮小所使用的時間幾乎可以忽略。

  選了幾幅有特點的圖進行了去燥測試,其中分界線左側的位處理的效果,右側為未處理的。

 

  但是,這個算法也還是不是很好,他對於圖像容易出現輕微的油畫效果,對於一些細節特別豐富的圖像非常明顯,比如下圖:

  這個應該是不太可以接受的,也許可以通過修改部分權重的規則來改變這個現象。這個屬於後期研究的問題了。

     另外,在GIMP里也提供了這個算法的OPENCL實現,有興趣的可以源代碼里找一找,不曉得速度怎麼樣。

  本文Demo下載地址:  ,見其中的Denoise -> Anisotroic Diffusion 菜單。

  寫博不易,歡迎土豪打賞讚助。

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

別翻了,這篇文章絕對讓你深刻理解java類的加載以及ClassLoader源碼分析【JVM篇二】

目錄

@
前言
你是否真的理解java的類加載機制?點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解java的類加載機制了。代碼如下

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}
運行結果:
        Father靜態代碼塊
        Son靜態代碼塊
        HelloJVM_Son

嗯哼?其實上面程序並不是關鍵,可能真的難不倒各位,不妨做下面一道面試題可好?如果下面這道面試題都做對了,那沒錯了,這篇文章你就不用看了,真的。

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

各位先用“畢生所學”來猜想一下運行的結果是啥…

注意了…
注意了…
注意了…

運行結果:
    YeYe靜態代碼塊
    Father靜態代碼塊
    HelloJVM_Father

是對是錯已經有個數了吧,我就不拆穿各位的小心思了…

以上的面試題其實就是典型的java類的加載問題,如果你對Java加載機制不理解,那麼你可能就錯了上面兩道題目的。這篇文章將通過對Java類加載機制的講解,讓各位熟練理解java類的加載機制。

其實博主還是想在給出一道題,畢竟各位都已經有了前面兩道題的基礎了,那麼請看代碼:

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

注意了
注意了
注意了

運行結果:HelloJVM_Father

衝動的小白童鞋看到了運行結果,果斷的註銷了博客賬戶….

1、什麼是類的加載(類初始化)

JVM重要的一個領域:類加載

當程序主動使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、連接、初始化3個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成3個步驟,所以有時也把這個3個步驟統稱為類加載或類初始化。

而類加載必然涉及類加載器,下面我們先來了解一下類的加載。

類的加載(類初始化):

1、在java代碼中,類型的加載、連接、與初始化過程都是在程序運行期間完成的(類從磁盤加載到內存中經歷的三個階段)【牢牢記在心裏】

 

2、提供了更大的靈活性,增加了更多的可能性

雖然上面的第一句話非常簡短,但是蘊含的知識量卻是巨大的!包含兩個重要的概念:

1、類型

定義的類、接口或者枚舉稱為類型而不涉及對象,在類加載的過程中,是一個創建對象之前的一些信息

2、程序運行期間

程序運行期間完成典型例子就是動態代理,其實很多語言都是在編譯期就完成了加載,也正因為這個特性給Java程序提供了更大的靈活性,增加了更多的可能性

1、1.類加載注意事項

1、類加載器並不需要等到某個類被 “首次主動使用” 時再加載它~關於首次主動使用這個重要概念下文將講解~
2、JVM規範允許類加載器在預料某個類將要被使用時就預先加載它
3、如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

首先給各位打個預防針:可能沒有了解過JVM的童鞋可能看的很蒙,感覺全是理論的感覺,不勉強一字一句的“死看”,只要達到一種概念印象就好!等到有一定理解認識之後再回頭看一遍就好很多了,畢竟學習是一種循進漸進的過程,記住沒有捷徑!

2、類的生命周期

從上圖可知,類從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生命周期包括 7 個階段,而驗證、準備、解析 3 個階段統稱為連接。

加載、驗證、準備、初始化和卸載這 5 個階段的順序是固定確定的,類的加載過程必須按照這種順序開始(注意是“開始”,而不是“進行”),而解析階段則不一定:它在某些情況下可以在初始化后再開始,這是為了支持 Java 語言的運行時綁定【也就是java的動態綁定/晚期綁定】。

2、1.加載

在上面已經提到過,加載階段是類加載的第一個階段!類的加載過程就是從加載階段開始~

加載階段指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class對象(JVM規範並未說明Class對象位於哪裡,HotSpot虛擬機將其放在方法區中),用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class對象, Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據才是存在方法區的。【元數據並不是類的Class對象。Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的】

JDK7創建Class實例存在堆中;因為JDK7中JavaObjectsInPerm參數值固定為false。
JDK8移除了永久代,轉而使用元空間來實現方法區,創建的Class實例依舊在java heap(堆)中

編寫一個新的java類時,JVM就會幫我們編譯成class對象,存放在同名的.class文件中。在運行時,當需要生成這個類的對象,JVM就會檢查此類是否已經裝載內存中。若是沒有裝載,則把.class文件裝入到內存中。若是裝載,則根據class文件生成實例對象。

怎麼理解Class對象與new出來的對象之間的關係呢?

new出來的對象以car為例。可以把car的Class類看成具體的一個人,而new car則是人物映像,具體的一個人(Class)是唯一的,人物映像(new car)是多個的。鏡子中的每個人物映像都是根據具體的人映造出來的,也就是說每個new出來的對象都是以Class類為模板參照出來的!為啥可以參照捏?因為Class對象提供了訪問方法區內的數據結構的接口哇,上面提及過了喔!

算了參照下面這張圖理解吧,理解是其次,重點是話說這妹砸蠻好看的。

總結:
加載階段簡單來說就是:
.class文件(二進制數據)——>讀取到內存——>數據放進方法區——>堆中創建對應Class對象——>並提供訪問方法區的接口

相對於類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。

加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個 java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。

加載.calss文件的方式:
類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱為系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。通過使用不同的類加載器,可以從不同來源加載類的二進制數據,二進制數據通常有如下幾種來源:

(1)從本地系統中直接加載
(2)通過網絡下載.class文件
(3)從zip,jar等歸檔文件中加載.class文件
(4)從專用數據庫中提取.class文件
(5)將java源文件動態編譯為.class文件

2、2.驗證

驗證:確保被加載的類的正確性。
關於驗證大可不必深入但是了解類加載機制必須要知道有這麼個過程以及知道驗證就是為了驗證確保Class文件的字節流中包含的信息符合當前虛擬機的要求即可。
所以下面關於驗證的內容作為了解即可!

驗證是連接階段的第一階段,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:

文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以 0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。

元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object之外。

字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用 -Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

2、3.準備【重點】

當完成字節碼文件的校驗之後,JVM 便會開始為類變量分配內存並初始化。準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。

這裏需要注意兩個關鍵點,即內存分配的對象以及初始化的類型。

內存分配的對象:要明白首先要知道Java 中的變量有類變量以及類成員變量兩種類型,==類變量指的是被 static 修飾的變量==,而==其他所有類型的變量都屬於類成員變量==。在準備階段,JVM 只會為類變量分配內存,而不會為類成員變量分配內存。類成員變量的內存分配需要等到初始化階段才開始(初始化階段下面會講到)。

舉個例子:例如下面的代碼在準備階段,只會為 LeiBianLiang屬性分配內存,而不會為 ChenYuanBL屬性分配內存。

public static int LeiBianLiang = 666;
public String ChenYuanBL = "jvm";

初始化的類型:在準備階段,JVM 會為類變量分配內存,併為其初始化(JVM 只會為類變量分配內存,而不會為類成員變量分配內存,類成員變量自然這個時候也不能被初始化)。==但是這裏的初始化指的是為變量賦予 Java 語言中該數據類型的默認值,而不是用戶代碼里初始化的值。==

例如下面的代碼在準備階段之後,LeiBianLiang 的值將是 0,而不是 666。

public static int LeiBianLiang = 666;

注意了!!!
注意了!!!
注意了!!!

但如果一個變量是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在準備階段之後,ChangLiang的值將是 666,而不再會是 0。

public static final int ChangLiang = 666;

之所以 static final 會直接被複制,而 static 變量會被賦予java語言類型的默認值。其實我們稍微思考一下就能想明白了。

兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 ChangLiang的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。

如果還不是很清晰理解final和static關鍵字的話建議參閱下面博主整理好的文章,希望對你有所幫助!

2、4.解析

當通過準備階段之後,進入解析階段。解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

==其實這個階段對於我們來說也是幾乎透明的,了解一下就好==。

2、5.初始化【重點】

到了初始化階段,用戶定義的 Java 程序代碼才真正開始執行。

Java程序對類的使用方式可分為兩種:主動使用與被動使用。一般來說只有當對類的==首次主動使用==的時候才會導致類的初始化,所以主動使用又叫做類加載過程中“初始化”開始的時機。那啥是主動使用呢?類的主動使用包括以下六種【超級重點】:

1、 創建類的實例,也就是new的方式

 

2、 訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)

 

3、 調用類的靜態方法

 

4、 反射(如 Class.forName(“com.gx.yichun”))

 

5、 初始化某個類的子類,則其父類也會被初始化

 

6、 Java虛擬機啟動時被標明為啟動類的類( JavaTest ),還有就是Main方法的類會
首先被初始化

 

最後注意一點對於靜態字段,只有直接定義這個字段的類才會被初始化(執行靜態代碼塊),這句話在繼承、多態中最為明顯!為了方便理解下文會陸續通過例子講解

2、6.使用

當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行用戶的程序代碼。這個使用階段也只是了解一下就可以了。

2、7.卸載

當用戶程序代碼執行完畢后,JVM 便開始銷毀創建的 Class 對象,最後負責運行的 JVM 也退出內存。這個卸載階段也只是了解一下就可以了。

2、8.結束生命周期

在如下幾種情況下,Java虛擬機將結束生命周期

1、 執行了 System.exit()方法

2、 程序正常執行結束

3、 程序在執行過程中遇到了異常或錯誤而異常終止

4、 由於操作系統出現錯誤而導致Java虛擬機進程終止

3、接口的加載過程

接口加載過程與類加載過程稍有不同。

==當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會初始化。==

4、解開開篇的面試題

package com.jvm.classloader;

class Father2{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {

       System.out.println(Son2.strSon);
    }
}

運行結果:
        Father靜態代碼塊
        Son靜態代碼塊
        HelloJVM_Son

再回頭看這個題,這也太簡單了吧,由於Son2.strSon是調用了Son類自己的靜態方法屬於主動使用,所以會初始化Son類,又由於繼承關係,類繼承原則是初始化一個子類,會先去初始化其父類,所以會先去初始化父類!

再看開篇的第二個題

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

運行結果:
    YeYe靜態代碼塊
    Father靜態代碼塊
    HelloJVM_Father

這個題就稍微要注意一下,不過要是你看懂這篇文章,這個題也很簡單。這個題要注意什麼呢?要注意子類Son類沒有被初始化,也就是Son的靜態代碼塊沒有執行!發現了咩?那我們來分析分析…

首先看到Son.strFather,你會發現是子類Son訪問父類Father的靜態變量strFather,這個時候就千萬要記住我在歸納主動使用概念時特別提到過的一個注意點了:對於靜態字段,只有直接定義這個字段的類才會被初始化(執行靜態代碼塊),這句話在繼承、多態中最為明顯!

嗯哼,對吧,Son.strFather中的靜態字段是屬於父類Father的對吧,也就是說直接定義這個字段的類是父類Father,所以在執行 System.out.println(Son.strFather); 這句代碼的時候會去初始化Father類而不是子類Son!是不是一下子明白了?如果明白了就支持一下博主點個讚唄,謝謝~

再看開篇的第三個題

package com.jvm.classloader;

class YeYe{
    static {
        System.out.println("YeYe靜態代碼塊");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";

    static{
        System.out.println("Father靜態代碼塊");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";

    static{
        System.out.println("Son靜態代碼塊");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

運行結果:HelloJVM_Father

這個題唯一的特點就在於final static !是的Son.strFather所對應的變量便是final static修飾的,依舊是在本篇文章中歸納的類的主動使用範疇第二點當中:訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)

所以,這個題並不會初始化任何類,當然除了Main方法所在的類!於是僅僅執行了System.out.println(Son.strFather);所以僅僅打印了Son.strFather的字段結果HelloJVM_Father,嗯哼,是不是又突然明白了?如果明白了就再支持一下博主點個讚唄,謝謝~

實際上上面的題目並不能完全說明本篇文章中歸納的類的主動使用範疇第二點!這話怎麼說呢?怎麼理解呢?再來一個程序各位就更加明了了

package com.jvm.classloader;

import sun.applet.Main;

import java.util.Random;
import java.util.UUID;

class Test{
    static {
        System.out.println("static 靜態代碼塊");
    }

//    public static final String str= UUID.randomUUID().toString();
    public static final double str=Math.random();  //編譯期不確定
}


public class FinalUUidTest {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
}

請試想一下結果,會不會執行靜態代碼塊里的內容呢?

重點來了
重點來了
重點來了
重點來了

運行結果

static 靜態代碼塊
0.7338688977344875

上面這個程序完全說明本篇文章中歸納的類的主動使用範疇第二點當中的這句話:凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外!

分析:==其實final不是重點,重點是編譯器把結果放入常量池!當一個常量的值並非編譯期可以確定的,那麼這個值就不會被放到調用類的常量池中,這時在程序運行時,會導致主動使用這個常量所在的類,所以這個類會被初始化==

到這裏,能理解完上面三個題已經很不錯了,但是要想更加好好的學習java,博主不得不給各位再來一頓燒腦盛宴,野心不大,只是單純的想巔覆各位對java代碼的認知,當然還望大佬輕拍哈哈哈,直接上代碼:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            System.out.println("輸出的打印語句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("構造方法");
            System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代碼塊");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("靜態代碼塊");
        }     

}

建議這個題不要花太多時間思考,否則看了結果你會發現自己想太多了,導致最後可能你看到結果想砸電腦哈哈哈

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

隔離運行結果專業跑龍套…

運行結果
        靜態代碼塊
        輸出的打印語句

怎麼樣,是不是沒有你想的那麼複雜呢?

下面我們來簡單分析一下,首先根據上面說到的觸發初始化的(主動使用)的第六點:Java虛擬機啟動時被標明為啟動類的類( JavaTest ),還有就是Main方法的類會首先被初始化

嗯哼?小白童鞋就有疑問了:不是說好有Main方法的類會被初始化的么?那怎麼好多東西都沒有執行捏?

那麼類的初始化順序到底是怎麼樣的呢?在我們代碼中,我們只知道有一個構造方法,但實際上Java代碼編譯成字節碼之後,最開始是沒有構造方法的概念的,只有==類初始化方法== 和 ==對象初始化方法== 。

這個時候我們就不得不深入理解了!那麼這兩個方法是怎麼來的呢?

類初始化方法:編譯器會按照其出現順序,收集:類變量(static變量)的賦值語句、靜態代碼塊,最終組成類初始化方法。==類初始化方法一般在類初始化的時候執行。==

所以,上面的這個例子,類初始化方法就會執行下面這段代碼了:

 static int QingShang = 666;  //類變量(static變量)的賦值語句

  static   //靜態代碼塊
   {
       System.out.println("靜態代碼塊");
   }

而不會執行普通賦值語句以及普通代碼塊了

對象初始化方法:編譯器會按照其出現順序,收集:成員變量的賦值語句、普通代碼塊,最後收集構造函數的代碼,最終組成對象初始化方法,值得特別注意的是,如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。==對象初始化方法一般在實例化類對象的時候執行。==

以上面這個例子,其對象初始化方法就是下面這段代碼了:

    {                        
       System.out.println("普通代碼塊");    //普通代碼塊
    }
 
    int ZhiShang = 250;   //成員變量的賦值語句
    
    System.out.println("構造方法");  //最後收集構造函數的代碼
    System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);

明白了類初始化方法 和 對象初始化方法 之後,我們再來看這個上面例子!是的!正如上面提到的:如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。上面的這個例子確實沒有執行對象初始化方法。忘了嗎?我們根本就沒有對類ClassAndObjectLnitialize 進行實例化!只是單純的寫了一個輸出語句。

如果我們給其實例化,驗證一下,代碼如下:

package com.jvm.classloader;

public class ClassAndObjectLnitialize {

        public static void main(String[] args) {
            new ClassAndObjectLnitialize();
            System.out.println("輸出的打印語句");
        }

      public ClassAndObjectLnitialize(){

            System.out.println("構造方法");
             System.out.println("我是熊孩子我的智商=" + ZhiShang +",情商=" + QingShang);
        }

        {
            System.out.println("普通代碼塊");
        }

        int ZhiShang = 250;
        static int QingShang = 666;
        
        static
        {
            System.out.println("靜態代碼塊");
        }      
}

運行結果:
        靜態代碼塊
        普通代碼塊
        構造方法
        我是熊孩子我的智商=250,情商=666
        輸出的打印語句     

到這裏博主必須要聲明一點了!我為什麼要用這些面試題作為這篇文章的一部分?因為關於學習有一定的方法,你可以設想一下,如果博主不涉及並分析這幾個面試題,你還有耐心看到這裏嗎?小白杠精童鞋說有。。。好的,就算有,大篇大篇的理論各位扣心自問,能掌握所有知識嗎?小白杠精童鞋說說能。。。額,就算能,那你能保證光記理論一個月不遺忘嗎?小白杠精童鞋說可以。。。我特么一老北京布鞋過去頭給你打歪(我這暴脾氣我天)。所以呢學習要帶着興趣、“目的”、“野心”!希望我這段話能對你有所幫助,哪怕是一點點…

5、理解首次主動使用

我在上面提到過Java程序對類的使用方式可分為兩種:主動使用與被動使用。一般來說只有當對類的首次主動使用的時候才會導致類的初始化,其中首次關鍵字很重要,因此特地用一小結將其講解!

怎麼理解呢?老規矩看個題:

package com.jvm.classloader;

class Father6{
    public static int a = 1;
    static {
        System.out.println("父類粑粑靜態代碼塊");
    }
}
class Son6{
    public static int b = 2;
    static {
        System.out.println("子類熊孩子靜態代碼塊");
    }
}

public class OverallTest {
    static {
        System.out.println("Main方法靜態代碼塊");
    }

    public static void main(String[] args) {
        Father6 father6;
        System.out.println("======");

         father6=new Father6();
        System.out.println("======");

        System.out.println(Father6.a);
        System.out.println("======");

        System.out.println(Son6.b);

    }
}

請試想一下運行結果

運行結果:
        Main方法靜態代碼塊
        ======
        父類粑粑靜態代碼塊
        ======
        1
        ======
        子類熊孩子靜態代碼塊
        2

分析:
首先根據主動使用概括的第六點:Main方法的類會首先被初始化。 所以最先執行Main方法靜態代碼塊,而 Father6 father6;只是聲明了一個引用不會執行什麼,當運行到father6=new Father6();的時候,看到關鍵字new並且將引用father6指向了Father6對象,說明主動使用了,所以父類Father6將被初始化,因此打印了:父類粑粑靜態代碼塊 ,之後執行 System.out.println(Father6.a);屬於訪問靜態變量所以也是主動使用,這個時候注意了,因為在上面執行father6=new Father6();的時候父類已經主動使用並且初始化過一次了,這次不再是首次主動使用了,所以Father6不會在被初始化,自然它的靜態代碼塊就不再執行了,所以直接打印靜態變量值1,而後面的System.out.println(Son6.b);同樣,也是只初始化自己,不會去初始化父類,只因為父類Father6以及不再是首次主動使用了!明白了沒?如果有疑問歡迎留言,絕對第一時間回復!

6、類加載器

喔o,終於到類加載器內容了!我們之前講的類加載都是給類加載器做的一個伏筆,在這之前講的所有類被加載都是由類加載器來完成的,可見類加載器是多麼重要。由於上面的面試題並不涉及類加載器的相關知識,所以到這裏再涉及涉及類加載器的知識!

類加載器負責加載所有的類,其為所有被載入內存中的類生成一個java.lang.Class實例對象。一旦一個類被加載入JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識。

關於唯一標識符:

在Java中,一個類用其全限定類名(包括包名和類名)作為標識;

 


但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標識。

類加載器的任務是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,然後轉換為一個與目標類對應的java.lang.Class對象實例,在虛擬機提供了3種類加載器,啟動(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),如下:

站在Java開發人員的角度來看,類加載器可以大致劃分為以下三類:

啟動類加載器: BootstrapClassLoader,啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,負責加載存放在 JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被 -Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被 BootstrapClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。==總結一句話:啟動類加載器加載java運行過程中的核心類庫JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的類,也就是JDK提供的類等常見的比如:Object、Stirng、List…==

擴展類加載器: ExtensionClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader實現,它負責加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器。

應用程序類加載器: ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。==總結一句話:應用程序類加載器加載CLASSPATH變量指定路徑下的類 即指你自已在項目工程中編寫的類==

線程上下文類加載器:除了以上列舉的三種類加載器,還有一種比較特殊的類型就是線程上下文類加載器。類似Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器

在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,因為JVM自帶的類加載器(ClassLoader)只是懂得從本地文件系統加載標準的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

1、在執行非置信代碼之前,自動驗證数字簽名。

2、動態地創建符合用戶特定需要的定製化構建類。

3、從特定的場所取得java class,例如數據庫中和網絡中。

需要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內存生成class對象,而且加載某個類的class文件時,Java虛擬機默認採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面將會詳細講到!

下面我們看一個程序:

package com.jvm.classloaderQi;

public class ClassloaderTest {
    public static void main(String[] args) {
        //獲取ClassloaderTest類的加載器
        ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); 
        
        System.out.println(classLoader);
        System.out.println(classLoader.getParent()); //獲取ClassloaderTest類的父類加載器
        System.out.println(classLoader.getParent().getParent());
    }
}

運行結果:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

從上面的結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(啟動類加載器)是用C++語言實現的(這裏僅限於Hotspot,也就是JDK1.5之後默認的虛擬機,有很多其他的虛擬機是用Java語言實現的),找不到一個確定的返回父Loader的方式,於是就返回null。至於$符號就是內部類的含義。

7、關於命名空間

我覺得講類加載器,還是很有必要知道命名空間這個概念!實際上類加載器的一個必不可少的前提就是命名空間!

命名空間概念:

每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。

 

特別注意:

在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。

在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
由子加載器加載的類能看見父加載器的類,由父親加載器加載的類不能看見子加載器加載的類

我們已經知道每個類只能被加載一次,其實這樣說是不夠準確的,怎樣才算是準確的呢?那就涉及到命名空間的概念了!只有在相同的命名空間中,每個類才只能被加載一次,反過來說就是一個類在不同的命名空間中是可以被加載多次的,而被加載多次的Class對象是互相獨立的!

7.1、如何理解?

當然,直接把命名空間的概念直接拋給大家,如果沒有接觸過,100%是看不懂其中的含義的,我敢打包票,假一賠100萬。。。那麼博主就舉出寫例子讓各位深刻體會一下!當然這些例子涉及自定義加載器的一些知識,建議先對自定義加載器有一定了解在看!

例子必知前提:
1、 自己在idea或者eclipse中創建的工程項目中只要編譯之後都會有對應的class文件成在classPath目錄中
2、 而這些目錄是由ApplicationClassLoader應用加載器加載
3、 我之後會將class文件放到系統桌面地址上,而這些系統地址由自定義加載器指定,所以由自定義加載器加載

7.2、準備

事先編譯好,然後將項目工程中的兩個字節碼class文件【File1和File2】拷貝到系統桌面路徑上,編譯main方法就會出現在項目工程(ClassPath)下,注意以下例子情況中系統桌面路徑的class文件一直都存在!

Main方法情況:

  • 1、創建一個自定義加載器classloader2,並聲明桌面class文件路徑,接着加載File1

  • 2、打印File1的加載器
  • 3、newInstance出File1的實例

File1類的方法情況:

  • 1、File1的構造方法中存在一行代碼:new File2 的new實例代碼

File2類的方法情況:

  • 1、打印File2的加載器

7.3、測試代碼情景一

刪除File1、File2項目工程中的class文件,工程項目的兩個class文件都刪除(只存在系統桌面路徑下的class文件)

結果:File1和File2的加載器都是自定義加載器

7.4、測試代碼情景二

只刪除File1項目工程中的class文件

結果:File1的加載器是自定義加載器,而執行到File2實例的加載器是App應用加載器

7.5、測試代碼情景三

只刪除File2項目工程中的class文件

結果:File1的加載器都是APP應用加載器,而執行到File2實例的時候報NoClassDefFoundError異常

得出結論:加載一個類(File1)的時候,這個類裏面調用了其他的類(File2)或者其他類方法的初始化代碼,那麼這裏面的類也會試着從這個類的加載器開始向上委託加載,如果全都加載不了加載不了就報NoClassDefFoundError異常。

當然這樣理解命名空間和類加載機制還是遠遠不夠的!

File2類中發生改變情況如下:

  • 1、File1的構造方法中存在一行new File2的實例這沒變
  • 2、在File2的構造方法中,打印(訪問)File1的class文件

7.6、測試代碼情景四

只刪除項目工程中File1的class文件

結果:File1的加載器都是自定義加載器,而執行到File2實例的加載器是App應用加載器,當運行到File2構造方法中的打印(訪問)File1的class文件的時候報NoClassDefFoundError異常

得出結論:父親加載器加載的類(File2)不能看見子加載器加載的類(File1)

File1方法發生改變情況如下:
1、Main方法中newInstance出File1的實例,File1的構造方法中存在一行new File2的實例這都沒變
2、在File1的構造方法中,打印File2的class文件

7.7、測試代碼情景五

只刪除File1項目工程中的class文件

結果:File1的加載器都是自定義加載器,而執行到File2實例的加載器是App應用加載器,當運行到File1構造方法中的打印File2的class文件的時候沒問題

得出結論:由子加載器加載的類(File1)能看見父加載器的類(File2)

當然還要注意知道的一點的是:如果兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載類相互不可見。

當然整對上面的情況還是相當比較抽象,畢竟沒上代碼,如果有任何疑問,歡迎留言,宜春絕對第一時間回復!

8、JVM類加載機制

JVM的類加載機制主要有如下3種。

全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非显示使用另外一個類加載器來載入

父類委託:先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類,通俗講就是兒子們都他么是懶豬,自己不管能不能做,就算能加載也先不幹,先給自己的父親做,一個一個往上拋,直到拋到啟動類加載器也就是最頂級父類,只有父親做不了的時候再沒辦法由下一個子類做,直到能某一個子類能做才做,之後的子類就直接返回,實力坑爹!

緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是為什麼修改了Class后,必須重啟JVM,程序的修改才會生效

9、雙親委派模型

雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。也就是實力坑爹!

雙親委派機制:

1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

 

2、當 ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

 

3、如果 BootStrapClassLoader加載失敗(例如在 $JAVA_HOME/jre/lib里未查找到該class),會使用

ExtClassLoader來嘗試加載;

 

4、若ExtClassLoader也加載失敗,則會使用 AppClassLoader來加載,如果

AppClassLoader也加載失敗,則會報出異常 ClassNotFoundException。

從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現,它們類圖關係如下:

從圖可以看出頂層的類加載器是
抽象類ClassLoader類,其後
所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),為了更好理解雙親委派模型,ClassLoader源碼中的loadClass(String)方法該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之後不再建議用戶重寫但用戶可以直接調用該方法,
loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作。源碼分析如下::

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用重新加載
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,則委託給父類加載器去加載
                      c = parent.loadClass(name, false);
                  } else {
                  //如果沒有父類,則委託給啟動加載器去加載
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都沒有找到,則通過自定義實現的findClass去查找並加載
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

既然存在這個雙親委派模型,那麼就一定有着存在的意義,其意義主要是:Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。

雙親委派模型意義總結來講就是:
1、系統類防止內存中出現多份同樣的字節碼
2、保證Java程序安全穩定運行

10、ClassLoader源碼分析

ClassLoader類是一個抽象類,所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),因此它顯得格外重要,分析ClassLoader抽象類也是非常重要的!

簡單小結一下ClassLoader抽象類中一些概念:

二進制概念(Binary name):格式如下

把二進制名字轉換成文件名字,然後在文件系統中磁盤上讀取其二進制文件(class文件),每一個class對象都包含了定義了這個類的classload對象,class類都是由類加載器加載的只有數組類型是有JVM根據需要動態生成。

特別注意數組類型:

1、 數組類的類對象不是由類加載器創建的,而是根據Java運行時的需要自動創建的。
2、 數組類的類加載器getClassLoader()與它的元素類型的類加載器相同;如果元素類型是基本類型,則數組類沒有類加載器也就是null,而這個null不同於根類加載器返回的null,它是單純的null。

到這裏,下面就主要分析ClassLoader抽象類中幾個比較重要的方法。

10、1.loadClass

該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之後不再建議用戶重寫但用戶可以直接調用該方法,loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼如下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數代表是否生成class對象的同時進行解析相關操作:

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用重新加載
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,則委託給父類加載器去加載
                      c = parent.loadClass(name, false);
                  } else {
                  //如果沒有父類,則委託給啟動加載器去加載
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都沒有找到,則通過自定義實現的findClass去查找並加載
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

正如loadClass方法所展示的,當類加載請求到來時,先從緩存中查找該類對象,如果存在直接返回,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級啟動類加載器去加載,最後倘若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也可以知道如果不想重新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載自己指定的類,那麼我們可以直接使用this.getClass().getClassLoder.loadClass(“className”),這樣就可以直接調用ClassLoader的loadClass方法獲取到class對象。

10、2.findClass

在JDK1.2之前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,但是在JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗后,則會調用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委託模式。需要注意的是ClassLoader類中並沒有實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的(稍後會分析),ClassLoader類中findClass()方法源碼如下:

//直接拋出異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

10、3.defineClass(byte[] b, int off, int len)

defineClass()方法是用來將byte字節流解析成JVM能夠識別的Class對象(ClassLoader中已實現該方法邏輯),通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象,如通過網絡接收一個類的字節碼,然後轉換為byte字節流創建對應的Class對象,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,然後調用defineClass()方法生成類的Class對象,簡單例子如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
      // 獲取類的字節數組
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
          //使用defineClass生成class對象
          return defineClass(name, classData, 0, classData.length);
      }
  }

需要注意的是,如果直接調用defineClass()方法生成類的Class對象,這個類的Class對象並沒有解析(也可以理解為鏈接階段,畢竟解析是鏈接的最後一步),其解析操作需要等待初始化階段進行。

10、4.resolveClass (Class<?>c)

使用該方法可以使用類的Class對象創建完成也同時被解析。前面我們說鏈接階段主要是對字節碼進行驗證,為類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換為直接引用。

10、5.ClassLoader小結

以上上述4個方法是ClassLoader類中的比較重要的方法,也是我們可能會經常用到的方法。接看SercureClassLoader擴展了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關聯,前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實現,比如 findClass()、findResource()等。而URLClassLoader這個實現類為這些方法提供了具體的實現,並新增了URLClassPath類協助取得Class字節碼流等功能,在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

檢查完父類加載器之後loadClass會去默認調用findClass方法,父類(ClassLoader)中的findClass方法主要是拋出一個異常。

findClass根據二進制名字找到對應的class文件,返回值為Class對象Class<?>

defineClass這個方法主要是將一個字節數組轉換成Class實例,會拋三個異常,但只是threws一個,因為其他兩個是運行時異常。

loadClass方法是一個加載一個指定名字的class文件,調用findLoadedClass (String)檢查類是否已經加載…如果已經加裝就不再加載而是直接返回第一次加載結果 所以一個類只會加載一次

11、自定義類加載器

自定義核心目的是擴展java虛擬機的動態加載類的機制,JVM默認情況是使用雙親委託機制,雖然雙親委託機制很安全極高但是有些情況我們需要自己的一種方式加載,==比如應用是通過網絡來傳輸 Java類的字節碼,為保證安全性,這些字節碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實現==。因此自定義類加載器也是很有必要的。

自定義類加載器一般都是繼承自 ClassLoader類,從上面對 loadClass方法來分析來看,我們只需要重寫 findClass 方法即可。自定義加載器中點:重寫findClass,下面直接看自定義類加載器代碼的流程:

package com.yichun.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\dirtemp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.yichun.classloader.Demo1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。上面代碼程序只是簡單Demo,並未對class文件進行加密,因此省略了解密的過程。這裡有幾點需要注意:

1、這裏傳遞的文件名需要是類的全限定性名稱,即com.yichun.test.classloading.Test格式的,因為
defineClass 方法是按這種格式進行處理的。

 

2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。

 

3、這類Test 類本身可以被 AppClassLoader類加載,因此我們不能把com/yichun/test/classloading/Test.class放在類路徑下。否則,由於雙親委託機制的存在,會直接導致該類由AppClassLoader加載,而不會通過我們自定義類加載器來加載。

12、加載類的三種方式

到這裏,相信大家已經對類的加載以及加載器有一定的了解了,那麼你知道嗎,其實加載類常見的有三種方式,如下:

1、靜態加載,也就是通過new關鍵字來創建實例對象。

 

2、動態加載,也就是通過Class.forName()方法動態加載(反射加載類型),然後調用類的newInstance()方法實例化對象。

 

3、動態加載,通過類加載器的loadClass()方法來加載類,然後調用類的newInstance()方法實例化對象

12、1.三種方式的區別:

1、第一種和第二種方式使用的類加載器是相同的,都是當前類加載器。(this.getClass.getClassLoader)。而3由用戶指定類加載器。

2、如果需要在當前類路徑以外尋找類,則只能採用第3種方式。第3種方式加載的類與當前類分屬不同的命名空間。

3、第一種是靜態加載,而第二、三種是動態加載。

12、2.兩種異常(exception)

1、靜態加載的時候如果在運行環境中找不到要初始化的類,拋出的是NoClassDefFoundError,它在JAVA的異常體系中是一個Error

2、動態態加載的時候如果在運行環境中找不到要初始化的類,拋出的是ClassNotFoundException,它在JAVA的異常體系中是一個checked異常

12、3.理解Class.forName

Class.forName()是一種獲取Class對象的方法,而且是靜態方法。

Class.forName()是一個靜態方法,同樣可以用來加載類,Class.forName()返回與給定的字符串名稱相關聯類或接口的Class對象。注意這是一種獲取Class對象的方法

官方給出的API文檔如下

publicstatic Class<?> forName(String className)

Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to:

Class.forName(className,true, currentLoader)

where currentLoader denotes the definingclass loader of the current class.

For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread:

Class t =Class.forName("java.lang.Thread")

A call to forName("X") causes theclass named X to beinitialized.

Parameters:

className - the fully qualifiedname of the desired class.

Returns:

the Class object for the classwith the specified name.

可以看出,Class.forName(className)實際上是調用Class.forName(className,true, this.getClass().getClassLoader())。第二個參數,是指Class被loading后是不是必須被初始化。可以看出,使用Class.forName(className)加載類時則已初始化。所以Class.forName()方法可以簡單的理解為:獲得字符串參數中指定的類,並初始化該類。

12、4.Class.forName與ClassLoader.loadClass區別

首先,我們必須先明確類加載機制的三個過程主要是:加載 –> 連接 –> 初始化。

  • Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;

  • ClassLoader.loadClass():只干一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。

  • Class.forName(name, initialize, loader):帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法採用調用構造函數,創建類的對象 。

這個時候,我們再來看一個程序:

package com.jvm.classloader;

class Demo{
    static {
        System.out.println("static 靜態代碼塊");
    }
}

public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader();
        //1、使用ClassLoader.loadClass()來加載類,不會執行初始化塊
        classLoader.loadClass("com.jvm.classloader.Demo");
        
        //2、使用Class.forName()來加載類,默認會執行初始化塊
        Class.forName("com.jvm.classloader.Demo");
        
        //3、使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊 
        Class.forName("com.jvm.classloader.Demo",false,classLoader);
    }
}

記得一個一個測試!我上面的程序是一次寫了三個的並且已經標明了標號1、2、3!!!各位再自個電腦上跑一遍,思路就很會清晰了!

13、總結

類的加載、連接與初始化:

1、加載:查找並加載類的二進制數據到java虛擬機中

 

2、 連接:

驗證: 確保被加載的類的正確性
準備:為類的靜態變量分配內存,並將其初始化為默認值,但是到達初始化之前類變量都沒有初始化為真正的初始值(如果是被 final 修飾的類變量,則直接會被初始成用戶想要的值。)
解析:把類中的符號引用轉換為直接引用,就是在類型的常量池中尋找類、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過程

3、 初始化:為類的靜態變量賦予正確的初始值

 

類從磁盤上加載到內存中要經歷五個階段:加載、連接、初始化、使用、卸載

Java程序對類的使用方式可分為兩種

(1)主動使用
(2)被動使用

所有的Java虛擬機實現必須在每個類或接口被Java程序“首次主動使用”時才能初始化他們
主動使用
(1)創建類的實例
(2)訪問某個類或接口的靜態變量 getstatic(助記符),或者對該靜態變量賦值 putstatic
(3)調用類的靜態方法 invokestatic
(4)反射(Class.forName(“com.test.Test”))
(5)初始化一個類的子類
(6)Java虛擬機啟動時被標明啟動類的類以及包含Main方法的類
(7)JDK1.7開始提供的動態語言支持(了解)

被動使用
除了上面七種情況外,其他使用java類的方式都被看做是對類的被動使用,都不會導致類的初始化

14、特別注意

初始化入口方法。當進入類加載的初始化階段后,JVM 會尋找整個 main 方法入口,從而初始化 main 方法所在的整個類。當需要對一個類進行初始化時,會首先初始化類構造器(),之後初始化對象構造器()。

初始化類構造器:JVM 會按順序收集類變量的賦值語句、靜態代碼塊,最終組成類構造器由 JVM 執行。
初始化對象構造器:JVM 會按照收集成員變量的賦值語句、普通代碼塊,最後收集構造方法,將它們組成對象構造器,最終由 JVM 執行。值得特別注意的是,如果沒有監測或者收集到構造函數的代碼,則將不會執行對象初始化方法。對象初始化方法一般在實例化類對象的時候執行。

如果在初始化 main 方法所在類的時候遇到了其他類的初始化,那麼就先加載對應的類,加載完成之後返回。如此反覆循環,最終返回 main 方法所在類。

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

[UWP]使用Win2D的BorderEffect實現圖片的平鋪功能

1. WPF有,而UWP沒有的圖片平鋪功能

在WPF中只要將ImageSource的TileMode屬性設置為Tile即可實現圖片的平鋪,具體可見WPF的這些文檔:

WPF圖片平鋪功能我幾乎沒用過,只是作為基礎中的基礎知識記住了用法。我以為那麼基礎的功能在UWP肯定有,根本不用懷疑,所以當我在UWP中發現這麼基礎的東西居然沒有時真的嚇了一跳。

上圖左面是WPF版本的TileBrush,右邊是UWP版本,可以看到UWP版本功能少了一大半。

這麼小的一個類,我覺得沒必要在這裏做簡化吧。幸好圖片平鋪可以使用Win2D里的實現。

2. UWP中的圖片平鋪功能

<Grid>
    <Rectangle x:Name="Background" />
</Grid>

假設有以上的XAML,要在名為Background的元素上應用合成畫筆,首先引用 nuget包,然後參考官方文檔中 的部分使用圖片創建一個合成畫筆:

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var imageBrush = compositor.CreateSurfaceBrush();
var loadedSurface = LoadedImageSurface.StartLoadFromUri(new Uri("ms-appx:///110Strawberry.png"));
imageBrush.Surface = loadedSurface;
imageBrush.Stretch = CompositionStretch.None;

現在就差創建一個SpriteVisual並把它應用到Background的VisualTree上了,順便一提,是這張圖片:

不過要實現平鋪功能還需要創建一個BorderEffect:

var borderEffect = new BorderEffect
{
    Source = new CompositionEffectSourceParameter("source")
};


var effectFactory = compositor.CreateEffectFactory(borderEffect);
var effectBrush = effectFactory.CreateBrush();
effectBrush.SetSourceParameter("source", imageBrush);


var sprite = compositor.CreateSpriteVisual();
sprite.Brush = effectBrush;

var backgroundVisual = ElementCompositionPreview.GetElementVisual(Background);
var bindSizeAnimation = compositor.CreateExpressionAnimation("backgroundVisual.Size");
bindSizeAnimation.SetReferenceParameter("backgroundVisual", backgroundVisual);
sprite.StartAnimation("Size", bindSizeAnimation);

ElementCompositionPreview.SetElementChildVisual(Background, sprite);

總之BorderEffect以imageBrush為Source,其它都保留默認值,將它它應用到Background的VisualTree上後效果如下:

這還不是我想要的平鋪效果。這是因為這時候ExtendX和ExtendY保持默認值的Clamp,這個類型會讓BorderEffect重複圖像邊緣的屬性。如果要實現我想要的平鋪需要將這兩個屬性設置為Wrap:

borderEffect.ExtendX = CanvasEdgeBehavior.Wrap;
borderEffect.ExtendY = CanvasEdgeBehavior.Wrap;

居然不是從左上角開始平鋪的,和我的想法還是有出入,不過這種細節就算了。順便一提ExtendX和ExtendY還可以設置為Mirror,效果如下:

3. 綁定Size

var backgroundVisual = ElementCompositionPreview.GetElementVisual(Background);
var bindSizeAnimation = compositor.CreateExpressionAnimation("backgroundVisual.Size");
bindSizeAnimation.SetReferenceParameter("backgroundVisual", backgroundVisual);
sprite.StartAnimation("Size", bindSizeAnimation);

最後順便提一下,上面的代碼中有這麼一段代碼沒介紹到,這是用來動態地設置SpriteVisual的尺寸。ExpressionAnimation有一直運行和永不停止這兩個特性,創建ExpressionAnimation並在SpriteVisual上運行動畫,實際上將SpriteVisual的Size永遠地綁定為backgroundVisual 的Size的值。其實簡單地訂閱SizeChanged事件也能達到這個效果,代碼好像還少些。

4. 結語

這麼簡單的功能居然都要這麼多代碼,或者有更簡單的實現?不過凡事都有要辯證地看,幸好它這麼複雜,又讓我水了一篇博客。

有給出其它的方案,可以參考一下。

5. 參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

擬砍伐森林建廠 Tesla歐洲「超級工廠」被叫停

摘錄自2020年2月17日星島日報報導

全球近年多現極端天氣,多國環保組織遞交請願,要求政府重視環保議題。美國最大電動車公司特斯拉(Tesla)早前計劃砍伐德國柏林的一處森林地建廠,事件遭到當地居民抗議後。當地法院昨(16日)表示,已應環保組織請求作出裁決,要求Tesla暫停在當地建廠的砍伐森林前期工程。

Tesla行政總監馬斯克去年11月宣布,將首家歐洲工廠落戶於柏林近郊的勃蘭登堡州,其後獲環境部許可,砍伐在選址區域內一片面積約92公頃的森林。不過此舉引來當地環保組織「綠色聯邦」不滿,擔憂工廠會威脅當地野生動物和水源,並向當地行政法院提出申請,要求緊急停止砍伐,惟被駁回。

最後當地高等行政法院推翻決定,同意要求特斯拉暫時停止砍伐。
 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

誆稱可治療武漢肺炎 微信流傳非法犀牛角藥品 成野生動物走私漏洞

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

印尼環保英雄 扮蜘蛛人清垃圾獲民眾回響

摘錄自2020年2月17日公視報導

印尼的南蘇拉威西省,有一位超級環保英雄「蜘蛛人」,天天穿梭在各種下水道、海灘跟街道,做環境清潔工作。他的努力終於成功讓印尼民眾開始關注垃圾問題。

印尼環保人士魯迪哈托諾平時在咖啡館工作,因為希望居住環境乾淨,常常會默默穿梭在大街小巷中,清理塑膠垃圾。一開始,他的善行並沒有引發別人注意,直到他為了逗小侄子開心,買了蜘蛛人的衣服,角色扮演後,他才突發奇想,打扮成蜘蛛人清潔街道,希望引發更多人、投入清掃環境的行動。

魯迪說:「儘量減少塑膠垃圾是最重要的事情。因為塑膠很難分解。我們政府解決的方案之一,是對一次性塑膠袋進行規限。但實際上,應該要有更嚴格的規定。」

根據美國科學雜誌的研究顯示,身為全球第四人口大國,印尼每年都會產出320萬垃圾,但因為國家特殊的島嶼地形,幾乎有一半垃圾,都漂流到海裡,也讓印尼成為中國之外,世界最大的塑膠垃圾生產國。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

containerd 與安全沙箱的 Kubernetes 初體驗

作者 | 易立  阿里雲資深技術專家

containerd 是一個開源的行業標準容器運行時,關注於簡單、穩定和可移植,同時支持 Linux 和 Windows。

  • 2016 年 12 月 14 日,Docker 公司宣布將 Docker Engine 的核心組件 containerd 捐贈到一個新的開源社區獨立發展和運營。阿里雲、AWS、 Google、IBM 和 Microsoft 作為初始成員,共同建設 containerd 社區;

  • 2017 年 3 月,Docker 將 containerd 捐獻給 CNCF(雲原生計算基金會)。containerd 得到了快速的發展和廣泛的支持;

  • Docker 引擎已經將 containerd 作為容器生命周期管理的基礎,Kubernetes 也在 2018 年 5 月,正式支持 containerd 作為容器運行時管理器;

  • 2019 年 2 月,CNCF 宣布 containerd 畢業,成為生產可用的項目。

containerd 從 1.1 版本開始就已經內置了 Container Runtime Interface (CRI) 支持,進一步簡化了對 Kubernetes 的支持。其架構圖如下:

在 Kubernetes 場景下,containerd 與完整 Docker Engine 相比,具有更少的資源佔用和更快的啟動速度。

圖片來源:

紅帽主導的 cri-o 是與 containerd 競爭的容器運行時管理項目。containerd 與 cri-o 項目相比,在性能上具備優勢,在社區支持上也更加廣泛。

圖片來源:

更重要的是 containerd 提供了靈活的擴展機制,支持各種符合 OCI(Open Container Initiative)的容器運行時實現,比如 runc 容器(也是熟知的 Docker 容器)、KataContainer、gVisor 和 Firecraker 等安全沙箱容器。

在 Kubernetes 環境中,可以用不同的 API 和命令行工具來管理容器 / Pod、鏡像等概念。為了便於大家理解,我們可以用下圖說明如何利用不同層次的 API 和 CLI 管理容器生命周期管理。

  • Kubectl:是集群層面的命令行工具,支持 Kubernetes 的基本概念
  • :是針對節點上 CRI 的命令行工具
  • :是針對 containerd 的命令行工具

體驗

Minikube 是體驗 containerd 作為 Kubernetes 容器運行時的最簡單方式,我們下面將其作為 Kubernetes 容器運行時,並支持 runc 和 gvisor 兩種不同的實現。

早期由於網絡訪問原因,很多朋友無法直接使用官方 Minikube 進行實驗。在最新的 Minikube 1.5 版本中,已經提供了完善的配置化方式,可以幫助大家利用阿里雲的鏡像地址來獲取所需 Docker 鏡像和配置,同時支持 Docker/Containerd 等不同容器運行時。我們一個 Minikube 虛擬機環境,注意需要指明 --container-runtime=containerd 參數設置 containerd 作為容器運行時。同時 registry-mirror 也要替換成自己的阿里雲鏡像加速地址。

$ minikube start --image-mirror-country cn \
    --iso-url=https://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/iso/minikube-v1.5.0.iso \
    --registry-mirror=https://XXX.mirror.aliyuncs.com \
    --container-runtime=containerd
  Darwin 10.14.6 上的 minikube v1.5.0
  Automatically selected the 'hyperkit' driver (alternates: [virtualbox])
️  您所在位置的已知存儲庫都無法訪問。正在將 registry.cn-hangzhou.aliyuncs.com/google_containers 用作後備存儲庫。
  正在創建 hyperkit 虛擬機(CPUs=2,Memory=2000MB, Disk=20000MB)...
️  VM is unable to connect to the selected image repository: command failed: curl -sS https://k8s.gcr.io/
stdout:
stderr: curl: (7) Failed to connect to k8s.gcr.io port 443: Connection timed out
: Process exited with status 7
  正在 containerd 1.2.8 中準備 Kubernetes v1.16.2…
  拉取鏡像 ...
  正在啟動 Kubernetes ...
⌛  Waiting for: apiserver etcd scheduler controller
  完成!kubectl 已經配置至 "minikube"
$ minikube dashboard
  Verifying dashboard health ...
  Launching proxy ...
  Verifying proxy health ...
  Opening http://127.0.0.1:54438/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...

部署測試應用

我們通過 Pod 部署一個 nginx 應用:

$ cat nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
$ kubectl apply -f nginx.yaml
pod/nginx created
$ kubectl exec nginx -- uname -a
Linux nginx 4.19.76 #1 SMP Fri Oct 25 16:07:41 PDT 2019 x86_64 GNU/Linux

然後,我們開啟 minikube 對 gvisor 支持:

$ minikube addons enable gvisor
  gvisor was successfully enabled
$ kubectl get pod,runtimeclass gvisor -n kube-system
NAME         READY   STATUS    RESTARTS   AGE
pod/gvisor   1/1     Running   0          60m
NAME                              CREATED AT
runtimeclass.node.k8s.io/gvisor   2019-10-27T01:40:45Z
$ kubectl get runtimeClass
NAME     CREATED AT
gvisor   2019-10-27T01:40:45Z

當 gvisor pod 進入 Running 狀態的時候,可以部署 gvisor 測試應用。

我們可以看到 K8s 集群中已經註冊了一個 gvisor 的“runtimeClassName”。之後,開發者可以通過在 Pod 聲明中的 “runtimeClassName” 來選擇不同類型的容器運行時實現。比如,如下我們創建一個運行在 gvisor 沙箱容器中的 nginx 應用。

$ cat nginx-untrusted.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-untrusted
spec:
  runtimeClassName: gvisor
  containers:
  - name: nginx
    image: nginx
$ kubectl apply -f nginx-untrusted.yaml
pod/nginx-untrusted created
$ kubectl exec nginx-untrusted -- uname -a
Linux nginx-untrusted 4.4 #1 SMP Sun Jan 10 15:06:54 PST 2016 x86_64 GNU/Linux

我們可以清楚地發現:由於基於 runc 的容器與宿主機共享操作系統內核,runc 容器中查看到的 OS 內核版本與 Minikube 宿主機 OS 內核版本相同;而 gvisor 的 runsc 容器採用了獨立內核,它和 Minikube 宿主機 OS 內核版本不同。

正是因為每個沙箱容器擁有獨立的內核,減小了安全攻擊面,具備更好的安全隔離特性。適合隔離不可信的應用,或者多租戶場景。注意:gvisor 在 minikube 中,通過 ptrace 對內核調用進行攔截,其性能損耗較大,此外 gvisor 的兼容性還有待增強。

使用 ctl 和 crictl 工具

我們現在可以進入進入 Minikube 虛擬機:

$ minikube ssh

containerd 支持通過名空間對容器資源進行隔離,查看現有 containerd 名空間:

$ sudo ctr namespaces ls
NAME   LABELS
k8s.io
# 列出所有容器鏡像
$ sudo ctr --namespace=k8s.io images ls
...
# 列出所有容器列表
$ sudo ctr --namespace=k8s.io containers ls

在 Kubernetes 環境更加簡單的方式是利用 crictl 對 pods 進行操作。

# 查看pod列表
$ sudo crictl pods
POD ID              CREATED             STATE               NAME                                         NAMESPACE              ATTEMPT
78bd560a70327       3 hours ago         Ready               nginx-untrusted                              default                0
94817393744fd       3 hours ago         Ready               nginx                                        default                0
...
# 查看名稱包含nginx的pod的詳細信息
$ sudo crictl pods --name nginx -v
ID: 78bd560a70327f14077c441aa40da7e7ad52835100795a0fa9e5668f41760288
Name: nginx-untrusted
UID: dda218b1-d72e-4028-909d-55674fd99ea0
Namespace: default
Status: Ready
Created: 2019-10-27 02:40:02.660884453 +0000 UTC
Labels:
    io.kubernetes.pod.name -> nginx-untrusted
    io.kubernetes.pod.namespace -> default
    io.kubernetes.pod.uid -> dda218b1-d72e-4028-909d-55674fd99ea0
Annotations:
    kubectl.kubernetes.io/last-applied-configuration -> {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx-untrusted","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx"}],"runtimeClassName":"gvisor"}}
    kubernetes.io/config.seen -> 2019-10-27T02:40:00.675588392Z
    kubernetes.io/config.source -> api
ID: 94817393744fd18b72212a00132a61c6cc08e031afe7b5295edafd3518032f9f
Name: nginx
UID: bfcf51de-c921-4a9a-a60a-09faab1906c4
Namespace: default
Status: Ready
Created: 2019-10-27 02:38:19.724289298 +0000 UTC
Labels:
    io.kubernetes.pod.name -> nginx
    io.kubernetes.pod.namespace -> default
    io.kubernetes.pod.uid -> bfcf51de-c921-4a9a-a60a-09faab1906c4
Annotations:
    kubectl.kubernetes.io/last-applied-configuration -> {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"nginx"}]}}
    kubernetes.io/config.seen -> 2019-10-27T02:38:18.206096389Z
    kubernetes.io/config.source -> api

containerd 與 Docker 的關係

很多同學都關心 containerd 與 Docker 的關係,以及是否 containerd 可以取代 Docker?

containerd 已經成為容器運行時的主流實現,也得到了 Docker 社區和 Kubernetes 社區的大力支持。Docker Engine 底層的容器生命周期管理也是基於 containerd 實現。

但是 Docker Engine 包含了更多的開發者工具鏈,比如鏡像構建。也包含了 Docker 自己的日誌、存儲、網絡、Swarm 編排等能力。此外,絕大多數容器生態廠商,如安全、監控、開發等對 Docker Engine 的支持比較完善,對 containerd 的支持也在逐漸補齊。

所以在 Kubernetes 運行時環境,對安全和效率和定製化更加關注的用戶可以選擇 containerd 作為容器運行時環境;對於大多數開發者,繼續使用 Docker Engine 作為容器運行時也是一個不錯的選擇。

阿里雲容器服務對 containerd 的支持

在阿里雲 Kubernetes 服務 ACK,我們已經採用 containerd 作為容器運行時管理,來支撐安全沙箱容器和 runc 容器的混合部署。在現有產品中,我們和阿里雲操作系統團隊、螞蟻金服一起支持了基於輕量虛擬化的 runV 沙箱容器,4Q 也將和操作系統團隊、安全團隊合作發布基於 Intel SGX 的可信加密沙箱容器。

具體產品信息可以參考。

Serverless Kubernetes(ASK)中,我們也利用 containerd 靈活的插件機制定製和剪裁了面向 nodeless 環境的容器運行時實現。

“ 阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!

SpringBoot 源碼解析 (三)—– Spring Boot 精髓:啟動時初始化數據

在我們用 springboot 搭建項目的時候,有時候會碰到在項目啟動時初始化一些操作的需求 ,針對這種需求 spring boot為我們提供了以下幾種方案供我們選擇:

  • ApplicationRunner 與 CommandLineRunner 接口

  • Spring容器初始化時InitializingBean接口和@PostConstruct

  • Spring的事件機制

ApplicationRunner與CommandLineRunner

我們可以實現 ApplicationRunner 或 CommandLineRunner 接口, 這兩個接口工作方式相同,都只提供單一的run方法,該方法在SpringApplication.run(…)完成之前調用,不知道大家還對我上一篇文章結尾有沒有印象,我們先來看看這兩個接口

public interface ApplicationRunner {
    void run(ApplicationArguments var1) throws Exception;
}

public interface CommandLineRunner {
    void run(String... var1) throws Exception;
}

都只提供單一的run方法,接下來我們來看看具體的使用

ApplicationRunner

構造一個類實現ApplicationRunner接口

//需要加入到Spring容器中
@Component public class ApplicationRunnerTest implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner");
    }
}

很簡單,首先要使用@Component將實現類加入到Spring容器中,為什麼要這樣做我們待會再看,然後實現其run方法實現自己的初始化數據邏輯就可以了

CommandLineRunner

對於這兩個接口而言,我們可以通過Order註解或者使用Ordered接口來指定調用順序, @Order() 中的值越小,優先級越高

//需要加入到Spring容器中
@Component
@Order(1)
public class CommandLineRunnerTest implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner...");
    }
}

同樣需要加入到Spring容器中,CommandLineRunner的參數是最原始的參數,沒有進行任何處理,ApplicationRunner的參數是ApplicationArguments,是對原始參數的進一步封裝

源碼分析

大家回顧一下我上一篇文章,也就是的最後一步,這裏我直接把代碼複製過來

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<Object>();
    //獲取容器中所有的ApplicationRunner的Bean實例
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); //獲取容器中所有的CommandLineRunner的Bean實例
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<Object>(runners)) {
        if (runner instanceof ApplicationRunner) {
            //執行ApplicationRunner的run方法
 callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            //執行CommandLineRunner的run方法
 callRunner((CommandLineRunner) runner, args);
        }
    }
}

很明顯,是直接從Spring容器中獲取ApplicationRunner和CommandLineRunner的實例,並調用其run方法,這也就是為什麼我要使用@Component將ApplicationRunner和CommandLineRunner接口的實現類加入到Spring容器中了。

InitializingBean

在spring初始化bean的時候,如果bean實現了 InitializingBean 接口,在對象的所有屬性被初始化后之後才會調用afterPropertiesSet()方法

@Component public class InitialingzingBeanTest implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean..");
    }
}

我們可以看出spring初始化bean肯定會在 ApplicationRunner和CommandLineRunner接口調用之前。

@PostConstruct

@Component public class PostConstructTest {

    @PostConstruct public void postConstruct() {
        System.out.println("init...");
    }
}

我們可以看到,只用在方法上添加@PostConstruct註解,並將類注入到Spring容器中就可以了。我們來看看@PostConstruct註解的方法是何時執行的

在Spring初始化bean時,對bean的實例賦值時,populateBean方法下面有一個initializeBean(beanName, exposedObject, mbd)方法,這個就是用來執行用戶設定的初始化操作。我們看下方法體:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
    if (System.getSecurityManager() != null) {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            // 激活 Aware 方法
            invokeAwareMethods(beanName, bean);
            return null;
        }, getAccessControlContext());
    }
    else {
        // 對特殊的 bean 處理:Aware、BeanClassLoaderAware、BeanFactoryAware
        invokeAwareMethods(beanName, bean);
    }

    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 后處理器
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 激活用戶自定義的 init 方法
 invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
    if (mbd == null || !mbd.isSynthetic()) {
        // 后處理器
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}

我們看到會先執行后處理器然後執行invokeInitMethods方法,我們來看下applyBeanPostProcessorsBeforeInitialization

public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)  
        throws BeansException {  

    Object result = existingBean;  
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { result = beanProcessor.postProcessBeforeInitialization(result, beanName);  
        if (result == null) {  
            return result;  
        }  
    }  
    return result;  
}

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)  
        throws BeansException {  

    Object result = existingBean;  
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {  
        result = beanProcessor.postProcessAfterInitialization(result, beanName);  
        if (result == null) {  
            return result;  
        }  
    }  
    return result;  
}

獲取容器中所有的後置處理器,循環調用後置處理器的postProcessBeforeInitialization方法,這裏我們來看一個BeanPostProcessor

public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {
    public CommonAnnotationBeanPostProcessor() {
        this.setOrder(2147483644);
        //設置初始化參數為PostConstruct.class
        this.setInitAnnotationType(PostConstruct.class); this.setDestroyAnnotationType(PreDestroy.class);
        this.ignoreResourceType("javax.xml.ws.WebServiceContext");
    }
    //略...
}

在構造器中設置了一個屬性為PostConstruct.class,再次觀察CommonAnnotationBeanPostProcessor這個類,它繼承自InitDestroyAnnotationBeanPostProcessor。InitDestroyAnnotationBeanPostProcessor顧名思義,就是在Bean初始化和銷毀的時候所作的一個前置/後置處理器。查看InitDestroyAnnotationBeanPostProcessor類下的postProcessBeforeInitialization方法:

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {  
  LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());  
   try {  
      metadata.invokeInitMethods(bean, beanName);  
   }  
   catch (InvocationTargetException ex) {  
       throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());  
   }  
   catch (Throwable ex) {  
       throw new BeanCreationException(beanName, "Couldn't invoke init method", ex);  
   }  
    return bean;  
}  

private LifecycleMetadata buildLifecycleMetadata(final Class clazz) {  
       final LifecycleMetadata newMetadata = new LifecycleMetadata();  
       final boolean debug = logger.isDebugEnabled();  
       ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {  
           public void doWith(Method method) {  
              if (initAnnotationType != null) {  
                   //判斷clazz中的methon是否有initAnnotationType註解,也就是PostConstruct.class註解
                  if (method.getAnnotation(initAnnotationType) != null) { //如果有就將方法添加進LifecycleMetadata中
 newMetadata.addInitMethod(method);  
                     if (debug) {  
                         logger.debug("Found init method on class [" + clazz.getName() + "]: " + method);  
                     }  
                  }  
              }  
              if (destroyAnnotationType != null) {  
                    //判斷clazz中的methon是否有destroyAnnotationType註解
                  if (method.getAnnotation(destroyAnnotationType) != null) {  
                     newMetadata.addDestroyMethod(method);  
                     if (debug) {  
                         logger.debug("Found destroy method on class [" + clazz.getName() + "]: " + method);  
                     }  
                  }  
              }  
           }  
       });  
       return newMetadata;  
} 

在這裡會去判斷某方法是否有PostConstruct.class註解,如果有,則添加到init/destroy隊列中,後續一一執行。@PostConstruct註解的方法會在此時執行,我們接着來看invokeInitMethods

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
        throws Throwable {

    // 是否實現 InitializingBean // 如果實現了 InitializingBean 接口,則只掉調用bean的 afterPropertiesSet()
    boolean isInitializingBean = (bean instanceof InitializingBean); if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        if (logger.isDebugEnabled()) {
            logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
        }
        if (System.getSecurityManager() != null) {
            try {
                AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                    ((InitializingBean) bean).afterPropertiesSet();
                    return null;
                }, getAccessControlContext());
            }
            catch (PrivilegedActionException pae) {
                throw pae.getException();
            }
        }
        else {
            // 直接調用 afterPropertiesSet()
 ((InitializingBean) bean).afterPropertiesSet();
        }
    }

    if (mbd != null && bean.getClass() != NullBean.class) {
        // 判斷是否指定了 init-method(),
        // 如果指定了 init-method(),則再調用制定的init-method
        String initMethodName = mbd.getInitMethodName();
        if (StringUtils.hasLength(initMethodName) &&
                !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 利用反射機制執行
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

首先檢測當前 bean 是否實現了 InitializingBean 接口,如果實現了則調用其 afterPropertiesSet(),然後再檢查是否也指定了 init-method(),如果指定了則通過反射機制調用指定的 init-method()。

我們也可以發現@PostConstruct會在實現 InitializingBean 接口的afterPropertiesSet()方法之前執行

Spring的事件機制

基礎概念

Spring的事件驅動模型由三部分組成

  • 事件: ApplicationEvent ,繼承自JDK的 EventObject ,所有事件都要繼承它,也就是被觀察者
  • 事件發布者: ApplicationEventPublisher 及 ApplicationEventMulticaster 接口,使用這個接口,就可以發布事件了
  • 事件監聽者: ApplicationListener ,繼承JDK的 EventListener ,所有監聽者都繼承它,也就是我們所說的觀察者,當然我們也可以使用註解 @EventListener ,效果是一樣的

事件

在Spring框架中,默認對ApplicationEvent事件提供了如下支持:

  • ContextStartedEvent:ApplicationContext啟動后觸發的事件
  • ContextStoppedEvent:ApplicationContext停止后觸發的事件
  • ContextRefreshedEvent: ApplicationContext初始化或刷新完成后觸發的事件 ;(容器初始化完成后調用,所以我們可以利用這個事件做一些初始化操作)
  • ContextClosedEvent:ApplicationContext關閉后觸發的事件;(如 web 容器關閉時自動會觸發spring容器的關閉,如果是普通 java 應用,需要調用ctx.registerShutdownHook();註冊虛擬機關閉時的鈎子才行) 

構造一個類繼承ApplicationEvent

public class TestEvent extends ApplicationEvent {

    private String message;
    
    public TestEvent(Object source) {
        super(source);
    }

    public void getMessage() {
        System.out.println(message);
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

創建事件監聽者

有兩種方法可以創建監聽者,一種是直接實現ApplicationListener的接口,一種是使用註解 @EventListener , 註解是添加在監聽方法上的 ,下面的例子是直接實現的接口

@Component
public class ApplicationListenerTest implements ApplicationListener<TestEvent> {
    @Override
    public void onApplicationEvent(TestEvent testEvent) {
        testEvent.getMessage();
    }
}

事件發布

對於事件發布,代表者是 ApplicationEventPublisher 和 ApplicationEventMulticaster ,ApplicationContext接口繼承了ApplicationEventPublisher,並在AbstractApplicationContext實現了具體代碼,實際執行是委託給ApplicationEventMulticaster(可以認為是多播)

下面是一個事件發布者的測試實例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EventTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void publishTest() {
        TestEvent testEvent = new TestEvent("");
        testEvent.setMessage("hello world");
       applicationContext.publishEvent(testEvent);
    }
}

利用ContextRefreshedEvent事件進行初始化操作

利用 ContextRefreshedEvent 事件進行初始化,該事件是 ApplicationContext 初始化完成后調用的事件,所以我們可以利用這個事件,對應實現一個 監聽器 ,在其 onApplicationEvent() 方法里初始化操作

@Component public class ApplicationListenerTest implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("容器刷新完成后,我被調用了..");
    }
}

 

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※台灣海運大陸貨務運送流程

※兩岸物流進出口一站式服務

特斯拉Model X中國售價96萬元起 Q2開始交付

2月1日,特斯拉公司宣佈將在中國市場首次推出Model X頂級限量 Signature Red版P90D(售價人民幣147.95萬元), 同時公佈目前在中國市場推出的Model X 標準版90D的官方價格區間(人民幣96.1-117.18萬元),並開始電話邀請預定Model X的消費者進行車輛選配,新車預計將於二季度開始在中國交付。

特斯拉目前在中國推出的Model X 90D擁有90千瓦時(kWh)的電池容量,續航里程高達470公里,零到百公里加速5.0秒,最高時速達到250公里/小時。Model X在中國的官方價格一直備受關注。得益於業內獨創的“官網電商+線下體驗店”的直營模式,特斯拉得以做到全球公平、透明定價。本次公佈的Model X 90D在中國的定價(人民幣96.1-117.18萬元)繼續秉持了這一原則,即只在美國本土售價的基礎上增加了關稅和運輸成本。消費者可以透明、自主得通過特斯拉官網定制和下單。

頂級限量Signature版是特斯拉在第一代超級跑車Roadster、第二代豪華轎跑車Model S的首發階段慣例推出的限量產品,車身飾有“Signature”字樣,可謂“頂配中的頂配”。本次在中國推出的Model X頂級限量Signature Red版P90D,其所擁有的名為“Signature Red”的紅色區別於所有其他量產的特斯拉車輛。當它開啟Ludicrous狂暴模式時,百公里加速僅需3.4秒,續航里程高達450公里,並配備主動擾流板和特斯拉紅色制動卡鉗等。


特斯拉頂配限量版“Signature Red” Model X

駕駛過程中,Model X的主動安全防護功能通過雷達和聲納系統即時監視路面情況,並主動向駕駛者提供回饋,最大幅度避免事故發生。獨特的車身結構能夠大幅度降低重心,與同類SUV相比,降低了50%的側翻風險。在NHTSA(美國國家公路交通安全管理局)的所有安全測試項目中,Model X都獲得了5星級評價。作為智慧SUV的代表,Model X最新的空中升級(OTA)7.1系統,擁有業界領先的自動駕駛(Autopilot)、自動泊車、召喚(Summon)等功能,讓駕車不再是一件麻煩的事情,而且該系統還在不斷前行。

Model X獨有的鷹翼門採用碳纖維雙鉸鏈結構,在傳統鷗翼門的設計之上,進一步提高承壓性及整車密封性。超聲波感應器智慧探測車身周圍的空間,以判斷車門是否能夠安全開啟。車門全部開啟只需要6-7秒,僅佔用30釐米的側方空間。全景擋風玻璃將前擋風玻璃和天窗連在一起,是目前轎車上安裝的最大單塊玻璃,給予使用者更加開闊的視野,優化的防曬隔熱玻璃膜全面提高安全係數。

7座Model X採用2+3+2的佈局,並且每個座位都採用獨立座椅,滿足全家的出行需求。此外還擁有豐富、超大的儲物空間。行李可以輕鬆置於Model X的前備箱、後備箱以及座椅下方。第三排座椅放倒之後,Model X的後備箱可輕鬆擺放高爾夫球包、折疊式兒童手推車、自行車、滑雪手杖及滑雪板等大型裝備。如果加裝車尾懸掛支架,Model X還可以攜帶更多行李。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!