使用ASP.NET Web API和Web API Client Gen使Angular 2應用程序的開發更加高效

本文介紹“ 為ASP.NET Web API生成TypeScript客戶端API ”,重點介紹Angular 2+代碼示例和各自的SDLC。如果您正在開發.NET Core Web API後端,則可能需要閱讀為ASP.NET Core Web API生成C#Client API。

背景

WebApiClientGenAngular 2仍然在RC2時,2016年6月v1.9.0-beta 以來,對Angular2的支持已經可用並且在WebApiClientGenv2.0中提供了對Angular 2產品發布的支持希望NG2的發展不會如此頻繁地破壞我的CodeGen和我的Web前端應用程序。🙂

在2016年9月底發布Angular 2的第一個產品發布幾周后,我碰巧啟動了一個使用Angular2的主要Web應用程序項目,因此我WebApiClientGen對NG2應用程序開發的使用方法幾乎相同

推定

  1. 您正在開發ASP.NET Web API 2.x應用程序,並將基於Angular 2+為SPA開發TypeScript庫。
  2. 您和其他開發人員喜歡在服務器端和客戶端都通過強類型數據和函數進行高度抽象。
  3. Web API和實體框架代碼優先使用POCO類,您可能不希望將所有數據類和成員發布到客戶端程序源碼

並且可選地,如果您或您的團隊支持基於Trunk的開發,那麼更好,因為使用的設計WebApiClientGen和工作流程WebApiClientGen假設基於Trunk的開發,這比其他分支策略(如Feature Branching和GitFlow等)更有效。對於熟練掌握TDD的團隊。

為了跟進這種開發客戶端程序的新方法,最好有一個ASP.NET Web API項目。您可以使用現有項目,也可以創建演示項目

使用代碼

本文重點介紹Angular 2+的代碼示例。假設您有一個ASP.NET Web API項目和一個Angular2項目作為VS解決方案中的兄弟項目。如果你將它們分開,那麼為了使開發步驟無縫地編寫腳本應該不難。

我認為您已閱讀“ 為ASP.NET Web API生成TypeScript客戶端API ”。為jQuery生成客戶端API的步驟幾乎與為Angular 2生成客戶端API的步驟相同。演示TypeScript代碼基於TUTORIAL:TOUR OF HEROES,許多人從中學習了Angular2。因此,您將能夠看到如何WebApiClientGen適應並改進Angular2應用程序的典型開發周期。

這是Web API代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Runtime.Serialization;
using System.Collections.Concurrent;

namespace DemoWebApi.Controllers
{
    [RoutePrefix("api/Heroes")]
    public class HeroesController : ApiController
    {
        public Hero[] Get()
        {
            return HeroesData.Instance.Dic.Values.ToArray();
        }

        public Hero Get(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryGetValue(id, out r);
            return r;
        }

        public void Delete(long id)
        {
            Hero r;
            HeroesData.Instance.Dic.TryRemove(id, out r);
        }

        public Hero Post(string name)
        {
            var max = HeroesData.Instance.Dic.Keys.Max();
            var hero = new Hero { Id = max + 1, Name = name };
            HeroesData.Instance.Dic.TryAdd(max + 1, hero);
            return hero;
        }

        public Hero Put(Hero hero)
        {
            HeroesData.Instance.Dic[hero.Id] = hero;
            return hero;
        }

        [HttpGet]
        public Hero[] Search(string name)
        {
            return HeroesData.Instance.Dic.Values.Where(d => d.Name.Contains(name)).ToArray();
        }          
    }

    [DataContract(Namespace = DemoWebApi.DemoData.Constants.DataNamespace)]
    public class Hero
    {
        [DataMember]
        public long Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }

    public sealed class HeroesData
    {
        private static readonly Lazy<HeroesData> lazy =
            new Lazy<HeroesData>(() => new HeroesData());

        public static HeroesData Instance { get { return lazy.Value; } }

        private HeroesData()
        {
            Dic = new ConcurrentDictionary<long, Hero>(new KeyValuePair<long, Hero>[] {
                new KeyValuePair<long, Hero>(11, new Hero {Id=11, Name="Mr. Nice" }),
                new KeyValuePair<long, Hero>(12, new Hero {Id=12, Name="Narco" }),
                new KeyValuePair<long, Hero>(13, new Hero {Id=13, Name="Bombasto" }),
                new KeyValuePair<long, Hero>(14, new Hero {Id=14, Name="Celeritas" }),
                new KeyValuePair<long, Hero>(15, new Hero {Id=15, Name="Magneta" }),
                new KeyValuePair<long, Hero>(16, new Hero {Id=16, Name="RubberMan" }),
                new KeyValuePair<long, Hero>(17, new Hero {Id=17, Name="Dynama" }),
                new KeyValuePair<long, Hero>(18, new Hero {Id=18, Name="Dr IQ" }),
                new KeyValuePair<long, Hero>(19, new Hero {Id=19, Name="Magma" }),
                new KeyValuePair<long, Hero>(20, new Hero {Id=29, Name="Tornado" }),

                });
        }

        public ConcurrentDictionary<long, Hero> Dic { get; private set; }
    }
}

 

步驟0:將NuGet包WebApiClientGen安裝到Web API項目

安裝還將安裝依賴的NuGet包Fonlow.TypeScriptCodeDOMFonlow.Poco2Ts項目引用。

此外,用於觸發CodeGen的CodeGenController.cs被添加到Web API項目的Controllers文件夾中。

CodeGenController只在調試版本開發過程中應該是可用的,因為客戶端API應該用於Web API的每個版本生成一次。

提示

如果您正在使用@ angular / http中定義的Angular2的Http服務,那麼您應該使用WebApiClientGenv2.2.5。如果您使用的HttpClient是@ angular / common / http中定義的Angular 4.3中可用服務,並且在Angular 5中已棄用,那麼您應該使用WebApiClientGenv2.3.0。

第1步:準備JSON配置數據

下面的JSON配置數據是POSTCodeGen Web API:

{
    "ApiSelections": {
        "ExcludedControllerNames": [
            "DemoWebApi.Controllers.Account"
        ],

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],
        "CherryPickingMethods": 1
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "DemoWebApi.ClientApi",
        "GenerateBothAsyncAndSync": true,

        "CamelCase": true,
        "TypeScriptNG2Folder": "..\\DemoAngular2\\clientapi",
        "NGVersion" : 5

    }
}

 

提示

Angular 6正在使用RxJS v6,它引入了一些重大變化,特別是對於導入Observable默認情況下,WebApiClientGen2.4和更高版本默認將導入聲明為import { Observable } from 'rxjs';  。如果您仍在使用Angular 5.x,則需要"NGVersion" : 5在JSON配置中聲明,因此生成的代碼中的導入將是更多詳細信息,import { Observable } from 'rxjs/Observable'; . 請參閱RxJS v5.x至v6更新指南RxJS:版本6的TSLint規則

備註

您應確保“ TypeScriptNG2Folder”存在的文件夾存在,因為WebApiClientGen不會為您創建此文件夾,這是設計使然。

建議到JSON配置數據保存到與文件類似的這一個位於Web API項目文件夾。

如果您在Web API項目中定義了所有POCO類,則應將Web API項目的程序集名稱放在“ DataModelAssemblyNames” 數組中如果您有一些專用的數據模型程序集可以很好地分離關注點,那麼您應該將相應的程序集名稱放入數組中。您可以選擇為jQuery或NG2或C#客戶端API代碼生成TypeScript客戶端API代碼,或者全部三種。

“ TypeScriptNG2Folder”是Angular2項目的絕對路徑或相對路徑。例如,“ .. \\ DemoAngular2 \\ ClientApi ”表示DemoAngular2作為Web API項目的兄弟項目創建的Angular 2項目“ ”。

CodeGen根據“從POCO類生成強類型打字稿接口CherryPickingMethods,其在下面的文檔註釋描述”:

/// <summary>
/// Flagged options for cherry picking in various development processes.
/// </summary>
[Flags]
public enum CherryPickingMethods
{
    /// <summary>
    /// Include all public classes, properties and properties.
    /// </summary>
    All = 0,

    /// <summary>
    /// Include all public classes decorated by DataContractAttribute,
    /// and public properties or fields decorated by DataMemberAttribute.
    /// And use DataMemberAttribute.IsRequired
    /// </summary>
    DataContract =1,

    /// <summary>
    /// Include all public classes decorated by JsonObjectAttribute,
    /// and public properties or fields decorated by JsonPropertyAttribute.
    /// And use JsonPropertyAttribute.Required
    /// </summary>
    NewtonsoftJson = 2,

    /// <summary>
    /// Include all public classes decorated by SerializableAttribute,
    /// and all public properties or fields
    /// but excluding those decorated by NonSerializedAttribute.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    Serializable = 4,

    /// <summary>
    /// Include all public classes, properties and properties.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    AspNet = 8,
}

 

默認選項是DataContract選擇加入。您可以使用任何方法或組合方法。

第2步:運行Web API項目的DEBUG構建

步驟3:POST JSON配置數據以觸發客戶端API代碼的生成

在IIS Express上的IDE中運行Web項目。

然後使用CurlPoster或任何您喜歡的客戶端工具POST到http:// localhost:10965 / api / CodeGen,with content-type=application/json

提示

基本上,每當Web API更新時,您只需要步驟2來生成客戶端API,因為您不需要每次都安裝NuGet包或創建新的JSON配置數據。

編寫一些批處理腳本來啟動Web API和POST JSON配置數據應該不難。為了您的方便,我實際起草了一個:Powershell腳本文件CreateClientApi.ps1,它在IIS Express上啟動Web(API)項目,然後發布JSON配置文件以觸發代碼生成

基本上,您可以製作Web API代碼,包括API控制器和數據模型,然後執行CreateClientApi.ps1而已!WebApiClientGenCreateClientApi.ps1將為您完成剩下的工作。

發布客戶端API庫

現在您在TypeScript中生成了客戶端API,類似於以下示例:

import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
export namespace DemoWebApi_DemoData_Client {
    export enum AddressType {Postal, Residential}

    export enum Days {Sat=1, Sun=2, Mon=3, Tue=4, Wed=5, Thu=6, Fri=7}

    export interface PhoneNumber {
        fullNumber?: string;
        phoneType?: DemoWebApi_DemoData_Client.PhoneType;
    }

    export enum PhoneType {Tel, Mobile, Skype, Fax}

    export interface Address {
        id?: string;
        street1?: string;
        street2?: string;
        city?: string;
        state?: string;
        postalCode?: string;
        country?: string;
        type?: DemoWebApi_DemoData_Client.AddressType;
        location?: DemoWebApi_DemoData_Another_Client.MyPoint;
    }

    export interface Entity {
        id?: string;
        name: string;
        addresses?: Array<DemoWebApi_DemoData_Client.Address>;
        phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
    }

    export interface Person extends DemoWebApi_DemoData_Client.Entity {
        surname?: string;
        givenName?: string;
        dob?: Date;
    }

    export interface Company extends DemoWebApi_DemoData_Client.Entity {
        businessNumber?: string;
        businessNumberType?: string;
        textMatrix?: Array<Array<string>>;
        int2DJagged?: Array<Array<number>>;
        int2D?: number[][];
        lines?: Array<string>;
    }

    export interface MyPeopleDic {
        dic?: {[id: string]: DemoWebApi_DemoData_Client.Person };
        anotherDic?: {[id: string]: string };
        intDic?: {[id: number]: string };
    }
}

export namespace DemoWebApi_DemoData_Another_Client {
    export interface MyPoint {
        x: number;
        y: number;
    }

}

export namespace DemoWebApi_Controllers_Client {
    export interface FileResult {
        fileNames?: Array<string>;
        submitter?: string;
    }

    export interface Hero {
        id?: number;
        name?: string;
    }
}

   @Injectable()
    export class Heroes {
        constructor(@Inject('baseUri') private baseUri: string = location.protocol + '//' + 
        location.hostname + (location.port ? ':' + location.port : '') + '/', private http: Http){
        }

        /**
         * Get all heroes.
         * GET api/Heroes
         * @return {Array<DemoWebApi_Controllers_Client.Hero>}
         */
        get(): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes').map(response=> response.json());
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         * @param {number} id
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.get(this.baseUri + 'api/Heroes/'+id).map(response=> response.json());
        }

        /**
         * DELETE api/Heroes/{id}
         * @param {number} id
         * @return {void}
         */
        delete(id: number): Observable<Response>{
            return this.http.delete(this.baseUri + 'api/Heroes/'+id);
        }

        /**
         * Add a hero
         * POST api/Heroes?name={name}
         * @param {string} name
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        post(name: string): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.post(this.baseUri + 'api/Heroes?name='+encodeURIComponent(name), 
            JSON.stringify(null), { headers: new Headers({ 'Content-Type': 
            'text/plain;charset=UTF-8' }) }).map(response=> response.json());
        }

        /**
         * Update hero.
         * PUT api/Heroes
         * @param {DemoWebApi_Controllers_Client.Hero} hero
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        put(hero: DemoWebApi_Controllers_Client.Hero): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.put(this.baseUri + 'api/Heroes', JSON.stringify(hero), 
            { headers: new Headers({ 'Content-Type': 'text/plain;charset=UTF-8' 
            }) }).map(response=> response.json());
        }

        /**
         * Search heroes
         * GET api/Heroes?name={name}
         * @param {string} name keyword contained in hero name.
         * @return {Array<DemoWebApi_Controllers_Client.Hero>} Hero array matching the keyword.
         */
        search(name: string): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes?name='+
            encodeURIComponent(name)).map(response=> response.json());
        }
    }

 

提示

如果您希望生成的TypeScript代碼符合JavaScript和JSON的camel大小寫,則可以在WebApiConfigWeb API的腳手架代碼添加以下行

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = 
            new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();

然後屬性名稱和函數名稱將在camel大小寫中,前提是C#中的相應名稱都在Pascal大小寫中。有關詳細信息,請查看camelCasing或PascalCasing

客戶端應用編程

在像Visual Studio這樣的正常文本編輯器中編寫客戶端代碼時,您可能會獲得很好的智能感知。

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import * as namespaces from '../clientapi/WebApiNG2ClientAuto';
import DemoWebApi_Controllers_Client = namespaces.DemoWebApi_Controllers_Client;

@Component({
    moduleId: module.id,
    selector: 'my-heroes',
    templateUrl: 'heroes.component.html',
    styleUrls: ['heroes.component.css']
})

 

通過IDE進行設計時類型檢查,並在生成的代碼之上進行編譯時類型檢查,可以更輕鬆地提高客戶端編程的效率和產品質量。

不要做計算機可以做的事情,讓計算機為我們努力工作。我們的工作是為客戶提供自動化解決方案,因此最好先自行完成自己的工作。

興趣點

在典型的角2個教程,包括官方的一個  這已經存檔,作者經常督促應用程序開發者製作一個服務類,如“ HeroService”,而黃金法則是:永遠委託給配套服務類的數據訪問

WebApiClientGen為您生成此服務類DemoWebApi_Controllers_Client.Heroes,它將使用真正的Web API而不是內存中的Web API。在開發過程中WebApiClientGen,我創建了一個演示項目DemoAngular2各自用於測試的Web API控制器

典型的教程還建議使用模擬服務進行單元測試。WebApiClientGen使用真正的Web API服務要便宜得多,因此您可能不需要創建模擬服務。您應該在開發期間平衡使用模擬或實際服務的成本/收益,具體取決於您的上下文。通常,如果您的團隊已經能夠在每台開發機器中使用持續集成環境,那麼使用真實服務運行測試可能非常無縫且快速。

在典型的SDLC中,在初始設置之後,以下是開發Web API和NG2應用程序的典型步驟:

  1. 升級Web API
  2. 運行CreateClientApi.ps1以更新TypeScript for NG2中的客戶端API。
  3. 使用生成的TypeScript客戶端API代碼或C#客戶端API代碼,在Web API更新時創建新的集成測試用例。
  4. 相應地修改NG2應用程序。
  5. 要進行測試,請運行StartWebApi.ps1以啟動Web API,並在VS IDE中運行NG2應用程序。

提示

對於第5步,有其他選擇。例如,您可以使用VS IDE同時以調試模式啟動Web API和NG2應用程序。一些開發人員可能更喜歡使用“ npm start”。

本文最初是為Angular 2編寫的,具有Http服務。Angular 4.3中引入了WebApiClientGen2.3.0支持HttpClient並且生成的API在接口級別保持不變。這使得從過時的Http服務遷移到HttpClient服務相當容易或無縫,與Angular應用程序編程相比,不使用生成的API而是直接使用Http服務。

順便說一句,如果你沒有完成向Angular 5的遷移,那麼這篇文章可能有所幫助:  升級到Angular 5和HttpClient如果您使用的是Angular 6,則應使用WebApiClientGen2.4.0+。

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

死磕 java同步系列之StampedLock源碼解析

問題

(1)StampedLock是什麼?

