特斯拉經典跑車 Roadster 推升級版 續航力達 644 公里

  美國豪華電動車商特斯拉 (Tesla) 的經典跑車「Roadster」,將推出強悍升級版!「 Roadster 3.0」充電一次可行駛 400 英哩 ( 644 公里),CNET 稱,續航力擊敗市面上所有電動車。   特斯拉執行長馬斯克 (Elon Musk) 26 日在推特宣布,新款 Roadster 可從洛杉磯一路開到舊金山,里程數將近 400 英哩,續航力無人能及,舊款 Roadster 僅能行駛 244 英哩,特斯拉熱銷的 Model S 也只有 265 英哩。   特斯拉表示,續航里程大增主要因為電池效能增加 31%,由 53kWh 增至 70kWh;新空氣力學設計減少汽車 15% 阻力;升級版的輪胎和軸承也降低 20% 的滾動阻力。   特斯拉 26 日股價漲 2.50%,收在 227.82 美元,但仍與 9 月 4 日歷史收盤新高的 286.04 美元相比,重挫了20%。     (圖片為特斯拉 Roadster 2.5,來源:)

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

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

北京2014年度剩餘新能源車指標將不再併入明年

12月25日,北京本年度最後一次小客車購車搖號舉行,227萬申請者爭奪普通小客車指標,基礎中簽比達到151∶1。新能源車指標綽綽有餘,無需搖號。

經市公安交通管理局審核確認,2014年4月26日中簽過期未用個人普通小客車配置指標734個,按規定納入本期個人普通小客車指標配置,因此本期將隨機搖出個人普通小客車指標19804個;搖出單位普通小客車指標1300個。2014年4月26日中簽過期未用個人示範應用新能源小客車配置指標1618個,2014年第5期未配置的個人示範應用新能源小客車指標869個,按規定納入本期個人范應用新能源小客車指標配置,因此本期將配置個人示範應用新能源小客車指標4157個;2014年第5期未配置的單位示範應用新能源小客車指標2646個,按規定納入本期單位示範應用新能源小客車指標配置,因此本期將配置單位示範應用新能源小客車指標4316個。因本期個人和單位示範應用新能源小客車指標申請數均小於本期指標配額,無需搖號,直接配置。

不過按照規定,本期剩餘的新能源車指標將不再併入下一年。

根據北京市去年底發佈的《北京市2013—2017年機動車排放污染控制工作方案》任務分解表,2015至2017年,每年共將配置機動車指標15萬個,但普通小客車指標將逐年縮水。2014年普通小客車指標13萬個、示範應用新能源小客車指標2萬個,2015年普通小客車指標將縮1萬個至12萬個,新能源車指標將增加1萬個至3萬個,新能源車的中簽幾率將進一步增加。2016年和2017年,普通車指標和新能源車指標將分別調整為9萬個和6萬個。

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

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

Day12-微信小程序實戰-交友小程序-優化“附近的人”頁面與serach組件的布局和樣式以及搜索歷史記錄和本地緩存*內附代碼)

回顧/:我們已經實現了显示附近的人的功能了,可以多個人看到附近的人頁面了

但是還是要進行優化有幾個問題:1、我們用戶選擇了其他的自定義頭像之後,在首頁可以看到頭像的變化,但是在附近的人中頭像會變成報錯的樣式:如:

 

 

 也就是500了,也就是找不到這個圖片了,解決方法:看開發文檔-》雲開發

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

其中有一個 “換取臨時鏈接”的功能(通過這個方法可以臨時的拿到一個圖片的路徑了),然後這個路徑就可以對應到我們的iconpath中了,有直接看demo

wx.cloud.getTempFileURL({
  fileList: ['cloud://xxx.png'],
  success: res => {
    // fileList 是一個有如下結構的對象數組
    // [{
    //    fileID: 'cloud://xxx.png', // 文件 ID
    //    tempFileURL: '', // 臨時文件網絡鏈接
    //    maxAge: 120 * 60 * 1000, // 有效期
    // }]
    console.log(res.fileList)
  },
  fail: console.error
})

我們剛剛換了頭像的測試號,可以看到在數據庫中

 

 

 

 正常的試https這樣的,但是我們修改了之後,它的路徑變成了我們設置的默認的,cloud開始的了

所以我們就可以直接在near.js裏面用for來判斷每個字段符不符合條件即可了,一旦找到了這個cloud開頭的路徑的話,也就是if裏面進行的東西

我們就要換取臨時的路徑即可了,如果else的話,我們還是和之前一樣的,直接push進去即可了

if裏面的話直接copy文檔裏面的demo即可了

我們通過

console.log(res.fileList) 打印出來的東西試一個數組: 

 

 裏面的那個tempFileURL就是一個臨時的路徑了

 getNearUsers(){
    db.collection('users').where({
      location: _.geoNear({
        geometry: db.Geo.Point(this.data.longitude, this.data.latitude),
        minDistance: 0,
        maxDistance: 5000
        //這1000和5000的單位是米
      }),
      islocation : true
    }).field({
      longitude : true,
      latitude : true ,
      userPhoto : true
    }).get().then((res)=>{
      console.log(res.data);
      let data = res.data;
      let result = [];
      if(data.length){

        for(let i=0;i<data.length;i++){
          if(data[i].userPhoto.includes('cloud://')){
            wx.cloud.getTempFileURL({
              fileList: [data[i].userPhoto ],
              success: res => {
                // console.log(res.fileList[0].tempFileURL)
                result.push({
                  // 然後就是把我們獲取到的臨時路徑直接賦值給iconpath即可了
                  iconPath: res.fileList[0].tempFileURL,
                  id: data[i]._id,
                  latitude: data[i].latitude,
                  longitude: data[i].longitude,
                  width: 30,
                  height: 30
                });
                
              }
            })
          }
          else{
            result.push({
              iconPath: data[i].userPhoto,
              id: data[i]._id,
              latitude: data[i].latitude,
              longitude: data[i].longitude,
              width: 30,
              height: 30
            });
          }
        
        }
        this.setData({
          markers : result
        });
      }
    });
  }

如果只是這個代碼的話,會發現我們測試賬號的如何信息都無法渲染出來,這個是因為js是異步操作的,我們要在if之後立馬就進行 setdata操作即可了

如何在全部for結束之後也再次的進行setdata操作即可了,完整代碼就是

getNearUsers(){
    db.collection('users').where({
      location: _.geoNear({
        geometry: db.Geo.Point(this.data.longitude, this.data.latitude),
        minDistance: 0,
        maxDistance: 5000
        //這1000和5000的單位是米
      }),
      islocation : true
    }).field({
      longitude : true,
      latitude : true ,
      userPhoto : true
    }).get().then((res)=>{
      console.log(res.data);
      let data = res.data;
      let result = [];
      if(data.length){

        for(let i=0;i<data.length;i++){
          if(data[i].userPhoto.includes('cloud://')){
            wx.cloud.getTempFileURL({
              fileList: [data[i].userPhoto ],
              success: res => {
                // console.log(res.fileList[0].tempFileURL)
                result.push({
                  // 然後就是把我們獲取到的臨時路徑直接賦值給iconpath即可了
                  iconPath: res.fileList[0].tempFileURL,
                  id: data[i]._id,
                  latitude: data[i].latitude,
                  longitude: data[i].longitude,
                  width: 30,
                  height: 30
                });
                this.setData({
                  markers: result
                });
              }
            })
          }
          else{
            result.push({
              iconPath: data[i].userPhoto,
              id: data[i]._id,
              latitude: data[i].latitude,
              longitude: data[i].longitude,
              width: 30,
              height: 30
            });
          }
        
        }
        this.setData({
          markers : result
        });
      }
    });
  }

 

 

 得到的效果就是,可以看到另外一個用戶剛剛它換的頭像了

