特斯拉經典跑車 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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

設計模式系列之中介者模式(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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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。

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

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

這一次搞懂SpringBoot核心原理(自動配置、事件驅動、Condition)

@

目錄

  • 前言
  • 正文
    • 啟動原理
    • 事件驅動
    • 自動配置原理
    • Condition註解原理
  • 總結

前言

SpringBoot是Spring的包裝,通過自動配置使得SpringBoot可以做到開箱即用,上手成本非常低,但是學習其實現原理的成本大大增加,需要先了解熟悉Spring原理。如果還不清楚Spring原理的,可以先查看博主之前的文章,本篇主要分析SpringBoot的啟動、自動配置、Condition、事件驅動原理。

正文

啟動原理

SpringBoot啟動非常簡單,因其內置了Tomcat,所以只需要通過下面幾種方式啟動即可:

@SpringBootApplication(scanBasePackages = {"cn.dark"})
public class SpringbootDemo {

    public static void main(String[] args) {
    	// 第一種
        SpringApplication.run(SpringbootDemo .class, args);

		// 第二種
        new SpringApplicationBuilder(SpringbootDemo .class)).run(args);

		// 第三種
        SpringApplication springApplication = new SpringApplication(SpringbootDemo.class);
        springApplication.run();		
    }
}

可以看到第一種是最簡單的,也是最常用的方式,需要注意類上面需要標註@SpringBootApplication註解,這是自動配置的核心實現,稍後分析,先來看看SpringBoot啟動做了些什麼?
在往下之前,不妨先猜測一下,run方法中需要做什麼?對比Spring源碼,我們知道,Spring的啟動都會創建一個ApplicationContext的應用上下文對象,並調用其refresh方法啟動容器,SpringBoot只是Spring的一層殼,肯定也避免不了這樣的操作。另一方面,以前通過Spring搭建的項目,都需要打成War包發布到Tomcat才行,而現在SpringBoot已經內置了Tomcat,只需要打成Jar包啟動即可,所以在run方法中肯定也會創建對應的Tomcat對象並啟動。以上只是我們的猜想,下面就來驗證,進入run方法:

	public ConfigurableApplicationContext run(String... args) {
		// 統計時間用的工具類
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		// 獲取實現了SpringApplicationRunListener接口的實現類,通過SPI機制加載
		// META-INF/spring.factories文件下的類
		SpringApplicationRunListeners listeners = getRunListeners(args);

		// 首先調用SpringApplicationRunListener的starting方法
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

			// 處理配置數據
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);

			// 啟動時打印banner
			Banner printedBanner = printBanner(environment);

			// 創建上下文對象
			context = createApplicationContext();

			// 獲取SpringBootExceptionReporter接口的類,異常報告
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);

			prepareContext(context, environment, listeners, applicationArguments, printedBanner);

			// 核心方法,啟動spring容器
			refreshContext(context);
			afterRefresh(context, applicationArguments);

			// 統計結束
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 調用started
			listeners.started(context);

			// ApplicationRunner
			// CommandLineRunner
			// 獲取這兩個接口的實現類,並調用其run方法
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			// 最後調用running方法
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

SpringBoot的啟動流程就是這個方法,先看getRunListeners方法,這個方法就是去拿到所有的SpringApplicationRunListener實現類,這些類是用於SpringBoot事件發布的,關於事件驅動稍後分析,這裏主要看這個方法的實現原理:

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
	}

	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = getClassLoader();
		// Use names and ensure unique to protect against duplicates
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 加載上來后反射實例化
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}

	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
	}

一步步追蹤下去可以看到最終就是通過SPI機制根據接口類型從META-INF/spring.factories文件中加載對應的實現類並實例化,SpringBoot的自動配置也是這樣實現的。為什麼要這樣實現呢?通過註解掃描不可以么?當然不行,這些類都在第三方jar包中,註解掃描實現是很麻煩的,當然你也可以通過@Import註解導入,但是這種方式不適合擴展類特別多的情況,所以這裏採用SPI的優點就顯而易見了。
回到run方法中,可以看到調用了createApplicationContext方法,見名知意,這個就是去創建應用上下文對象:

	public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

	protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET:
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