(2)StampedLock具有什麼特性?

(3)StampedLock是否支持可重入?

(4)StampedLock與ReentrantReadWriteLock的對比?

簡介

StampedLock是java8中新增的類,它是一個更加高效的讀寫鎖的實現,而且它不是基於AQS來實現的,它的內部自成一片邏輯,讓我們一起來學習吧。

StampedLock具有三種模式:寫模式、讀模式、樂觀讀模式。

ReentrantReadWriteLock中的讀和寫都是一種悲觀鎖的體現,StampedLock加入了一種新的模式——樂觀讀,它是指當樂觀讀時假定沒有其它線程修改數據,讀取完成后再檢查下版本號有沒有變化,沒有變化就讀取成功了,這種模式更適用於讀多寫少的場景。

使用方法

讓我們通過下面的例子了解一下StampedLock三種模式的使用方法:

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) {
        // 獲取寫鎖,返回一個版本號(戳)
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 釋放寫鎖,需要傳入上面獲取的版本號
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() {
        // 樂觀讀
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        // 驗證版本號是否有變化
        if (!sl.validate(stamp)) {
            // 版本號變了,樂觀讀轉悲觀讀
            stamp = sl.readLock();
            try {
                // 重新讀取x、y的值
                currentX = x;
                currentY = y;
            } finally {
                // 釋放讀鎖,需要傳入上面獲取的版本號
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) {
        // 獲取悲觀讀鎖
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 轉為寫鎖
                long ws = sl.tryConvertToWriteLock(stamp);
                // 轉換成功
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                }
                else {
                    // 轉換失敗
                    sl.unlockRead(stamp);
                    // 獲取寫鎖
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 釋放鎖
            sl.unlock(stamp);
        }
    }
}

從上面的例子我們可以與ReentrantReadWriteLock進行對比:

(1)寫鎖的使用方式基本一對待;

(2)讀鎖(悲觀)的使用方式可以進行升級,通過tryConvertToWriteLock()方式可以升級為寫鎖;

(3)樂觀讀鎖是一種全新的方式,它假定數據沒有改變,樂觀讀之後處理完業務邏輯再判斷版本號是否有改變,如果沒改變則樂觀讀成功,如果有改變則轉化為悲觀讀鎖重試;

下面我們一起來學習它的源碼是怎麼實現的。

源碼分析

主要內部類

static final class WNode {
    // 前一個節點
    volatile WNode prev;
    // 后一個節點
    volatile WNode next;
    // 讀線程所用的鏈表(實際是一個棧結果)
    volatile WNode cowait;    // list of linked readers
    // 阻塞的線程
    volatile Thread thread;   // non-null while possibly parked
    // 狀態
    volatile int status;      // 0, WAITING, or CANCELLED
    // 讀模式還是寫模式
    final int mode;           // RMODE or WMODE
    WNode(int m, WNode p) { mode = m; prev = p; }
}

隊列中的節點,類似於AQS隊列中的節點,可以看到它組成了一個雙向鏈表,內部維護着阻塞的線程。

主要屬性

// 一堆常量
// 讀線程的個數佔有低7位
private static final int LG_READERS = 7;
// 讀線程個數每次增加的單位
private static final long RUNIT = 1L;
// 寫線程個數所在的位置
private static final long WBIT  = 1L << LG_READERS;  // 128 = 1000 0000
// 讀線程個數所在的位置
private static final long RBITS = WBIT - 1L;  // 127 = 111 1111
// 最大讀線程個數
private static final long RFULL = RBITS - 1L;  // 126 = 111 1110
// 讀線程個數和寫線程個數的掩碼
private static final long ABITS = RBITS | WBIT;  // 255 = 1111 1111
// 讀線程個數的反數,高25位全部為1
private static final long SBITS = ~RBITS;  // -128 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000

// state的初始值
private static final long ORIGIN = WBIT << 1;  // 256 = 1 0000 0000
// 隊列的頭節點
private transient volatile WNode whead;
// 隊列的尾節點
private transient volatile WNode wtail;
// 存儲着當前的版本號,類似於AQS的狀態變量state
private transient volatile long state;

通過屬性可以看到,這是一個類似於AQS的結構,內部同樣維護着一個狀態變量state和一個CLH隊列。

構造方法

public StampedLock() {
    state = ORIGIN;
}

state的初始值為ORIGIN(256),它的二進制是 1 0000 0000,也就是初始版本號。

writeLock()方法

獲取寫鎖。

public long writeLock() {
    long s, next;
    // ABITS = 255 = 1111 1111
    // WBITS = 128 = 1000 0000
    // state與ABITS如果等於0,嘗試原子更新state的值加WBITS
    // 如果成功則返回更新的值,如果失敗調用acquireWrite()方法
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}

我們以state等於初始值為例,則state & ABITS的結果為:

此時state為初始狀態,與ABITS與運算后的值為0,所以執行後面的CAS方法,s + WBITS的值為384 = 1 1000 0000。

到這裏我們大膽猜測:state的高24位存儲的是版本號,低8位存儲的是是否有加鎖,第8位存儲的是寫鎖,低7位存儲的是讀鎖被獲取的次數,而且如果只有第8位存儲寫鎖的話,那麼寫鎖只能被獲取一次,也就不可能重入了。

到底我們猜測的對不對呢,走着瞧^^

我們接着來分析acquireWrite()方法:

(手機橫屏看源碼更方便)

private long acquireWrite(boolean interruptible, long deadline) {
    // node為新增節點,p為尾節點(即將成為node的前置節點)
    WNode node = null, p;
    
    // 第一次自旋——入隊
    for (int spins = -1;;) { // spin while enqueuing
        long m, s, ns;
        // 再次嘗試獲取寫鎖
        if ((m = (s = state) & ABITS) == 0L) {
            if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
                return ns;
        }
        else if (spins < 0)
            // 如果自旋次數小於0,則計算自旋的次數
            // 如果當前有寫鎖獨佔且隊列無元素,說明快輪到自己了
            // 就自旋就行了,如果自旋完了還沒輪到自己才入隊
            // 則自旋次數為SPINS常量
            // 否則自旋次數為0
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            // 當自旋次數大於0時,當前這次自旋隨機減一次自旋次數
            if (LockSupport.nextSecondarySeed() >= 0)
                --spins;
        }
        else if ((p = wtail) == null) {
            // 如果隊列未初始化,新建一個空節點並初始化頭節點和尾節點
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            // 如果新增節點還未初始化,則新建之,並賦值其前置節點為尾節點
            node = new WNode(WMODE, p);
        else if (node.prev != p)
            // 如果尾節點有變化,則更新新增節點的前置節點為新的尾節點
            node.prev = p;
        else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
            // 嘗試更新新增節點為新的尾節點成功,則退出循環
            p.next = node;
            break;
        }
    }

    // 第二次自旋——阻塞並等待喚醒
    for (int spins = -1;;) {
        // h為頭節點,np為新增節點的前置節點,pp為前前置節點,ps為前置節點的狀態
        WNode h, np, pp; int ps;
        // 如果頭節點等於前置節點,說明快輪到自己了
        if ((h = whead) == p) {
            if (spins < 0)
                // 初始化自旋次數
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                // 增加自旋次數
                spins <<= 1;
            
            // 第三次自旋,不斷嘗試獲取寫鎖
            for (int k = spins;;) { // spin at head
                long s, ns;
                if (((s = state) & ABITS) == 0L) {
                    if (U.compareAndSwapLong(this, STATE, s,
                                             ns = s + WBIT)) {
                        // 嘗試獲取寫鎖成功,將node設置為新頭節點並清除其前置節點(gc)
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                }
                // 隨機立減自旋次數,當自旋次數減為0時跳出循環再重試
                else if (LockSupport.nextSecondarySeed() >= 0 &&
                         --k <= 0)
                    break;
            }
        }
        else if (h != null) { // help release stale waiters
            // 這段代碼很難進來,是用於協助喚醒讀節點的
            // 我是這麼調試進來的:
            // 起三個寫線程,兩個讀線程
            // 寫線程1獲取鎖不要釋放
            // 讀線程1獲取鎖,讀線程2獲取鎖(會阻塞)
            // 寫線程2獲取鎖(會阻塞)
            // 寫線程1釋放鎖,此時會喚醒讀線程1
            // 在讀線程1裏面先不要喚醒讀線程2
            // 寫線程3獲取鎖,此時就會走到這裏來了
            WNode c; Thread w;
            // 如果頭節點的cowait鏈表(棧)不為空,喚醒裏面的所有節點
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        
        // 如果頭節點沒有變化
        if (whead == h) {
            // 如果尾節點有變化,則更新
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                // 如果尾節點狀態為0,則更新成WAITING
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                // 如果尾節點狀態為取消,則把它從鏈表中刪除
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                // 有超時時間的處理
                long time; // 0 argument to park means no timeout
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    // 已超時,剔除當前節點
                    return cancelWaiter(node, node, false);
                // 當前線程
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                // 把node的線程指向當前線程
                node.thread = wt;
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                    whead == h && node.prev == p)
                    // 阻塞當前線程
                    U.park(false, time);  // 等同於LockSupport.park()
                    
                // 當前節點被喚醒后,清除線程
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                // 如果中斷了,取消當前節點
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

這裏對acquireWrite()方法做一個總結,這個方法裏面有三段自旋邏輯:

第一段自旋——入隊:

(1)如果頭節點等於尾節點,說明沒有其它線程排隊,那就多自旋一會,看能不能嘗試獲取到寫鎖;

(2)否則,自旋次數為0,直接讓其入隊;

第二段自旋——阻塞並等待被喚醒 + 第三段自旋——不斷嘗試獲取寫鎖:

(1)第三段自旋在第二段自旋內部;

(2)如果頭節點等於前置節點,那就進入第三段自旋,不斷嘗試獲取寫鎖;

(3)否則,嘗試喚醒頭節點中等待着的讀線程;

(4)最後,如果當前線程一直都沒有獲取到寫鎖,就阻塞當前線程並等待被喚醒;

這麼一大段邏輯看着比較鬧心,其實真正分解下來還是比較簡單的,無非就是自旋,把很多狀態的處理都糅合到一個for循環裏面處理了。

unlockWrite()方法

釋放寫鎖。

public void unlockWrite(long stamp) {
    WNode h;
    // 檢查版本號對不對
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    // 這行代碼實際有兩個作用:
    // 1. 更新版本號加1
    // 2. 釋放寫鎖
    // stamp + WBIT實際會把state的第8位置為0,也就相當於釋放了寫鎖
    // 同時會進1,也就是高24位整體加1了
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    // 如果頭節點不為空,並且狀態不為0,調用release方法喚醒它的下一個節點
    if ((h = whead) != null && h.status != 0)
        release(h);
}
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 將其狀態改為0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果頭節點的下一個節點為空或者其狀態為已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 從尾節點向前遍歷找到一個可用的節點
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 喚醒q節點所在的線程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

寫鎖的釋放過程比較簡單:

(1)更改state的值,釋放寫鎖;

(2)版本號加1;

(3)喚醒下一個等待着的節點;

readLock()方法

獲取讀鎖。

public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    // 沒有寫鎖佔用,並且讀鎖被獲取的次數未達到最大值
    // 嘗試原子更新讀鎖被獲取的次數加1
    // 如果成功直接返回,如果失敗調用acquireRead()方法
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

獲取讀鎖的時候先看看現在有沒有其它線程佔用着寫鎖,如果沒有的話再檢測讀鎖被獲取的次數有沒有達到最大,如果沒有的話直接嘗試獲取一次讀鎖,如果成功了直接返回版本號,如果沒成功就調用acquireRead()排隊。

下面我們一起來看看acquireRead()方法,這又是一個巨長無比的方法,請保持耐心,我們一步步來分解:

(手機橫屏看源碼更方便)

private long acquireRead(boolean interruptible, long deadline) {
    // node為新增節點,p為尾節點
    WNode node = null, p;
    // 第一段自旋——入隊
    for (int spins = -1;;) {
        // 頭節點
        WNode h;
        // 如果頭節點等於尾節點
        // 說明沒有排隊的線程了,快輪到自己了,直接自旋不斷嘗試獲取讀鎖
        if ((h = whead) == (p = wtail)) {
            // 第二段自旋——不斷嘗試獲取讀鎖
            for (long m, s, ns;;) {
                // 嘗試獲取讀鎖,如果成功了直接返回版本號
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                    // 如果讀線程個數達到了最大值,會溢出,返回的是0
                    return ns;
                else if (m >= WBIT) {
                    // m >= WBIT表示有其它線程先一步獲取了寫鎖
                    if (spins > 0) {
                        // 隨機立減自旋次數
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    }
                    else {
                        // 如果自旋次數為0了,看看是否要跳出循環
                        if (spins == 0) {
                            WNode nh = whead, np = wtail;
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        // 設置自旋次數
                        spins = SPINS;
                    }
                }
            }
        }
        // 如果尾節點為空,初始化頭節點和尾節點
        if (p == null) { // initialize queue
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            // 如果新增節點為空,初始化之
            node = new WNode(RMODE, p);
        else if (h == p || p.mode != RMODE) {
            // 如果頭節點等於尾節點或者尾節點不是讀模式
            // 當前節點入隊
            if (node.prev != p)
                node.prev = p;
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
                p.next = node;
                break;
            }
        }
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            // 接着上一個elseif,這裏肯定是尾節點為讀模式了
            // 將當前節點加入到尾節點的cowait中,這是一個棧
            // 上面的CAS成功了是不會進入到這裏來的
            node.cowait = null;
        else {
            // 第三段自旋——阻塞當前線程並等待被喚醒
            for (;;) {
                WNode pp, c; Thread w;
                // 如果頭節點不為空且其cowait不為空,協助喚醒其中等待的讀線程
                if ((h = whead) != null && (c = h.cowait) != null &&
                    U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null) // help release
                    U.unpark(w);
                // 如果頭節點等待前前置節點或者等於前置節點或者前前置節點為空
                // 這同樣說明快輪到自己了
                if (h == (pp = p.prev) || h == p || pp == null) {
                    long m, s, ns;
                    // 第四段自旋——又是不斷嘗試獲取鎖
                    do {
                        if ((m = (s = state) & ABITS) < RFULL ?
                            U.compareAndSwapLong(this, STATE, s,
                                                 ns = s + RUNIT) :
                            (m < WBIT &&
                             (ns = tryIncReaderOverflow(s)) != 0L))
                            return ns;
                    } while (m < WBIT); // 只有當前時刻沒有其它線程佔有寫鎖就不斷嘗試
                }
                // 如果頭節點未曾改變且前前置節點也未曾改
                // 阻塞當前線程
                if (whead == h && p.prev == pp) {
                    long time;
                    // 如果前前置節點為空,或者頭節點等於前置節點,或者前置節點已取消
                    // 從第一個for自旋開始重試
                    if (pp == null || h == p || p.status > 0) {
                        node = null; // throw away
                        break;
                    }
                    // 超時檢測
                    if (deadline == 0L)
                        time = 0L;
                    else if ((time = deadline - System.nanoTime()) <= 0L)
                        // 如果超時了,取消當前節點
                        return cancelWaiter(node, p, false);
                    
                    // 當前線程
                    Thread wt = Thread.currentThread();
                    U.putObject(wt, PARKBLOCKER, this);
                    // 設置進node中
                    node.thread = wt;
                    // 檢測之前的條件未曾改變
                    if ((h != pp || (state & ABITS) == WBIT) &&
                        whead == h && p.prev == pp)
                        // 阻塞當前線程並等待被喚醒
                        U.park(false, time);
                    
                    // 喚醒之後清除線程
                    node.thread = null;
                    U.putObject(wt, PARKBLOCKER, null);
                    // 如果中斷了,取消當前節點
                    if (interruptible && Thread.interrupted())
                        return cancelWaiter(node, p, true);
                }
            }
        }
    }
    
    // 只有第一個讀線程會走到下面的for循環處,參考上面第一段自旋中有一個break,當第一個讀線程入隊的時候break出來的
    
    // 第五段自旋——跟上面的邏輯差不多,只不過這裏單獨搞一個自旋針對第一個讀線程
    for (int spins = -1;;) {
        WNode h, np, pp; int ps;
        // 如果頭節點等於尾節點,說明快輪到自己了
        // 不斷嘗試獲取讀鎖
        if ((h = whead) == p) {
            // 設置自旋次數
            if (spins < 0)
                spins = HEAD_SPINS;
            else if (spins < MAX_HEAD_SPINS)
                spins <<= 1;
                
            // 第六段自旋——不斷嘗試獲取讀鎖
            for (int k = spins;;) { // spin at head
                long m, s, ns;
                // 不斷嘗試獲取讀鎖
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
                    // 獲取到了讀鎖
                    WNode c; Thread w;
                    whead = node;
                    node.prev = null;
                    // 喚醒當前節點中所有等待着的讀線程
                    // 因為當前節點是第一個讀節點,所以它是在隊列中的,其它讀節點都是掛這個節點的cowait棧中的
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(w);
                    }
                    // 返回版本號
                    return ns;
                }
                // 如果當前有其它線程佔有着寫鎖,並且沒有自旋次數了,跳出當前循環
                else if (m >= WBIT &&
                         LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
                    break;
            }
        }
        else if (h != null) {
            // 如果頭節點不等待尾節點且不為空且其為讀模式,協助喚醒裏面的讀線程
            WNode c; Thread w;
            while ((c = h.cowait) != null) {
                if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
                    (w = c.thread) != null)
                    U.unpark(w);
            }
        }
        
        // 如果頭節點未曾變化
        if (whead == h) {
            // 更新前置節點及其狀態等
            if ((np = node.prev) != p) {
                if (np != null)
                    (p = np).next = node;   // stale
            }
            else if ((ps = p.status) == 0)
                U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
            else if (ps == CANCELLED) {
                if ((pp = p.prev) != null) {
                    node.prev = pp;
                    pp.next = node;
                }
            }
            else {
                // 第一個讀節點即將進入阻塞
                long time;
                // 超時設置
                if (deadline == 0L)
                    time = 0L;
                else if ((time = deadline - System.nanoTime()) <= 0L)
                    // 如果超時了取消當前節點
                    return cancelWaiter(node, node, false);
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);
                node.thread = wt;
                if (p.status < 0 &&
                    (p != h || (state & ABITS) == WBIT) &&
                    whead == h && node.prev == p)
                    // 阻塞第一個讀節點並等待被喚醒
                    U.park(false, time);
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

讀鎖的獲取過程比較艱辛,一共有六段自旋,Oh my god,讓我們來大致地分解一下:

(1)讀節點進來都是先判斷是頭節點如果等於尾節點,說明快輪到自己了,就不斷地嘗試獲取讀鎖,如果成功了就返回;

(2)如果頭節點不等於尾節點,這裏就會讓當前節點入隊,這裏入隊又分成了兩種;

(3)一種是首個讀節點入隊,它是會排隊到整個隊列的尾部,然後跳出第一段自旋;

(4)另一種是非第一個讀節點入隊,它是進入到首個讀節點的cowait棧中,所以更確切地說應該是入棧;

(5)不管是入隊還入棧后,都會再次檢測頭節點是不是等於尾節點了,如果相等,則會再次不斷嘗試獲取讀鎖;

(6)如果頭節點不等於尾節點,那麼才會真正地阻塞當前線程並等待被喚醒;

(7)上面說的首個讀節點其實是連續的讀線程中的首個,如果是兩個讀線程中間夾了一個寫線程,還是老老實實的排隊。

自旋,自旋,自旋,旋轉的木馬,讓我忘了傷^^

unlockRead()方法

釋放讀鎖。

public void unlockRead(long stamp) {
    long s, m; WNode h;
    for (;;) {
        // 檢查版本號
        if (((s = state) & SBITS) != (stamp & SBITS) ||
            (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
            throw new IllegalMonitorStateException();
        // 讀線程個數正常
        if (m < RFULL) {
            // 釋放一次讀鎖
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                // 如果讀鎖全部都釋放了,且頭節點不為空且狀態不為0,喚醒它的下一個節點
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L)
            // 讀線程個數溢出檢測
            break;
    }
}

private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 將其狀態改為0
        U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
        // 如果頭節點的下一個節點為空或者其狀態為已取消
        if ((q = h.next) == null || q.status == CANCELLED) {
            // 從尾節點向前遍歷找到一個可用的節點
            for (WNode t = wtail; t != null && t != h; t = t.prev)
                if (t.status <= 0)
                    q = t;
        }
        // 喚醒q節點所在的線程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

讀鎖釋放的過程就比較簡單了,將state的低7位減1,當減為0的時候說明完全釋放了讀鎖,就喚醒下一個排隊的線程。

tryOptimisticRead()方法

樂觀讀。

public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

如果沒有寫鎖,就返回state的高25位,這裏把寫所在位置一起返回了,是為了後面檢測數據有沒有被寫過。

validate()方法

檢測樂觀讀版本號是否變化。

public boolean validate(long stamp) {
    // 強制加入內存屏障,刷新數據
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

檢測兩者的版本號是否一致,與SBITS與操作保證不受讀操作的影響。

變異的CLH隊列

StampedLock中的隊列是一種變異的CLH隊列,圖解如下:

總結

StampedLock的源碼解析到這裏就差不多了,讓我們來總結一下:

(1)StampedLock也是一種讀寫鎖,它不是基於AQS實現的;

(2)StampedLock相較於ReentrantReadWriteLock多了一種樂觀讀的模式,以及讀鎖轉化為寫鎖的方法;

(3)StampedLock的state存儲的是版本號,確切地說是高24位存儲的是版本號,寫鎖的釋放會增加其版本號,讀鎖不會;

(4)StampedLock的低7位存儲的讀鎖被獲取的次數,第8位存儲的是寫鎖被獲取的次數;

(5)StampedLock不是可重入鎖,因為只有第8位標識寫鎖被獲取了,並不能重複獲取;

(6)StampedLock中獲取鎖的過程使用了大量的自旋操作,對於短任務的執行會比較高效,長任務的執行會浪費大量CPU;

(7)StampedLock不能實現條件鎖;

彩蛋

StampedLock與ReentrantReadWriteLock的對比?

答:StampedLock與ReentrantReadWriteLock作為兩種不同的讀寫鎖方式,彤哥大致歸納了它們的異同點:

(1)兩者都有獲取讀鎖、獲取寫鎖、釋放讀鎖、釋放寫鎖的方法,這是相同點;

(2)兩者的結構基本類似,都是使用state + CLH隊列;

(3)前者的state分成三段,高24位存儲版本號、低7位存儲讀鎖被獲取的次數、第8位存儲寫鎖被獲取的次數;

(4)後者的state分成兩段,高16位存儲讀鎖被獲取的次數,低16位存儲寫鎖被獲取的次數;

(5)前者的CLH隊列可以看成是變異的CLH隊列,連續的讀線程只有首個節點存儲在隊列中,其它的節點存儲的首個節點的cowait棧中;

(6)後者的CLH隊列是正常的CLH隊列,所有的節點都在這個隊列中;

(7)前者獲取鎖的過程中有判斷首尾節點是否相同,也就是是不是快輪到自己了,如果是則不斷自旋,所以適合執行短任務;

(8)後者獲取鎖的過程中非公平模式下會做有限次嘗試;

(9)前者只有非公平模式,一上來就嘗試獲取鎖;

(10)前者喚醒讀鎖是一次性喚醒連續的讀鎖的,而且其它線程還會協助喚醒;

(11)後者是一個接着一個地喚醒的;

(12)前者有樂觀讀的模式,樂觀讀的實現是通過判斷state的高25位是否有變化來實現的;

(13)前者各種模式可以互轉,類似tryConvertToXxx()方法;

(14)前者寫鎖不可重入,後者寫鎖可重入;

(15)前者無法實現條件鎖,後者可以實現條件鎖;

差不多就這麼多吧,如果你還能想到,也歡迎補充哦^^

推薦閱讀

1、死磕 java同步系列之開篇

2、死磕 java魔法類之Unsafe解析

3、死磕 java同步系列之JMM(Java Memory Model)

4、死磕 java同步系列之volatile解析

5、死磕 java同步系列之synchronized解析

6、死磕 java同步系列之自己動手寫一個鎖Lock

7、死磕 java同步系列之AQS起篇

8、死磕 java同步系列之ReentrantLock源碼解析(一)——公平鎖、非公平鎖

9、死磕 java同步系列之ReentrantLock源碼解析(二)——條件鎖

10、死磕 java同步系列之ReentrantLock VS synchronized

11、死磕 java同步系列之ReentrantReadWriteLock源碼解析

12、死磕 java同步系列之Semaphore源碼解析

13、死磕 java同步系列之CountDownLatch源碼解析

14、死磕 java同步系列之AQS終篇

歡迎關注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢遊源碼的海洋。

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

SpringBoot啟動流程分析(四):IoC容器的初始化過程

SpringBoot系列文章簡介

SpringBoot源碼閱讀輔助篇:

  Spring IoC容器與應用上下文的設計與實現

SpringBoot啟動流程源碼分析:

  1. SpringBoot啟動流程分析(一):SpringApplication類初始化過程
  2. SpringBoot啟動流程分析(二):SpringApplication的run方法
  3. SpringBoot啟動流程分析(三):SpringApplication的run方法之prepareContext()方法
  4. SpringBoot啟動流程分析(四):IoC容器的初始化過程
  5. SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現
  6. SpringBoot啟動流程分析(六):IoC容器依賴注入

筆者註釋版Spring Framework與SpringBoot源碼git傳送門:請不要吝嗇小星星

  1. spring-framework-5.0.8.RELEASE
  2. SpringBoot-2.0.4.RELEASE

第五步:刷新應用上下文

一、前言

  在前面的博客中談到IoC容器的初始化過程,主要分下面三步:

1 BeanDefinition的Resource定位
2 BeanDefinition的載入
3 向IoC容器註冊BeanDefinition

  在上一篇文章介紹了prepareContext()方法,在準備刷新階段做了什麼工作。本文我們主要從refresh()方法中總結IoC容器的初始化過程。
  從run方法的,refreshContext()方法一路跟下去,最終來到AbstractApplicationContext類的refresh()方法。

 1 @Override
 2 public void refresh() throws BeansException, IllegalStateException {
 3     synchronized (this.startupShutdownMonitor) {
 4         // Prepare this context for refreshing.
 5         //刷新上下文環境
 6         prepareRefresh();
 7         // Tell the subclass to refresh the internal bean factory.
 8         //這裡是在子類中啟動 refreshBeanFactory() 的地方
 9         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
10         // Prepare the bean factory for use in this context.
11         //準備bean工廠,以便在此上下文中使用
12         prepareBeanFactory(beanFactory);
13         try {
14             // Allows post-processing of the bean factory in context subclasses.
15             //設置 beanFactory 的後置處理
16             postProcessBeanFactory(beanFactory);
17             // Invoke factory processors registered as beans in the context.
18             //調用 BeanFactory 的后處理器,這些處理器是在Bean 定義中向容器註冊的
19             invokeBeanFactoryPostProcessors(beanFactory);
20             // Register bean processors that intercept bean creation.
21             //註冊Bean的后處理器,在Bean創建過程中調用
22             registerBeanPostProcessors(beanFactory);
23             // Initialize message source for this context.
24             //對上下文中的消息源進行初始化
25             initMessageSource();
26             // Initialize event multicaster for this context.
27             //初始化上下文中的事件機制
28             initApplicationEventMulticaster();
29             // Initialize other special beans in specific context subclasses.
30             //初始化其他特殊的Bean
31             onRefresh();
32             // Check for listener beans and register them.
33             //檢查監聽Bean並且將這些監聽Bean向容器註冊
34             registerListeners();
35             // Instantiate all remaining (non-lazy-init) singletons.
36             //實例化所有的(non-lazy-init)單件
37             finishBeanFactoryInitialization(beanFactory);
38             // Last step: publish corresponding event.
39             //發布容器事件,結束Refresh過程
40             finishRefresh();
41         } catch (BeansException ex) {
42             if (logger.isWarnEnabled()) {
43                 logger.warn("Exception encountered during context initialization - " +
44                         "cancelling refresh attempt: " + ex);
45             }
46             // Destroy already created singletons to avoid dangling resources.
47             destroyBeans();
48             // Reset 'active' flag.
49             cancelRefresh(ex);
50             // Propagate exception to caller.
51             throw ex;
52         } finally {
53             // Reset common introspection caches in Spring's core, since we
54             // might not ever need metadata for singleton beans anymore...
55             resetCommonCaches();
56         }
57     }
58 }

   從以上代碼中我們可以看到,refresh()方法中所作的工作也挺多,我們沒辦法面面俱到,主要根據IoC容器的初始化步驟和IoC依賴注入的過程進行分析,圍繞以上兩個過程,我們主要介紹重要的方法,其他的請看註釋。

 

二、obtainFreshBeanFactory();

  在啟動流程的第三步:初始化應用上下文。中我們創建了應用的上下文,並觸發了GenericApplicationContext類的構造方法如下所示,創建了beanFactory,也就是創建了DefaultListableBeanFactory類。

1 public GenericApplicationContext() {
2     this.beanFactory = new DefaultListableBeanFactory();
3 }

  關於obtainFreshBeanFactory()方法,其實就是拿到我們之前創建的beanFactory。

 1 protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
 2     //刷新BeanFactory
 3     refreshBeanFactory();
 4     //獲取beanFactory
 5     ConfigurableListableBeanFactory beanFactory = getBeanFactory();
 6     if (logger.isDebugEnabled()) {
 7         logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
 8     }
 9     return beanFactory;
10 }

  從上面代碼可知,在該方法中主要做了三個工作,刷新beanFactory,獲取beanFactory,返回beanFactory。

  首先看一下refreshBeanFactory()方法,跟下去來到GenericApplicationContext類的refreshBeanFactory()發現也沒做什麼。

1 @Override
2 protected final void refreshBeanFactory() throws IllegalStateException {
3     if (!this.refreshed.compareAndSet(false, true)) {
4         throw new IllegalStateException(
5                 "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
6     }
7     this.beanFactory.setSerializationId(getId());
8 }
TIPS:
  1,AbstractApplicationContext類有兩個子類實現了refreshBeanFactory(),但是在前面第三步初始化上下文的時候,
實例化了GenericApplicationContext類,所以沒有進入AbstractRefreshableApplicationContext中的refreshBeanFactory()方法。
  2,this.refreshed.compareAndSet(false, true) 
  這行代碼在這裏表示:GenericApplicationContext只允許刷新一次   
  這行代碼,很重要,不是在Spring中很重要,而是這行代碼本身。首先看一下this.refreshed屬性: 
private final AtomicBoolean refreshed = new AtomicBoolean(); 
  java J.U.C併發包中很重要的一個原子類AtomicBoolean。通過該類的compareAndSet()方法可以實現一段代碼絕對只實現一次的功能。
感興趣的自行百度吧。

 

三、prepareBeanFactory(beanFactory);

  從字面意思上可以看出準備BeanFactory。

  看代碼,具體看看做了哪些準備工作。這個方法不是重點,看註釋吧。

 1 protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
 2     // Tell the internal bean factory to use the context's class loader etc.
 3     // 配置類加載器:默認使用當前上下文的類加載器
 4     beanFactory.setBeanClassLoader(getClassLoader());
 5     // 配置EL表達式:在Bean初始化完成,填充屬性的時候會用到
 6     beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
 7     // 添加屬性編輯器 PropertyEditor
 8     beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
 9 
10     // Configure the bean factory with context callbacks.
11     // 添加Bean的後置處理器
12     beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
13     // 忽略裝配以下指定的類
14     beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
15     beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
16     beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
17     beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
18     beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
19     beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
20 
21     // BeanFactory interface not registered as resolvable type in a plain factory.
22     // MessageSource registered (and found for autowiring) as a bean.
23     // 將以下類註冊到 beanFactory(DefaultListableBeanFactory) 的resolvableDependencies屬性中
24     beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
25     beanFactory.registerResolvableDependency(ResourceLoader.class, this);
26     beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
27     beanFactory.registerResolvableDependency(ApplicationContext.class, this);
28 
29     // Register early post-processor for detecting inner beans as ApplicationListeners.
30     // 將早期后處理器註冊為application監聽器,用於檢測內部bean
31     beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
32 
33     // Detect a LoadTimeWeaver and prepare for weaving, if found.
34     //如果當前BeanFactory包含loadTimeWeaver Bean,說明存在類加載期織入AspectJ,
35     // 則把當前BeanFactory交給類加載期BeanPostProcessor實現類LoadTimeWeaverAwareProcessor來處理,
36     // 從而實現類加載期織入AspectJ的目的。
37     if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
38         beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
39         // Set a temporary ClassLoader for type matching.
40         beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
41     }
42 
43     // Register default environment beans.
44     // 將當前環境變量(environment) 註冊為單例bean
45     if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
46         beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
47     }
48     // 將當前系統配置(systemProperties) 註冊為單例Bean
49     if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
50         beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
51     }
52     // 將當前系統環境 (systemEnvironment) 註冊為單例Bean
53     if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
54         beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
55     }
56 }

 

四、postProcessBeanFactory(beanFactory);

  postProcessBeanFactory()方法向上下文中添加了一系列的Bean的後置處理器。後置處理器工作的時機是在所有的beanDenifition加載完成之後,bean實例化之前執行。簡單來說Bean的後置處理器可以修改BeanDefinition的屬性信息。

  關於這個方法就先這樣吧,有興趣的可以直接百度該方法。篇幅有限,對該方法不做過多介紹。

 

五、invokeBeanFactoryPostProcessors(beanFactory);(重點)

  上面說過,IoC容器的初始化過程包括三個步驟,在invokeBeanFactoryPostProcessors()方法中完成了IoC容器初始化過程的三個步驟。

  1,第一步:Resource定位

  在SpringBoot中,我們都知道他的包掃描是從主類所在的包開始掃描的,prepareContext()方法中,會先將主類解析成BeanDefinition,然後在refresh()方法的invokeBeanFactoryPostProcessors()方法中解析主類的BeanDefinition獲取basePackage的路徑。這樣就完成了定位的過程。其次SpringBoot的各種starter是通過SPI擴展機制實現的自動裝配,SpringBoot的自動裝配同樣也是在invokeBeanFactoryPostProcessors()方法中實現的。還有一種情況,在SpringBoot中有很多的@EnableXXX註解,細心點進去看的應該就知道其底層是@Import註解,在invokeBeanFactoryPostProcessors()方法中也實現了對該註解指定的配置類的定位加載。

  常規的在SpringBoot中有三種實現定位,第一個是主類所在包的,第二個是SPI擴展機制實現的自動裝配(比如各種starter),第三種就是@Import註解指定的類。(對於非常規的不說了)

  2,第二步:BeanDefinition的載入

  在第一步中說了三種Resource的定位情況,定位后緊接着就是BeanDefinition的分別載入。所謂的載入就是通過上面的定位得到的basePackage,SpringBoot會將該路徑拼接成:classpath*:org/springframework/boot/demo/**/*.class這樣的形式,然後一個叫做PathMatchingResourcePatternResolver的類會將該路徑下所有的.class文件都加載進來,然後遍歷判斷是不是有@Component註解,如果有的話,就是我們要裝載的BeanDefinition。大致過程就是這樣的了。

TIPS:
    @Configuration,@Controller,@Service等註解底層都是@Component註解,只不過包裝了一層罷了。

  3、第三個過程:註冊BeanDefinition

   這個過程通過調用上文提到的BeanDefinitionRegister接口的實現來完成。這個註冊過程把載入過程中解析得到的BeanDefinition向IoC容器進行註冊。通過上文的分析,我們可以看到,在IoC容器中將BeanDefinition注入到一個ConcurrentHashMap中,IoC容器就是通過這個HashMap來持有這些BeanDefinition數據的。比如DefaultListableBeanFactory 中的beanDefinitionMap屬性。

  OK,總結完了,接下來我們通過代碼看看具體是怎麼實現的。

 1 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
 2     PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
 3     ...
 4 }
 5 // PostProcessorRegistrationDelegate類
 6 public static void invokeBeanFactoryPostProcessors(
 7         ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
 8     ...
 9     invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
10     ...
11 }
12 // PostProcessorRegistrationDelegate類
13 private static void invokeBeanDefinitionRegistryPostProcessors(
14         Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
15 
16     for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
17         postProcessor.postProcessBeanDefinitionRegistry(registry);
18     }
19 }
20 // ConfigurationClassPostProcessor類
21 @Override
22 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
23     ...
24     processConfigBeanDefinitions(registry);
25 }
26 // ConfigurationClassPostProcessor類
27 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
28     ...
29     do {
30         parser.parse(candidates);
31         parser.validate();
32         ...
33     }
34     ...
35 }

   一路跟蹤調用棧,來到ConfigurationClassParser類的parse()方法。

 1 // ConfigurationClassParser類
 2 public void parse(Set<BeanDefinitionHolder> configCandidates) {
 3     this.deferredImportSelectors = new LinkedList<>();
 4     for (BeanDefinitionHolder holder : configCandidates) {
 5         BeanDefinition bd = holder.getBeanDefinition();
 6         try {
 7             // 如果是SpringBoot項目進來的,bd其實就是前面主類封裝成的 AnnotatedGenericBeanDefinition(AnnotatedBeanDefinition接口的實現類)
 8             if (bd instanceof AnnotatedBeanDefinition) {
 9                 parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
10             } else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
11                 parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
12             } else {
13                 parse(bd.getBeanClassName(), holder.getBeanName());
14             }
15         } catch (BeanDefinitionStoreException ex) {
16             throw ex;
17         } catch (Throwable ex) {
18             throw new BeanDefinitionStoreException(
19                     "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
20         }
21     }
22     // 加載默認的配置---》(對springboot項目來說這裏就是自動裝配的入口了)
23     processDeferredImportSelectors();
24 }

   看上面的註釋,在前面的prepareContext()方法中,我們詳細介紹了我們的主類是如何一步步的封裝成AnnotatedGenericBeanDefinition,並註冊進IoC容器的beanDefinitionMap中的。