(後面的優化就是可以點擊這個用戶的頭像之後我們就可以跳轉到它的詳情頁面了

這個功能在實現起來其實頁不複雜的,有一個和markers對應的事件,也就是點擊了這個markers就會觸發這個事件了  

 

通過這個事件其實我們是可以拿到id值得

 

 markertap(ev){
    console.log(ev);
  }

通過在near.js裏面得這個函數,然後我們點擊一下地圖裡面的marker圖片之後,我們得到的值就是:

 

這個markerID其實對應的就是用戶的id值了

  markertap(ev){
    // console.log(ev);
    wx.navigateTo({
      url: '/pages/detail/detail?userId=' + ev.markerId
    })
  }

通過這個代碼其實就可以實現,點擊地圖裡面的圖標的話我們就可以跳轉到這個用戶的詳情頁面去了

3、後面要測試的就是假如測試賬號關閉了共享位置的話

通過測試我們發現,測試號關閉了共享位置的話,在地圖裡面即使是刷新了還是會看到這個用戶的頭像的

 (其實代碼是沒有錯的,把項目關了再重啟之後會看到這個關閉了共享位置的用戶頭像就消失了

(其實還有其他可以優化的,就是可以在地圖的頭像上面加一段語音介紹自己等等的,因為小程序其實也是支持的,或者是可以計算我和你的距離

或者是我去你那邊的話我過去的導航和路線是怎麼樣的

 

二、search組件的布局和樣式

(就是在主頁的上面添加一個查找的框)

1、實現新建一個叫search的組件

 

 創立好了之後,就可以在首頁進行引用了

2、先在index.JSON文件裏面引入這個組件

{
  "usingComponents": {
    "search" : "/components/search/search"
  }
}

3、在主頁裏面和用標籤一樣引用就可以了

可以直接在index.wxml中通過 <search /> 來使用即可了

 

該search組件就被引入了

通過基本的結構wxml

<!--components/search/search.wxml-->
<view class="container">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" />
    </view>
    <view class="search-cancel">取消</view>
  </view>
</view>

得到的效果:

 

會發現我們放大鏡圖標沒有显示出來,所以我們要配置一下,讓這個圖標可以穿透出來即可了

也就是之前copyText.js寫過的

  options: {
    styleIsolation: 'apply-shared'
  },

就是為了讓這個圖標可以生效的

 

 

 這樣的話,我們的放大鏡就進來了

之後就可以對search.wxss的樣式進行設計了

 

/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 70rpx;z-index: 999;}
.search{ display: flex ; align-items: center;}
.search-text{ display: flex; align-items: center;flex: 1;} 

 

但是發現,圖片和這個組件融合在一起了

 

 這是因為因為是組件的引入的話,就不像在主頁面一樣,可以佔位置的,所以就要到index.wxss設置一下讓index騰出一個空間來放這個搜索框的

通過在

 

 就是直接通過margin來騰出位置即可了

 

 上面其實是在index.wxss中給上面的騰出來100rpx的空間

/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 70rpx;z-index: 999;}
.search{ display: flex ; align-items: center; margin:20rpx;}
.search-text{ display: flex; align-items: center;flex: 1;border: 1px #cdcdcd solid;border-radius:10rpx; height: 65rpx} 
.search-text .iconsousuo{margin: 0 10rpx;}
.search-cancel{margin: 0 10rpx;}

得到的效果就是:

 

 但是有一個問題就是:我們在還沒點擊搜索的時候,其實不用显示後面的“取消”按鈕的,這個的話就要通過js邏輯來實現了

定義了一個isfocus來表示光標有沒有显示的(這個取消的按鈕其實是在我們獲取了光標之後才會有的)

通過在取消按鈕加上了一個wx:if判斷之後,得到的效果就是:

 

 並且當我們獲取到了光標之後,這個搜索框會適應整個頁面的高度了

 給contaner加上了  overflow: hidden; 之後得到的效果就是這個搜索框的下邊框“不見了”

 

 這個是因為,我們得container這個大得塊要比我們輸入框得高度要小了,這個時候就可以在wxss裏面通過調節container得height

 

 即可了

因為如果我們點擊了那個輸入框得胡,也就是聚焦了得話,我們得上面得搜索框的大容器显示的樣式是和沒聚焦的時候显示的不同的,所以我們就可以用三目運算符來通過這個isfocus來決定使用哪個容器,也就是說我們可以定義兩個樣式不同的容器了

<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
.containerFocus{position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: 999;
background: #ccc}

然後我們自行的吧js文件裏面定義的isFocus變量 定義weighted是true來看看我們獲取光標之後的效果是怎麼樣的:

 

之後我們就要通過邏輯里控制他們的聚焦切換不同的container了,如果是已經點擊聚焦的了話,還有一個就是可以看到我們搜索的歷史記錄,還有列表等等

 

通過:

<view class="search-history">
    <text>歷史記錄</text>
    <text class="iconfont iconshanchu"></text>
  </view>
.search-history{ display: flex;justify-content: space-between;margin:20rpx;}

效果:

 

 然後就是要搞一個搜索池了:

  <view class="search-history-btn">
    <text>小明</text>
    <text>123213</text>
    <text>dsadasd</text>
  </view>
.search-history{ display: flex;justify-content: space-between;margin:20rpx;}
.search-history-btn text{ border: 1px #cdcdcd solid; padding: 10rpx 20rpx;background: white;
border-radius: 20rpx; margin:10rpx;}

效果:(注意上面是給每一個搜索的text進行樣式的定義

上面就吧搜索的關鍵詞的布局搞好了,下面就是要對搜索的列表進行定義了(其實這個搜索的列表和我們好友的列表是很像的,可以直接直接copy 在friendList.wxml裏面的這個結構了

  <navigator wx:for="{{ friendList }}" wx:key="{{ index }}" url="{{ '../detail/detail?userId=' + item._id}}" open-type="navigate">
      <view class="friendList-item">
        <view>
         <image src="{{ item.userPhoto }}" />
         <text> {{ item.nickName }} </text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

然後對  searchList-item 的樣式也是直接拷貝friendList的wxss

.friendList-item{
  /* 這裏可以直接把user.wxss中的樣式複印過來了 */
  height: 120rpx;border-bottom:1px #b4b5b6 dashed;
padding: 10rpx; display: flex;align-items: center;justify-content: space-between;
}
.friendList-item view{display : flex; align-items: center;}
.friendList-item image{width: 100rpx;height: 100rpx;border-radius: 50%;}

 

綜上所述,我們的代碼就是:

CSS

<!--components/search/search.wxml-->
<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" />
    </view>
    <view wx:if="{{ isFocus }}" class="search-cancel">取消</view>
  </view>

  <view class="search-history">
    <text>歷史記錄</text>
    <text class="iconfont iconshanchu"></text>
  </view>
  <view class="search-history-btn">
    <text>小明</text>
    <text>123213</text>
    <text>dsadasd</text>
  </view>

    <navigator url="" open-type="navigate">
      <view class="searchList-item">
        <view>
         <image src="" />
         <text>小喵喵</text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

</view>

html

然後還要在search.js裏面通過

options: { styleIsolation: ‘apply-shared’ } 引入外部樣式 效果圖:(選中搜索框時)

 

(未選中搜索框時

 

 

 三、實現搜索歷史記錄及本地緩存

1、我們先在searc.wxml的輸入框標籤加一個處理點擊這個輸入框的一個點擊事件

bindfocus=”handleFocus”

 還有我們在取消的標籤中,也要加一個點擊事件,點擊了的話就吧isFocus變成是false即可了

 <input type="text" placeholder="搜索喵星人" bindfocus="handleFocus" />


<view wx:if="{{ isFocus }}" class="search-cancel" bindtap="handleCancel">取消</view>
 methods: {
    handleFocus(){
     this.setData({
       isFocus : true
     }); 
    },
    handleCancel(){
      this.setData({
        isFocus: false
      }); 
    }
  }

得到的效果就是:點擊輸入框,就跳轉到輸入,點擊取消,就跳轉到首頁

還有一個小bug就是,因為輸入框的話,會默認只有在一個範圍以內,才可以輸入的,所以我們就可以讓這個輸入框適應整個範圍,可以在

給 search.wxss中添加一個代碼:

.search-text input {flex: 1;}

就讓這個輸入框可以自動的填滿整個的搜索框了

3、之後就是對輸入的東西進行處理了,可以是邊輸入邊搜索,也可以是輸入之後回車了才進行搜索,如果是邊輸入就邊搜索的話,我們可以通過bindinput來進行監聽的,那如果要是按回車的時候搜索怎麼辦呢—這個其實小程序幫我們搞好了

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

就可以通過在input中加上 bindconfirm 這個屬性來完成的,我們就定義了一個 handleConfirm 這個方法是只有我們回車了才會進行觸發的

 

 

 在手機端裏面的回車 其實默認的是 “完成”兩個字的(就是點擊這個輸入框的時候,手機就會彈出軟鍵盤了,它的確定按鈕是“搜索”兩個字的,那這個該怎麼樣去修改呢==微信也提供了

 

 默認的是我們的 done 也就是完成

所以就在input標籤中,吧confirm-type 屬性變成是 search 即可了,(這樣的話在手機的軟鍵盤就會显示 搜索 兩個字了)

(下面我們要做的就是 吧這個搜索的 放在歷史裏面管理起來了)

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

demo:

wx.setStorage({
  key:"key",
  data:"value"
})

設置的話,就是我們用戶點擊回車 之後,就可以吧這個搜索裏面的 ev.detail.value放到本地存儲裏面即可了

因為這個setStorage的話,我們要讓這個data是一個數組才行的,然後我們先通過

data : [111]看看能不能吧這個111存放到這個數組裡面

 

 可以在下面的調試板中 找到Storage 讓我們查看一下

可以看到,我們隨便輸入一點東西,然後按 回車 之後可以看到

先在search.js的data裏面定義一個 數組

然後我們就可以在wxml中,吧我們的歷史消息text,用一個數組來for出來了

 <view class="search-history-btn">
    <text wx:for="{{ historyList }}" wx:key="{{ index }}">{{ item }}</text>
  </view>

 

然後我們在一開始 聚焦了之後,就立馬從storage裏面吧數組拿出來,用getStorage方法:

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

wx.getStorage({
  key: 'key',
  success (res) {
    console.log(res.data)
  }
})

 

使用上面的demo之後,會報錯,這個報錯一般都是因為success的回調的時候要用箭頭函數才行的

   wx.getStorage({
        key: 'searchHistory',
        success:(res)=> {
          this.setData({
            historyList: res.data
          });
        }
      })

修改了之後,我們點擊 聚焦 之後

這個 111 就是我們剛剛寫入到 searchStorage 數組裡面的

 

 (這個有一個小bug,就是,假如我們輸入了兩次相同的搜索,然後存入到歷史記錄再打印出來的話,會有兩個的,我們不應該有兩個相同的歷史記錄的

 但是我們搜索重複詞的話,我們也是显示一次,然後把這個搜索的提升到最前面去),表示最近搜索,並且歷史記錄也要有一個數量的,不能把在一年之間的全部搜索記錄都显示出來的

這個去重的功能:1、實現克隆一份數組

 (unshift的話就是往數組的頭添加東西的,ES6本身就帶有一個set來完成去重功能的)

   handleConfirm(ev){
      // console.log(ev.detail.value);
      let cloneHistoryList = [...this.data.historyList];
      cloneHistoryList.unshift(ev.detail.value);
      wx.setStorage({
        key: "searchHistory",
        data: [...new Set(cloneHistoryList)]
      })
    }

我們的效果就達到了,重複輸入的話,會被提前,=

然後下面我們就要實現 歷史記錄的刪除功能了

就可以直接在這個刪除圖標的wxml中添加一個 bindtap點擊事件  handleDelete 即可了(這個刪除的話,是刪除掉全部的歷史記錄的)

(微信給我們提供的對storage的操作中,remove是操作某一項的,而clear是刪除掉所有的

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

wx.clearStorage()

直接這樣寫即可了

====**但是這樣可能如果我們後面在storage裏面也定義了其他的東西,這個語句的話會把其他緩存也會清理掉的,所以我們這裏還是使用remove好點的

wx.removeStorage({
  key: 'key',
  success (res) {
    console.log(res)
  }
})

因為我們也是要在這個成功的回到中,把這個歷史數據數組設置為空數組,所以我們就要使用成功返回的箭頭函數才行的

即可實現刪除功能了,

效果就是:

 

 

 之後再次輸入1的時候,

 

 然後就是清空 歷史記錄:

 

 

 

 

下面是這個部分的代碼

//components/search/search.js
Component({
  /**
   * 組件的屬性列表
   */
  options: {
    styleIsolation: 'apply-shared'
  },
  properties: {

  },

  /**
   * 組件的初始數據
   */
  data: {
    isFocus : false,
    historyList : []
  },

  /**
   * 組件的方法列表
   */
  methods: {

    handleFocus(){

      wx.getStorage({
        key: 'searchHistory',
        success:(res)=> {
          this.setData({
            historyList: res.data
          });
        }
      })

     this.setData({
       isFocus : true
     }); 
    },
    handleCancel(){
      this.setData({
        isFocus: false
      }); 
    },
    handleConfirm(ev){
      // console.log(ev.detail.value);
      let cloneHistoryList = [...this.data.historyList];
      cloneHistoryList.unshift(ev.detail.value);
      wx.setStorage({
        key: "searchHistory",
        data: [...new Set(cloneHistoryList)]
      })
    },
    handleHistoryDelete(){
      wx.removeStorage({
        key: 'searchHistory',
        success:(res)=>{
          this.setData({
            historyList : []
          });

        }
      })
    }
  }
})
<!--components/search/search.wxml-->
<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" bindfocus="handleFocus" bindconfirm="handleConfirm" confirm-type="search"/>
    </view>
    <view wx:if="{{ isFocus }}" class="search-cancel" bindtap="handleCancel">取消</view>
  </view>

  <view class="search-history">
    <text>歷史記錄</text>
    <text bindtap="handleHistoryDelete" class="iconfont iconshanchu"></text>
  </view>
  <view class="search-history-btn">
    <text wx:for="{{ historyList }}" wx:key="{{ index }}">{{ item }}</text>
  </view>

    <navigator url="" open-type="navigate">
      <view class="searchList-item">
        <view>
         <image src="" />
         <text>小喵喵</text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

</view>
/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 90rpx;z-index: 999;overflow: hidden;}
.containerFocus{position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: 999;
background: #ccc}
.search{ display: flex ; align-items: center; margin:20rpx;}
.search-text{ display: flex; align-items: center;flex: 1;border: 1px #cdcdcd solid;border-radius:10rpx; height: 65rpx; background: white;} 
.search-text input {flex: 1;}
.search-text .iconsousuo{margin: 0 10rpx;}
.search-cancel{margin: 0 10rpx;}

.search-history{ display: flex;justify-content: space-between;margin:20rpx;margin-bottom: 30rpx;}
.search-history-btn{ margin-bottom: 30rpx; }
.search-history-btn text{ border: 1px #cdcdcd solid; padding: 10rpx 20rpx;background: white;
border-radius: 20rpx; margin:10rpx;}


.searchList-item{
  /* 這裏可以直接把user.wxss中的樣式複印過來了 */
  height: 120rpx;border-bottom:1px #b4b5b6 dashed;
padding: 10rpx; display: flex;align-items: center;justify-content: space-between;
}
.searchList-item view{display : flex; align-items: center;}
.searchList-item image{width: 100rpx;height: 100rpx;border-radius: 50%;}

 

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

【其他文章推薦】

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

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

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

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

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

※教你寫出一流的銷售文案?

Python元類實戰,通過元類實現數據庫ORM框架

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是Python專題的第19篇文章,我們一起來用元類實現一個簡易的ORM數據庫框架。

本文主要是受到了廖雪峰老師Python3入門教程的啟發,不過廖老師的博客有些精簡,一些小白可能看起來比較吃力。我在他的基礎上做了一些補充和註釋,盡量寫得淺顯一些。

ORM框架是什麼

如果是沒有做過後端的小夥伴上來估計會有點蒙,這個ORM框架究竟是什麼?ORM框架是後端工程師常用的一個框架,它的英文全稱是Object Relational Mapping,即對象-關係映射框架。顧名思義就是把關係轉化成對象的框架,關係這個詞我們在哪裡用的最多呢?

顯然應該是數據庫。之前我們在分佈式的文章介紹關係型數據庫和非關係型數據庫的時候就着重介紹過關係的含義。我們常用的MySQL就是經典的關係型數據庫,它存儲的形式是表,但是表承載的數據其實是兩個實體之間的”關係”。比如學生上課這個場景,學生和課程是兩個主體(entity),我們要記錄的是這兩個主體之間的關係,也就是學生上課這件事。

而ORM框架做的事情是將這些關係映射成類,這樣我們可以將這張表當中增刪改查的功能抽象成類當中的方法。這樣我們就可以通過調用類的方式來操作數據庫了,從而達到高度抽象業務邏輯、降低用戶使用難度的目的。

比如Java後端工程師常用的hibernate和ibatis都是用來做這件事情的,明確了框架的功能之後,我們先來設想一下最後的成果。假設我們現在開發出來了這麼一套框架,那麼它用起來的感覺應該是怎樣的?

我們來看下廖老師博客里給的例子:

class User(Model):
    # 定義類的屬性到列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

User類代表了數據庫當中的一張表,它有4個字段:id, name, email和password,我們在定義字段的同時也通過類別指定了它們的類型。這個應該不難理解,上面的這個類等價於我們在數據庫當中執行了這麼一段建表的SQL:

create table if not exists user (
 id int,
    name string,
    email string,
    password string
)

我們定義了表字段之後,接下來要做的就是根據字段創建數據了,其實也就是根據類創建實例。我們希望User類型的實例就對應User表當中的一條記錄,並且我們可以通過調用實例當中的方法,來操作這張表進行增刪改查。

# 創建一個實例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到數據庫:
u.save()

那麼,我們怎樣可以實現這樣的功能呢?

功能實現

我們先從簡單的功能開始實現,首先是Field類,Field類表示數據庫表當中一個字段的類型。這裏的邏輯很容易理清楚,我們需要定義多種類型,比如IntegerField和StringField。我們可以對這些field類抽象出一個父類來:

class Field(object):
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type
        
    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)

__str__方法當中打印出來的兩個字段,分別是類別的名稱和字段的名稱,這段代碼應該不難理解。

接着,我們實現它的兩個子類,分別是IntegerField和StringField:

class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')
        
        
class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')

這裏也不難理解,只是一個簡單的繼承應用而已。

接下來就到了最關鍵的部分,也就是Model類的實現。我們先來分析一下我們希望Model這個類擁有的功能,由於它是我們定義出來的每一張表的父類,所以它應該能夠獲取子類當中的字段,並且將它存放在一個容器當中。由於我們需要存儲的是字段名和類型的映射,所以將它存儲在dict當中比較合理。

另外一個功能是我們希望它能夠提供增刪改查的接口,能夠根據子類當中定義的字段自動生成相應的SQL語句去調用數據庫。這個也是ORM框架的意義所在。

第二個功能容易實現,只要第一個功能搞定了,做一下字符串處理即可。但是第一個功能有些麻煩,它也是元類的意義所在。因為父類當中的方法是無法獲取子類中定義的類屬性的,只能通過元類,在構建類的時候可以拿到屬性的信息。

所以我們已經很明確了,我們實現元類的目的就是為了實現這個功能。理清楚了之後,再來寫代碼就不難了。我們先來實現這個元類:

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        # 創建model類的時候不做任何處理
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        # 打印表名的信息
        print('Found model: %s' % name)
        # mappings用來存儲字段的信息
        mappings = dict()
        for k, v in attrs.items():
            # 判斷v的類型,只有是Field的子類才會存儲起來
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        # 將mappings當中的數據從類屬性當中移除,防止關鍵字衝突
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 保存屬性和列的映射關係
        attrs['__table__'] = name # 假設表名和類名一致
        return type.__new__(cls, name, bases, attrs)

如果你看過之前的文章,對元類已經很熟悉了,那麼這段代碼對你來說應該不難理解。元類搞定了,剩下的Model就更簡單了。按照規範,我們需要實現增刪改查四個函數,但是這裏我們只是為了展示,所以就只實現其中一個作為例子,其他幾個都可以如法炮製。

class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        # 由於Model的基類是dict,所以創造Model的字段會被解析成dict的構造參數
        # 也就是說字段名和字段值的映射會存儲在dict當中
        super(Model, self).__init__(**kw)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            # fields存儲字段名
            fields.append(v.name)
            # params填充問號
            params.append('?')
            # 獲取字段的值
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

Model當中的save方法不難看懂,但是前面的幾個方法看起來有些多餘。但實際上它們也很重要,這裡有一個關鍵信息是Model類的父類是dict,我們在構建Model的時候傳入的參數會被用來初始化一個dict。所以我們創建數據實例的時候數據的名稱和數據值的映射會被存儲在dict當中,所以我們在save方法當中才會從self的attr當中獲取字段的值。並且我們在初始化User的時候,也必須要填寫每個字段的名稱,原因就在這裏。

最後我們來運行一下:

從結果上來看,我們輸出了User這個類的插入SQL以及它的字段的值。只需要鏈接一下數據庫,我們的這個ORM框架就可以真正投入使用了。

總結

在整個ORM框架實現的過程當中,最重要的是我們對Model這個類創建了元類,但是真正應用的地方卻是在Model的子類。實際上在實際創建User類的時候,解釋器會先搜索User內部是否定義了元類,如果沒有,會上一層去往User的父類也就是Model類搜索元類,如果找到了元類,就會使用元類來創建User。相當於元類被隱形地繼承了下來,但是我們在使用子類的時候卻感知不到。

對於框架的使用者來說,也的確不需要了解框架內部的實現機制,只需要明白使用方法,照着使用就行了。雖然元類的實現和理解很複雜,但是使用起來卻很簡單,這也是它的一個顯著特點。

最後,本文的代碼示例源於廖雪峰老師的博客,向廖雪峰老師致敬。想要查看廖老師博客原文的,請點擊查看原文。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

Flutter開發初探

目前跨端開發比較熱門的就是 React NativeFlutter 了,到底該選哪門技術似乎也快成了大前端圈的一個熱門話題。對於web前端來說,基於web生態的 React Native 應該是一個更加順暢而自然的選擇;但 Flutter 讓人動心的地方就是高性能和 跨端UI一致性。而React Native 發展不太明朗和 Flutter 越發成熟的走勢對比促使我從觀望的心態轉為加入 Flutter

這裏主要就是記錄一下學習Flutter的一些感想和看法:

  • 包管理
  • 布局和樣式
  • json
  • 狀態管理

包管理

pubspec.yaml 文件的作用類似於 npmpackage.json ,而yaml格式也比json方便。但是不能用命令行自動安裝包卻讓習慣了npm的我覺得麻煩。因為Flutter 安裝依賴包是這麼一個流程:

  1. 打開pub.dev網站;
  2. 搜索需要的包,得到包的名稱和版本;
  3. 把包名稱和版本填入pubspec.yaml,最後才開始下載包。

我覺得應該直接命令行安裝包,讓它幫我們下載,名稱版本自動寫入pubspec.yaml。如果沒有指定版本就是默認下載最新版本,因為很多時候我們並不想知道版本號,給我個能用的最新的版本號就ok了。

布局和樣式

就和很多人想的一樣,為什麼不使用 jsx 或者 xml 格式進行布局,因為基於代碼的方式看起來太不直觀了,之所以這樣聽說主要是能更方便的和Dart的hot reload特性配合使用,代碼改動能立刻反映布局變化。但我還是期待有適配轉化 DSL 的框架出現。

Flutter一切都是widget,但是連很多屬性都當成widget 這就讓人有些看不明白了,比如 CenterAlignPadding,為什麼不把常用的樣式屬性都加入到布局組件裏面呢?這導致出現了這麼一種情況:嵌套嚴重,一個很簡單的功能需要層層嵌套才能實現,而且樣式也不能方便的復用。目前比較合理的建議就是適當抽取齣子組件減少嵌套。

Json

Dart 作為強類型的語言,一切皆是對象。Dart要方便操作json就得把json轉化為對象,這就意味着每用到一個json,就需要定義一個對應的類,這也是強類型語言的通病了。這絕對讓人很懷念 js/ts 這種對json操作非常自然順暢的弱類型/函數式語言。當然也不是沒有妥協的解決方案,比較方便的就是 json_model,Flutter實戰作者寫的一個工具庫,步驟也簡單:

  1. 在工程根目錄下創建一個名為 “jsons” 的目錄;
  2. 創建或拷貝Json文件到”jsons” 目錄中 ;
  3. 運行 pub run json_model (Dart VM工程)or flutter packages pub run json_model(Flutter中) 命令生成Dart model類,生成的文件默認在”lib/models”目錄下

狀態管理

Flutter 使用initStatesetState方法設置widget狀態,原理類似React。當然這隻是widget內部控制狀態用的,跨組件通信還是需要其他方案的。官方推薦是使用Provider,使用下來中規中矩吧,當然還可以使用大名鼎鼎的 Redux 以及 mbox。不過Redux本身就以過多的樣板代碼而出名,寫React的時候就不喜歡用,hooks 出來后就果斷就放棄Redux了。hooks才是真香啊,Flutter什麼時候才支持類似的函數式狀態管理方案呢?

總結

說了這麼多,本質就是為什麼 Flutter 不向以 React 為代表的 web 生態看齊?更大的原因是Flutter的很多理念和開發模式其實遠遠落後於 React 。這也是為什麼習慣 react/vue 的 web前端 對於Flutter 感覺很彆扭不順手的原因了。

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

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

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Java是如何實現Future模式的?萬字詳解!

JDK1.8源碼分析項目(中文註釋)Github地址:

https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs

1 Future是什麼?

先舉個例子,我們平時網購買東西,下單後會生成一個訂單號,然後商家會根據這個訂單號發貨,發貨后又有一個快遞單號,然後快遞公司就會根據這個快遞單號將網購東西快遞給我們。在這一過程中,這一系列的單號都是我們收貨的重要憑證。

因此,JDK的Future就類似於我們網購買東西的單號,當我們執行某一耗時的任務時,我們可以另起一個線程異步去執行這個耗時的任務,同時我們可以干點其他事情。當事情幹完后我們再根據future這個”單號”去提取耗時任務的執行結果即可。因此Future也是多線程中的一種應用模式。

擴展: 說起多線程,那麼Future又與Thread有什麼區別呢?最重要的區別就是Thread是沒有返回結果的,而Future模式是有返回結果的。

2 如何使用Future

前面搞明白了什麼是Future,下面我們再來舉個簡單的例子看看如何使用Future。

假如現在我們要打火鍋,首先我們要準備兩樣東西:把水燒開和準備食材。因為燒開水是一個比較漫長的過程(相當於耗時的業務邏輯),因此我們可以一邊燒開水(相當於另起一個線程),一邊準備火鍋食材(主線程),等兩者都準備好了我們就可以開始打火鍋了。

// DaHuoGuo.java

public class DaHuoGuo {
	public static void main(String[] args) throws Exception {
		FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
			@Override
			public String call() throws Exception {
				System.out.println(Thread.currentThread().getName() + ":" + "開始燒開水...");
				// 模擬燒開水耗時
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName() + ":"  + "開水已經燒好了...");
				return "開水";
			}
		});

		Thread thread = new Thread(futureTask);
		thread.start();

		// do other thing
		System.out.println(Thread.currentThread().getName() + ":"  + " 此時開啟了一個線程執行future的邏輯(燒開水),此時我們可以干點別的事情(比如準備火鍋食材)...");
		// 模擬準備火鍋食材耗時
		Thread.sleep(3000);
		System.out.println(Thread.currentThread().getName() + ":"  + "火鍋食材準備好了");
		String shicai = "火鍋食材";

		// 開水已經稍好,我們取得燒好的開水
		String boilWater = futureTask.get();

		System.out.println(Thread.currentThread().getName() + ":"  + boilWater + "和" + shicai + "已經準備好,我們可以開始打火鍋啦");
	}
}

執行結果如下截圖,符合我們的預期:

從以上代碼中可以看到,我們使用Future主要有以下步驟:

  1. 新建一個Callable匿名函數實現類對象,我們的業務邏輯在Callablecall方法中實現,其中Callable的泛型是返回結果類型;
  2. 然後把Callable匿名函數對象作為FutureTask的構造參數傳入,構建一個futureTask對象;
  3. 然後再把futureTask對象作為Thread構造參數傳入並開啟這個線程執行去執行業務邏輯;
  4. 最後我們調用futureTask對象的get方法得到業務邏輯執行結果。

可以看到跟Future使用有關的JDK類主要有FutureTaskCallable兩個,下面主要對FutureTask進行源碼分析。

擴展: 還有一種使用Future的方式是將Callable實現類提交給線程池執行的方式,這裏不再介紹,自行百度即可。

3 FutureTask類結構分析

我們先來看下FutureTask的類結構:

可以看到FutureTask實現了RunnableFuture接口,而RunnableFuture接口又繼承了FutureRunnable接口。因為FutureTask間接實現了Runnable接口,因此可以作為任務被線程Thread執行;此外,最重要的一點就是FutureTask還間接實現了Future接口,因此還可以獲得任務執行的結果。下面我們就來簡單看看這幾個接口的相關api

// Runnable.java

@FunctionalInterface
public interface Runnable {
    // 執行線程任務
    public abstract void run();
}

Runnable沒啥好說的,相信大家都已經很熟悉了。

// Future.java

public interface Future<V> {
    /**
     * 嘗試取消線程任務的執行,分為以下幾種情況:
     * 1)如果線程任務已經完成或已經被取消或其他原因不能被取消,此時會失敗並返回false;
     * 2)如果任務還未開始執行,此時執行cancel方法,那麼任務將被取消執行,此時返回true;TODO 此時對應任務狀態state的哪種狀態???不懂!!
     * 3)如果任務已經開始執行,那麼mayInterruptIfRunning這個參數將決定是否取消任務的執行。
     *    這裏值得注意的是,cancel(true)實質並不能真正取消線程任務的執行,而是發出一個線程
     *    中斷的信號,一般需要結合Thread.currentThread().isInterrupted()來使用。
     */
    boolean cancel(boolean mayInterruptIfRunning);
    /**
     * 判斷任務是否被取消,在執行任務完成前被取消,此時會返回true
     */
    boolean isCancelled();
    /**
     * 這個方法不管任務正常停止,異常還是任務被取消,總是返回true。
     */
    boolean isDone();
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     */
    V get() throws InterruptedException, ExecutionException;
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     * 只不過在規定的時間內未獲取到結果,此時會拋出超時異常
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future接口象徵著異步執行任務的結果即執行一個耗時任務完全可以另起一個線程執行,然後此時我們可以去做其他事情,做完其他事情我們再調用Future.get()方法獲取結果即可,此時若異步任務還沒結束,此時會一直阻塞等待,直到異步任務執行完獲取到結果。

// RunnableFuture.java

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

RunnableFutureFutureRunnable接口的組合,即這個接口表示又可以被線程異步執行,因為實現了Runnable接口,又可以獲得線程異步任務的執行結果,因為實現了Future接口。因此解決了Runnable異步任務沒有返回結果的缺陷。

接下來我們來看下FutureTaskFutureTask實現了RunnableFuture接口,因此是FutureRunnable接口的具體實現類,是一個可被取消的異步線程任務,提供了Future的基本實現,即異步任務執行后我們能夠獲取到異步任務的執行結果,是我們接下來分析的重中之重。FutureTask可以包裝一個CallableRunnable對象,此外,FutureTask除了可以被線程執行外,還可以被提交給線程池執行。

我們先看下FutureTask類的api,其中重點方法已經紅框框出。

上圖中FutureTaskrun方法是被線程異步執行的方法,get方法即是取得異步任務執行結果的方法,還有cancel方法是取消任務執行的方法。接下來我們主要對這三個方法進行重點分析。

思考

  1. FutureTask覆寫的run方法的返回類型依然是void,表示沒有返回值,那麼FutureTaskget方法又是如何獲得返回值的呢?
  2. FutureTaskcancel方法能真正取消線程異步任務的執行么?什麼情況下能取消?

因為FutureTask異步任務執行結果還跟Callable接口有關,因此我們再來看下Callable接口:

// Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     */
    V call() throws Exception;
}

我們都知道,Callable<V>接口和Runnable接口都可以被提交給線程池執行,唯一不同的就是Callable<V>接口是有返回結果的,其中的泛型V就是返回結果,而Runnable接口是沒有返回結果的。

思考: 一般情況下,Runnable接口實現類才能被提交給線程池執行,為何Callable接口實現類也可以被提交給線程池執行?想想線程池的submit方法內部有對Callable做適配么?

4 FutureTask源碼分析

4.1 FutureTask成員變量

我們首先來看下FutureTask的成員變量有哪些,理解這些成員變量對後面的源碼分析非常重要。

// FutureTask.java

/** 封裝的Callable對象,其call方法用來執行異步任務 */
private Callable<V> callable;
/** 在FutureTask裏面定義一個成員變量outcome,用來裝異步任務的執行結果 */
private Object outcome; // non-volatile, protected by state reads/writes
/** 用來執行callable任務的線程 */
private volatile Thread runner;
/** 線程等待節點,reiber stack的一種實現 */
private volatile WaitNode waiters;
/** 任務執行狀態 */
private volatile int state;

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 對應成員變量state的偏移地址
private static final long stateOffset;
// 對應成員變量runner的偏移地址
private static final long runnerOffset;
// 對應成員變量waiters的偏移地址
private static final long waitersOffset;

這裏我們要重點關注下FutureTaskCallable成員變量,因為FutureTask的異步任務最終是委託給Callable去實現的。

思考

  1. FutureTask的成員變量runner,waitersstate都被volatile修飾,我們可以思考下為什麼這三個成員變量需要被volatile修飾,而其他成員變量又不用呢?volatile關鍵字的作用又是什麼呢?
  2. 既然已經定義了成員變量runner,waitersstate了,此時又定義了stateOffset,runnerOffsetwaitersOffset變量分別對應runner,waitersstate的偏移地址,為何要多此一舉呢?

我們再來看看stateOffset,runnerOffsetwaitersOffset變量這三個變量的初始化過程:

// FutureTask.java

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> k = FutureTask.class;
        stateOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("state"));
        runnerOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("runner"));
        waitersOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("waiters"));
    } catch (Exception e) {
        throw new Error(e);
    }
    }