注意這裏通過反射實例化了一個新的沒見過的上下文對象AnnotationConfigServletWebServerApplicationContext,這個是SpringBoot擴展的,看看其構造方法:

	public AnnotationConfigServletWebServerApplicationContext() {
		this.reader = new AnnotatedBeanDefinitionReader(this);
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

如果你有看過Spring註解驅動的實現原理,這兩個對象肯定不會陌生,一個實支持註解解析的,另外一個是掃描包用的。
上下文創建好了,下一步自然就是調用refresh方法啟動容器:


	private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		if (this.registerShutdownHook) {
			try {
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

	protected void refresh(ApplicationContext applicationContext) {
		Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
		((AbstractApplicationContext) applicationContext).refresh();
	}

這裏首先會調用到其父類中ServletWebServerApplicationContext

	public final void refresh() throws BeansException, IllegalStateException {
		try {
			super.refresh();
		}
		catch (RuntimeException ex) {
			stopAndReleaseWebServer();
			throw ex;
		}
	}

可以看到是直接委託給了父類:

	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

這個方法不會陌生吧,之前已經分析過了,這裏不再贅述,至此SpringBoot的容器就啟動了,但是Tomcat啟動是在哪裡呢?run方法中也沒有看到。實際上Tomcat的啟動也是在refresh流程中,這個方法其中一步是調用了onRefresh方法,在Spring中這是一個沒有實現的模板方法,而SpringBoot就通過這個方法完成了Tomcat的啟動:

	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

	private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();
			// 主要看這個方法
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

這裏首先拿到TomcatServletWebServerFactory對象,通過該對象再去創建和啟動Tomcat:

	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

上面的每一步都可以對比Tomcat的配置文件,需要注意默認只支持了http協議:

	Connector connector = new Connector(this.protocol);

	private String protocol = DEFAULT_PROTOCOL;
	public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";

如果想要擴展的話則可以對additionalTomcatConnectors屬性設置值,需要注意這個屬性沒有對應的setter方法,只有addAdditionalTomcatConnectors方法,也就是說我們只能通過實現BeanFactoryPostProcessor接口的postProcessBeanFactory方法,而不能通過BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法,因為前者可以通過傳入的BeanFactory對象提前獲取到TomcatServletWebServerFactory對象調用addAdditionalTomcatConnectors即可;而後者只能拿到BeanDefinition對象,該對象只能通過setter方法設置值。

事件驅動

Spring原本就提供了事件機制,而在SpringBoot中又對其進行擴展,通過發布訂閱事件在容器的整個生命周期的不同階段進行不同的操作。我們先來看看SpringBoot啟動關閉的過程中默認會發布哪些事件,使用下面的代碼即可:

@SpringBootApplication
public class SpringEventDemo {

    public static void main(String[] args) {
        new SpringApplicationBuilder(SpringEventDemo.class)
                .listeners(event -> {
                    System.err.println("接收到事件:" + event.getClass().getSimpleName());
                })
                .run()
                .close();
    }

}

這段代碼會在控制台打印所有的事件名稱,按照順序如下:

  • ApplicationStartingEvent:容器啟動
  • ApplicationEnvironmentPreparedEvent:環境準備好
  • ApplicationContextInitializedEvent:上下文初始化完成
  • ApplicationPreparedEvent:上下文準備好
  • ContextRefreshedEvent:上下文刷新完
  • ServletWebServerInitializedEvent:webServer初始化完成
  • ApplicationStartedEvent:容器啟動完成
  • ApplicationReadyEvent:容器就緒
  • ContextClosedEvent:容器關閉

以上是正常啟動關閉,如果發生異常還有發布ApplicationFailedEvent事件。事件的發布遍布在整個容器的啟動關閉周期中,事件發布對象剛剛我們也看到了是通過SPI加載的SpringApplicationRunListener實現類EventPublishingRunListener,同樣事件監聽器也是在spring.factories文件中配置的,默認實現了以下監聽器:

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

可以看到有用於文件編碼的(FileEncodingApplicationListener),有加載日誌框架的(LoggingApplicationListener),還有加載配置的(ConfigFileApplicationListener)等等一系列監聽器,SpringBoot也就是通過這系列監聽器將必要的配置和組件加載到容器中來,這裏不再詳細分析,感興趣的讀者可以通過其實現的onApplicationEvent方法看到每個監聽器究竟是監聽的哪一個事件,當然事件發布和監聽我們自己也是可以擴展的。

自動配置原理

SpringBoot最核心的還是自動配置,為什麼它能做到開箱即用,不再需要我們手動使用@EnableXXX等註解來開啟?這一切的答案就在@SpringBootApplication註解中:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

這裏重要的註解有三個:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。@ComponentScan就不用再說了,@SpringBootConfiguration等同於@Configuration,而@EnableAutoConfiguration就是開啟自動配置:

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

@AutoConfigurationPackage註解的作用就是將該註解所標記類所在的包作為自動配置的包,簡單看看就行,主要看AutoConfigurationImportSelector,這個就是實現自動配置的核心類,注意這個類是實現的DeferredImportSelector接口。
在這個類中有一個selectImports方法。這個方法在我之前的文章這一次搞懂Spring事務註解的解析也有分析過,只是實現類不同,它同樣會被ConfigurationClassPostProcessor類調用,先來看這個方法做了些什麼:

	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		// 獲取所有的自動配置類
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

	protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		// SPI獲取EnableAutoConfiguration為key的所有實現類
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		// 把某些自動配置類過濾掉
		configurations = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		// 包裝成自動配置實體類
		return new AutoConfigurationEntry(configurations, exclusions);
	}

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		// SPI獲取EnableAutoConfiguration為key的所有實現類
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

追蹤源碼最終可以看到也是從META-INF/spring.factories文件中拿到所有EnableAutoConfiguration對應的值(在spring-boot-autoconfigure中)並通過反射實例化,過濾后包裝成AutoConfigurationEntry對象返回。
看到這裏你應該會覺得自動配置的實現就是通過這個selectImports方法,但實際上這個方法通常並不會被調用到,而是會調用該類的內部類AutoConfigurationGroupprocessselectImports方法,前者同樣是通過getAutoConfigurationEntry拿到所有的自動配置類,而後者這是過濾排序並包裝后返回。
下面就來分析ConfigurationClassPostProcessor是怎麼調用到這裏的,直接進入processConfigBeanDefinitions方法:

	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
		String[] candidateNames = registry.getBeanDefinitionNames();

		for (String beanName : candidateNames) {
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}

		// Return immediately if no @Configuration classes were found
		if (configCandidates.isEmpty()) {
			return;
		}

		// Sort by previously determined @Order value, if applicable
		configCandidates.sort((bd1, bd2) -> {
			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
			return Integer.compare(i1, i2);
		});

		// Detect any custom bean name generation strategy supplied through the enclosing application context
		SingletonBeanRegistry sbr = null;
		if (registry instanceof SingletonBeanRegistry) {
			sbr = (SingletonBeanRegistry) registry;
			if (!this.localBeanNameGeneratorSet) {
				BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
						AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
				if (generator != null) {
					this.componentScanBeanNameGenerator = generator;
					this.importBeanNameGenerator = generator;
				}
			}
		}

		if (this.environment == null) {
			this.environment = new StandardEnvironment();
		}

		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
			parser.parse(candidates);
			parser.validate();

			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			configClasses.removeAll(alreadyParsed);

			// Read the model and create bean definitions based on its content
			if (this.reader == null) {
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}
			this.reader.loadBeanDefinitions(configClasses);
			alreadyParsed.addAll(configClasses);

			// 省略。。。。
	}

前面一大段主要是拿到合格的Configuration配置類,主要邏輯是在ConfigurationClassParser.parse方法中,該方法完成了對@Component、@Bean、@Import、@ComponentScans等註解的解析,這裏主要看對@Import的解析,其它的讀者可自行分析。一步步追蹤,最終會進入到processConfigurationClass方法:

	protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
		if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
			return;
		}

		ConfigurationClass existingClass = this.configurationClasses.get(configClass);
		if (existingClass != null) {
			if (configClass.isImported()) {
				if (existingClass.isImported()) {
					existingClass.mergeImportedBy(configClass);
				}
				// Otherwise ignore new imported config class; existing non-imported class overrides it.
				return;
			}
			else {
				// Explicit bean definition found, probably replacing an import.
				// Let's remove the old one and go with the new one.
				this.configurationClasses.remove(configClass);
				this.knownSuperclasses.values().removeIf(configClass::equals);
			}
		}

		// Recursively process the configuration class and its superclass hierarchy.
		SourceClass sourceClass = asSourceClass(configClass);
		do {
			sourceClass = doProcessConfigurationClass(configClass, sourceClass);
		}
		while (sourceClass != null);

		this.configurationClasses.put(configClass, configClass);
	}