TIPS:
  至於processDeferredImportSelectors();方法,後面我們分析SpringBoot的自動裝配的時候會詳細講解,各種starter是如何一步步的實現自動裝配的。<SpringBoot啟動流程分析(五):SpringBoot自動裝配原理實現>

 

  繼續沿着parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());方法跟下去

  1 // ConfigurationClassParser類
  2 protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
  3     processConfigurationClass(new ConfigurationClass(metadata, beanName));
  4 }
  5 // ConfigurationClassParser類
  6 protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
  7     ...
  8     // Recursively process the configuration class and its superclass hierarchy.
  9     //遞歸地處理配置類及其父類層次結構。
 10     SourceClass sourceClass = asSourceClass(configClass);
 11     do {
 12         //遞歸處理Bean,如果有父類,遞歸處理,直到頂層父類
 13         sourceClass = doProcessConfigurationClass(configClass, sourceClass);
 14     }
 15     while (sourceClass != null);
 16 
 17     this.configurationClasses.put(configClass, configClass);
 18 }
 19 // ConfigurationClassParser類
 20 protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
 21         throws IOException {
 22 
 23     // Recursively process any member (nested) classes first
 24     //首先遞歸處理內部類,(SpringBoot項目的主類一般沒有內部類)
 25     processMemberClasses(configClass, sourceClass);
 26 
 27     // Process any @PropertySource annotations
 28     // 針對 @PropertySource 註解的屬性配置處理
 29     for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
 30             sourceClass.getMetadata(), PropertySources.class,
 31             org.springframework.context.annotation.PropertySource.class)) {
 32         if (this.environment instanceof ConfigurableEnvironment) {
 33             processPropertySource(propertySource);
 34         } else {
 35             logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
 36                     "]. Reason: Environment must implement ConfigurableEnvironment");
 37         }
 38     }
 39 
 40     // Process any @ComponentScan annotations
 41     // 根據 @ComponentScan 註解,掃描項目中的Bean(SpringBoot 啟動類上有該註解)
 42     Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
 43             sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
 44     if (!componentScans.isEmpty() &&
 45             !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
 46         for (AnnotationAttributes componentScan : componentScans) {
 47             // The config class is annotated with @ComponentScan -> perform the scan immediately
 48             // 立即執行掃描,(SpringBoot項目為什麼是從主類所在的包掃描,這就是關鍵了)
 49             Set<BeanDefinitionHolder> scannedBeanDefinitions =
 50                     this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
 51             // Check the set of scanned definitions for any further config classes and parse recursively if needed
 52             for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
 53                 BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
 54                 if (bdCand == null) {
 55                     bdCand = holder.getBeanDefinition();
 56                 }
 57                 // 檢查是否是ConfigurationClass(是否有configuration/component兩個註解),如果是,遞歸查找該類相關聯的配置類。
 58                 // 所謂相關的配置類,比如@Configuration中的@Bean定義的bean。或者在有@Component註解的類上繼續存在@Import註解。
 59                 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
 60                     parse(bdCand.getBeanClassName(), holder.getBeanName());
 61                 }
 62             }
 63         }
 64     }
 65 
 66     // Process any @Import annotations
 67     //遞歸處理 @Import 註解(SpringBoot項目中經常用的各種@Enable*** 註解基本都是封裝的@Import)
 68     processImports(configClass, sourceClass, getImports(sourceClass), true);
 69 
 70     // Process any @ImportResource annotations
 71     AnnotationAttributes importResource =
 72             AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
 73     if (importResource != null) {
 74         String[] resources = importResource.getStringArray("locations");
 75         Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
 76         for (String resource : resources) {
 77             String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
 78             configClass.addImportedResource(resolvedResource, readerClass);
 79         }
 80     }
 81 
 82     // Process individual @Bean methods
 83     Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
 84     for (MethodMetadata methodMetadata : beanMethods) {
 85         configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
 86     }
 87 
 88     // Process default methods on interfaces
 89     processInterfaces(configClass, sourceClass);
 90 
 91     // Process superclass, if any
 92     if (sourceClass.getMetadata().hasSuperClass()) {
 93         String superclass = sourceClass.getMetadata().getSuperClassName();
 94         if (superclass != null && !superclass.startsWith("java") &&
 95                 !this.knownSuperclasses.containsKey(superclass)) {
 96             this.knownSuperclasses.put(superclass, configClass);
 97             // Superclass found, return its annotation metadata and recurse
 98             return sourceClass.getSuperClass();
 99         }
100     }
101 
102     // No superclass -> processing is complete
103     return null;
104

  看doProcessConfigurationClass()方法。(SpringBoot的包掃描的入口方法,重點哦)

  我們先大致說一下這個方法裏面都幹了什麼,然後稍後再閱讀源碼分析。