4.2 FutureTask的狀態變化

前面講了FutureTask的成員變量,有一個表示狀態的成員變量state我們要重點關注下,state變量表示任務執行的狀態。

// FutureTask.java

/** 任務執行狀態 */
private volatile int state;
/** 任務新建狀態 */
private static final int NEW          = 0;
/** 任務正在完成狀態,是一個瞬間過渡狀態 */
private static final int COMPLETING   = 1;
/** 任務正常結束狀態 */
private static final int NORMAL       = 2;
/** 任務執行異常狀態 */
private static final int EXCEPTIONAL  = 3;
/** 任務被取消狀態,對應cancel(false) */
private static final int CANCELLED    = 4;
/** 任務中斷狀態,是一個瞬間過渡狀態 */
private static final int INTERRUPTING = 5;
/** 任務被中斷狀態,對應cancel(true) */
private static final int INTERRUPTED  = 6;

可以看到任務狀態變量state有以上7種狀態,0-6分別對應着每一種狀態。任務狀態一開始是NEW,然後由FutureTask的三個方法set,setExceptioncancel來設置狀態的變化,其中狀態變化有以下四種情況:

  1. NEW -> COMPLETING -> NORMAL:這個狀態變化表示異步任務的正常結束,其中COMPLETING是一個瞬間臨時的過渡狀態,由set方法設置狀態的變化;
  2. NEW -> COMPLETING -> EXCEPTIONAL:這個狀態變化表示異步任務執行過程中拋出異常,由setException方法設置狀態的變化;
  3. NEW -> CANCELLED:這個狀態變化表示被取消,即調用了cancel(false),由cancel方法來設置狀態變化;
  4. NEW -> INTERRUPTING -> INTERRUPTED:這個狀態變化表示被中斷,即調用了cancel(true),由cancel方法來設置狀態變化。

4.3 FutureTask構造函數

FutureTask有兩個構造函數,我們分別來看看:

// FutureTask.java

// 第一個構造函數
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

可以看到,這個構造函數在我們前面舉的“打火鍋”的例子代碼中有用到,就是Callable成員變量賦值,在異步執行任務時再調用Callable.call方法執行異步任務邏輯。此外,此時給任務狀態state賦值為NEW,表示任務新建狀態。

我們再來看下FutureTask的另外一個構造函數:

// FutureTask.java

// 另一個構造函數
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

這個構造函數在執行Executors.callable(runnable, result)時是通過適配器RunnableAdapter來將Runnable對象runnable轉換成Callable對象,然後再分別給callablestate變量賦值。

注意,這裏我們需要記住的是FutureTask新建時,此時的任務狀態stateNEW就好了。

4.4 FutureTask.run方法,用來執行異步任務

前面我們有講到FutureTask間接實現了Runnable接口,覆寫了Runnable接口的run方法,因此該覆寫的run方法是提交給線程來執行的,同時,該run方法正是執行異步任務邏輯的方法,那麼,執行完run方法又是如何保存異步任務執行的結果的呢?

我們現在着重來分析下run方法:

// FutureTask.java

public void run() {
    // 【1】,為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:
    // 1)若任務狀態state為NEW且runner為null,說明還未有線程執行過異步任務,此時滿足執行異步任務的條件,
    // 此時同時調用CAS方法為成員變量runner設置當前線程的值;
    // 2)若任務狀態state為NEW且runner不為null,任務狀態雖為NEW但runner不為null,說明有線程正在執行異步任務,
    // 此時不滿足執行異步任務的條件,直接返回;
    // 1)若任務狀態state不為NEW,此時不管runner是否為null,說明已經有線程執行過異步任務,此時沒必要再重新
    // 執行一次異步任務,此時不滿足執行異步任務的條件;
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        // 拿到之前構造函數傳進來的callable實現類對象,其call方法封裝了異步任務執行的邏輯
        Callable<V> c = callable;
        // 若任務還是新建狀態的話,那麼就調用異步任務
        if (c != null && state == NEW) {
            // 異步任務執行結果
            V result;
            // 異步任務執行成功還是始遍標誌
            boolean ran;
            try {
                // 【2】,執行異步任務邏輯,並把執行結果賦值給result
                result = c.call();
                // 若異步任務執行過程中沒有拋出異常,說明異步任務執行成功,此時設置ran標誌為true
                ran = true;
            } catch (Throwable ex) {
                result = null;
                // 異步任務執行過程拋出異常,此時設置ran標誌為false
                ran = false;
                // 【3】設置異常,裏面也設置state狀態的變化
                setException(ex);
            }
            // 【3】若異步任務執行成功,此時設置異步任務執行結果,同時也設置狀態的變化
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        // 異步任務正在執行過程中,runner一直是非空的,防止併發調用run方法,前面有調用cas方法做判斷的
        // 在異步任務執行完后,不管是正常結束還是異常結束,此時設置runner為null
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        // 線程執行異步任務后的任務狀態
        int s = state;
        // 【4】如果執行了cancel(true)方法,此時滿足條件,
        // 此時調用handlePossibleCancellationInterrupt方法處理中斷
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

可以看到執行異步任務的run方法主要分為以下四步來執行:

  1. 判斷線程是否滿足執行異步任務的條件:為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件;
  2. 若滿足條件,執行異步任務:因為異步任務邏輯封裝在Callable.call方法中,此時直接調用Callable.call方法執行異步任務,然後返回執行結果;
  3. 根據異步任務的執行情況做不同的處理:1) 若異步任務執行正常結束,此時調用set(result);來設置任務執行結果;2)若異步任務執行拋出異常,此時調用setException(ex);來設置異常,詳細分析請見4.4.1小節
  4. 異步任務執行完后的善後處理工作:不管異步任務執行成功還是失敗,若其他線程有調用FutureTask.cancel(true),此時需要調用handlePossibleCancellationInterrupt方法處理中斷,詳細分析請見4.4.2小節