這裏需要注意this.conditionEvaluator.shouldSkip方法的調用,這個方法就是進行Bean加載過濾的,即根據@Condition註解的匹配值判斷是否加載該Bean,具體實現稍後分析,繼續跟蹤主流程doProcessConfigurationClass

	protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
			throws IOException {
		省略....

		// Process any @Import annotations
		processImports(configClass, sourceClass, getImports(sourceClass), true);

		省略....
		return null;
	}

這裏就是完成對一系列註解的支撐,我省略掉了,主要看processImports方法,這個方法就是處理@Import註解的:

	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);
						if (selector instanceof DeferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
							processImports(configClass, currentSourceClass, importSourceClasses, false);
						}
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass));
					}
				}
			}
		}
	}

剛剛我提醒過AutoConfigurationImportSelector是實現DeferredImportSelector接口的,如果不是該接口的實現類則是直接調用selectImports方法,反之則是調用DeferredImportSelectorHandler.handle方法:

		private List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();
		
		public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
			DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
					configClass, importSelector);
			if (this.deferredImportSelectors == null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				handler.register(holder);
				handler.processGroupImports();
			}
			else {
				this.deferredImportSelectors.add(holder);
			}
		}

首先創建了一個DeferredImportSelectorHolder對象,如果是第一次執行則是添加到deferredImportSelectors屬性中,等到ConfigurationClassParser.parse的最後調用process方法:

	public void parse(Set<BeanDefinitionHolder> configCandidates) {
		省略.....

		this.deferredImportSelectorHandler.process();
	}

	public void process() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		try {
			if (deferredImports != null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
				deferredImports.forEach(handler::register);
				handler.processGroupImports();
			}
		}
		finally {
			this.deferredImportSelectors = new ArrayList<>();
		}
	}