TIPS:
  在以上代碼的第60行parse(bdCand.getBeanClassName(), holder.getBeanName());會進行遞歸調用,
因為當Spring掃描到需要加載的類會進一步判斷每一個類是否滿足是@Component/@Configuration註解的類,
如果滿足會遞歸調用parse()方法,查找其相關的類。
  同樣的第68行processImports(configClass, sourceClass, getImports(sourceClass), true);
通過@Import註解查找到的類同樣也會遞歸查找其相關的類。
  兩個遞歸在debug的時候會很亂,用文字敘述起來更讓人難以理解,所以,我們只關注對主類的解析,及其類的掃描過程。

  上面代碼的第29行 for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(… 獲取主類上的@PropertySource註解(關於該註解是怎麼用的請自行百度),解析該註解並將該註解指定的properties配置文件中的值存儲到Spring的 Environment中,Environment接口提供方法去讀取配置文件中的值,參數是properties文件中定義的key值。

  42行 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); 解析主類上的@ComponentScan註解,呃,怎麼說呢,42行後面的代碼將會解析該註解並進行包掃描。

  68行 processImports(configClass, sourceClass, getImports(sourceClass), true); 解析主類上的@Import註解,並加載該註解指定的配置類。

TIPS:

  在spring中好多註解都是一層一層封裝的,比如@EnableXXX,是對@Import註解的二次封裝。@SpringBootApplication註解=@ComponentScan+@EnableAutoConfiguration+@Import+@Configuration+@Component。@Controller,@Service等等是對@Component的二次封裝。。。

 

5.1、看看42-64行幹了啥

  從上面的42行往下看,來到第49行 Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); 

  進入該方法

 1 // ComponentScanAnnotationParser類
 2 public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
 3     ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
 4             componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
 5     ...
 6     // 根據 declaringClass (如果是SpringBoot項目,則參數為主類的全路徑名)
 7     if (basePackages.isEmpty()) {
 8         basePackages.add(ClassUtils.getPackageName(declaringClass));
 9     }
10     ...
11     // 根據basePackages掃描類
12     return scanner.doScan(StringUtils.toStringArray(basePackages));
13 }

  發現有兩行重要的代碼

  為了驗證代碼中的註釋,debug,看一下declaringClass,如下圖所示確實是我們的主類的全路徑名。

  跳過這一行,繼續debug,查看basePackages,該set集合中只有一個,就是主類所在的路徑。

TIPS:
  為什麼只有一個還要用一個集合呢,因為我們也可以用@ComponentScan註解指定掃描路徑。

  到這裏呢IoC容器初始化三個步驟的第一步,Resource定位就完成了,成功定位到了主類所在的包。

  接着往下看 return scanner.doScan(StringUtils.toStringArray(basePackages)); Spring是如何進行類掃描的。進入doScan()方法。

 1 // ComponentScanAnnotationParser類
 2 protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
 3     Assert.notEmpty(basePackages, "At least one base package must be specified");
 4     Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
 5     for (String basePackage : basePackages) {
 6         // 從指定的包中掃描需要裝載的Bean
 7         Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
 8         for (BeanDefinition candidate : candidates) {
 9             ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
10             candidate.setScope(scopeMetadata.getScopeName());
11             String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
12             if (candidate instanceof AbstractBeanDefinition) {
13                 postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
14             }
15             if (candidate instanceof AnnotatedBeanDefinition) {
16                 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
17             }
18             if (checkCandidate(beanName, candidate)) {
19                 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
20                 definitionHolder =
21                         AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
22                 beanDefinitions.add(definitionHolder);
23                 //將該 Bean 註冊進 IoC容器(beanDefinitionMap)
24                 registerBeanDefinition(definitionHolder, this.registry);
25             }
26         }
27     }
28     return beanDefinitions;
29 }

  這個方法中有兩個比較重要的方法,第7行 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); 從basePackage中掃描類並解析成BeanDefinition,拿到所有符合條件的類后在第24行 registerBeanDefinition(definitionHolder, this.registry); 將該類註冊進IoC容器。也就是說在這個方法中完成了IoC容器初始化過程的第二三步,BeanDefinition的載入,和BeanDefinition的註冊。

 

5.1.1、findCandidateComponents(basePackage);

  跟蹤調用棧

 1 // ClassPathScanningCandidateComponentProvider類
 2 public Set<BeanDefinition> findCandidateComponents(String basePackage) {
 3     ...
 4     else {
 5         return scanCandidateComponents(basePackage);
 6     }
 7 }
 8 // ClassPathScanningCandidateComponentProvider類
 9 private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
10     Set<BeanDefinition> candidates = new LinkedHashSet<>();
11     try {
12         //拼接掃描路徑,比如:classpath*:org/springframework/boot/demo/**/*.class
13         String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
14                 resolveBasePackage(basePackage) + '/' + this.resourcePattern;
15         //從 packageSearchPath 路徑中掃描所有的類
16         Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
17         boolean traceEnabled = logger.isTraceEnabled();
18         boolean debugEnabled = logger.isDebugEnabled();
19         for (Resource resource : resources) {
20             if (traceEnabled) {
21                 logger.trace("Scanning " + resource);
22             }
23             if (resource.isReadable()) {
24                 try {
25                     MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
26                     // //判斷該類是不是 @Component 註解標註的類,並且不是需要排除掉的類
27                     if (isCandidateComponent(metadataReader)) {
28                         //將該類封裝成 ScannedGenericBeanDefinition(BeanDefinition接口的實現類)類
29                         ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
30                         sbd.setResource(resource);
31                         sbd.setSource(resource);
32                         if (isCandidateComponent(sbd)) {
33                             if (debugEnabled) {
34                                 logger.debug("Identified candidate component class: " + resource);
35                             }
36                             candidates.add(sbd);
37                         } else {
38                             if (debugEnabled) {
39                                 logger.debug("Ignored because not a concrete top-level class: " + resource);
40                             }
41                         }
42                     } else {
43                         if (traceEnabled) {
44                             logger.trace("Ignored because not matching any filter: " + resource);
45                         }
46                     }
47                 } catch (Throwable ex) {
48                     throw new BeanDefinitionStoreException(
49                             "Failed to read candidate component class: " + resource, ex);
50                 }
51             } else {
52                 if (traceEnabled) {
53                     logger.trace("Ignored because not readable: " + resource);
54                 }
55             }
56         }
57     } catch (IOException ex) {
58         throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
59     }
60     return candidates;
61 }

  在第13行將basePackage拼接成classpath*:org/springframework/boot/demo/**/*.class,在第16行的getResources(packageSearchPath);方法中掃描到了該路徑下的所有的類。然後遍歷這些Resources,在第27行判斷該類是不是 @Component 註解標註的類,並且不是需要排除掉的類。在第29行將掃描到的類,解析成ScannedGenericBeanDefinition,該類是BeanDefinition接口的實現類。OK,IoC容器的BeanDefinition載入到這裏就結束了。

  回到前面的doScan()方法,debug看一下結果(截圖中所示的就是我定位的需要交給Spring容器管理的類)。

 

5.1.2、registerBeanDefinition(definitionHolder, this.registry);

  查看registerBeanDefinition()方法。是不是有點眼熟,在前面介紹prepareContext()方法時,我們詳細介紹了主類的BeanDefinition是怎麼一步一步的註冊進DefaultListableBeanFactory的beanDefinitionMap中的。在此呢我們就省略1w字吧。完成了BeanDefinition的註冊,就完成了IoC容器的初始化過程。此時,在使用的IoC容器DefaultListableFactory中已經建立了整個Bean的配置信息,而這些BeanDefinition已經可以被容器使用了。他們都在BeanbefinitionMap里被檢索和使用。容器的作用就是對這些信息進行處理和維護。這些信息是容器簡歷依賴反轉的基礎。