這裏值得注意的是判斷線程滿不滿足執行異步任務條件時,runner是否為null是調用UNSAFECAS方法compareAndSwapObject來判斷和設置的,同時compareAndSwapObject是通過成員變量runner的偏移地址runnerOffset來給runner賦值的,此外,成員變量runner被修飾為volatile是在多線程的情況下, 一個線程的volatile修飾變量的設值能夠立即刷進主存,因此值便可被其他線程可見。

4.4.1 FutureTask的set和setException方法

下面我們來看下當異步任務執行正常結束時,此時會調用set(result);方法:

// FutureTask.java

protected void set(V v) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將任務執行結果賦值給成員變量outcome
        outcome = v;
        // 【3】將任務狀態設置為NORMAL,表示任務正常結束
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到當異步任務正常執行結束后,且異步任務沒有被cancel的情況下,此時會做以下事情:將任務執行結果保存到FutureTask的成員變量outcome中的,賦值結束後會調用finishCompletion方法來喚醒阻塞的線程(哪裡來的阻塞線程?後面會分析),值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> NORMAL

我們繼續來看下當異步任務執行過程中拋出異常,此時會調用setException(ex);方法。

// FutureTask.java

protected void setException(Throwable t) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將異常賦值給成員變量outcome
        outcome = t;
        // 【3】將任務狀態設置為EXCEPTIONAL
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到setException(Throwable t)的代碼邏輯跟前面的set(V v)幾乎一樣,不同的是任務執行過程中拋出異常,此時是將異常保存到FutureTask的成員變量outcome中,還有,值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> EXCEPTIONAL

因為異步任務不管正常還是異常結束,此時都會調用FutureTaskfinishCompletion方法來喚醒喚醒阻塞的線程,這裏阻塞的線程是指我們調用Future.get方法時若異步任務還未執行完,此時該線程會阻塞。

// FutureTask.java

private void finishCompletion() {
    // assert state > COMPLETING;
    // 取出等待線程鏈表頭節點,判斷頭節點是否為null
    // 1)若線程鏈表頭節點不為空,此時以“後進先出”的順序(棧)移除等待的線程WaitNode節點
    // 2)若線程鏈表頭節點為空,說明還沒有線程調用Future.get()方法來獲取任務執行結果,固然不用移除
    for (WaitNode q; (q = waiters) != null;) {
        // 調用UNSAFE的CAS方法將成員變量waiters設置為空
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                // 取出WaitNode節點的線程
                Thread t = q.thread;
                // 若取出的線程不為null,則將該WaitNode節點線程置空,且喚醒正在阻塞的該線程
                if (t != null) {
                    q.thread = null;
                    //【重要】喚醒正在阻塞的該線程
                    LockSupport.unpark(t);
                }
                // 繼續取得下一個WaitNode線程節點
                WaitNode next = q.next;
                // 若沒有下一個WaitNode線程節點,說明已經將所有等待的線程喚醒,此時跳出for循環
                if (next == null)
                    break;
                // 將已經移除的線程WaitNode節點的next指針置空,此時好被垃圾回收
                q.next = null; // unlink to help gc
                // 再把下一個WaitNode線程節點置為當前線程WaitNode頭節點
                q = next;
            }
            break;
        }
    }
    // 不管任務正常執行還是拋出異常,都會調用done方法
    done();
    // 因為異步任務已經執行完且結果已經保存到outcome中,因此此時可以將callable對象置空了
    callable = null;        // to reduce footprint
}

finishCompletion方法的作用就是不管異步任務正常還是異常結束,此時都要喚醒且移除線程等待鏈表的等待線程節點,這個鏈表實現的是一個是Treiber stack,因此喚醒(移除)的順序是”後進先出”即後面先來的線程先被先喚醒(移除),關於這個線程等待鏈表是如何成鏈的,後面再繼續分析。

4.4.2 FutureTask的handlePossibleCancellationInterrupt方法

4.4小節分析的run方法里的最後有一個finally塊,此時若任務狀態state >= INTERRUPTING,此時說明有其他線程執行了cancel(true)方法,此時需要讓出CPU執行的時間片段給其他線程執行,我們來看下具體的源碼:

// FutureTask.java

private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a
    // chance to interrupt us.  Let's spin-wait patiently.
    // 當任務狀態是INTERRUPTING時,此時讓出CPU執行的機會,讓其他線程執行
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt

    // assert state == INTERRUPTED;

    // We want to clear any interrupt we may have received from
    // cancel(true).  However, it is permissible to use interrupts
    // as an independent mechanism for a task to communicate with
    // its caller, and there is no way to clear only the
    // cancellation interrupt.
    //
    // Thread.interrupted();
}

思考: 為啥任務狀態是INTERRUPTING時,此時就要讓出CPU執行的時間片段呢?還有為什麼要在義務任務執行后才調用handlePossibleCancellationInterrupt方法呢?

4.5 FutureTask.get方法,獲取任務執行結果

前面我們起一個線程在其`run`方法中執行異步任務后,此時我們可以調用`FutureTask.get`方法來獲取異步任務執行的結果。

// FutureTask.java

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 【1】若任務狀態<=COMPLETING,說明任務正在執行過程中,此時可能正常結束,也可能遇到異常
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 【2】最後根據任務狀態來返回任務執行結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消
    return report(s);
}

可以看到,如果任務狀態state<=COMPLETING,說明異步任務正在執行過程中,此時會調用awaitDone方法阻塞等待;當任務執行完后,此時再調用report方法來報告任務結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消。

4.5.1 FutureTask.awaitDone方法

FutureTask.awaitDone方法會阻塞獲取異步任務執行結果的當前線程,直到異步任務執行完成。

// FutureTask.java

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // 計算超時結束時間
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 線程鏈表頭節點
    WaitNode q = null;
    // 是否入隊
    boolean queued = false;
    // 死循環
    for (;;) {
        // 如果當前獲取任務執行結果的線程被中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        // 【5】如果任務狀態>COMPLETING,此時返回任務執行結果,其中此時任務可能正常結束(NORMAL),可能拋出異常(EXCEPTIONAL)
        // 或任務被取消(CANCELLED,INTERRUPTING或INTERRUPTED狀態的一種)
        if (s > COMPLETING) {
            // 【問】此時將當前WaitNode節點的線程置空,其中在任務結束時也會調用finishCompletion將WaitNode節點的thread置空,
            // 這裏為什麼又要再調用一次q.thread = null;呢?
            // 【答】因為若很多線程來獲取任務執行結果,在任務執行完的那一刻,此時獲取任務的線程要麼已經在線程等待鏈表中,要麼
            // 此時還是一個孤立的WaitNode節點。在線程等待鏈表中的的所有WaitNode節點將由finishCompletion來移除(同時喚醒)所有
            // 等待的WaitNode節點,以便垃圾回收;而孤立的線程WaitNode節點此時還未阻塞,因此不需要被喚醒,此時只要把其屬性置為
            // null,然後其有沒有被誰引用,因此可以被GC。
            if (q != null)
                q.thread = null;
            // 【重要】返回任務執行結果
            return s;
        }
        // 【4】若任務狀態為COMPLETING,此時說明任務正在執行過程中,此時獲取任務結果的線程需讓出CPU執行時間片段
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // 【1】若當前線程還沒有進入線程等待鏈表的WaitNode節點,此時新建一個WaitNode節點,並把當前線程賦值給WaitNode節點的thread屬性
        else if (q == null)
            q = new WaitNode();
        // 【2】若當前線程等待節點還未入線程等待隊列,此時加入到該線程等待隊列的頭部
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // 若有超時設置,那麼處理超時獲取任務結果的邏輯
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        // 【3】若沒有超時設置,此時直接阻塞當前線程
        else
            LockSupport.park(this);
    }
}

FutureTask.awaitDone方法主要做的事情總結如下:

  1. 首先awaitDone方法裏面是一個死循環;
  2. 若獲取結果的當前線程被其他線程中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException;
  3. 如果任務狀態state>COMPLETING,此時返回任務執行結果;
  4. 若任務狀態為COMPLETING,此時獲取任務結果的線程需讓出CPU執行時間片段;
  5. q == null,說明當前線程還未設置到WaitNode節點,此時新建WaitNode節點並設置其thread屬性為當前線程;
  6. queued==false,說明當前線程WaitNode節點還未加入線程等待鏈表,此時加入該鏈表的頭部;
  7. timed設置為true時,此時該方法具有超時功能,關於超時的邏輯這裏不詳細分析;
  8. 當前面6個條件都不滿足時,此時阻塞當前線程。

我們分析到這裏,可以直到執行異步任務只能有一個線程來執行,而獲取異步任務結果可以多線程來獲取,當異步任務還未執行完時,此時獲取異步任務結果的線程會加入線程等待鏈表中,然後調用調用LockSupport.park(this);方法阻塞當前線程。直到異步任務執行完成,此時會調用finishCompletion方法來喚醒並移除線程等待鏈表的每個WaitNode節點,這裏這裏喚醒(移除)WaitNode節點的線程是從鏈表頭部開始的,前面我們也已經分析過。

還有一個特別需要注意的就是awaitDone方法裏面是一個死循環,當一個獲取異步任務的線程進來后可能會多次進入多個條件分支執行不同的業務邏輯,也可能只進入一個條件分支。下面分別舉兩種可能的情況進行說明:

情況1
當獲取異步任務結果的線程進來時,此時異步任務還未執行完即state=NEW且沒有超時設置時:

  1. 第一次循環:此時q = null,此時進入上面代碼標號【1】的判斷分支,即為當前線程新建一個WaitNode節點;
  2. 第二次循環:此時queued = false,此時進入上面代碼標號【2】的判斷分支,即將之前新建的WaitNode節點加入線程等待鏈表中;
  3. 第三次循環:此時進入上面代碼標號【3】的判斷分支,即阻塞當前線程;
  4. 第四次循環:加入此時異步任務已經執行完,此時進入上面代碼標號【5】的判斷分支,即返回異步任務執行結果。

情況2
當獲取異步任務結果的線程進來時,此時異步任務已經執行完即state>COMPLETING且沒有超時設置時,此時直接進入上面代碼標號【5】的判斷分支,即直接返回異步任務執行結果即可,也不用加入線程等待鏈表了。

4.5.2 FutureTask.report方法

get方法中,當異步任務執行結束后即不管異步任務正常還是異常結束,亦或是被cancel,此時獲取異步任務結果的線程都會被喚醒,因此會繼續執行FutureTask.report方法報告異步任務的執行情況,此時可能會返回結果,也可能會拋出異常。

// FutureTask.java

private V report(int s) throws ExecutionException {
    // 將異步任務執行結果賦值給x,此時FutureTask的成員變量outcome要麼保存着
    // 異步任務正常執行的結果,要麼保存着異步任務執行過程中拋出的異常
    Object x = outcome;
    // 【1】若異步任務正常執行結束,此時返回異步任務執行結果即可
    if (s == NORMAL)
        return (V)x;
    // 【2】若異步任務執行過程中,其他線程執行過cancel方法,此時拋出CancellationException異常
    if (s >= CANCELLED)
        throw new CancellationException();
    // 【3】若異步任務執行過程中,拋出異常,此時將該異常轉換成ExecutionException后,重新拋出。
    throw new ExecutionException((Throwable)x);
}

4.6 FutureTask.cancel方法,取消執行任務

我們最後再來看下FutureTask.cancel方法,我們一看到FutureTask.cancel方法,肯定一開始就天真的認為這是一個可以取消異步任務執行的方法,如果我們這樣認為的話,只能說我們猜對了一半。

// FutureTask.java

public boolean cancel(boolean mayInterruptIfRunning) {
    // 【1】判斷當前任務狀態,若state == NEW時根據mayInterruptIfRunning參數值給當前任務狀態賦值為INTERRUPTING或CANCELLED
    // a)當任務狀態不為NEW時,說明異步任務已經完成,或拋出異常,或已經被取消,此時直接返回false。
    // TODO 【問題】此時若state = COMPLETING呢?此時為何也直接返回false,而不能發出中斷異步任務線程的中斷信號呢??
    // TODO 僅僅因為COMPLETING是一個瞬時態嗎???
    // b)當前僅當任務狀態為NEW時,此時若mayInterruptIfRunning為true,此時任務狀態賦值為INTERRUPTING;否則賦值為CANCELLED。
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        // 【2】如果mayInterruptIfRunning為true,此時中斷執行異步任務的線程runner(還記得執行異步任務時就把執行異步任務的線程就賦值給了runner成員變量嗎)
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    // 中斷執行異步任務的線程runner
                    t.interrupt();
            } finally { // final state
                // 最後任務狀態賦值為INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    // 【3】不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點
    } finally {
        finishCompletion();
    }
    // 返回true
    return true;
}

以上代碼中,當異步任務狀態state != NEW時,說明異步任務已經正常執行完或已經異常結束亦或已經被cancel,此時直接返回false;當異步任務狀態state = NEW時,此時又根據mayInterruptIfRunning參數是否為true分為以下兩種情況:

  1. mayInterruptIfRunning = false時,此時任務狀態state直接被賦值為CANCELLED,此時不會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> CANCELLED
  2. mayInterruptIfRunning = true時,此時會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> INTERRUPTING -> INTERRUPTED

最後不管mayInterruptIfRunningtrue還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點。

FutureTask.cancel源碼中我們可以得出答案,該方法並不能真正中斷正在執行異步任務的線程,只能對執行異步任務的線程發出中斷信號。如果執行異步任務的線程處於sleepwaitjoin的狀態中,此時會拋出InterruptedException異常,該線程可以被中斷;此外,如果異步任務需要在while循環執行的話,此時可以結合以下代碼來結束異步任務線程,即執行異步任務的線程被中斷時,此時Thread.currentThread().isInterrupted()返回true,不滿足while循環條件因此退出循環,結束異步任務執行線程,如下代碼:

public Integer call() throws Exception {
    while (!Thread.currentThread().isInterrupted()) {
        // 業務邏輯代碼
        System.out.println("running...");

    }
    return 666;
}

注意:調用了FutureTask.cancel方法,只要返回結果是true,假如異步任務線程雖然不能被中斷,即使異步任務線程正常執行完畢,返回了執行結果,此時調用FutureTask.get方法也不能夠獲取異步任務執行結果,此時會拋出CancellationException異常。請問知道這是為什麼嗎?

因為調用了FutureTask.cancel方法,只要返回結果是true,此時的任務狀態為CANCELLEDINTERRUPTED,同時必然會執行finishCompletion方法,而finishCompletion方法會喚醒獲取異步任務結果的線程等待列表的線程,而獲取異步任務結果的線程喚醒后發現狀態s >= CANCELLED,此時就會拋出CancellationException異常了。

5 總結