反之則是直接執行,首先通過register拿到AutoConfigurationGroup對象:

	public void register(DeferredImportSelectorHolder deferredImport) {
		Class<? extends Group> group = deferredImport.getImportSelector()
				.getImportGroup();
		DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
				(group != null ? group : deferredImport),
				key -> new DeferredImportSelectorGrouping(createGroup(group)));
		grouping.add(deferredImport);
		this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
				deferredImport.getConfigurationClass());
	}

	public Class<? extends Group> getImportGroup() {
		return AutoConfigurationGroup.class;
	}

然後在processGroupImports方法中進行真正的處理:

		public void processGroupImports() {
			for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
				grouping.getImports().forEach(entry -> {
					ConfigurationClass configurationClass = this.configurationClasses.get(
							entry.getMetadata());
					try {
						processImports(configurationClass, asSourceClass(configurationClass),
								asSourceClasses(entry.getImportClassName()), false);
					}
					catch (BeanDefinitionStoreException ex) {
						throw ex;
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to process import candidates for configuration class [" +
										configurationClass.getMetadata().getClassName() + "]", ex);
					}
				});
			}
		}

		public Iterable<Group.Entry> getImports() {
			for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
				this.group.process(deferredImport.getConfigurationClass().getMetadata(),
						deferredImport.getImportSelector());
			}
			return this.group.selectImports();
		}

getImports方法中就完成了對processselectImports方法的調用,拿到自動配置類后再遞歸調用調用processImports方法完成對自動配置類的加載。至此,自動配置的加載過程就分析完了,下面是時序圖:

Condition註解原理

在自動配置類中有很多Condition相關的註解,以AOP為例:

Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Advice.class)
	static class AspectJAutoProxyingConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
				matchIfMissing = false)
		static class JdkDynamicAutoProxyConfiguration {

		}

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		static class CglibAutoProxyConfiguration {

		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
			matchIfMissing = true)
	static class ClassProxyingConfiguration {

		ClassProxyingConfiguration(BeanFactory beanFactory) {
			if (beanFactory instanceof BeanDefinitionRegistry) {
				BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
				AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
		}

	}

}