1 protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
2     BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
3 }

   OK,到這裏IoC容器的初始化過程的三個步驟就梳理完了。當然這隻是針對SpringBoot的包掃描的定位方式的BeanDefinition的定位,加載,和註冊過程。前面我們說過,還有兩種方式@Import和SPI擴展實現的starter的自動裝配。

 

5.2、@Import註解的解析過程

  相信不說大家也應該知道了,各種@EnableXXX註解,很大一部分都是對@Import的二次封裝(其實也是為了解耦,比如當@Import導入的類發生變化時,我們的業務系統也不需要改任何代碼)。

  呃,我們又要回到上文中的ConfigurationClassParser類的doProcessConfigurationClass方法的第68行processImports(configClass, sourceClass, getImports(sourceClass), true);,跳躍性比較大。上面解釋過,我們只針對主類進行分析,因為這裡有遞歸。

  processImports(configClass, sourceClass, getImports(sourceClass), true);中configClass和sourceClass參數都是主類相對應的哦。

TIPS:
  在分析這一塊的時候,我在主類上加了@EnableCaching註解。

  首先看getImports(sourceClass);

1 private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
2     Set<SourceClass> imports = new LinkedHashSet<>();
3     Set<SourceClass> visited = new LinkedHashSet<>();
4     collectImports(sourceClass, imports, visited);
5     return imports;
6 }

   debug

  正是@EnableCaching註解中的@Import註解指定的類。另外兩個呢是主類上的@SpringBootApplication中的@Import註解指定的類。不信你可以一層層的剝開@SpringBootApplication註解的皮去一探究竟。

  至於processImports()方法,大家自行debug吧,相信看到這裏,思路大家都已經很清楚了。

  

  凌晨兩點了,睡覺,明天繼續上班。

 

 

  原創不易,轉載請註明出處。

  如有錯誤的地方還請留言指正。

 

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

asp.net core 系列之Response caching(1)

這篇文章簡單的講解了response caching:

講解了cache-control,及對其中的頭和值的作用,及設置來控制response caching;

簡單的羅列了其他的緩存技術:In-memory caching , Distributed Cache , Cache Tag Helper , Distributed Cache Tag Helper ;

講解了使用ResponseCache attribute來控制response caching生成合適的頭

 主要翻譯於官網,水平有限,見諒

Overview

響應緩存減少了客戶端或者代理到web服務器的請求的次數響應緩存也可以減少web服務器的生成響應的執行的工作量。響應緩存被頭部控制,頭部指出了你想要客戶端,代理和中間件怎樣緩存響應。

ResponseCache attribute 參加設置響應緩存頭部,which clients may honor when caching responses. (當緩存響應時,客戶端會受這些屬性影響)Response Caching Middleware 可以被用來在服務器上緩存響應。 中間件可以使用ResponseCacheAttribute屬性來影響服務端緩存行為。 

HTTP-based response caching 

HTTP 1.1 Caching specification(規格,詳述,說明書)描述了網絡緩存應該怎樣表現(Internet caches should behave.) 主要的用於緩存的HTTP頭,是Cache-Control, 它被用於指定緩存指令。這個指令控制緩存行為,當請求從客戶端到服務端的時候,並且當響應從服務端返回客戶端的時候。

公共的Cache-Control 指令在下錶中被展示了:

其他緩存頭在緩存中扮演的角色,羅列在下面了:

注意:Cache-Control,是用在從請求中的HTTP頭,可以用來控制服務器中緩存行為。

HTTP-based caching respects request Cache-Control directives

HTTP 1.1 Caching specification for the Cache-Control header (HTTP 1.1 緩存規格對於Cache-Control)要求一個緩存遵守一個有效的Cache-Control ,這個Cache-Control頭是被客戶端發送的。一個客戶端可以發送一個帶no-cacheheader,並且強制要求服務器為每個請求生成一個新的響應。

總是遵守客戶端Cache-Control請求頭是有意義的,如果你考慮HTTP緩存的目標。在官方的說明書下,

緩存意味着減少潛在因素和網絡管理,對滿足請求跨客戶端,代理和服務器網絡。它不是一種控制原服務器上的加載的必須的方式。

當使用Response Caching 中間件時,開發者是沒法對緩存行為控制的。因為中間件附着於官方緩存說明書。當決定提供一個緩存響應時,對這个中間件的計劃豐富(planned enhancements to the middleware)對於達到配置中間件來忽視請求的Cache-Control頭的目的,是一個機會(Planned enhancements to middleware are an opportunity to middleware to ignore a request’s Cache-Control header when deciding to serve a cached response.)。計劃的豐富提供了一個機會來更好的控制服務器加載。

Other caching technology in ASP.NET Core ASP.NET Core上的其他緩存技術

  • In-memory caching 內存緩存

    In-memory caching 使用服務器內存來存儲緩存數據。這種類型的緩存適合使用sticky sessionsticky:不動的)的一個或者多個服務器Sticky sessions 意味着從客戶端發出的請求總是路由到同一台服務器處理。

    更多信息:Cache in-memory in ASP.NET Core.

  • Distributed Cache 分佈式緩存

    使用一個分佈式緩存來存儲數據在內存中,當應用部署在雲上或者服務器集群上時。緩存是在這些處理請求的服務器之間共享的。客戶端可以提交一個請求,請求可以被組群里的任意服務器處理,如果緩存數據對於客戶端是可用的。ASP.NET Core提供了SQL ServerRedis分佈式緩存

    更多信息:Distributed caching in ASP.NET Core.

  • Cache Tag Helper

    使用Cache Tagmvc頁面或者Razor Page中緩存內容Cache Tag Helper用內存緩存數據

    更多信息:Cache Tag Helper in ASP.NET Core MVC

  • Distributed Cache Tag Helper

    在分佈式雲或者web集群場景中使用Distributed Cache Tag Helper 來緩存Mvc view或者Razor Page中的內容The Distributed Cache Tag Helper SQL Server或者Redis來緩存數據

    更多信息:Distributed Cache Tag Helper in ASP.NET Core.

ResponseCache attribute

為了在response caching (響應緩存)上設置合適的頭,ResponseCacheAttribute 指出了必須的參數。(即,可以通過ResponseCacheAttribute,設置response caching上的頭的值)

注意:對於包含驗證信息的客戶端內容,不允許緩存。對於那些不會基於用戶的身份或者用戶是否登錄而改變的內容,才應該允許被緩存。

VaryByQueryKeys 隨着給出的query keys的集合的值,改變存儲的響應。When a single value of * is provided, the middleware varies responses by all request query string parameters. 

Response Caching Middleware 必須被允許設置VaryByQueryKeys屬性。否則,一個運行時異常會被拋出。對於VaryByQueryKeys屬性,並沒有一個對應的HTTP頭部。這個屬性是一個被Response Caching Middleware 處理的HTTP 功能。對於中間件提供一個緩存的響應,查詢字符串和查詢字符串值必須匹配之前的請求.(即,如果查詢字符串和查詢字符串值和之前的一樣時,中間件會直接返回一個緩存的響應;否則,返回一個新的響應。)例如,考慮下錶中的一系列的請求和結果:

第一個請求被服務器返回,並且緩存到中間件中。第二個請求是被中間件返回,因為查詢字符串匹配之前的請求。第三個請求不是在中間件緩存中的,因為查詢字符串值不匹配之前的請求。

ResponseCacheAttribute用於配置和創建一個ResponseCacheFilter.    

ResponseCacheFilter執行的工作,更新合適的HTTP頭和響應的功能(即,ResponseCacheAttribute的功能)The filter:

  • 移除任何存在的Vary, Cache-Control, Pragma頭部
  • 根據設置在ResponseCacheAttribute中的屬性輸出合適的頭部

  • 更新the response caching HTTP feature如果VaryByQueryKeys被設置了

Vary