好了,本篇文章對FutureTask的源碼分析就到此結束了,下面我們再總結下FutureTask的實現邏輯:

  1. 我們實現Callable接口,在覆寫的call方法中定義需要執行的業務邏輯;
  2. 然後把我們實現的Callable接口實現對象傳給FutureTask,然後FutureTask作為異步任務提交給線程執行;
  3. 最重要的是FutureTask內部維護了一個狀態state,任何操作(異步任務正常結束與否還是被取消)都是圍繞着這個狀態進行,並隨時更新state任務的狀態;
  4. 只能有一個線程執行異步任務,當異步任務執行結束后,此時可能正常結束,異常結束或被取消。
  5. 可以多個線程併發獲取異步任務執行結果,當異步任務還未執行完,此時獲取異步任務的線程將加入線程等待列表進行等待;
  6. 當異步任務線程執行結束后,此時會喚醒獲取異步任務執行結果的線程,注意喚醒順序是”後進先出”即後面加入的阻塞線程先被喚醒。
  7. 當我們調用FutureTask.cancel方法時並不能真正停止執行異步任務的線程,只是發出中斷線程的信號。但是只要cancel方法返回true,此時即使異步任務能正常執行完,此時我們調用get方法獲取結果時依然會拋出CancellationException異常。

擴展: 前面我們提到了FutureTaskrunner,waitersstate都是用volatile關鍵字修飾,說明這三個變量都是多線程共享的對象(成員變量),會被多線程操作,此時用volatile關鍵字修飾是為了一個線程操作volatile屬性變量值后,能夠及時對其他線程可見。此時多線程操作成員變量僅僅用了volatile關鍵字仍然會有線程安全問題的,而此時Doug Lea老爺子沒有引入任何線程鎖,而是採用了UnsafeCAS方法來代替鎖操作,確保線程安全性。

6 分析FutureTask源碼,我們能學到什麼?

我們分析源碼的目的是什麼?除了弄懂FutureTask的內部實現原理外,我們還要借鑒大佬寫寫框架源碼的各種技巧,只有這樣,我們才能成長。

分析了FutureTask源碼,我們可以從中學到:

  1. 利用LockSupport來實現線程的阻塞\喚醒機制;
  2. 利用volatileUNSAFECAS方法來實現線程共享變量的無鎖化操作;
  3. 若要編寫超時異常的邏輯可以參考FutureTaskget(long timeout, TimeUnit unit)的實現邏輯;
  4. 多線程獲取某一成員變量結果時若需要等待時的線程等待鏈表的邏輯實現;
  5. 某一異步任務在某一時刻只能由單一線程執行的邏輯實現;
  6. FutureTask中的任務狀態satate的變化處理的邏輯實現。

以上列舉的幾點都是我們可以學習參考的地方。

若您覺得不錯,請無情的轉發和點贊吧!

【源碼筆記】Github地址:

https://github.com/yuanmabiji/Java-SourceCode-Blogs

公眾號【源碼筆記】,專註於Java後端系列框架的源碼分析。

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

設計模式系列之中介者模式(Mediator Pattern)——協調多個對象之間的交互

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

很多在一線城市漂泊的朋友或多或少都會遇到租房的難題,你是怎樣找到物美價廉的房子的呢,可以在評論區分享經驗哦。相信大多數小夥伴是通過中介找房子的,實話說,通過中介,只要說出你的預算以及大致需求(比如單間帶獨衛朝南大卧室帶陽台等),中介會快速提供符合你情況房源。這裏可以看出,中介者協調了房東與租客之間錯綜複雜的關係,將一個網狀的關係結構變成一個以中介者為中心的星形結構,讓多對多的關係更容易維護。

未引入中介者,對象之間(這些對象稱為同事對象,它們之間通過彼此的相互作用實現系統的行為)的關係圖如下所示:

引入中介者可以使對象之間的關係數量急劇減少。在這個星形結構中,對象不再直接與另一個對象聯繫,它通過中介者對象與另一個對象發生相互作用。

模式定義

如果在一個系統中對象之間存在多對多的相互關係,我們可以將對象之間的一些交互行為從各個對象中分離出來,並集中封裝在一个中介者對象中,並由該中介者進行統一協調,這樣對象之間多對多的複雜關係就轉化為相對簡單的一對多關係。通過引入中介者來簡化對象之間的複雜交互,中介者模式是“迪米特法則”的一個典型應用。

中介者模式(Mediator Pattern):用一个中介對象(中介者)來封裝一系列的對象交互,中介者使各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。中介者模式又稱為調停者模式,它是一種對象行為型模式。

模式結構圖

在中介者模式中,引入了用於協調其他對象/類之間相互調用的中介者類,為了讓系統具有更好的靈活性和可擴展性,通常還提供了抽象中介者,其結構圖如下圖所示:

在中介者模式結構圖中包含如下幾個角色:

  • Mediator(抽象中介者):它定義一個接口,該接口用於與各同事對象之間進行通信。

  • ConcreteMediator(具體中介者):它是抽象中介者的子類,通過協調各個同事對象來實現協作行為,它維持了對各個同事對象的引用。

  • Colleague(抽象同事類):它定義各個同事類公有的方法,並聲明了一些抽象方法來供子類實現,同時它維持了一個對抽象中介者類的引用,其子類可以通過該引用來與中介者通信。

  • ConcreteColleague(具體同事類):它是抽象同事類的子類;每一個同事對象在需要和其他同事對象通信時,先與中介者通信,通過中介者來間接完成與其他同事類的通信;在具體同事類中實現了在抽象同事類中聲明的抽象方法。

中介者模式的核心在於中介者類的引入,在中介者模式中,中介者類承擔了兩方面的職責:

(1) 中轉作用(結構性):通過中介者提供的中轉作用,各個同事對象就不再需要顯式引用其他同事,當需要和其他同事進行通信時,可通過中介者來實現間接調用。該中轉作用屬於中介者在結構上的支持。

(2) 協調作用(行為性):中介者可以更進一步的對同事之間的關係進行封裝,同事可以一致的和中介者進行交互,而不需要指明中介者需要具體怎麼做,中介者根據封裝在自身內部的協調邏輯,對同事的請求進行進一步處理,將同事成員之間的關係行為進行分離和封裝。該協調作用屬於中介者在行為上的支持。

模式偽代碼

典型的抽象中介者類、抽象同事類典型代碼如下:

// 抽象中介者類
public abstract class Mediator {
    // 用於存儲同事對象
    protected List<Colleague> colleagues = new ArrayList<>();

    // 註冊方法,用於增加同事對象
    public void register(Colleague colleague) {
        colleagues.add(colleague);
    }

    // 聲明抽象的業務方法
    public abstract void operation();
}

// 抽象同事類
public abstract class Colleague {
    // 維持一個抽象中介者的引用
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    // 聲明自身方法,處理自己的行為
    public abstract void method1();

    // 定義依賴方法,與中介者進行通信
    public void method2() {
        mediator.operation();
    }
}

具體中介者類、具體同事類實現這些抽象方法,典型代碼如下:

// 具體同事類
public class ConcreteColleague extends Colleague {

    public ConcreteColleague(Mediator mediator) {
        super(mediator);
    }
    
    @Override
    public void method1() {
        // 實現自身方法
    }
}

// 具體中介者類
public class ConcreteMediator extends Mediator {
    @Override
    public void operation() {
        // 通過調用同事類的方法,並增加其他業務邏輯來控制同事之間的交互
    }
}

模式應用

待完善…

模式總結

中介者模式將一個網狀的系統結構變成一個以中介者對象為中心的星形結構,在這個星型結構中,使用中介者對象與其他對象的一對多關係來取代原有對象之間的多對多關係。中介者模式在事件驅動類軟件中應用較為廣泛,特別是基於GUIGraphical User Interface,圖形用戶界面)的應用軟件,此外,在類與類之間存在錯綜複雜的關聯關係的系統中,中介者模式都能得到較好的應用。

主要優點

(1) 中介者模式簡化了對象之間的交互,它用中介者和同事的一對多交互代替了原來同事之間的多對多交互,一對多關係更容易理解、維護和擴展,將原本難以理解的網狀結構轉換成相對簡單的星型結構。

(2) 中介者模式可將各同事對象解耦。中介者有利於各同事之間的松耦合,我們可以獨立的改變和復用每一個同事和中介者,增加新的中介者和新的同事類都比較方便,更好地符合“開閉原則”。

(3) 可以減少子類生成,中介者將原本分佈於多個對象間的行為集中在一起,改變這些行為只需生成新的中介者子類即可,這使各個同事類可被重用,無須對同事類進行擴展。

主要缺點

中介者類中包含了大量同事之間的交互細節,可能會導致具體中介者類非常複雜,使得系統難以維護。

適用場景

(1) 系統中對象之間存在複雜的引用關係,系統結構混亂且難以理解。

(2) 一個對象由於引用了其他很多對象並且直接和這些對象通信,導致難以復用該對象。

(3) 想通過一个中間類來封裝多個類中的行為,而又不想生成太多的子類。可以通過引入中介者類來實現,在中介者中定義對象交互的公共行為,如果需要改變行為則可以增加新的具體中介者類。

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

Spring IoC bean 的加載

前言

本系列全部基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的源碼解析。

本篇文章主要介紹 Spring IoC 容器是怎麼加載 bean 的。

正文

我們先看一下Spring IoC BeanDefinition 的加載和註冊一文中獲取 bean 的實例代碼:

public class BeanDefinitionDemo {

    public static void main(String[] args) {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("META-INF/bean-definition.xml");
        User user = beanFactory.getBean("user", User.class);
        System.err.println(user);
    }

}

通過 beanFactory.getBean() 這個方法就獲取了在 XML 中定義的 bean,下面我們就重點分析這個方法背後做了什麼操作。

在正式開始之前,我們先了解一下 FactoryBean 及其用法。

FactoryBean 介紹

FactoryBean 接口對於 Spring 框架來說佔有重要的地位,Spring 自身就提供了70多個 FactoryBean 的實現。它們隱藏了一下複雜 bean 的細節,給上層應用帶來了便利。下面是該接口的定義:

public interface FactoryBean<T> {

    // 返回由FactoryBean創建的bean實例,如果isSingleton()返回true,
    // 則該實例會放到Spring容器中單例緩存池中
    @Nullable
    T getObject() throws Exception;
	
    // 返回FactoryBean創建的bean類型
    @Nullable
    Class<?> getObjectType();

    // 返回由FactoryBean創建的bean實例的作用域是singleton還是prototype
    default boolean isSingleton() {
        return true;
    }

}

當配置文件中 <bean>class 屬性配置的實現類時 FactoryBean 時,通過 getBean() 返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 所返回的對象,相當於 FactoryBean#getObject() 代理了 getBean()。下面用簡單的代碼演示一下:

首先定義一個 Car 實體類:

public class Car {
	
    private Integer maxSpeed;
    private String brand;
    private Double price;

    public Integer getMaxSpeed() {
        return maxSpeed;
    }

    public void setMaxSpeed(Integer maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }
}

上面的實體類,如果用傳統方式配置,每一個屬性都會對應一個 <property> 元素標籤。如果用 FactoryBean 的方式實現就會靈活一點,下面通過逗號分隔的方式一次性的為 Car 的所有屬性配置值。

public class CarFactoryBean implements FactoryBean<Car> {
	
    private String carInfo;
	
    @Override
    public Car getObject() throws Exception {
        Car car = new Car();
        String[] infos = carInfo.split(",");
        car.setBrand(infos[0]);
        car.setMaxSpeed(Integer.valueOf(infos[1]));
        car.setPrice(Double.valueOf(infos[2]));
        return car;
    }

    @Override
    public Class<?> getObjectType() {
        return Car.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public String getCarInfo() {
        return carInfo;
    }

    public void setCarInfo(String carInfo) {
        this.carInfo = carInfo;
    }
}

接下來,我們在 XML 中配置。

<bean id="car" class="com.leisurexi.ioc.domain.CarFactoryBean">
    <property name="carInfo" value="超級跑車,400,2000000"/>
</bean>

最後看下測試代碼和運行結果:

@Test
public void factoryBeanTest() {
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
    reader.loadBeanDefinitions("META-INF/factory-bean.xml");
    Car car = beanFactory.getBean("car", Car.class);
    System.out.println(car);
    CarFactoryBean carFactoryBean = beanFactory.getBean("&car", CarFactoryBean.class);
    System.out.println(carFactoryBean);
}

可以看到如果 beanName 前面加上 & 獲取的是 FactoryBean 本身,不加獲取的 getObject() 返回的對象。

FactoryBean 的特殊之處在於它可以向容器中註冊兩個 bean,一個是它本身,一個是 FactoryBean.getObject() 方法返回值所代表的 bean

bean 的加載

AbstractBeanFactory#getBean

public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    // 調用doGetBean方法(方法以do開頭實際做操作的方法)
    return doGetBean(name, requiredType, null, false);
}

/**
* @param name          bean的名稱
* @param requiredType  bean的類型
* @param args          显示傳入的構造參數
* @param typeCheckOnly 是否僅僅做類型檢查
*/
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    // 獲取bean的實際名稱,見下文詳解
    final String beanName = transformedBeanName(name);
    Object bean;

    // 直接嘗試從緩存獲取或 singletonFactories 中的 ObjectFactory 中獲取,見下文詳解
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // 檢查bean是否是FactoryBean的實現。不是直接返回bean,
        // 是的話首先檢查beanName是否以&開頭,如果是返回FactoryBean本身,
        // 不是調用FactoryBean#getObject()返回對象,見下文詳解
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }

    else {
        // 只有在單例情況下才會去嘗試解決循環依賴,原型模式下,如果存在A中有
        // B屬性,B中有A屬性,那麼當依賴注入時,就會產生當A還未創建完的時候
        // 對於B的創建而在此返回創建A,造成循環依賴
        if (isPrototypeCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
        }

        // 檢查當前bean的BeanDefinition是否在當前的bean工廠中,
        // 不在遞歸調用父工廠的getBean()去獲取bean
        BeanFactory parentBeanFactory = getParentBeanFactory();
        if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
            String nameToLookup = originalBeanName(name);
            if (parentBeanFactory instanceof AbstractBeanFactory) {
                return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                    nameToLookup, requiredType, args, typeCheckOnly);
            }
            else if (args != null) {
                return (T) parentBeanFactory.getBean(nameToLookup, args);
            }
            else if (requiredType != null) {
                return parentBeanFactory.getBean(nameToLookup, requiredType);
            }
            else {
                return (T) parentBeanFactory.getBean(nameToLookup);
            }
        }
        // 如果不是僅僅做類型檢查,則是創建bean,這裏要進行記錄
        if (!typeCheckOnly) {
            // 記錄bean已經創建過
            markBeanAsCreated(beanName);
        }

        try {
            // 合併BeanDefinition,見下文詳解
            final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);

            // 實例化bean前先實例化依賴bean,也就是depends-on屬性中配置的beanName
            String[] dependsOn = mbd.getDependsOn();
            if (dependsOn != null) {
                for (String dep : dependsOn) {
                    // 檢查是否循環依賴,即當前bean依賴dep,dep依賴當前bean,見下文詳解
                    if (isDependent(beanName, dep)) {
                        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                    }
                    // 將dep和beanName的依賴關係放入到緩存中,見下文詳解
                    registerDependentBean(dep, beanName);
                    try {
                        // 獲取依賴dep對應的bean實例,如果還未創建實例,則先去創建
                        getBean(dep);
                    }
                    catch (NoSuchBeanDefinitionException ex) {
                        throw new BeanCreationException(mbd.getResourceDescription(), beanName,"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
                    }
                }
            }

            // 如果 bean 的作用域是單例
            if (mbd.isSingleton()) {
                // 創建和註冊單例 bean,見下文詳解
                sharedInstance = getSingleton(beanName, () -> {
                    try {
                        // 創建 bean 實例
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                // 獲取bean實例
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }
            // bean 的作用域是原型
            else if (mbd.isPrototype()) {
                Object prototypeInstance = null;
                try {
                    // 原型 bean 創建前回調,
                    // 默認實現是將 beanName 保存到 prototypesCurrentlyInCreation 緩存中
                    beforePrototypeCreation(beanName);
                    // 創建 bean 實例
                    prototypeInstance = createBean(beanName, mbd, args);
                }
                finally {
                    // 原型 bean 創建后回調,
                    // 默認實現是將 beanName 從prototypesCurrentlyInCreation 緩存中移除
                    afterPrototypeCreation(beanName);
                }
                // 獲取bean實例
                bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
            }
            // 自定義作用域
            else {
                // 獲取自定義作用域名稱
                String scopeName = mbd.getScope();
                // 獲取作用域對象
                final Scope scope = this.scopes.get(scopeName);
                // 如果為空表示作用域未註冊,拋出異常
                if (scope == null) {
                    throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                }
                try {
                    // 其他 Scope 的 bean 創建
                    // 新建了一個 ObjectFactory,並且重寫了 getObject 方法
                    Object scopedInstance = scope.get(beanName, () -> {
                        // 調用原型 bean 創建前回調
                        beforePrototypeCreation(beanName);
                        try {
                            // 創建 bean 實例,下篇文章詳解
                            return createBean(beanName, mbd, args);
                        }
                        finally {
                            // 調用原型 bean 創建后回調
                            afterPrototypeCreation(beanName);
                        }
                    });
                    // 獲取bean實例
                    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                }
                catch (IllegalStateException ex) {
                    throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + "defining a scoped proxy for this bean if you intend to refer to it from a singleton",ex);
                }
            }
        }
        catch (BeansException ex) {
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
    }

    // 檢查所需的類型是否與實際 bean 實例的類型匹配
    if (requiredType != null && !requiredType.isInstance(bean)) {
        try {
            // 如果類型不等,進行轉換,轉換失敗拋出異常;轉換成功直接返回
            T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
            if (convertedBean == null) {
                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
            return convertedBean;
        }
        catch (TypeMismatchException ex) {
            throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
        }
    }
    // 返回 bean 實例
    return (T) bean;
}

上面方法就是獲取 bean 的整個流程,下面我們對其調用的其它主要方法來一一分析。

轉換對應的 beanName

AbstractBeanFactory#transformedBeanName

protected String transformedBeanName(String name) {
    return canonicalName(BeanFactoryUtils.transformedBeanName(name));
}

// BeanFactoryUtils.java
public static String transformedBeanName(String name) {
    Assert.notNull(name, "'name' must not be null");
    // 如果name不是&開頭,直接返回
    if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
        return name;
    }
    // 去除name的&前綴
    return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
        do {
            beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
        }
        while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
        return beanName;
    });
}