這裏就能看到@ConditionalOnProperty、@ConditionalOnClass、@ConditionalOnMissingClass,另外還有@ConditionalOnBean、@ConditionalOnMissingBean等等很多條件匹配註解。這些註解表示條件匹配才會加載該Bean,以@ConditionalOnProperty為例,表明配置文件中符合條件才會加載對應的Bean,prefix表示在配置文件中的前綴,name表示配置的名稱,havingValue表示配置為該值時才匹配,matchIfMissing則是表示沒有該配置是否默認加載對應的Bean。其它註解可類比理解記憶,下面主要來分析該註解的實現原理。
這裏註解點進去看會發現每個註解上都標註了@Conditional註解,並且value值都對應一個類,比如OnBeanCondition,而這些類都實現了Condition接口,看看其繼承體系:

上面只展示了幾個實現類,但實際上Condition的實現類是非常多的,我們還可以自己實現該接口來擴展@Condition註解。
Condition接口中有一個matches方法,這個方法返回true則表示匹配。該方法在ConfigurationClassParser中多處都有調用,也就是剛剛我提醒過的shouldSkip方法,具體實現是在ConditionEvaluator類中:

	public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
		if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
			return false;
		}

		if (phase == null) {
			if (metadata instanceof AnnotationMetadata &&
					ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
				return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
			}
			return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
		}

		List<Condition> conditions = new ArrayList<>();
		for (String[] conditionClasses : getConditionClasses(metadata)) {
			for (String conditionClass : conditionClasses) {
				Condition condition = getCondition(conditionClass, this.context.getClassLoader());
				conditions.add(condition);
			}
		}

		AnnotationAwareOrderComparator.sort(conditions);

		for (Condition condition : conditions) {
			ConfigurationPhase requiredPhase = null;
			if (condition instanceof ConfigurationCondition) {
				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
			}
			if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
				return true;
			}
		}

		return false;
	}

再來看看matches的實現,但OnBeanCondition類中沒有實現該方法,而是在其父類SpringBootCondition中:

	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			logOutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
			return outcome.isMatch();
		}

getMatchOutcome方法也是一個模板方法,具體的匹配邏輯就在這個方法中實現,該方法返回的ConditionOutcome對象就包含了是否匹配日誌消息兩個字段。進入到OnBeanCondition類中:

	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
		ConditionMessage matchMessage = ConditionMessage.empty();
		MergedAnnotations annotations = metadata.getAnnotations();
		if (annotations.isPresent(ConditionalOnBean.class)) {
			Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
				String reason = createOnBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
			Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
			}
			else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
					spec.getStrategy() == SearchStrategy.ALL)) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
						.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
			}
			matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
			Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
					ConditionalOnMissingBean.class);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (matchResult.isAnyMatched()) {
				String reason = createOnMissingBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
		}
		return ConditionOutcome.match(matchMessage);
	}

可以看到該類支持了@ConditionalOnBean、@ConditionalOnSingleCandidate、@ConditionalOnMissingBean註解,主要的匹配邏輯在getMatchingBeans方法中:

	protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {
		ClassLoader classLoader = context.getClassLoader();
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
		Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
		if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
			BeanFactory parent = beanFactory.getParentBeanFactory();
			Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
					"Unable to use SearchStrategy.ANCESTORS");
			beanFactory = (ConfigurableListableBeanFactory) parent;
		}
		MatchResult result = new MatchResult();
		Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,
				spec.getIgnoredTypes(), parameterizedContainers);
		for (String type : spec.getTypes()) {
			Collection<String> typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,
					parameterizedContainers);
			typeMatches.removeAll(beansIgnoredByType);
			if (typeMatches.isEmpty()) {
				result.recordUnmatchedType(type);
			}
			else {
				result.recordMatchedType(type, typeMatches);
			}
		}
		for (String annotation : spec.getAnnotations()) {
			Set<String> annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation,
					considerHierarchy);
			annotationMatches.removeAll(beansIgnoredByType);
			if (annotationMatches.isEmpty()) {
				result.recordUnmatchedAnnotation(annotation);
			}
			else {
				result.recordMatchedAnnotation(annotation, annotationMatches);
			}
		}
		for (String beanName : spec.getNames()) {
			if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
				result.recordMatchedName(beanName);
			}
			else {
				result.recordUnmatchedName(beanName);
			}
		}
		return result;
	}