這個頭部會被寫,當VaryByHeader屬性被設置了。這個屬性(VaryByHeader)設置Vary屬性的值。下面是使用VaryByHeader屬性的例子:

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)] public class Cache1Model : PageModel
{

用這個例子,使用瀏覽器工具觀察response headers(響應頭)。 下面的響應頭隨着Cache1 page response 被發送了。

Cache-Control: public,max-age=30
Vary: User-Agent
NoStore and Location.None

NoStore重寫了大部分的其他屬性。當這個屬性被設置為true,Cache-Control頭被設置為no-store.

如果Location設置為None:

  • Cache-Control 設置為no-store, no-cache

  • Pragma設置為no-cache.

如果NoStorefalse並且LocationNoneCache-Control ,Pragma被設置為no-cache.

NoStore是典型的被設置為true,為了error pages. 示例中的Cache2 page生成響應頭,指示客戶端不要存儲響應。

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class Cache2Model : PageModel
{

這個示例應用返回Cache2 page 帶有下面的頭:

Cache-Control: no-store,no-cache
Pragma: no-cache
Location and Duration

為了可以緩存,Duration必須被設置為一個積極的值並且Location必須是任意的或者Client. 這種情況下Cache-Control頭被設置為location的值,並且跟着一個響應的max-age.

注意:

Location’s options of Any and Client轉化為Cache-Control頭的值分別為publicprivate. 正如之前提到的,設置LocationNone會設置Cache-ControlPramga頭為no-cache:

[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)] public class Cache3Model : PageModel
{

示例應用返回的Cache3 page 帶有下面的頭:

Cache-Control: public,max-age=10
Cache profiles

取代重複的在很多controller action attributes響應緩存設置,cache profiles 可以被設置為options,當在Startup.ConfigureService中設置MVC/Razor Pages. 在引用的cache profiles中發現的值被用作默認值,隨着ResponseCacheAttribute並且被這個attribute上指定的任意properties重寫。(即很多重複的響應緩存設置可以在Startup.ConfigureService中設置,再隨着ResponseCacheAttribute設置在action上)

建立一個cache profile. 下面的例子展示了30秒的cache profile,在示例應用的Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.CacheProfiles.Add("Default30", new CacheProfile() { Duration = 30 });
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

這個示例應用的Cache4 page model 引用了Default30 cache profile:

[ResponseCache(CacheProfileName = "Default30")] public class Cache4Model : PageModel
{

這個ResponseCacheAttribute可以被用在

  • Razor Page handlers(classes) – 屬性可以被用到處理方法

  • MVC controllers(classes)

  • MVC actions (methods) – Method-level attributes override the settings specified in class level attributes. 方法級別的會覆蓋類級別的

Default30 profile導致的應用於Cache4 page response 的頭是:

Cache-Control: public,max-age=30

 

下一篇:Cache in-memory

 參考資料:

https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

自定義Spring Shell

目錄

  • 概述
  • 自定義內置命令
    • 禁用內置命令
    • 覆蓋內置命令
  • 自定義命令提示符
  • 自定義命令行選項行為
  • 自定義參數轉換器

概述

官網:https://projects.spring.io/spring-shell/。

Spring Shell除了提供一些常用的內置命令之外,還允許開發者對一些默認功能進行定製。

自定義內置命令

禁用內置命令

禁用Spring Shell的內置命令非常簡單,只需要在pom.xml文件中進行簡單配置即可,如下所示:

<!-- Spring Shell -->
<dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell-starter</artifactId>
    <version>2.0.0.RELEASE</version>
    <exclusions>
        <!-- 禁用內置命令 -->
        <exclusion>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-standard-commands</artifactId>
        </exclusion>
    </exclusions>
</dependency>
shell:>help
No command found for 'help'
shell:>exit
No command found for 'exit'
shell:>

完全禁用了所有內置命令之後,將無法通過help命令查詢其他命令信息,也不能再使用exit命令退出應用。
因此,如果有需要的情況下,應該只是禁用某些內置命令。

如果需要禁用指定內置命令,需要在代碼中設置對應的命令屬性為false,格式為:spring.shell.command.<command>.enabled=true
例如,需要禁用help命令:

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        String[] disabledCommands = new String[]{"--spring.shell.command.help.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
# help命令將不再能使用
shell:>help
No command found for 'help'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>exit

如果禁用的是其他命令,如:clear,在Spring Shell應用啟動之後通過help命令不再能看被禁用的命令了。

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        // 禁用了內置的clear命令
        String[] disabledCommands = new String[]{"--spring.shell.command.clear.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

顯然,在禁用了指定的內置命令之後,通過help命令將不能看到該命令了。

覆蓋內置命令

如果希望重寫內置命令的實現,可以通過實現接口org.springframework.shell.standard.commands.<Command>.Command來完成(如:需要重寫clear命令的實現,實現接口org.springframework.shell.standard.commands.Clear.Command)。
如下為重寫內置命令script的實現:

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.commands.Script;
// 實現接口org.springframework.shell.standard.commands.Script.Command
@ShellComponent
public class MyScript implements Script.Command {
    // 注意:命令名稱與內置命令保持一致
    @ShellMethod("Read and execute commands from a file.")
    public void script() {
      / // 實現自定義邏輯
        System.out.println("override default script command");
    }
}

有意思的是,此時在內置命令“Built-In Commands”分組中將不能看到script命令了,而是在自定義的分組中,

shell:>help
AVAILABLE COMMANDS

Built-In Commands  # 在內置命令分組中看不到重寫的命令了
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        stacktrace: Display the full stacktrace of the last error.

My Script          # 重寫的命令此時在自定義分組中
        scriptdo: Read and execute commands from a file.

如果希望被覆蓋的內置命令依然能夠在“Built-In Commands”分組中看到,可以通過註解@ShellMethod的group屬性指定。

// 指定被覆蓋的內置命令分組為“Built-In Commands”
@ShellMethod(value = "Read and execute commands from a file.", group = "Built-In Commands")
public void script() {
    System.out.println("override default script command");
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

shell:>script
override default script command

自定義命令提示符

默認情況下,Spring Shell啟動之後显示的是一個黃色的命令提示符(shell:>)等待用戶輸入。
可以通過Spring Shell提供的接口org.springframework.shell.jline.PromptProvider對該命令提示符進行定製。

// 通過實現接口org.springframework.shell.jline.PromptProvider定製命令提示符
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.stereotype.Component;
@Component
public class MyPromptProvider implements PromptProvider {
    @Override
    public AttributedString getPrompt() {
        // 定製命令提示符為紅色的“#”
        return new AttributedString("#", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
    }
}

如下為定製的命令提示符:

自定義命令行選項行為

Spring Shell提供了2個默認的ApplicationRunner,用於實現命令行選項的行為。

1.InteractiveShellApplicationRunner用於啟動交互式界面,接收用戶輸入命令。
2.ScriptShellApplicationRunner用於在應用啟動時從程序參數中讀取指定文件中的命令並執行,具體來講:將多個命令寫在文件中,並通過參數的形式將包含了批量命令的文件路徑傳遞給程序,傳遞的文件路徑參數必須以“@”開始,如下示例:

$ java -jar /home/test/sun/workspace/test-springshell/target/test-springshell-0.0.1-SNAPSHOT.jar @/home/test/cmd

文件/home/test/cmd中的內容為:

$ cat /home/test/cmd 
help

這樣,在啟動程序時,將會自動執行/home/test/cmd文件中的命令(如果文件不存在,啟動應用時報錯)。
值得注意的是: 當在程序參數中存在“@local_file_path”這樣的參數時,應用啟動后執行完文件“local_file_path”內命令之後就退出了,不會進入交互式命令行界面(上述示例中,應用啟動后執行help命令之後就退出了)。

如果Spring Shell默認提供的上述2個ApplicationRunner無法滿足需求,可以自定義其他的命令行選項行為,直接實現接口org.springframework.boot.ApplicationRunner即可。

自定義參數轉換器

默認情況下,Spring Shell使用標準的Spring類型轉換機制將命令行的文本參數轉換為指定的類型。
實際上,Spring Shell是通過DefaultConversionService註冊Converter<S, T>GenericConverter或者ConverterFactory<S, R>類型的Bean對象來實現對命令行參數進行類型轉換的。

換句話說,如果我們需要自定義類型轉換器,只需要簡單實現接口org.springframework.core.convert.converter.Converter<S, T>就可以了。

// 自定義類型
public class Food {
    private String value = null;
    public Food(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append("Food{").append("value='").append(value).append("'}")
                .toString();
    }
}

// 自定義類型轉換器
@Component
public class MyConverter implements Converter<String, Food> {
    @Override
    public Food convert(String s) {
        // 將輸入參數轉換為Food類型實例
        return new Food(s);
    }
}

// 使用自定義轉換類型
@ShellComponent
public class ConvertionCmd {
    // 在命令方法中直接可以獲取Food對象,這是通過前面的自定義類型轉換器MyConverter實現的
    @ShellMethod("Conversion food")
    public String food(Food food) {
        return food.toString();
    }
}

在命令行指定命令food

#food apple
Food{value='apple'}

顯然,通過自定義類型轉換器可以實現對命令參數的特殊處理,非常實用。

【參考】
https://blog.csdn.net/zknxx/article/details/52196427 SpringBoot之CommandLineRunner接口和ApplicationRunner接口
https://www.jianshu.com/p/5d4ffe267596 CommandLineRunner或者ApplicationRunner接口

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

spring源碼解析之IOC容器(二)——加載和註冊

  上一篇跟蹤了IOC容器對配置文件的定位,現在我們繼續跟蹤代碼,看看IOC容器是怎麼加載和註冊配置文件中的信息的。開始之前,首先我們先來了解一下IOC容器所使用的數據結構——-BeanDefinition,它是一個上層接口,有很多實現類,分別對應不同的數據載體。我們平時開發的時候,也會定義很多pojo類,來作為獲取數據的載體。最常見的就是,從數據庫中獲取數據之後,使用一個定義的pojo來裝載,然後我們就可以在程序中使用這個pojo類來編寫各種業務邏輯。同樣,IOC容器首先會讀取配置的XML中各個節點,即各個標籤元素,然後根據不同的標籤元素,使用不同的數據結構來裝載該元素中的各種屬性的值。比如我們最熟悉的<bean>標籤,就是使用AbstractBeanDefinition這個數據結構,接下來的分析中我們可以看到。

  先回到上篇資源的定位那裡,代碼如下:

 1 public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
 2         ResourceLoader resourceLoader = getResourceLoader();
 3         if (resourceLoader == null) {
 4             throw new BeanDefinitionStoreException(
 5                     "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
 6         }
 7 
 8         if (resourceLoader instanceof ResourcePatternResolver) {
 9             // Resource pattern matching available.
10             try {
11                 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
12                 int loadCount = loadBeanDefinitions(resources);
13                 if (actualResources != null) {
14                     for (Resource resource : resources) {
15                         actualResources.add(resource);
16                     }
17                 }
18                 if (logger.isDebugEnabled()) {
19                     logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
20                 }
21                 return loadCount;
22             }
23             catch (IOException ex) {
24                 throw new BeanDefinitionStoreException(
25                         "Could not resolve bean definition resource pattern [" + location + "]", ex);
26             }
27         }
28         else {
29             // 定位到資源之後,封裝成一個resource對象
30             Resource resource = resourceLoader.getResource(location);
31             int loadCount = loadBeanDefinitions(resource);
32             if (actualResources != null) {
33                 actualResources.add(resource);
34             }
35             if (logger.isDebugEnabled()) {
36                 logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
37             }
38             return loadCount;
39         }
40     }

  進入loadBeanDefinitions(resource)方法,正式開始加載源碼的跟蹤:

1         @Override
2     public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
3         return loadBeanDefinitions(new EncodedResource(resource));
4     }    
 1 public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
 2         Assert.notNull(encodedResource, "EncodedResource must not be null");
 3         if (logger.isInfoEnabled()) {
 4             logger.info("Loading XML bean definitions from " + encodedResource);
 5         }
 6 
 7         Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
 8         if (currentResources == null) {
 9             currentResources = new HashSet<EncodedResource>(4);
10             this.resourcesCurrentlyBeingLoaded.set(currentResources);
11         }
12         if (!currentResources.add(encodedResource)) {
13             throw new BeanDefinitionStoreException(
14                     "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
15         }
16         try {
17             InputStream inputStream = encodedResource.getResource().getInputStream();
18             try {
19                 InputSource inputSource = new InputSource(inputStream);
20                 if (encodedResource.getEncoding() != null) {
21                     inputSource.setEncoding(encodedResource.getEncoding());
22                 }
23                 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
24             }
25             finally {
26                 inputStream.close();
27             }
28         }
29         catch (IOException ex) {
30             throw new BeanDefinitionStoreException(
31                     "IOException parsing XML document from " + encodedResource.getResource(), ex);
32         }
33         finally {
34             currentResources.remove(encodedResource);
35             if (currentResources.isEmpty()) {
36                 this.resourcesCurrentlyBeingLoaded.remove();
37             }
38         }
39     }

  進入doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法:

 1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
 2             throws BeanDefinitionStoreException {
 3         try {
 4             Document doc = doLoadDocument(inputSource, resource);
 5             return registerBeanDefinitions(doc, resource);
 6         }
 7         catch (BeanDefinitionStoreException ex) {
 8             throw ex;
 9         }
10         catch (SAXParseException ex) {
11             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
12                     "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
13         }
14         catch (SAXException ex) {
15             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
16                     "XML document from " + resource + " is invalid", ex);
17         }
18         catch (ParserConfigurationException ex) {
19             throw new BeanDefinitionStoreException(resource.getDescription(),
20                     "Parser configuration exception parsing XML from " + resource, ex);
21         }
22         catch (IOException ex) {
23             throw new BeanDefinitionStoreException(resource.getDescription(),
24                     "IOException parsing XML document from " + resource, ex);
25         }
26         catch (Throwable ex) {
27             throw new BeanDefinitionStoreException(resource.getDescription(),
28                     "Unexpected exception parsing XML document from " + resource, ex);
29         }
30     }

  繼續進入registerBeanDefinitions(doc, resource)方法:

1 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
2         //此時documentReader已經是DefaultBeanDefinitionDocumentReader類了
3         BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
4         int countBefore = getRegistry().getBeanDefinitionCount();
5         documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
6         //返回當前註冊的beanDefinition的個數
7         return getRegistry().getBeanDefinitionCount() - countBefore;
8     }

  進入registerBeanDefinitions(doc, createReaderContext(resource))方法:

1 public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
2         this.readerContext = readerContext;
3         logger.debug("Loading bean definitions");
4         Element root = doc.getDocumentElement();
5         doRegisterBeanDefinitions(root);
6     }

  進入doRegisterBeanDefinitions(root)方法:

 1 protected void doRegisterBeanDefinitions(Element root) {
 2         // Any nested <beans> elements will cause recursion in this method. In
 3         // order to propagate and preserve <beans> default-* attributes correctly,
 4         // keep track of the current (parent) delegate, which may be null. Create
 5         // the new (child) delegate with a reference to the parent for fallback purposes,
 6         // then ultimately reset this.delegate back to its original (parent) reference.
 7         // this behavior emulates a stack of delegates without actually necessitating one.
 8         BeanDefinitionParserDelegate parent = this.delegate;
 9         this.delegate = createDelegate(getReaderContext(), root, parent);
10 
11         if (this.delegate.isDefaultNamespace(root)) {
12             //profile屬性平時使用非常少,該屬性可以用於配置數據庫的切換(常用),使用時,需要在web.xml中配置context-parm
13             //<context-parm>
14             //    <parm-name>Spring.profiles.active</parm-name>
15             //    <parm-value>dev(在applicationContext.xml中配置的profile屬性的beans的profile屬性值)</parm-name>
16             //</context-parm>
17             //在applicationContext.xml中的配置
18             //<beans profile="dev">    </beans>
19             //<beans profile="produce">   </beans>
20             String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
21             if (StringUtils.hasText(profileSpec)) {
22                 String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
23                         profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
24                 if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
25                     if (logger.isInfoEnabled()) {
26                         logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
27                                 "] not matching: " + getReaderContext().getResource());
28                     }
29                     return;
30                 }
31             }
32         }
33 
34         preProcessXml(root);
35         parseBeanDefinitions(root, this.delegate);
36         postProcessXml(root);
37 
38         this.delegate = parent;
39     }

  這裏也用到了模板方法,preProcessXml(root)和postProcessXml(root)這兩個方法都是空實現,是留給客戶來實現自己的邏輯的。重點研究一下parseBeanDefinitions(root, this.delegate)方法:

 1 protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
 2         if (delegate.isDefaultNamespace(root)) {
 3             NodeList nl = root.getChildNodes();
 4             for (int i = 0; i < nl.getLength(); i++) {
 5                 Node node = nl.item(i);
 6                 if (node instanceof Element) {
 7                     Element ele = (Element) node;
 8                     if (delegate.isDefaultNamespace(ele)) {
 9                         parseDefaultElement(ele, delegate);
10                     }
11                     else {
12                         delegate.parseCustomElement(ele);
13                     }
14                 }
15             }
16         }
17         else {
18             delegate.parseCustomElement(root);
19         }
20     }

  parseCustomElement(root)方法不需要怎麼研究,我們平時幾乎不會用到自定義的標籤,所以只跟蹤parseDefaultElement(ele, delegate)裏面的代碼:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    //import標籤
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
            importBeanDefinitionResource(ele);
        }
    //alias標籤
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
            processAliasRegistration(ele);
        }
   //bean標籤
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
            processBeanDefinition(ele, delegate);
        }
  //beans標籤
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

  可以看到,對於不同的標籤,spring採用不同的策略進行處理,重點跟蹤一下處理bean標籤的方法processBeanDefinition(ele, delegate):

 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 2         //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
 3         //並將解析好的內容封裝成BeanDefinitionHolder對象
 4         BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 5         if (bdHolder != null) {
 6             bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
 7             try {
 8                 // Register the final decorated instance.
 9                 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10             }
11             catch (BeanDefinitionStoreException ex) {
12                 getReaderContext().error("Failed to register bean definition with name '" +
13                         bdHolder.getBeanName() + "'", ele, ex);
14             }
15             // Send registration event.
16             getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17         }
18     }

  在這個方法中,delegate.parseBeanDefinitionElement(ele)是解析bean元素中各種屬性的方法,registerBeanDefinition(bdHolder, getReaderContext().getRegistry())是將封裝好的數據進行存儲的方法。先看一下解析的方法:

 1 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
 2         //獲取bean標籤的id屬性的值
 3         String id = ele.getAttribute(ID_ATTRIBUTE);
 4         //獲取bean標籤上name屬性的值
 5         String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
 6 
 7         List<String> aliases = new ArrayList<String>();
 8         if (StringUtils.hasLength(nameAttr)) {
 9             //將name的值進行分割,並將它們當作別名存到aliases中
10             String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
11             aliases.addAll(Arrays.asList(nameArr));
12         }
13 
14         String beanName = id;
15         //如果bean標籤的id沒有值,但是name屬性有值,則將name屬性的第一個值當作id的值,並從aliases中將第一個別名移除掉
16         if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
17             beanName = aliases.remove(0);
18             if (logger.isDebugEnabled()) {
19                 logger.debug("No XML 'id' specified - using '" + beanName +
20                         "' as bean name and " + aliases + " as aliases");
21             }
22         }
23 
24         if (containingBean == null) {
25             //檢查bean的唯一性
26             checkNameUniqueness(beanName, aliases, ele);
27         }
28 
29         //這裏已經是將XML中bean元素中的所有屬性都封裝到beanDefinition對象中了
30         AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
31         if (beanDefinition != null) {
32             if (!StringUtils.hasText(beanName)) {
33                 try {
34                     if (containingBean != null) {
35                         beanName = BeanDefinitionReaderUtils.generateBeanName(
36                                 beanDefinition, this.readerContext.getRegistry(), true);
37                     }
38                     else {
39                         beanName = this.readerContext.generateBeanName(beanDefinition);
40                         // Register an alias for the plain bean class name, if still possible,
41                         // if the generator returned the class name plus a suffix.
42                         // This is expected for Spring 1.2/2.0 backwards compatibility.
43                         String beanClassName = beanDefinition.getBeanClassName();
44                         if (beanClassName != null &&
45                                 beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
46                                 !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
47                             aliases.add(beanClassName);
48                         }
49                     }
50                     if (logger.isDebugEnabled()) {
51                         logger.debug("Neither XML 'id' nor 'name' specified - " +
52                                 "using generated bean name [" + beanName + "]");
53                     }
54                 }
55                 catch (Exception ex) {
56                     error(ex.getMessage(), ele);
57                     return null;
58                 }
59             }
60             String[] aliasesArray = StringUtils.toStringArray(aliases);
61             //最後將封裝好的beanDefinition、它的id、以及它的別名一起封裝成BeanDefinitionHolder對象返回
62             return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
63         }
64 
65         return null;
66     }

  我們可以得到如下信息:

  1、獲取bean標籤的id屬性和name屬性的值;

  2、name屬性是可以都有多個值的,以逗號或者分號分割;

  3、如果id沒有賦值,則取name的第一個值作為id的值。所以,我們一般都會給id賦值,這樣效率高一些;

  4、檢查以這個id標識的bean是不是唯一的;

  5、進行其他屬性的解析,並最終封裝測AbstractBeanDefinition對象,也就是我們前文中提到的數據結構;

  6、最後封裝成BeanDefinitionHolder對象之後返回。

  進入parseBeanDefinitionElement(ele, beanName, containingBean)方法,看一下其他元素的解析過程:

 1 public AbstractBeanDefinition parseBeanDefinitionElement(
 2             Element ele, String beanName, BeanDefinition containingBean) {
 3 
 4         this.parseState.push(new BeanEntry(beanName));
 5 
 6         String className = null;
 7         if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
 8             className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
 9         }
10 
11         try {
12             String parent = null;
13             if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
14                 parent = ele.getAttribute(PARENT_ATTRIBUTE);
15             }
16             AbstractBeanDefinition bd = createBeanDefinition(className, parent);
17 
18             parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
19             bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
20 
21             parseMetaElements(ele, bd);
22             parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
23             parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
24 
25             parseConstructorArgElements(ele, bd);
26             parsePropertyElements(ele, bd);
27             parseQualifierElements(ele, bd);
28 
29             bd.setResource(this.readerContext.getResource());
30             bd.setSource(extractSource(ele));
31 
32             return bd;
33         }
34         catch (ClassNotFoundException ex) {
35             error("Bean class [" + className + "] not found", ele, ex);
36         }
37         catch (NoClassDefFoundError err) {
38             error("Class that bean class [" + className + "] depends on not found", ele, err);
39         }
40         catch (Throwable ex) {
41             error("Unexpected failure during bean definition parsing", ele, ex);
42         }
43         finally {
44             this.parseState.pop();
45         }
46 
47         return null;
48     }

  解析封裝成BeanDefinitionHolder對象之後,就可以進行註冊了,先回到之前的processBeanDefinition(ele, delegate):

 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 2         //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
 3         //並將解析好的內容封裝成BeanDefinitionHolder對象
 4         BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 5         if (bdHolder != null) {
 6             bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
 7             try {
 8                 // Register the final decorated instance.
 9                 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10             }
11             catch (BeanDefinitionStoreException ex) {
12                 getReaderContext().error("Failed to register bean definition with name '" +
13                         bdHolder.getBeanName() + "'", ele, ex);
14             }
15             // Send registration event.
16             getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17         }
18     }

  現在進入BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())方法進行分析:

 1 public static void registerBeanDefinition(
 2             BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
 3             throws BeanDefinitionStoreException {
 4 
 5         // Register bean definition under primary name.
 6         String beanName = definitionHolder.getBeanName();
 7         registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
 8 
 9         // Register aliases for bean name, if any.
10         String[] aliases = definitionHolder.getAliases();
11         if (aliases != null) {
12             for (String alias : aliases) {
13                 registry.registerAlias(beanName, alias);
14             }
15         }
16     }

  這裏的beanName就是之前封裝好的bean的id。這個方法中分別以id和別名作為key來註冊bean,其實就是存儲在map中。

  進入registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()),在其子類DefaultListableBeanFactory中有實現:

 1 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
 2             throws BeanDefinitionStoreException {
 3 
 4         Assert.hasText(beanName, "Bean name must not be empty");
 5         Assert.notNull(beanDefinition, "BeanDefinition must not be null");
 6 
 7         if (beanDefinition instanceof AbstractBeanDefinition) {
 8             try {
 9                 ((AbstractBeanDefinition) beanDefinition).validate();
10             }
11             catch (BeanDefinitionValidationException ex) {
12                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
13                         "Validation of bean definition failed", ex);
14             }
15         }
16 
17         BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
18         if (existingDefinition != null) {
19             if (!isAllowBeanDefinitionOverriding()) {
20                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
21                         "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
22                         "': There is already [" + existingDefinition + "] bound.");
23             }
24             else if (existingDefinition.getRole() < beanDefinition.getRole()) {
25                 // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
26                 if (logger.isWarnEnabled()) {
27                     logger.warn("Overriding user-defined bean definition for bean '" + beanName +
28                             "' with a framework-generated bean definition: replacing [" +
29                             existingDefinition + "] with [" + beanDefinition + "]");
30                 }
31             }
32             else if (!beanDefinition.equals(existingDefinition)) {
33                 if (logger.isInfoEnabled()) {
34                     logger.info("Overriding bean definition for bean '" + beanName +
35                             "' with a different definition: replacing [" + existingDefinition +
36                             "] with [" + beanDefinition + "]");
37                 }
38             }
39             else {
40                 if (logger.isDebugEnabled()) {
41                     logger.debug("Overriding bean definition for bean '" + beanName +
42                             "' with an equivalent definition: replacing [" + existingDefinition +
43                             "] with [" + beanDefinition + "]");
44                 }
45             }
46             this.beanDefinitionMap.put(beanName, beanDefinition);
47         }
48         else {
49             if (hasBeanCreationStarted()) {
50                 // Cannot modify startup-time collection elements anymore (for stable iteration)
51                 synchronized (this.beanDefinitionMap) {
52                     this.beanDefinitionMap.put(beanName, beanDefinition);
53                     List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
54                     updatedDefinitions.addAll(this.beanDefinitionNames);
55                     updatedDefinitions.add(beanName);
56                     this.beanDefinitionNames = updatedDefinitions;
57                     if (this.manualSingletonNames.contains(beanName)) {
58                         Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
59                         updatedSingletons.remove(beanName);
60                         this.manualSingletonNames = updatedSingletons;
61                     }
62                 }
63             }
64             else {
65                 // Still in startup registration phase
66                 this.beanDefinitionMap.put(beanName, beanDefinition);
67                 this.beanDefinitionNames.add(beanName);
68                 this.manualSingletonNames.remove(beanName);
69             }
70             this.frozenBeanDefinitionNames = null;
71         }
72 
73         if (existingDefinition != null || containsSingleton(beanName)) {
74             resetBeanDefinition(beanName);
75         }
76     }

  我們可以看到:這個beanDefinitionMap就是用來存儲解析好的bean的,以id作為key。至此,就將所有的bean標籤解析好之後封裝成BeanDefinition註冊到了IOC容器中。但是,到目前為止,IOC容器並沒有為我們將這些解析好的數據生成一個一個bean實例,我們仍然不能就這樣直接使用。下一篇接着跟蹤。

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