// SimpleAliasRegistry.java
public String canonicalName(String name) {
    String canonicalName = name;
    String resolvedName;
    // 如果name是別名,則會循環去查找bean的實際名稱
    do {
        resolvedName = this.aliasMap.get(canonicalName);
        if (resolvedName != null) {
            canonicalName = resolvedName;
        }
    }
    while (resolvedName != null);
    return canonicalName;
}

嘗試從單例緩存獲取 bean

AbstractBeanFactory#getSingleton

public Object getSingleton(String beanName) {
    // allowEarlyReference設置為true表示允許早期依賴
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先從一級緩存中,檢查單例緩存是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果為空,並且當前bean正在創建中,鎖定全局變量進行處理
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從二級緩存中獲取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 二級緩存為空 && bean允許提前曝光
            if (singletonObject == null && allowEarlyReference) {
                // 從三級緩存中獲取bean對應的ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 調用預先設定的getObject(),獲取bean實例
                    singletonObject = singletonFactory.getObject();
                    // 放入到二級緩存中,並從三級緩存中刪除
                    // 這時bean已經實例化完但還未初始化完
                    // 在該bean未初始化完時如果有別的bean引用該bean,可以直接從二級緩存中取出返回
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

上面方法主要就是嘗試從緩存中獲取 bean,緩存有三級,這也是 Spring 解決循環依賴的關鍵所在;後續會在 循環依賴 中重點講述。

獲取 bean 實例對象

AbstractBeanFactory#getObjectForBeanInstance

protected Object getObjectForBeanInstance(Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {

    // name 是否以 & 開頭
    if (BeanFactoryUtils.isFactoryDereference(name)) {
        // 如果是 null 直接返回
        if (beanInstance instanceof NullBean) {
            return beanInstance;
        }
        // beanName 以 & 開頭,但又不是 FactoryBean 類型,拋出異常
        if (!(beanInstance instanceof FactoryBean)) {
            throw new BeanIsNotAFactoryException(beanName, beanInstance.getClass());
        }
        // 設置 isFactoryBean 為 true
        if (mbd != null) {
            mbd.isFactoryBean = true;
        }
        // 返回 bean 實例
        return beanInstance;
    }

    // name 不是 & 開頭,並且不是 FactoryBean 類型,直接返回
    if (!(beanInstance instanceof FactoryBean)) {
        return beanInstance;
    }

    // 到這裏就代表name不是&開頭,且是FactoryBean類型
    // 即獲取FactoryBean.getObject()方法返回值所代表的bean
    Object object = null;
    if (mbd != null) {	
        mbd.isFactoryBean = true;
    }
    else {
        // 從緩存中獲取實例
        object = getCachedObjectForFactoryBean(beanName);
    }
    if (object == null) {
        // 將 beanInstance 強轉成 FactoryBean
        FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
        // 合併 BeanDefinition
        if (mbd == null && containsBeanDefinition(beanName)) {
            mbd = getMergedLocalBeanDefinition(beanName);
        }
        boolean synthetic = (mbd != null && mbd.isSynthetic());
        // 獲取實例
        object = getObjectFromFactoryBean(factory, beanName, !synthetic);
    }
    return object;
}

// FactoryBeanRegistrySupport.java
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
    // 如果是單例 bean,並且已經存在緩存中
    if (factory.isSingleton() && containsSingleton(beanName)) {
        // 加鎖
        synchronized (getSingletonMutex()) {
            // 從緩存中獲取
            Object object = this.factoryBeanObjectCache.get(beanName);
            if (object == null) {
                // 調用 FactoryBean 的 getObject() 獲取實例
                object = doGetObjectFromFactoryBean(factory, beanName);
                Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
                // 如果該 beanName 已經在緩存中存在,則將 object 替換成緩存中的
                if (alreadyThere != null) {
                    object = alreadyThere;
                }
                else {
                    if (shouldPostProcess) {
                        // 如果當前 bean 還在創建中,直接返回
                        if (isSingletonCurrentlyInCreation(beanName)) {
                            return object;
                        }
                        // 單例 bean 創建前回調
                        beforeSingletonCreation(beanName);
                        try {
                            // 對從 FactoryBean 獲得給定對象的後置處理,默認按原樣返回
                            object = postProcessObjectFromFactoryBean(object, beanName);
                        }
                        catch (Throwable ex) {
                            throw new BeanCreationException(beanName,
                                                            "Post-processing of FactoryBean's singleton object failed", ex);
                        }
                        finally {
                            // 單例 bean 創建后回調
                            afterSingletonCreation(beanName);
                        }
                    }
                    if (containsSingleton(beanName)) {
                        // 將 beanName 和 object 放到 factoryBeanObjectCache 緩存中
                        this.factoryBeanObjectCache.put(beanName, object);
                    }
                }
            }
            // 返回實例
            return object;
        }
    }
    else {
        // 調用 FactoryBean 的 getObject() 獲取實例
        Object object = doGetObjectFromFactoryBean(factory, beanName);
        if (shouldPostProcess) {
            try {
                // 對從 FactoryBean 獲得給定對象的後置處理,默認按原樣返回
                object = postProcessObjectFromFactoryBean(object, beanName);
            }
            catch (Throwable ex) {
                throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
            }
        }
        // 返回實例
        return object;
    }
}

// FactoryBeanRegistrySupport.java
private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName) throws BeanCreationException {

    Object object;
    try {
        // 調用 getObject() 獲取實例
        object = factory.getObject();
    }
    // 省略異常處理...

    // 如果 object 為 null,並且當前 singleton bean 正在創建中,拋出異常
    if (object == null) {
        if (isSingletonCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName, "FactoryBean which is currently in creation returned null from getObject");
        }
        object = new NullBean();
    }
    // 返回 object 實例
    return object;
}

上面代碼總結起來就是:如果 beanName& 開頭,直接返回 FactoryBean 實例;否則調用 getObject() 方法獲取實例,然後執行 postProcessObjectFromFactoryBean() 回調,可以在回調方法中修改實例,默認按原樣返回。

合併 bean 定義元信息

AbstractBeanFactory#getMergedLocalBeanDefinition

下文將合併后的 BeanDefinition 簡稱為 MergedBeanDefinition

protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException {
    // 從緩存獲取MergedBeanDefinition
    RootBeanDefinition mbd = this.mergedBeanDefinitions.get(beanName);
    // 如果存在MergedBeanDefinition,並且不是過期的,直接返回
    if (mbd != null && !mbd.stale) {
        return mbd;
    }
    // 獲取已經註冊的BeanDefinition然後去合併
    return getMergedBeanDefinition(beanName, getBeanDefinition(beanName));
}

protected RootBeanDefinition getMergedBeanDefinition(String beanName, BeanDefinition bd)
		throws BeanDefinitionStoreException {
    // 頂級bean獲取合併后的BeanDefinition
    return getMergedBeanDefinition(beanName, bd, null);
}

/**
 * @param containingBd 如果是嵌套bean該值為頂級bean,如果是頂級bean該值為null
 */
protected RootBeanDefinition getMergedBeanDefinition(
		String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
		throws BeanDefinitionStoreException {
	// 加鎖
    synchronized (this.mergedBeanDefinitions) {
        // 本次的RootBeanDefinition
        RootBeanDefinition mbd = null;
        // 以前的RootBeanDefinition
        RootBeanDefinition previous = null;

        // 如果bean是頂級bean,直接獲取MergedBeanDefinition
        if (containingBd == null) {
            mbd = this.mergedBeanDefinitions.get(beanName);
        }
		// 沒有MergedBeanDefinition || BeanDefinition過期了
        if (mbd == null || mbd.stale) {
            previous = mbd;
            // 如果bean沒有parent
            if (bd.getParentName() == null) {
                // 如果bd本身就是RootBeanDefinition直接複製一份,否則創建一個
                if (bd instanceof RootBeanDefinition) {
                    mbd = ((RootBeanDefinition) bd).cloneBeanDefinition();
                }
                else {
                    mbd = new RootBeanDefinition(bd);
                }
            }
            else {	
                // bean有parent
                BeanDefinition pbd;
                try {
                    // 獲取parent bean的實際名稱
                    String parentBeanName = transformedBeanName(bd.getParentName());
                    if (!beanName.equals(parentBeanName)) {
                        // 當前beanName不等於它的parentBeanName
                      	// 獲取parent的MergedBeanDefinition
                        pbd = getMergedBeanDefinition(parentBeanName);
                    }
                    else {
                        // 如果parentBeanName與bd的beanName相同,則拿到父BeanFactory
                        // 只有在存在父BeanFactory的情況下,才允許parentBeanName與自己相同
                        BeanFactory parent = getParentBeanFactory();
                        if (parent instanceof ConfigurableBeanFactory) {
                            // 如果父BeanFactory是ConfigurableBeanFactory類型
                            // 則通過父BeanFactory獲取parent的MergedBeanDefinition
                            pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName);
                        }
                        else {
                            // 如果父BeanFactory不是ConfigurableBeanFactory,拋出異常
                            throw new NoSuchBeanDefinitionException(parentBeanName,
"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName + "': cannot be resolved without an AbstractBeanFactory parent");
                        }
                    }
                }
                catch (NoSuchBeanDefinitionException ex) {
                    throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanName, "Could not resolve parent bean definition '" + bd.getParentName() + "'", ex);
                }
                // 使用父MergedBeanDefinition構建一個新的RootBeanDefinition對象(深拷貝)
                mbd = new RootBeanDefinition(pbd);
                // 覆蓋與parent相同的屬性
                mbd.overrideFrom(bd);
            }
            
            // 如果bean沒有設置scope屬性,默認是singleton
            if (!StringUtils.hasLength(mbd.getScope())) {
                mbd.setScope(RootBeanDefinition.SCOPE_SINGLETON);
            }

            // 當前bean是嵌套bean && 頂級bean的作用域不是單例 && 當前bean的作用域是單例
            // 這裏總結起來就是,如果頂層bean不是單例的,那麼嵌套bean也不能是單例的
            if (containingBd != null && !containingBd.isSingleton() && mbd.isSingleton()) {
                // 設置當前bean的作用域和頂級bean一樣
                mbd.setScope(containingBd.getScope());
            }

            // 當前bean是頂級bean && 緩存bean的元數據(該值默認為true)
            if (containingBd == null && isCacheBeanMetadata()) {
                // 將當前bean的MergedBeanDefinition緩存起來
                this.mergedBeanDefinitions.put(beanName, mbd);
            }
        }
        // 以前的RootBeanDefinition不為空,拷貝相關的BeanDefinition緩存
        if (previous != null) {
            copyRelevantMergedBeanDefinitionCaches(previous, mbd);
        }
        return mbd;
    }
}	

上面代碼主要是獲取 MergedBeanDefinition ,主要步驟如下:

  1. 首先從緩存中獲取 beanMergedBeanDefinition,如果存在並且未過期直接返回。

  2. 不存在或者已過期的 MergedBeanDefinition ,獲取已經註冊的 BeanDefinition 去作為頂級 bean 合併。

  3. bean 沒有 parent (就是 XML 中的 parent 屬性),直接封裝成 RootBeanDefinition

  4. beanparent ,先去獲取父 MergedBeanDefinition ,然後覆蓋和合併與 parent 相同的屬性。

    注意:這裏只有 abstractscopelazyInitautowireModedependencyCheckdependsOnfactoryBeanNamefactoryMethodNameinitMethodNamedestroyMethodName 會覆蓋,而 constructorArgumentValuespropertyValuesmethodOverrides 會合併。

  5. 如果沒有設置作用域,默認作用域為 singleton

  6. 緩存 MergedBeanDefinition

上文中提到如果 beanparent,會合併一些屬性,這裏我們稍微展示一下合併后的 propertyValues:

首先定義一個 SuperUser 繼承上面定義的 User,如下:

public class SuperUser extends User {

    private String address;

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "SuperUser{" +
            "address='" + address + '\'' +
            '}';
    }

}

然後我們在 XML 文件中配置一下,如下:

<bean id="superUser" class="com.leisurexi.ioc.domain.SuperUser" parent="user">
    <property name="address" value="北京"/>
</bean>

然後下圖是我 Debug 的截圖,可以看到 superUserpropertyValues 合併了 useridname 屬性。

上文還提到了嵌套 bean ,下面我們簡單看一下什麼是嵌套 bean

在 Spring 中,如果某個 bean 所依賴的 bean 不想被 Spring 容器直接訪問,可以使用嵌套 bean。和普通的 bean 一樣,使用 bean 元素來定義嵌套的 bean,嵌套 bean 只對它的外部 bean 有效,Spring 無法直接訪問嵌套 bean ,因此定義嵌套 bean 也無需指定 id 屬性。如下配置片段是一個嵌套 bean 示例:

採用上面的配置形式可以保證嵌套 bean 不能被容器訪問,因此不用擔心其他程序修改嵌套 bean。外部 bean 的用法和使用結果和以前沒有區別。

嵌套 bean 提高了 bean 的內聚性,但是降低了程序的靈活性。只有在確定無需通過 Spring 容器訪問某個 bean 實例時,才考慮使用嵌套 bean 來定義。

尋找依賴

DefaultSingletonBeanRegistry#isDependent

protected boolean isDependent(String beanName, String dependentBeanName) {
    // 加鎖
    synchronized (this.dependentBeanMap) {
        // 檢測beanName和dependentBeanName是否有循環依賴
        return isDependent(beanName, dependentBeanName, null);
    }
}