這裏邏輯看起來比較複雜,但實際上就做了兩件事,首先通過getNamesOfBeansIgnoredByType方法調用beanFactory.getBeanNamesForType拿到容器中對應的Bean實例,然後根據返回的結果判斷哪些Bean存在,哪些Bean不存在(Condition註解中是可以配置多個值的)並返回MatchResult對象,而MatchResult中只要有一個Bean沒有匹配上就返回false,也就決定了當前Bean是否需要實例化。

總結

本篇分析了SpringBoot核心原理的實現,通過本篇相信讀者也將能更加熟練地使用和擴展SpringBoot。另外還有一些常用的組件我沒有展開分析,如事務、MVC、監聽器的自動配置,這些我們有了Spring源碼基礎的話下來看一下就明白了,這裏就不贅述了。最後讀者可以思考一下我們應該如何自定義starter啟動器,相信看完本篇應該難不倒你。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

比預估高三倍 氣候暖化海平面上升快 超過2億人將面臨洪水威脅

摘錄自2019年10月30日民視新聞、上報報導

最新研究顯示,到了2050年,海平面上升對人類的影響,超過以前預估的三倍,全球有2.37億人面臨沿海城市淹沒的威脅,比先前預估數字1.83億人多出許多。

近期刊登於國際期刊《自然通訊》(Nature Communications)的這份研究報告指出,到了2050年,約有3億人居住的地方每年至少會發生一次洪患。除非全球碳排放量有效降低,並且加強國家的沿海防洪措施。

根據研究結果顯示,擁有最多人口的亞洲地區將會面臨比其他洲更大的影響。到了2050年,全球面臨環境威脅的人口超過70%主要來自8個亞洲國家:中國、孟加拉、印度、越南、 印尼、泰國、菲律賓和日本。

其中又將以中國、泰國、印尼和印度的沿海地區最為嚴重,預計中國屆時將有1億人面臨氣候變遷所帶來的環境問題,孟加拉和印度也分別將有4200萬人和3500萬人受影響。

這項研究尚未將未來人口增長,及海岸侵蝕造成的土地損失考慮進去,因此實際災損可能更嚴重。這項研究並非是對這些地區宣判死刑。

數據顯示,全球已有1.1億人生活在漲潮線以下的地方,只要各國通力合作,減少溫室氣體排放,加上政府投入資金,盡快做好海堤防護措施,還有救。但各國也要開始思考備案,為沿海居民重新安置做好準備。

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

【其他文章推薦】

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

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

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

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

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

加州野火肆虐!洛杉磯發布史上首次終極紅旗警告

摘錄自2019年10月30日民視新聞報導

加州野火災情看來是益發危及,洛杉磯29日發布了史上第一次終極紅旗警告,要求南加州2100萬人隨時準備撤離,因為焚風只休息了不到24小時,又再次吹起,氣象當局憂心,每小時上看129公里,如颶風般的強風,將助長火勢,造成前所未見的災情。

洛杉磯消防局長拉薩斯也說:「這是聖塔安那(焚風)季創紀錄的事件,這是預測過最強勁的風,我接到的數據是每小時70哩(113公里),風勢真的很強,最終我們可能連直升機都不能飛。」被大火波及的民宅內景象讓人鼻酸。

專家悲觀估計,北部的風勢將達每小時達104公里,而南部的上看129公里、如颶風般兇猛,而南部洛杉磯還是人口密集區,屆時2100萬人如何全身而退,讓人擔憂。

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

【其他文章推薦】

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

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

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

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

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

冰淇淋豆 亞馬遜雨林的「綠金」新希望

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

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

西班牙將接手智利取消主辦的氣候峰會

摘錄自2019年11月1日聯合報報導

智利先前以街頭示威愈演愈烈為由,宣布取消主辦COP25氣候峰會,西班牙政府31日稍早發聲明說,總理桑傑士準備盡力促成Cop25改在馬德里辦。

消息來源說,西班牙政府下周一(4日)將做成正式決定:「此事幾成定局,我們有機會舉辦。」

智利的臨陣退縮是首度有國家取消主辦氣候峰會,目前距開會只剩一個月時間。但近日暴亂頻傳已使智利首都聖地牙哥的大眾運輸系統遭遇近四億美元的破壞,智利政府因而放棄主辦11月中旬的亞太經合會(APEC)高峰會和12月的氣候峰會。

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

【其他文章推薦】

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

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

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

※超省錢租車方案