【拆分版】Docker-compose構建Elasticsearch 7.1.0集群

寫在前邊

搞了兩三天了,一直有個問題困擾着我,ES集群中配置怎麼能正確映射到主機上,這邊經常報ClusterFormationFailureHelper master not discovered or elected yet.原因是容器中的ES節點沒有正確的映射到主機上,而且容器內ip是易變的,我該怎麼配置呢?

臨下班了,終於想到個法子,固定容器ip——使用network_mode: host

看到主機模式的我眼前一亮,容器就相當於一個主機服務,你占哪個端口就是哪個,沒有必要再去自己指定port去映射到主機。這樣只要主機ip不變,容器中ip不就沒問題了么!!!

本文內容架構圖

解釋:

Master節點作為Master節點與協調節點,為防止腦裂問題,降低負載,不存數據

Node1~Node3為數據節點,不參與Master競選

TribeNode節點不存數據,不參與Master競選

準備環境

  • GNU/Debain Stretch 9.9 linux-4.19
  • Docker 18.09.6
  • Docker-Compose 1.17.1
  • elasticsearch:7.1.0

配置腳本參見我的Github <https://github.com/hellxz/docker-es-cluster.git>

宿主機環境準備參考ELK集群搭建,基於7.1.1 文中開始搭建的前四步

目錄結構

.
├── docker-es-data01
│   ├── data01
│   ├── data01-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-data02
│   ├── data02
│   ├── data02-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-data03
│   ├── data03
│   ├── data03-logs
│   ├── docker-compose.yml
│   ├── .env
│   └── es-config
│       └── elasticsearch.yml
├── docker-es-master
│   ├── docker-compose.yml
│   ├── .env
│   ├── es-config
│   │   └── elasticsearch.yml
│   ├── master-data
│   └── master-logs
└── docker-es-tribe
    ├── docker-compose.yml
    ├── .env
    ├── es-config
    │   └── elasticsearch.yml
    ├── tribe-data
    └── tribe-logs

最終效果

各目錄代表節點與端口號

節點目錄 節點名稱 協調端口號 說明 查詢端口號
docker-es-data01 data01 9301 數據節點1,非master節點 9201
docker-es-data02 data02 9302 數據節點2,非master節點 9202
docker-es-data03 data03 9303 數據節點3,非master節點 9203
docker-es-master master 9300 master節點,非數據節點 9200
docker-es-tribe tribe 9304 協調節點,非master非數據節點 9204

想測試這些節點是否可用,只需要修改每個節點目錄下的es-config/elasticsearch.yml中的ip地址,全部換成你需要的ip即可。

各文件功用舉例說明

鑒於這裏邊有很多是重複操作,這裏僅拿其中的master節點進行舉例,其餘代碼參見Github

.env 這個文件為docker-compose.yml提供默認參數,方便修改

# the default environment for es-master
# set es node jvm args
ES_JVM_OPTS=-Xms256m -Xmx256m
# set master node data folder
MASTER_DATA_DIR=./master-data
# set master node logs folder
MASTER_LOGS_DIR=./master-logs

docker-compose.yml docker-compose的配置文件

version: "3"
services:
    es-master:
        image: elasticsearch:7.1.0
        container_name: es-master
        environment: # setting container env
            - ES_JAVA_OPTS=${ES_JVM_OPTS}   # set es bootstrap jvm args
        restart: always
        volumes:
            - ./es-config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
            - ${MASTER_DATA_DIR}:/usr/share/elasticsearch/data:rw
            - ${MASTER_LOGS_DIR}:/usr/share/elasticsearch/logs:rw
        network_mode: "host"

簡單來說,就是修改pull的鏡像,替換其中的變量與配置文件,掛載數據與日誌目錄,最後用的host主機模式,讓節點服務佔用到實體機端口

elaticsearch.yml elasticsearch的配置文件,搭建集群最關鍵的文件之一

# ======================== Elasticsearch Configuration =========================
cluster.name: es-cluster
node.name: master 
node.master: true
node.data: false
node.attr.rack: r1 
bootstrap.memory_lock: true 
http.port: 9200
network.host: 10.2.114.110
transport.tcp.port: 9300
discovery.seed_hosts: ["10.2.114.110:9301","10.2.114.110:9302","10.2.114.110:9303","10.2.114.110:9304"] 
cluster.initial_master_nodes: ["master"] 
gateway.recover_after_nodes: 2

按照前幾篇文章下來,大家對這些參數已經不是很陌生了,這裏簡單說下幾個比較重要的參數

  • transport.tcp.port 設置es多節點協調的端口號
  • discovery.seed_hosts 設置當前節點啟動后要發現的協調節點位置,當然自己不需要發現自己,推薦使用ip:port形式,集群形成快
  • cluster.initial_master_nodes 集群中可以成為master節點的節點名,這裏指定唯一的一個,防止腦裂

使用說明

  1. 若想將此腳本使用到生產上,需要修改每個節點下的.env文件,將掛載數據、日誌目錄修改為啟動es的集群的用戶可讀寫的位置,可以通過sudo chmod 777 -R 目錄sudo chown -R 當前用戶名:用戶組 目錄 來修改被掛載的目錄權限
  2. 修改.env下的JVM參數,擴大堆內存,啟動與最大值最好相等,以減少gc次數,提高效率
  3. 修改所有節點下的docker-compose.yml 中的network.host地址 為當前所放置的主機的ip,discovery.seed_hosts需要填寫具體各待發現節點的實體機ip,以確保可以組成集群
  4. 確保各端口在其宿主機上沒有被佔用,如有佔用需確認是否有用,無用kill,有用則更新docker-compose.ymlhttp.porttransport.tcp.port,注意與此同時要更新其它節點的discovery.seed_hosts對應的port
  5. 如果在同一台主機上,可以參考使用文章後邊的簡單的shell腳本

各節點操作命令

後台啟動命令均為docker-compose up -d

關閉命令:

  • docker-compose down: 關閉同時移除容器與多餘虛擬網卡
  • docker stop contains_name: 根據容器名稱關閉容器,不移除容器

簡單的Shell腳本

docker-es-cluster-up.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for start up the docker-es-cluster designed in the one of linux server.
cd docker-es-master && docker-compose up -d && \
cd ../docker-es-data01 && docker-compose up -d && \
cd ../docker-es-data02 && docker-compose up -d && \
cd ../docker-es-data03 && docker-compose up -d && \
cd ../docker-es-tribe && docker-compose up -d && \
cd ..

docker-es-cluster-down.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for remove the docker-es-cluster's containers and networks designed in the one of linux server.
cd docker-es-tribe && docker-compose down && \
cd ../docker-es-data03 && docker-compose down && \
cd ../docker-es-data02 && docker-compose down && \
cd ../docker-es-data01 && docker-compose down && \
cd ../docker-es-master && docker-compose down && \
cd ..

docker-es-cluster-stop.sh

#/bin/bash
# please put this shell script to the root of each node folder.
# this shell script for stop the docker-es-cluster's containers designed in the one of linux server.
docker stop es-tribe es-data03 es-data02 es-data01 es-master

如果你想讓這些腳本有執行權限,不妨試試sudo chmod +x *.sh

這些腳本中沒有使用sudo,如需要使用sudo才能啟動docker,請添加當前用戶到docker組

Enjoy.

本文系原創文章,禁止轉載。

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

kubernetes高級之動態准入控制

系列目錄

動態准入控制器文檔介紹了如何使用標準的,插件式的准入控制器.但是,但是由於以下原因,插件式的准入控制器在一些場景下並不靈活:

  • 它們需要編譯到kube-apiserver里

  • 它們僅在apiserver啟動的時候可以配置

准入鈎子(Admission Webhooks 從1.9版本開始)解決了這些問題,它允許准入控制器獨立於核心代碼編譯並且可以在運行時配置.

什麼是准入鈎子

准入鈎子是一種http回調,它接收准入請求然後做一些處理.你可以定義兩種類型的准入鈎子:驗證鈎子和變換鈎子.對於驗證鈎子,你可以拒絕請求以使自定義准入策略生效.對於變換鈎子,你可以改變請求來使自定義的默認配置生效.

體驗准入鈎子

准入控制鈎子是集群管制面板不可缺少的一部分.你在編寫部署它們時必須要警惕.如果你想要編寫/布置生產級別的准入控制器,請閱讀以下用戶指南.下面我們將介紹如何快速體驗准入鈎子.

準備工作:

  • 確保你的kubernetes集群版本至少是1.9版本.

  • 確保變換鈎子(MutatingAdmissionWebhook) 和驗證鈎子(ValidatingAdmissionWebhook)已經啟用.這裡是推薦開啟的一組准入控制器.

編寫一個准入鈎子服務器(admission webhook server)

請參閱已經被kubernetes e2e測試驗證通過的准入服務器鈎子( admission webhook server)的實現.這個web鈎子處理apiserver發出的admissionReview請求,然後把結果封裝成一個admissionResponse返回給請求者.

admissionReview請求可能有多個版本( v1beta1 或者 未來的v1),web鈎子可以通過admissionReviewVersions字段來定義它們接受的版本.apiserver會嘗試使用列表中出現的,支持的第一個版本.如果列表中的版本沒有一個是被支持的,驗證將失敗.如果webhook配置已經持久化,對web鈎子的請求將會失敗並被失敗策略控制.

示例鈎子服務器(admission webhook server)把ClientAuth字段留空,默認為NoClientCert.這意味着鈎子服務器不驗證客戶端身份.如果你需要使用mutual TLS或者其它方法來驗證客戶端請求,請參考如何認證apiserver

部署准入控制服務