private boolean isDependent(String beanName, String dependentBeanName, @Nullable Set<String> alreadySeen) {
    // 如果當前bean已經檢測過,直接返回false
    if (alreadySeen != null && alreadySeen.contains(beanName)) {
        return false;
    }
    // 解析別名,獲取實際的beanName
    String canonicalName = canonicalName(beanName);
    // 獲取canonicalName所依賴beanName集合
    Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);
    // 如果為空,兩者還未確定依賴關係,返回false
    if (dependentBeans == null) {
        return false;
    }
    // 如果dependentBeanName已經存在於緩存中,兩者已經確定依賴關係,返回true
    if (dependentBeans.contains(dependentBeanName)) {
        return true;
    }
    // 循環檢查,即檢查依賴canonicalName的所有beanName是否被dependentBeanName依賴(即隔層依賴)
    for (String transitiveDependency : dependentBeans) {
        if (alreadySeen == null) {
            alreadySeen = new HashSet<>();
        }
        // 將已經檢查過的記錄下來,下次直接跳過
        alreadySeen.add(beanName);
        if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) {
            return true;
        }
    }
    return false;
}

DefaultSingletonBeanRegistry#registerDependentBean

public void registerDependentBean(String beanName, String dependentBeanName) {
    // 解析別名,獲取實際的beanName
    String canonicalName = canonicalName(beanName);
    // 加鎖
    synchronized (this.dependentBeanMap) {
        // 獲取canonicalName依賴beanName集合,如果為空默認創建一個LinkedHashSet當做默認值
        Set<String> dependentBeans =
            this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
        // 如果dependentBeanName已經記錄過了,直接返回
        if (!dependentBeans.add(dependentBeanName)) {
            return;
        }
    }
    // 加鎖
    synchronized (this.dependenciesForBeanMap) {
        // 這裡是和上面的dependentBeanMap倒過來,key為dependentBeanName
        Set<String> dependenciesForBean =
            this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
        dependenciesForBean.add(canonicalName);
    }
}

下面我們舉個A、B的 depends-on 屬性都是對方的例子:

首先獲取A,調用 isDependent() 方法,因為第一次獲取A,所以 dependentBeanMap 中沒有記錄依賴關係,直接返回 false;接着調用registerDependentBean(),這裡會向 dependentBeanMap 中反過來存儲依賴關係,也就是以B為 key value 是一個包含A的 Set集合。

接着會調用 getBean() 方法獲取B,首先調用 isDependent() 方法,因為在獲取A時已經存儲了B的依賴關係,所以獲取到的dependentBeans 的集合中包含A,所以直接返回true,拋出循環引用異常。

這個方法又引入了一個跟 dependentBeanMap 類似的緩存 dependenciesForBeanMap。這兩個緩存很容易搞混,這裏再舉一個簡單的例子:A 依賴 B,那麼 dependentBeanMap 存放的是 key 為 B,value 為含有 A 的 Set;而 dependenciesForBeanMap 存放的是key 為 A,value 為含有 B 的 Set

創建和註冊單例 bean

DefaultSingletonBeanRegistry#getSingleton

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 緩存中不存在當前 bean,也就是當前 bean 第一次創建
        if (singletonObject == null) {
            // 如果當前正在銷毀 singletons,拋出異常
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            // 創建單例 bean 之前的回調
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 獲取 bean 實例,在此處才會去真正調用創建 bean 的方法
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            // 省略異常處理...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 創建單例 bean 之後的回調
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 將 singletonObject 放入緩存
                addSingleton(beanName, singletonObject);
            }
        }
        // 返回 bean 實例
        return singletonObject;
    }
}

// 單例 bean 創建前的回調方法,默認實現是將 beanName 加入到當前正在創建 bean 的緩存中,
// 這樣便可以對循環依賴進行檢測
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

// 單例 bean 創建后的回調方法,默認實現是將 beanName 從當前正在創建 bean 的緩存中移除
protected void afterSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
        throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 這邊bean已經初始化完成了,放入一級緩存
        this.singletonObjects.put(beanName, singletonObject);
        // 移除三級緩存
        this.singletonFactories.remove(beanName);
        // 移除二級緩存
        this.earlySingletonObjects.remove(beanName);
        // 將 beanName 添加到已註冊 bean 緩存中
        this.registeredSingletons.add(beanName);
    }
}

自定義作用域示例

我們實現一個 ThreadLocal 級別的作用域,也就是同一個線程內 bean 是同一個實例,不同線程的 bean 是不同實例。首先我們繼承 Scope 接口實現,其中方法。如下:

public class ThreadLocalScope implements Scope {

    /** scope 名稱,在 XML 中的 scope 屬性就配置此名稱 */
    public static final String SCOPE_NAME = "thread-local";

    private final NamedThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal<>("thread-local-scope");

    /**
    * 返回實例對象,該方法被 Spring 調用
    */
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> context = getContext();
        Object object = context.get(name);
        if (object == null) {
            object = objectFactory.getObject();
            context.put(name, object);
        }
        return object;
    }

    /**
    * 獲取上下文 map
    */
    @NonNull
    private Map<String, Object> getContext() {
        Map<String, Object> map = threadLocal.get();
        if (map == null) {
            map = new HashMap<>();
            threadLocal.set(map);
        }
        return map;
    }

    @Override
    public Object remove(String name) {	
        return getContext().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // TODO
    }
	
    @Override
    public Object resolveContextualObject(String key) {
        Map<String, Object> context = getContext();
        return context.get(key);
    }

    @Override
    public String getConversationId() {
        return String.valueOf(Thread.currentThread().getId());
    }

}

上面的 ThreadLocalScope 重點關注下 get() 即可,該方法是被 Spring 調用的。

然後在 XML 中配置 beanscopethread-local。如下:

<bean id="user" name="user" class="com.leisurexi.ioc.domain.User" scope="thread-local">
    <property name="id" value="1"/>
    <property name="name" value="leisurexi"/>
</bean>

接着我們測試一下。測試類:

@Test
public void test() throws InterruptedException {
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    // 註冊自定義作用域
    beanFactory.registerScope(ThreadLocalScope.SCOPE_NAME, new ThreadLocalScope());
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
    reader.loadBeanDefinitions("META-INF/custom-bean-scope.xml");
    for (int i = 0; i < 3; i++) {
        Thread thread = new Thread(() -> {
            User user = beanFactory.getBean("user", User.class);
            System.err.printf("[Thread id :%d] user = %s%n", Thread.currentThread().getId(), user.getClass().getName() + "@" + Integer.toHexString(user.hashCode()));
            User user1 = beanFactory.getBean("user", User.class);
            System.err.printf("[Thread id :%d] user1 = %s%n", Thread.currentThread().getId(), user1.getClass().getName() + "@" + Integer.toHexString(user1.hashCode()));
        });
        thread.start();
        thread.join();
    }
}

說一下我們這裏的主要思路,新建了三個線程,查詢線程內 user bean 是否相等,不同線程是否不等。

結果如下圖:

總結

本文主要介紹了 getBean() 方法流程,我們可以重新梳理一下思路:

  1. 獲取 bean 實際名稱,如果緩存中存在直接取出實際 bean 返回。
  2. 緩存中不存在,判斷當前工廠是否有 BeanDefinition ,沒有遞歸去父工廠創建 bean
  3. 合併 BeanDefinition ,如果 depends-on 不為空,先去初始化依賴的 bean
  4. 如果 bean 的作用域是單例,調用 createBean() 方法創建實例,這個方法會執行 bean 的其它生命周期回調,以及屬性賦值等操作;接着執行單例 bean 創建前後的生命周期回調方法,並放入 singletonObjects 緩存起來。
  5. 如果 bean 的作用域是原型,調用 createBean() 方法創建實例,並執行原型 bean 前後調用生命周期回調方法。
  6. 如果 bean 的作用域是自定義的,獲取對應的 Scope 對象,調用重寫的 get() 方法獲取實例,並執行原型 bean 前後調用生命周期回調方法。
  7. 最後檢查所需的類型是否與實際 bean 實例的類型匹配,如果不等進行轉換,最後返回實例。

關於 createBean() 方法的細節,會在後續文章中進行分析。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

參考

  • 《Spring 源碼深度解析》—— 郝佳
  • https://github.com/geektime-geekbang/geekbang-lessons

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

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

vue3 全家桶體驗

前置

從創建一個簡單瀏覽器導航首頁項目展開,該篇隨筆包含以下內容的簡單上手

  • vite
  • vue3
  • vuex4
  • vue-router next

預覽效果有助於理清這些內容,限於篇幅,不容易展開敘述。由於項目邏輯簡單,只使用了少量 API,我只是寫這個小項目過把手癮,所以對應標題 上手。如果您只是想學習 vue 周邊的 API,那麼,這篇文章將給您帶來有限的知識。

初始化項目

使用 vite 初始化 vue3 項目。什麼是 vite?Vite 是一個 Web 構建工具。開發過程中通過瀏覽器 ES Module 導入為您的代碼提供服務,生成環境與 Rollup 捆綁在一起進行打包。

特性:

  • 閃電般快速的冷服務器啟
  • 動即時熱模塊更換(HMR)
  • 真正的按需編譯

vite 截至今天支持的功能:

  • Bare Module Resolving
  • Hot Module Replacement
  • TypeScript
  • CSS / JSON Importing
  • Asset URL Handling
  • PostCSS
  • CSS Modules
  • CSS Pre-processors
  • JSX
  • Web Assembly
  • Inline Web Workers
  • Custom Blocks
  • Config File
  • HTTPS/2
  • Dev Server Proxy
  • Production Build
  • Modes and Environment Variables
npm init vite-app aweshome
npm install
npm run dev
npm run build

最終生成的目錄結構與使用 vue-cli 相似:

│  .npmignore
│  a.txt
│  index.html
│  package.json
├─public
│      favicon.ico
└─src
    │  App.vue
    │  index.css
    │  main.js
    ├─assets
    │      logo.png
    └─components
            HelloWorld.vue

可以在項目根目錄下創建 vite.config.js 配置 Vite:

module.exports = {
  // 導入別名
  // 這些條目可以是精確的請求->請求映射*(精確,無通配符語法)
  // 也可以是請求路徑-> fs目錄映射。 *使用目錄映射時
  // 鍵**必須以斜杠開頭和結尾**
  alias: {
    // 'react': '@pika/react',
    // 'react-dom': '@pika/react-dom'
    // '/@foo/': path.resolve(__dirname, 'some-special-dir'),
  },
  // 配置Dep優化行為
  optimizeDeps: {
    // exclude: ['dep-a', 'dep-b'],
  },
  // 轉換Vue自定義塊的功能。
  vueCustomBlockTransforms: {
    // i18n: src => `export default Comp => { ... }`,
  },
  // 為開發服務器配置自定義代理規則。
  proxy: {
    // proxy: {
    //   '/foo': 'http://localhost:4567/foo',
    //   '/api': {
    //     target: 'http://jsonplaceholder.typicode.com',
    //     changeOrigin: true,
    //     rewrite: path => path.replace(/^\/api/, ''),
    //   },
    // },
  },
  // ...
}

更多配置可以參考Github。

另外,現在可以使用 vitepress 代替原來的 vuepress 構建文檔或博客。

vue-router next

npm i vue-router@next

src/router/index.js

import {createRouter, createWebHistory} from 'vue-router'
import Home from '../components/home/Home.vue'
import Cards from '../components/cards/Cards.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // route -> routes
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/cards',
      name: 'cards',
      component: Cards,
    },
  ],
})

export default router

vue router next 還添加了動態路由,解決規則衝突的問題。做過權限管理應該深有體會。更多配置可以參考 Github。

vuex4

使用與 vuex3 相同的 API。

安裝

npm i vuex@next

src/constants 下存放了靜態數據,它們都是如下形式:

export const vue = [
  {
    title: 'vue',
    desc: 'Vue 是用於構建用戶界面的漸進式的框架',
    link: 'https://cn.vuejs.org/v2/guide/',
    img: import('../assets/images/vue.png'), // require -> import
  },
  {
    title: 'vue Router',
    desc: 'Vue Router 是 Vue.js 官方的路由管理器。',
    link: 'https://router.vuejs.org/zh/',
    img: import('../assets/images/vue.png'),
  },
  // ...
]

src/store/index.js

import {createStore} from 'vuex'

import {vue, react, wechat, across, compileBuild} from '../constants/docs'
import {frontEndTools, OfficeTools} from '../constants/tools'
import {tools, docs, community} from '../constants/asideData'
import {blogs} from '../constants/community'

const store = createStore({
  state: {
    asideData: [],
    mainData: [],
  },
  mutations: {
    setAsideData(state, key) {
      const asideActions = {
        '2': tools,
        '3': docs,
        '4': community,
      }
      state.asideData = asideActions[key]
    },
    setMainData(state, menuItemText) {
      const actions = new Map([
        ['前端工具', frontEndTools],
        ['辦公工具', OfficeTools],
        ['vue', vue],
        ['react', react],
        ['微信開發', wechat],
        ['跨端框架', across],
        ['編譯構建', compileBuild],
        ['博客', blogs],
      ])
      state.mainData = actions.get(menuItemText)
    },
  },
  actions: {},
  modules: {},
})

export default store

main.js

結合上文的 vuex vue-router 可以看出,vue3 核心插件的 api 都做了簡化。

import './index.css'
import {createApp} from 'vue'
import store from './store'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(store)
app.use(router)
app.mount('#app')

sass

npm i sass

package.json > dependencies

{
  "dependencies": {
    "vue": "^3.0.0-beta.15",
    "vue-router": "^4.0.0-alpha.13",
    "vuex": "^4.0.0-beta.2"
  },
  "devDependencies": {
    "@vue/compiler-sfc": "^3.0.0-beta.15",
    "sass": "^1.26.8",
    "vite": "^1.0.0-beta.1"
  }
}

components

這個小項目本質上可以只有一個頁面 .vue 構成,我將它拆分,便於閱讀。

App.vue

<template>
  <Header />
  <main>
    <router-view></router-view>
  </main>
  <Footer />
</template>

<script>
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'

export default {
  name: 'app',
  components: {
    Header,
    Footer,
  },
}
</script>

<style>
main {
  flex: 1;
}
</style>

components/cards/Aside.vue

<template>
  <aside>
    <ul>
      <li :index="item.index" v-for="item in this.$store.state.asideData" :key="item.index" ref="menuItem" @click="handleSelect(item.value)">
        <i class="fas fa-home"></i>
        <span>{{ item.value }}</span>
      </li>
    </ul>
  </aside>
</template>

<script>
import store from '../../store'

export default {
  setup(props, context) {
    return {
      handleSelect(value) {
        store.commit('setMainData', value)
      },
    }
  },
}
</script>

<style lang="scss">
aside {
  flex: 1;
  background-color: rgb(238, 238, 238);
  height: 100%;
  li {
    display: flex;
    align-items: center;
    height: 56px;
    line-height: 56px;
    font-size: 14px;
    color: #303133;
    padding: 0 1.4rem;
    list-style: none;
    cursor: pointer;
    transition: border-color 0.3s, background-color 0.3s, color 0.3s;
    white-space: nowrap;
    &:hover {
      background-color: rgb(224, 224, 224);
    }
  }
}

@media screen and (max-width: 768px) {
  aside {
    display: none;
    &.active {
      display: block;
    }
  }
}
</style>

components/cards/Cards.vue

<template>
  <div id="card-outer">
    <Aside />
    <section></section>
  </div>
</template>

<script>
import Aside from './Aside.vue'
import router from '../../router'

export default {
  components: {
    Aside,
  },
}
</script>

<style lang="scss">
#card-outer {
  display: flex;
  align-content: stretch;
  height: 100%;
  & > section {
    flex: 8;
  }
}