e2e測試的鈎子服務器通過部署api(https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#deployment-v1beta1-apps)被部署到kubernetes集群中.測試項目也為鈎子服務器創建了一個前端服務,代碼

你也可以把你的鈎子服務部署到集群外,你需要相應地更新web鈎子客戶端配置

運行時配置准入web鈎子

你可以通過ValidatingWebhookConfiguration和MutatingWebhookConfiguration動態地配置哪些資源被哪些web鈎子控制.

以下是一個validatingWebhookConfiguration配置的示例,變換鈎子的配置也類似

apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: <name of this configuration object>
webhooks:
- name: <webhook name, e.g., pod-policy.example.io>
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: "Namespaced"
  clientConfig:
    service:
      namespace: <namespace of the front-end service>
      name: <name of the front-end service>
    caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
  admissionReviewVersions:
  - v1beta1
  timeoutSeconds: 1

scope字段指定了集群級別的資源(“Cluster”)或者名稱空間級別的資源(“Namespaced”)需要匹配這些規則.”*”表示沒有任何範圍限制.

注意,如果使用clientConfig.service,服務端證書必須對<svc_name>.<svc_namespace>.svc有效.

web鈎子請求默認超時時間為30秒,但是從1.14版本開始,你可以自由設置超時時間但是建議設置較小的時間.如果web鈎子請求超時,請求將被web鈎子的失敗策略處理.

當apiserver接收到一個匹配規則的請求,apiserver將會發送一個admissionReview請求到clientConfig配置的web鈎子里.

創建web鈎子配置以後,系統將會經過一段時間使新配置生效.

認證apiserver

如果你的准入web鈎子需要認證,你可以配置apiserver使用基本認證(basic auth), bearer token 或者證書認證.需要三個步驟來完成認證配置.

  • 當啟動apiserver時,通過--admission-control-config-file選項來指定準入控制配置文件的位置.

  • 在准入控制配置文件里,指定變換控制器(MutatingAdmissionWebhook)和驗證控制器(ValidatingAdmissionWebhook)從哪裡讀取證書.證書存儲在kubeConfig文件里(和kubectl使用的相同),字段名為kubeConfigFile.下面是准入控制配置文件示例

apiVersion: apiserver.k8s.io/v1alpha1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: <path-to-kubeconfig-file>
- name: MutatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: <path-to-kubeconfig-file>

這裡是admissionConfiguration的schema定義

  • 在kubeConfig文件里,提供證書
apiVersion: v1
kind: Config
users:
# DNS name of webhook service, i.e., <service name>.<namespace>.svc, or the URL
# of the webhook server.
- name: 'webhook1.ns1.svc'
  user:
    client-certificate-data: <pem encoded certificate>
    client-key-data: <pem encoded key>
# The `name` supports using * to wildmatch prefixing segments.
- name: '*.webhook-company.org'
  user:
    password: <password>
    username: <name>
# '*' is the default match.
- name: '*'
  user:
    token: <token>

當然,你需要設置web鈎子服務器來處理這些認證.

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高速輸出-我們戲說緩存

前言

緩存要解決的問題是速度的問題,使用緩存的目的是為了減少對物理資源的訪問,緩存大量的應用在軟硬件的方方面面,從 CPU 到硬盤,就應用了 一級緩存、二級緩存,少部分高速緩存和大量低速緩存相結合,以提高 CPU 的計算能力,本文講的主要是系統集成項目中的軟件級別的緩存。

緩存因果圖

緩存在現代系統中的位置可以說是舉足輕重,不是可有可無的問題了,而是怎麼樣用好的問題。怎麼樣講好緩存這個話題,我思考了很久;如果是生搬硬套,我估計我肯定是涼涼,但要是上來就一頓代碼操作,顯得字太多,各位看官老爺也是要噴死我的(你們噴我算我贏)。各位看官就當故事隨便看看就好了。

客戶端緩存

小明同學是一個大學生,每個月都會從爸爸那裡領取生活費,由於爸爸給的生活費比較充裕,他就將一部分存了起來,周而復始,固定頻率,這就是瀏覽器網頁緩存;有一天,他接到爸爸的一個電話,說每個月給現金,我總是從銀行拿也不方便,這樣,咱給存銀行卡裡頭,但是說好了,每個月1號的時候轉1000塊進去,3號之前可以領取,過時不侯。這就是 Cookie!小明說爸,這時間太緊迫了,我萬一太過於專心學習忘記取錢那你兒子可是要餓死的吖,到時候沒人給你養老送終事兒就大了。他爸爸經過深思熟慮后,決定放開取現時間的限制:行,你想什麼時候取都成,就是別把卡給弄掉了就行。這就是LocalStorage/IndexDB,瀏覽器本地存儲。如果銀行卡掉了(瀏覽器重裝,刪庫跑路),那還得找爸爸。

服務器緩存

我們本次討論的重點就是服務器緩存,對於小明的爸爸來說,每次給小明生活費之前都要到銀行(數據庫)去取錢出來,實在是太麻煩,不如提前把錢取出來準備好,每次取半年的錢出來放家裡保險箱里(緩存),小明申請生活費的時候,直接給他就好了,節約時間效率又高,唯一的問題就是可能沒那麼安全,有可能被小偷把錢給偷了(緩存更新),然後就是半年時間過後再重新取一筆出來(緩存過期策略)。

靜態對象緩存

靜態對象也是一種特殊的緩存,靜態對象作用於程序的整個生命周期中。需要注意的是,靜態對象不會被 GC 回收 ,但是,如果靜態對象被多次引用覆蓋,那麼之前的引用就有可能被 GC 回收。這就好像,小明在向爸爸領取生活費的時候,發現這次領到手的錢實在是太破舊了,都不好意思花出去,爸爸只好重新給了另一份。

會話級緩存

在 Web 站點中,Session 是私有制的,各個 Session 之間是不會共享內存對象的,我們可以利用這個特性(在Asp.NET 時代常用)來暫時保存一些數據,例如用戶購物車。還是以小明的需求為例子,小明下面還有一個妹妹,妹妹每個月都可以從爸爸那裡多領取200元生活費,看到妹妹的資源這麼好,小明憤憤不平,就像妹妹提議共享生活費,一家人一起用,多好吖!結果小明被爸爸揍了一頓,還收回了部分生活費。

進程級別緩存

基本上每個應用程序都具有本地緩存的能力,在 Asp.NET 中就有 MemoryCache ,也叫做進程級緩存(本地緩存),MemoryCache 和 分佈式緩存的作用基本一致,所不同的是,本地緩存在應用程序停止后就會被釋放掉,無法進行持久存儲。就好像,小明在上大學期間,每個月都是生活費的,但是到暑假的時候就沒有了(只能靠苦逼的暑期工掙點生活費啊)。

分佈式緩存

分佈式緩存是個好東西,目前市場上出現了非常多的 nosql 數據庫,都可用作緩存數據庫,有時候,這些緩存數據庫也提供持久化的能力。

小明家的生活條件,在經過爸爸的不懈努力之後,生活水平漸漸提高了;有一天,爸爸對小明說:明仔吖,咱們家現在生活水平提高了,但是爸爸也越來越忙了,這樣,如果我忙的時候,你問媽媽或者爺爺奶奶,也是可以拿到生活費的,咱們家這幾位長輩手裡都有錢了,這就是分佈式緩存。

但是分佈式緩存又分為主從模式和集群模式,上面說的是集群模式,爸爸媽媽爺爺奶奶都可以拿錢,但是主從模式就不同了,主從模式就是錢都在爸爸手裡,爸爸會把錢分給每個長輩一份,如果當時正好爸爸太忙了,沒來得及分給媽媽,而小明又恰好問媽媽要生活費的話;媽媽只能對他說:小命呀,不好意思,媽媽這裏也沒有,你看看再問問其它人(客戶端自己輪詢),在問了媽媽、爺爺后(引用指向),終於知道,錢在爸爸那裡,還得問爸爸要生活費。而且爸爸給其它人分錢的時候,還要佔用他工作的時間。

緩存雪崩

緩存雪崩就是在某一個時刻,大量的緩存同時失效,造成數據庫訪問壓力倍增。小明的爸爸最近壓力比較大,因為收入減少了,他爸爸做的一個工程因為種種原因,貨款只能分期付清,為了防止小明和妹妹同時申請生活費,造成資金周轉困難;爸爸規定妹妹 1 號領取生活費,而小明在 5 號才能領取,小明心裏的苦啊!

緩存穿透

緩存穿透就是客戶端總是嘗試訪問某個不存在的緩存,造成了每次都要取請求數據庫讀取數據。就好像小明吧,本來生活挺平靜的,這剛上大二,就交了個女朋友,每個月的 1000 元生活費有點捉襟見肘,然後他又不能讓爸爸知道,就在申請生活費的時候,每次都多要一點;這樣就搞得爸爸很被動,本來計劃得好好的,每個月都是 1000 塊,這沒次都不夠,老是要跑銀行取現金,終於在3個月後,爸爸發現了這個問題,想著兒子也大了,為了早日抱上孫子,就提高了小明的生活費,解決了每次都要跑銀行的問題。

緩存擊穿

緩存擊穿和緩存雪崩有點類似,其中不同的是;緩存雪崩是大量緩存 key 同時過期,而緩存擊穿是大量的請求指向同一個緩存key,在這個 key 過期的時候,大量的請求湧入數據庫中,造成了瞬間巨大的壓力。舉個栗子,因為小明交了女朋友的原因,他的生活費總是很快用完了;這種情況下,他也必須在 1 號的時候拿到生活費,不然就要吃土了,但是媽媽不允許他們一起取打擾爸爸,媽媽就指定了妹妹去問,在妹妹沒有回來之前,小明只能等着,這就是為了解決緩存擊穿而採用的策略:互斥鎖(mutex key)

運維級別緩存

除了在應用程序中可以接入緩存以外,部分運維工具也集成了緩存服務,比如 Nginx、IIS。

代理緩存

Nginx 就是反向代理緩存,通過配置 Nginx 的緩存功能,在客戶端請求到來到時候去加載緩存內容,用以提高響應能力,IIS 緩存又分為用戶緩存和內核緩存。IIS 的輸出緩存設置中,內核模式緩存不會對驗證等用戶信息進行檢查,就好像小明等爸爸因為太趕時間,把錢放門衛大媽那裡了,結果隨便來了個學生就把小明等生活費給領走了,但是加上用戶模式緩存后,就可以添加對身份的檢查(請求標頭),這樣大媽就會知道誰是小明而不會隨便把生活費交給別人了。

網頁靜態化

這種技術,在 Web1.0 時代非常的流行,我還記得那些個年頭的網站開發項目中的要求,大部分項目的需求分析文檔裏面就明晃晃的寫着:網頁靜態化!,而靜態化常見於各種企業型、論壇帖子,在發表這些信息后就將其生成靜態網頁,客戶端訪問的時候,直接重定向到該靜態網頁,基本無需訪問數據庫。

CDN 緩存

CDN 緩存類似於上面提到的分佈式緩存,但是實際上 CDN 緩存服務目前來說,主要說針對靜態資源的,比如圖片、視頻、文件等等;大家經常可以看到,很多站點都號稱提供了 CDN 加速服務,這些站點就像一個個代辦信用卡的銷售中介,實際上拿的都是銀行的資源。

更形象一些的說法,就是大家的錢都是中國人民銀行發行的,我們可以通過各個不同的銀行(CDN節點)查詢我們的銀行卡餘額(我怎麼可能有餘額),在以前沒有手機銀行的時候,大家就可以到附近的銀行去查詢,然後取款(CDN緩存),如果附近的銀行的櫃員機沒有現金,那麼可能就需要到總行去取了(回源),如果發生了太多回源的事件,就會造成 CDN 的擁堵,所以 CDN 服務商也不敢打包票自己的基礎服務沒有問題,反正我是沒見到哪家 CDN 服務商敢註明服務穩定性 100%,基本上都是 99.99%

結束語

緩存的理論知識,其實是非常宏大的,我這裏只是拋磚引玉,希望能給入門的朋友帶來一點幫助,如果你喜歡這篇文章,請給我點贊,讓更多同學可以看到。

【精選推薦文章】

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

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

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

一文帶你了解爬蟲

六月分享主題:爬蟲
HTTP詳解
網頁結構簡介

前段時間我媽突然問我:兒子,爬蟲是什麼?我當時既驚訝又尷尬,驚訝的是為什麼我媽會對爬蟲好奇?尷尬的是我該怎麼給她解釋呢?

一、爬蟲介紹

1.爬蟲是什麼

網絡爬蟲(web crawler 簡稱爬蟲)就是按照一定規則從互聯網上抓取信息的程序,既然是程序那和正常用戶訪問頁面有何區別?爬蟲與用戶正常訪問信息的區別就在於:用戶是緩慢、少量的獲取信息,而爬蟲是大量的獲取信息。

這裏還需要注意的是:爬蟲並不是Python語言的專利,Java、Js、C、PHP、Shell、Ruby等等語言都可以實現,那為什麼Python爬蟲會這麼火?我覺得相比其他語言做爬蟲Python可能就是各種庫完善點、上手簡單大家都在用,社區自然活躍,而社區活躍促成Python爬蟲慢慢變成熟,成熟又促使更多用戶來使用,如此良性循環,所以Python爬蟲相比其他語言的爬蟲才更火。

下面是一段hello world級別的Python爬蟲,它等效於你在百度搜索關鍵字:python

2.爬蟲案例

既然爬蟲是大量抓取網頁,那是不是爬蟲都是不好的呢?答案當然不是,可以說我們的日常上網已經離不開爬蟲了,為什麼這麼說?下面我就為大家盤點幾個爬蟲日常應用:

  1. 搜索引擎:如Google、百度、雅虎、搜狗、必應等等很多搜索引擎其本質就是一個(可能多個)巨大爬蟲,這些搜索引擎工作原理是:頁面收錄->頁面分析->頁面排序->響應關鍵字查詢,也就是說它會先把互聯網上很多頁面保存到服務器,然後分析網頁內容建立關鍵字索引,最後用戶輸入關鍵字的時候去查詢內容,然後根據相關性排序(百度害人的競價排名毫無相關性可言),第一步的頁面收錄就是爬蟲,百度查看一個網站有多少網頁被收錄方法,百度輸入:site:你想查詢的網站,如:site:blog.csdn.net。
  2. 搶票軟件:很多人在吐槽12306卡,可你不知道12306幾乎每天都相當於淘寶雙11的流量,這誰受得了。為什麼每天都是如此高的流量?答案自然是爬蟲了,為什麼搶票軟件可以搶票?因為它在不斷的刷新和監控是否有餘票,大大小小這麼多搶票app,訪問量之高可想而知。之前很多公司都出過搶票插件,如:百度、360、金山、搜狗等等,後來都被鐵道部約談下線了,而現在又流行搶票app,為什麼搶票app可以,插件就不可以?可能是因為管理和可控性的原因。
  3. 惠惠購物助手:這是一款能進行多個網站比價並能知道最低價的一個網站,其工作原理也是通過大量爬蟲爬取商品價格然後存儲,這樣就可以製作一個價格走勢圖,幫助你了解商品最低價。

二、爬蟲的價值

從上面舉的幾個例子來看,爬蟲對整個互聯網的價值真的無法估量,那對於小我而言,爬蟲能給我們帶來哪些價值?

1.隱形的翅膀

如果你問我學完Python基礎之後該學習什麼技能?我會毫不猶疑的說爬蟲,為什麼是爬蟲?

  1. 爬蟲相對其他技能簡單易學,且效果立即可見,會有一定的成就感
  2. 爬蟲可以說是其他技能的基石,因為他是數據的來源,現在這個時代誰有數據誰才能稱王,所以會爬蟲絕對會讓你如虎添翼
  3. 在國內,很多企業巴不得你啥都會,所以在應聘時,爬蟲會是一個不錯的加分項

2.看不見的商戰

職場對話:

老闆:小明給你個重要任務。
小明:就算996我也在所不辭(第一次接到老闆的直接需求)!
老闆:你能不能搞到競爭對手的商品價格?
小明:沒問題(牛逼先吹出去),小事!
老闆:這可不是小事,只要你能持續獲取競品價格,我們就可以標價低於他們一點點,持續如此大家就知道我們的價格肯定是比他們低,這樣大家就直接來我們這裏購買商品了,到時候慶功宴上你就是最大功臣(先畫個餅)。
小明:老闆牛逼,老闆英明!

3.會爬蟲就可以創業

工作之後很多同學會利用上班空餘時間,來做自己的東西或者項目,別看開始是一個小打小鬧的東西,慢慢豐富將來也許會成為一款成熟的產品。

而爬蟲可以讓你很輕鬆的實現自己的產品,做的好的話就可以創業。這裏豬哥給大家列幾個簡單的創業項目,當然作為一個思維引導。

如果你想做一款好的產品,你需要從用戶需求出發去思考,做解決目前存在的問題的產品或者目前還沒有的服務,也許你的產品就是下一個頭條。

三、盜亦有道

爬蟲既然如此強大而優秀,那是不是會了爬蟲就可以為所欲為?

延伸話題:其實我內心一直有一個疑問:為什麼互聯網公司都偏愛用動植物來命名或作為logo?如:螞蟻金服、天貓、菜鳥、金東狗、騰訊企鵝,百度的熊掌、搜狗、途牛、美團的袋鼠。。。真的是太多了,難道僅僅是因為好記?我認為好記是一個原因,其根本原因是受到編程行業的影響,你想想編程行業有多少動植物:Java(咖啡)、Python(蟒蛇)、Go(囊地鼠)、PHP(大象)、Linux(企鵝)、Perl(駱駝)、Mysql(海豚)等等,具體為什麼編程行業喜歡用動植物這豬哥就不清楚,還請明白的同學留言告知大家!

講上面的延伸話題想表達的是,大自然世間萬物,相生相剋,衡而不禍!而爬蟲也是如此,下面就為大家介紹一下約束爬蟲的幾個點。

1.robots協議

做過網站的同學也許知道,我們在建站的時候需要在網站根目錄下放一個文件:robots.txt,這個文件是干什麼用的呢?

Robots協議,也稱為爬蟲協議、機器人協議等,其全稱為“網絡爬蟲排除標準(Robots Exclusion Protocol)”。網站通過Robots協議告訴搜索引擎哪些頁面可以抓取,哪些頁面不能抓取。

每個網站的根目錄下都必須放置robots.txt文件,否則搜索引擎將不收錄網站任何網頁。

下面我們以百度為例子,來看看百度的robots.txt文件:

我們在百度robots協議的最下面,有這樣一條:

User-agent: *
Disallow: /

這表示除上面那些定義過的爬蟲以外其他爬蟲都不允許爬取百度任何東西!

2.法律

我們都知道在發起請求時User-agent是可以自定義的,也就是說我們可以繞過robots協議和以User-agent來定義的反爬蟲技術,所以robots協議可能更像一個君子協議,那咱們國家有沒有法律明文規定呢?我們來了解一下非法侵入計算機信息系統罪

第二百八十五條 非法侵入計算機信息系統罪:違反國家規定,侵入前款規定以外的計算機信息系統或者採用其他技術手段,獲取該計算機信息系統中存儲、處理或者傳輸的數據,或者對該計算機信息系統實施非法控制,情節嚴重的,處三年以下有期徒刑或者拘役,並處或者單處罰金;情節特別嚴重的,處三年以上七年以下有期徒刑,並處罰金。

我們可以看到重點信息:入侵計算機獲取數據是違法的,也就是說爬蟲技術本身是無罪的,因為它是獲取的公開信息,並未非法入侵計算機。但是如果你用爬取到的數據去從事商業化操作,那也許就構成了違法犯罪行為!

給大家介紹下因爬蟲而被判刑的案件:

  1. 頭條爬蟲官司:https://dwz.cn/qBBHc2fq
  2. 酷米客公司官司:https://dwz.cn/9IpAOHGB
  3. 360和百度爬蟲官司:https://dwz.cn/RGRa3HJc

我發現這幾起案件的通性有兩點:1、公司性質。2、競爭對手。3、讓人家找到線索。

最後提醒大家:技術人,要守住自己的底線,違反國家法律法規的事情咱們堅決不能做!

3.反爬蟲工程師

本來想採訪一位攜程反爬蟲工程師,但是他說由於工作保密原因不便接受採訪,所以只好尊重他的選擇!

四、爬蟲現狀

之前豬哥說過互聯網中有超過50%的流量來自爬蟲,那我們就來簡單聊聊目前爬蟲的現狀吧!

1.技術

反爬蟲幾乎和爬蟲同時誕生,它們是相愛相殺的連個技術,如果沒有爬蟲就沒有反扒,而反扒技術又反過來又可以促進爬蟲技術的發展。

  1. 交互問題:各種變態的驗證碼充斥,尤其是12306,分分鐘都想爆粗口,以後會越來越變態。。。
  2. js加密:最近很流行的一種反扒技術,會爬蟲還得先學js,然後反爬蟲工程師在js裏面各種投毒,殺人誅心。。。
  3. IP限制:限制單個ip某段時間內訪問次數

豬哥只介紹了一個反扒技術,當然都有現成的技術解決,但是作為爬蟲員最重的並不是會使用工具或框架去應對反扒,而是能通過自己的思考和摸索破解反扒,因為反扒技術更新快且多樣化。

2.就業

就業的話主要從招聘、薪資、就業形勢三個方面分析,我從網上查閱了一些資料,然後整理以圖片形式提供給大家參考。

數據來源:職友集:https://dwz.cn/6PeU46QY

3.前景

現在很多人並不看好爬蟲的前景,只搞爬蟲的話技術只停留在當前水平,不再學習新知識,不再進步的話,那麼是沒有前途的,總有一天會被時代淘汰,其實其他職位也只如此。

每個職業都是有一個橫向和縱向的發展,也就是所謂的廣度和深度的意思。第一、如果專研得夠深,你的爬蟲功能很強大,性能很高,擴展性很好等等,那麼還是很有前途的。第二、爬蟲作為數據的來源,後面還有很多方向可以發展,比如可以往大數據分析、數據展示、機器學習等方面發展,前途不可限量,現在作為大數據時代,你佔據在數據的的入口,還怕找不到發展方向?所以爬蟲也許只是一個起點一個跳板,是你走向人生巔峰的一個基石,總有一天你會迎娶白富美!

五、總結

本期為大家講解什麼是爬蟲、爬蟲的價值、爬蟲的合法性以及爬蟲的現狀,下期豬哥將採訪一位牛逼的爬蟲工程師,讓他為大家講講實際工作中的一些知識以及作為一名爬蟲工程師該掌握和學習哪些技能,讓大家能更近距離接觸爬蟲,如果你有想對他提問的問題,可以在下方留言哦,我會抽取一些問題讓他為大家解答,問題不限

【精選推薦文章】

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

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

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

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"