.main-card {
  margin: 10px 0;
  cursor: pointer;
  .main-card-content {
    display: flex;
    align-items: center;
    img {
      width: 30px;
      height: 30px;
      margin-right: 10px;
    }
    .main-card-content-info {
      width: 90%;
      h3 {
        font-size: 14px;
      }
      p {
        font-size: 12px;
        color: #888ea2;
        white-space: nowrap;
        text-overflow: ellipsis;
        overflow: hidden;
        width: 100%;
        line-height: 1.8;
      }
    }
    span {
      margin-left: 10px;
      text-decoration: none;
      &:nth-of-type(1) {
        font-size: 18px;
        font-weight: 700;
        color: #ffa502;
        white-space: nowrap;
      }
      &:nth-of-type(2) {
        font-size: 14px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    }
  }
}
</style>

components/home/Home.vue

<template>
  <section id="search">
    <div class="search-sources" style="margin-bottom: 10px;">
      <span size="mini" type="primary" v-for="(item, index) in source" @click="changeSource(item.name)" :key="index" :style="`background:${item.color};border-color:${item.color}`"
        >{{ item.name }}
      </span>
    </div>
    <div class="searchbox" :class="searchbarStyle.className">
      <input :placeholder="searchbarStyle.placeholder" v-model="searchValue" clearable v-on:keyup.enter="submit" />
      <button @click="submit" slot="append" icon="el-icon-search">
        <i class="fas fa-search"></i>
      </button>
    </div>
  </section>
</template>

<script>
export default {
  data: () => ({
    baseUrl: 'https://www.baidu.com/s?ie=UTF-8&wd=',
    searchValue: '',
    searchbarStyle: {
      className: 'baidu',
      placeholder: '百度一下,你就知道',
    },
    source: [
      {
        name: '百度',
        color: '#2932E1',
      },
      {
        name: '搜狗',
        color: '#FF6F17',
      },
      {
        name: 'Bing',
        color: '#0c8484',
      },
      {
        name: 'Google',
        color: '#4285F4',
      },
      {
        name: 'NPM',
        color: '#EA4335',
      },
    ],
  }),
  methods: {  // 可以在 vue3 中使用 options API
    changeSource(name) {
      const actions = new Map([
        [
          '百度',
          () => {
            this.baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
            this.searchbarStyle = {
              className: 'baidu',
              placeholder: '百度一下,你就知道',
            }
          },
        ],
        [
          'Bing',
          () => {
            this.baseUrl = 'https://cn.bing.com/search?FORM=BESBTB&q='
            this.searchbarStyle = {
              className: 'bing',
              placeholder: '必應搜索',
            }
          },
        ],
        [
          '搜狗',
          () => {
            this.baseUrl = 'https://www.sogou.com/web?query='
            this.searchbarStyle = {
              className: 'sougou',
              placeholder: '搜狗搜索',
            }
          },
        ],
        [
          'Google',
          () => {
            this.baseUrl = 'https://www.google.com/search?q='
            this.searchbarStyle = {
              className: 'google',
              placeholder: 'Google Search',
            }
          },
        ],
        [
          'NPM',
          () => {
            this.baseUrl = 'https://www.npmjs.com/search?q='
            this.searchbarStyle = {
              className: 'npm',
              placeholder: 'Search Packages',
            }
          },
        ],
      ])
      actions.get(name)()
    },
    submit() {
      const url = this.baseUrl + this.searchValue
      window.open(url)
    },
  },
}
</script>

<style lang="scss">
#search {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-content: stretch;
  margin: 0 auto;
  height: 40vh;
  width: 40%;
  & > div {
    display: flex;
  }
}

.search-sources {
  span {
    margin-right: 0.5rem;
    padding: 0.4rem 0.6rem;
    color: #fff;
    font-size: 14px;
    line-height: 14px;
    border-radius: 2px;
    &:hover {
      filter: contrast(80%);
      transition: 0.3s;
    }
  }
}

.searchbox {
  padding-left: 1rem;
  height: 2.6rem;
  border-radius: 6px;
  background-color: #fff;
  border: 1px #ccc solid;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  input {
    flex: 7;
    border: none;
    font-size: 1rem;
  }

  button {
    flex: 1;
    i {
      margin-right: 0;
    }
  }
}

$sources-color: (
  baidu: #2932e1,
  bing: #0c8484,
  sougou: #ff6f17,
  google: #4285f4,
  npm: #ea4335,
);

$source-list: baidu bing sougou google npm;

@each $source in $source-list {
  .#{$source} {
    &:hover {
      border-color: map-get($sources-color, $source);
      box-shadow: 0 2px 4px map-get($sources-color, $source);
      transition: all 0.5s;
    }
    input {
      &:hover {
        border-color: map-get($sources-color, $source);
      }
    }
  }
}

@media screen and (max-width: 768px) {
  #search {
    width: 90%;
  }
}
</style>

components/Header.vue

<template>
  <header>
    <ul class="nav">
      <li @click="handleSelect('home')">
        <i class="fas fa-home"></i>
        <span>首頁</span>
      </li>
      <li @click="handleSelect('tools')">
        <i class="fas fa-tools"></i>
        <span>工具</span>
      </li>
      <li @click="handleSelect('docs')">
        <i class="fas fa-file-alt"></i>
        <span>文檔</span>
      </li>
      <li @click="handleSelect('community')">
        <i class="fas fa-comment-dots"></i>
        <span>社區</span>
      </li>
    </ul>
    <MobileMenu />
  </header>
</template>

<script>
import MobileMenu from './MobileMenu.vue'
import store from '../store'
import router from '../router'

export default {
  components: {
    MobileMenu,
  },

  setup() {
    const handleSelect = item => {
      store.commit('setAsideData', item)
      if (item === 'home') {
        router.replace({name: 'home'})
      } else {
        const actions = {
          tools: ['setMainData', '前端工具'],
          docs: ['setMainData', 'vue'],
          community: ['setMainData', '博客'],
        }
        store.commit(actions[item][0], actions[item][1])
        router.replace({name: 'cards'})
      }
    }

    return {
      handleSelect,
    }
  },
}
</script>

<style lang="scss">
header {
  display: flex;
  height: 60px;
  align-content: stretch;
  padding: 0 9.5rem;
}

.nav {
  display: flex;
  align-items: center;
  align-content: stretch;
  li {
    padding: 0.5rem 0.75rem;
    &:hover {
      background-color: #f3f1f1;
      & span {
        color: #3273dc;
      }
    }
  }
}

@media screen and (max-width: 768px) {
  header {
    padding: 0;
  }
}
</style>

components/MobileMenu.vue

<template>
  <section id="mobile-menu">
    <div id="navbarBurger" class="navbar-burger burger" data-target="navMenuMore" :class="{active}" @click="sideToggle">
      <span></span>
      <span></span>
      <span></span>
    </div>
  </section>
</template>

<script>
export default {
  data: () => ({
    active: false,
  }),
  methods: {
    sideToggle() {
      this.active = !this.active
      const classList = document.querySelectorAll('aside')[0].classList
      this.active ? classList.add('active') : classList.remove('active')
    },
  },
}
</script>

<style lang="scss">
#mobile-menu {
  display: none;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 999999;
}

@media screen and (max-width: 768px) {
  #mobile-menu {
    display: block;
    .navbar-burger {
      position: relative;
      color: #835656;
      cursor: pointer;
      height: 60px;
      width: 60px;
      margin-left: auto;
      span {
        background-color: #333;
        display: block;
        height: 1px;
        left: calc(50% - 8px);
        position: absolute;
        transform-origin: center;
        transition-duration: 86ms;
        transition-property: background-color, opacity, transform;
        transition-timing-function: ease-out;
        width: 16px;
        &:nth-child(1) {
          top: calc(50% - 6px);
        }
        &:nth-child(2) {
          top: calc(50% - 1px);
        }
        &:nth-child(3) {
          top: calc(50% + 4px);
        }
      }
      &.active {
        span {
          &:nth-child(1) {
            transform: translateY(5px) rotate(45deg);
          }
          &:nth-child(2) {
            opacity: 0;
          }
          &:nth-child(3) {
            transform: translateY(-5px) rotate(-45deg);
          }
        }
      }
    }
  }
}
</style>

最後

一套流程下來,vite 給我的感覺就是“快”。對於 vue 周邊, API 都是做了一些簡化,如果你對 esm 有些了解,將更有利於組織項目,可讀性相比 vue2.x 也更高。也有一些問題,限於篇幅,本文沒有探討。做項目還是上 vue2.x 及其周邊。另外,我沒找到 vue3 組件庫。

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

【其他文章推薦】

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

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

Java 數組最佳指南,快收藏讓它吃灰

兩年前,我甚至寫過一篇文章,吐槽數組在 Java 中挺雞肋的,因為有 List 誰用數組啊,現在想想那時候的自己好幼稚,好可笑。因為我只看到了表面現象,實際上呢,List 的內部仍然是通過數組實現的,比如說 ArrayList,在它的源碼里可以看到下面這些內容:

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */

transient Object[] elementData; // non-private to simplify nested class access

/**
 * The size of the ArrayList (the number of elements it contains).
 *
 * @serial
 */

private int size;

數組在 Java 中,必須算是核心,神一般的存在。

01、什麼是數組

按照 Javadoc 給出的解釋,數組是一個對象,它包含了一組固定數量的元素,並且這些元素的類型是相同的。數組會按照索引的方式將元素放在指定的位置上,意味着我們可以通過索引來訪問到這些元素。在 Java 中,索引是從 0 開始的。

我們可以將數組理解為一個個整齊排列的單元格,每個單元格裏面存放着一個元素。

數組元素的類型可以是基本數據類型(比如說 int、double),也可以是引用數據類型(比如說 String),包括自定義類型的對象。

了解了數組的定義后,讓我們來深入地研究一下數組的用法。

在 Java 中,數組的聲明方式有兩種。

先來看第一種:

int[] anArray;

再來看第二種:

int anOtherArray[];

不同之處就在於中括號的位置,是緊跟類型,還是放在變量名的後面。前者比後者的使用頻率更高一些。

接下來就該看看怎麼初始化數組了,同樣有多種方式可以初始化數組,比如說最常見的是:

int[] anArray = new int[10];

使用了 new 關鍵字,對吧?這就意味着數組的確是一個對象。然後,在方括號中指定了數組的長度,這是必須的。

這時候,數組中的每個元素都會被初始化為默認值,int 類型的就為 0,Object 類型的就為 null。

另外,還可以使用大括號的方式,直接初始化數組中的元素:

int anOtherArray[] = new int[] {12345};

這時候,數組的元素分別是 1、2、3、4、5,索引依次是 0、1、2、3、4。

02、訪問數組

前面提到過,可以通過索引來訪問數組的元素,就像下面這樣:

anArray[0] = 10;
System.out.println(anArray[0]);

通過數組的變量名,加上中括號,加上元素的索引,就可以訪問到數組,通過“=”操作符進行賦值。

如果索引的值超出了數組的界限,就會拋出 ArrayIndexOutOfBoundException,關於這方面的知識,我之前特意寫過一篇文章,如果你感興趣的話,可以跳轉過去看看。

為什麼會發生ArrayIndexOutOfBoundsException

我覺得原因挺有意思的。

既然數組的索引是從 0 開始,那就是到數組的 length - 1 結束,不要使用超出這個範圍內的索引訪問數組,就不會拋出數組越界的異常了。

03、遍曆數組

當數組的元素非常多的時候,逐個訪問數組就太辛苦了,所以需要通過遍歷的方式。

第一種,使用 for 循環:

int anOtherArray[] = new int[] {12345};
for (int i = 0; i < anOtherArray.length; i++) {
    System.out.println(anOtherArray[i]);
}

通過 length 屬性獲取到數組的長度,然後索引從 0 開始遍歷,就得到了數組的所有元素。

第二種,使用 for-each 循環:

for (int element : anOtherArray) {
    System.out.println(element);
}

如果不需要關心索引的話(意味着不需要修改數組的某個元素),使用 for-each 遍歷更簡潔一些。當然,也可以使用 while 和 do-while 循環。

04、可變參數

可變參數用於將任意數量的參數傳遞給方法:

void varargsMethod(String... varargs) {}

varargsMethod() 方法可以傳遞任意數量的字符串參數,可以是 0 個或者 N 個,本質上,可變參數就是通過數組實現的,為了證明這一點,我們可以通過 jad 反編譯一下字節碼:

public class VarargsDemo
{

    public VarargsDemo()
    
{
    }

    transient void varargsMethod(String as[])
    
{
    }
}

所以我們其實可以直接將數組作為參數傳遞給可變參數的方法:

VarargsDemo demo = new VarargsDemo();
String[] anArray = new String[] {"沉默王二""一枚有趣的程序員"};
demo.varargsMethod(anArray);

也可以直接傳遞多個字符串,通過逗號隔開的方式:

demo.varargsMethod("沉默王二""一枚有趣的程序員");

05、把數組轉成 List

List 封裝了很多常用的方法,方便我們對集合進行一些操作,而如果直接操作數組的話,多有不便,因此有時候我們需要把數組轉成 List。

最原始的方式,就是通過遍曆數組的方式,一個個將數組添加到 List 中。

int[] anArray = new int[] {12345};

List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
    aList.add(element);
}

更優雅的方式是通過 Arrays 類的 asList() 方法:

List<Integer> aList = Arrays.asList(anArray);

但需要注意的是,該方法返回的 ArrayList 並不是 java.util.ArrayList,它其實是 Arrays 類的一個內部類:

private static class ArrayList<Eextends AbstractList<E>
        implements RandomAccessjava.io.Serializable
{}

如果需要添加元素或者刪除元素的話,最好把它轉成 java.util.ArrayList

new ArrayList<>(Arrays.asList(anArray));

06、把數組轉成 Stream

Java 8 新增了 Stream 流的概念,這就意味着我們也可以將數組轉成 Stream 進行操作,而不是 List。

String[] anArray = new String[] {"沉默王二""一枚有趣的程序員""好好珍重他"};
Stream<String> aStream = Arrays.stream(anArray);

也可以直接對數組的元素進行剪輯,通過指定索引的方式:

Stream<String> anotherStream = Arrays.stream(anArray, 13);

結果包含”一枚有趣的程序員”和”好好珍重他”,1 這個索引位置包括,3 這個索引位置不包括。

07、數組排序

Arrays 類提供了一個 sort() 方法,可以對數組進行排序。

  • 基本數據類型按照升序排列
  • 實現了 Comparable 接口的對象按照 compareTo() 的排序

來看第一個例子:

int[] anArray = new int[] {52148};
Arrays.sort(anArray);

排序后的結果如下所示:

[12458]

來看第二個例子:

String[] yetAnotherArray = new String[] {"A""E""Z""B""C"};
Arrays.sort(yetAnotherArray, 13,
                Comparator.comparing(String::toString).reversed());

只對 1-3 位置上的元素進行反序,所以結果如下所示:

[A, Z, E, B, C]

08、數組搜索

有時候,我們需要從數組中查找某個具體的元素,最直接的方式就是通過遍歷的方式:

int[] anArray = new int[] {52148};
for (int i = 0; i < anArray.length; i++) {
    if (anArray[i] == 4) {
        System.out.println("找到了 " + i);
        break;
    }
}

上例中從數組中查詢元素 4,找到后通過 break 關鍵字退出循環。

如果數組提前進行了排序,就可以使用二分查找法,這樣效率就會更高一些。Arrays.binarySearch() 方法可供我們使用,它需要傳遞一個數組,和要查找的元素。

int[] anArray = new int[] {12345};
int index = Arrays.binarySearch(anArray, 4);

09、總結

除了一維數組,還有二維數組,但說實話,二維數組不太常用,這裏就不再介紹了,感興趣的話,可以嘗試打印以下楊輝三角。

這篇文章,我們介紹了 Java 數組的基本用法和一些高級用法,我想小夥伴們應該已經完全掌握了。

我是沉默王二,一枚有趣的程序員。如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。

原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。

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

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案