政府重視電動機車電池電芯國產化,2020 持續補助購買電動機車

行政院副院長陳其邁在 7 月 15 日參訪 Gogoro 智能工廠,強調政府對電動機車電池電芯國產化的重視,並表示明年還是會持續補助消費者購買電動機車。

行政院副院長陳其邁與政務委員龔明鑫、經濟部次長林全能和環保署副署長沈志修等人前往桃園市龜山區,參訪 Gogoro 的智能工廠。除了現場試乘 5 月發表的新車種 Gogoro 3 車款並體驗交換電池外,也與 Gogoro 執行長陸學森針對電池電芯國產化進程與傳統機車產業合作等議題進行交流。

陳其邁表示,政府非常重視電動機車電池電芯國產化,目前除了經濟部的科專與 A+ 計畫外,對於國產電池的模組設計、儲能技術與電池的智慧管理也提供計畫類型的補助,希望加速國產電芯量產,並建立相關驗證機制,為國內電動機車產業鏈提供幫助。陳其邁強調,政府也相當重視電動機車產業與傳統機車行合作的問題,希望傳統機車行未來不論在營運、銷售與後續維修,都能與電動機車業者有更多合作機會,協助推動傳統機車行升級和轉型。

根據陳其邁的說法,政府在 2019 年對購買電動機車的補助維持不變,2020 年的補助還會持續下去。政府也研議鼓勵民眾淘汰排放污染的老舊機車,補助購買更環保的燃油機車,讓民眾使用的機車兼顧便捷與環保。

Gogoro 執行長陸學森表示,截至 2019 年 6 月底,Gogoro 生產銷售的電動機車已近 18 萬輛,全台已布建 1,283 個電池交換站,在 6 大都會區更是每 5 分鐘車程就有可即時換電的電池充電站。Gogoro 的每一部車子都是在桃園市龜山區的工廠生產製造,而且機車上的所有零組件,均來自全台 192 家供應商的供應鏈,做到電動機車的國產化。陸學森也向陳其邁建言,希望在台灣目前重型電動機車技術仍領先全球 3 到 5 年的優勢下,政府能夠支持電動機車產業,讓台灣的電動機車產業團結一致,一起為台灣打贏「世界盃」。

(合作媒體:。首圖來源: CC BY 2.0)

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

【其他文章推薦】

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

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

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

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

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

eMOVING 極速 100 公里電動機車 iE125 上市,快充 10 分鐘可騎 78 公里

中華汽車在 7 月 19 日發表新款電動機車 eMOVING iE125,極速可達到時速 100 公里,成為市場關注的焦點。

iE 125 的命名除了代表 intelligent electricity,也有 industrial engineering 的含義,125 則象徵 125cc 等級燃油機車的性能與操控。iE125 安全極速達到時速 100 公里,靜止加速到時速 50 公里僅需 3.9 秒,30% 坡度爬坡最高速為時速 32 公里。iE125 在充滿電之後,時速 30 公里之下續航里程為 155 公里,TES 變速續航里程為 82 公里。支援超級快充功能,充電 10 分鐘就能充滿 50% 的電力,可以行駛 78 公里。

iE125 含電池車重為 124 公斤,配備 CBS 連動煞車和 IP67 防水等級。提供 ECO、SPEED 和 BOOST 三種行車模式,讓消費者在不同情境下使用。特別的電動駐車功能只要按下按鈕就能直接立起中柱,車主不再需要為立中柱而困擾。配置 QC 3.0 USB 充電座,方便騎乘時進行充電。

eMOVING iE125 藍色版。

eMOVING iE125 白色版。

eMOVING iE125 橘色版。

eMOVING iE125 灰色版。

iE125 精緻型儀錶板螢幕為彩色液晶螢幕,豪華型和旗艦型則為汽車級 TFT。豪華型和旗艦型搭載車輛診斷系統,能在儀表板上顯示車身、動力、電池和胎壓等資訊,並提供異常提示與保養提醒。儀表板還可以進行個人化設定,自由更換儀表板主題與桌布。

iE125 豪華型和旗艦型具有遙控防盜中控鎖,能夠連結手機進行上鎖、解鎖和座墊開啟。iOS 版本的 App 支援即時來電提醒,來電與訊息通知會即時顯示。旗艦型還特別內建前方行車記錄器和胎壓偵測器,進一步確保行車安全。

儀錶板能顯示各種車身資訊。

eMOVING 將充電分為 3 種類型,分別在不同需求時使用。家用滿足平時充電需求,約 160 分鐘可以充滿 50% 的電力。快速充電站則供在外逛街或用餐時補充電力,約 30 分鐘可以充滿 50% 的電力。超級充電站供臨時路途中繼充電,約 10 分鐘可以充滿 50% 的電力。

消費者可以自行選擇電池租賃方案,eMOVING 提供電池永久保固。基礎型在家充電每月 399 元,輕量型每月 599 元額外提供 100 分鐘的超級充電分鐘數,進階型每月 799 元可以不限時數進行超級充電。為了推廣超級充電,12 月以前輕量型和進階型方案皆以每月 499 元計價,而且享受不限時數的超級充電。

中華汽車預計在 12 月佈建 70 座以上的快速充電站,2020 年 6 月更要佈建超過 150 座快速充電站,早期合作夥伴包括肯德基、家樂福、順益汽車和滙豐汽車。中華汽車也宣布捐贈 5 座超級充電站給桃園市政府,未來讓符合快充共通規格電動機車車主都能免費充電。

中華汽車捐贈 5 座超級充電站給桃園市政府。

eMOVING 推出了 10 月底前購車,就贈送 5,000 元購車金的優惠,可以全額折抵車價或購買配件。iE125 提供藍色、白色、灰色和橘色 4 種顏色讓消費者選擇,精緻型定價為台幣 73,800 元,豪華型定價為台幣 79,800 元,旗艦型定價為台幣 85,800 元。補助最高的桃園市汰換二行程機車換購電動機車補助最高 29,000 元,再加上 10 月底前購車贈送的購車金 5,000 元,精緻型最低台幣 39,800 元起,豪華型最低台幣 45,800 元起,旗艦型最低台幣 51,800 元起。

(合作媒體:。圖片來源:)

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

【其他文章推薦】

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

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

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

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

科大訊飛研發的全球中文學習平台上線

  隨着中國國際影響力的日益提升,漢語學習的需求與日俱增,為此,教育部、國家語委在《國家語言文字事業“十三五”發展規劃》中明確提出要“建設適應面廣、影響力大、權威性強的全球中文學習網絡平台”。10 月 25 日,在教育部、國家語委的指導下,由科大訊飛研發的全球中文學習平台正式上線。

  該平台上線發布儀式在京舉行。教育部副部長、國家語委主任田學軍,北京市人民政府副秘書長韓耕、科大訊飛股份有限公司董事長劉慶峰和人民教育出版社社長黃強共同為平台發布舉行了啟動儀式,200 多位中外嘉賓見證了全球中文學習平台(www.chinese-learning.cn)的正式上線。

  針對海外學習者的“譯學中文”模塊,學習者可以通過語音或文本輸入其母語內容,實時翻譯出中文並自動分句。學習者學習每個語句的標準音並錄音跟讀,系統會實時反饋評價,指出發音問題;針對錯誤字詞,可以反覆學習,直到掌握正確中文發音。

  這是科大訊飛承建國家語委的又一個重大項目!2004 年,科大訊飛承擔了國家語委“十五”重點科研項目“智能語音技術在普通話輔助學習中的應用研究”;2016 年,承擔國家語委“十三五”重大科研項目“智能語音及人工智能技術在語言學習中的應用研究”。目前,上述兩大項目均已成功落地,並取得了良好的社會效益。

  全球中文學習平台,匯聚各類中文學習資源,以更好地為廣大中文學習者提供優質服務為宗旨,於 2016 年底啟動建設,是落實《國家語言文字事業“十三五”發展規劃》相關任務要求的具體舉措。在教育部、國家語委與科大訊飛的共同努力下,歷經兩年多時間的不斷完善和改進,平台建設取得积極成效,相關基礎研究取得重要進展,為平台提供了堅實技術保障。其中智能語音、智能寫作和批改等關鍵技術研究成果在中小學語言能力評價、少數民族國家通用語言學習等方面得到實際應用。平台示範功能已分別在“砥礪奮進的五年”大型成就展、第二屆語博會、第十二屆孔子學院大會等不同場合進行展示,得到了各方好評。

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

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

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

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

新發現一種抗體有望開發為通用型流感疫苗

  新華社華盛頓 10 月 26 日電(記者周舟)美國科研團隊發現一種能“嵌入”流感病毒表面蛋白的抗體,可保護小鼠免遭多種流感病毒毒株的感染,未來有望開發為通用型流感疫苗。

  血凝素(H蛋白)和神經氨酸酶(N蛋白)是流感病毒表面的兩種蛋白,它們將流感病毒分為不同的亞型。目前開發的流感疫苗主要靶向血凝素。2017 年冬,美國華盛頓大學病理學和免疫學助理教授阿里·艾利貝迪發現一個流感患者的血樣不僅含有靶向血凝素的抗體,還含有可靶向其他蛋白的抗體。

  艾利貝迪將其中三種靶向不明的抗體送至芒特西奈伊坎醫學院進行檢測,該院微生物學教授弗洛里安·克拉默發現其中一種被稱為“1G01”的抗體,可阻斷多種流感病毒毒株上幾乎所有已知的神經氨酸酶的活動。

  克拉默團隊讓實驗小鼠感染致命性劑量的流感病毒,發現這種抗體可以對抗 12 種被測試的流感毒株,其中包括三類人類流感病毒毒株、禽流感和其他不在人際間傳播的病毒毒株。實驗發現,所有小鼠都生存了下來,即便在感染 72 小時以後給葯。相比而言,達菲必須在癥狀出現 24 小時內給葯。

  美國斯克里普斯研究所的結構生物學家伊安·威爾遜分析了這種抗體的結構,發現這種抗體將一個環狀結構嵌入神經氨酸酶的活性部位,阻止了神經氨酸酶從細胞表面釋放新的病毒顆粒。

  研究显示,這種抗體只阻斷神經氨酸酶的活性部位,而不同流感毒株間的活性部位幾乎不發生變異,因此它對多種流感病毒均有效。目前研究人員正在以抗體 1G01 為基礎設計新的流感藥物和疫苗。

  這一研究成果日前發表在美國《科學》雜誌上。

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

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

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

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

Springboot 系列(十六)你真的了解 Swagger 文檔嗎?

前言

目前來說,在 Java 領域使用 Springboot 構建微服務是比較流行的,在構建微服務時,我們大多數會選擇暴漏一個 REST API 以供調用。又或者公司採用前後端分離的開發模式,讓前端和後端的工作由完全不同的工程師進行開發完成。不管是微服務還是這種前後端分離開發,維持一份完整的及時更新的 REST API 文檔,會極大的提高我們的工作效率。而傳統的文檔更新方式(如手動編寫),很難保證文檔的及時性,經常會年久失修,失去應有的意義。因此選擇一種新的 API 文檔維護方式很有必要,這也是這篇文章要介紹的內容。

1. OpenAPI 規範介紹

OpenAPI Specification 簡稱 OAS,中文也稱 OpenAPI 描述規範,使用 OpenAPI 文件可以描述整個 API,它制定了一套的適合通用的與語言無關的 REST API 描述規範,如 API 路徑規範、請求方法規範、請求參數規範、返回格式規範等各種相關信息,使人類和計算機都可以不需要訪問源代碼就可以理解和使用服務的功能。

下面是 OpenAPI 規範中建議的 API 設計規範,基本路徑設計規範。

https://api.example.com/v1/users?role=admin&status=active
\________________________/\____/ \______________________/
         server URL       endpoint    query parameters
                            path

對於傳參的設計也有規範,可以像下面這樣:

  • , 例如 /users/{id}
  • , 例如 /users?role=未讀代碼
  • , 例如 X-MyHeader: Value
  • , 例如 Cookie: debug=0; csrftoken=BUSe35dohU3O1MZvDCU

OpenAPI 規範的東西遠遠不止這些,目前 OpenAPI 規範最新版本是 3.0.2,如果你想了解更多的 OpenAPI 規範,可以訪問下面的鏈接。

2. Swagger 介紹

很多人都以為 Swagger 只是一個接口文檔生成框架,其實並不是。 Swagger 是一個圍繞着 OpenAPI Specification(OAS,中文也稱 OpenAPI規範)構建的一組開源工具。可以幫助你從 API 的設計到 API 文檔的輸出再到 API 的測試,直至最後的 API 部署等整個 API 的開發周期提供相應的解決方案,是一個龐大的項目。 Swagger 不僅免費,而且開源,不管你是企業用戶還是個人玩家,都可以使用 Swagger 提供的方案構建令人驚艷的 REST API

Swagger 有幾個主要的產品。

  • – 一個基於瀏覽器的 Open API 規範編輯器。
  • – 一個將 OpenAPI 規範呈現為可交互在線文檔的工具。
  • – 一個根據 OpenAPI 生成調用代碼的工具。

如果你想了解更多信息,可以訪問 Swagger 官方網站 。

3. Springfox 介紹

源於 Java 中 Spring 框架的流行,讓一個叫做 Marrty Pitt 的老外有了為 SpringMVC 添加接口描述的想法,因此他創建了一個遵守 OpenAPI 規範(OAS)的項目,取名為 swagger-springmvc,這個項目可以讓 Spring 項目自動生成 JSON 格式的 OpenAPI 文檔。這個框架也仿照了 Spring 項目的開發習慣,使用註解來進行信息配置。

後來這個項目發展成為 Springfox,再後來擴展出 springfox-swagger2 ,為了讓 JSON 格式的 API 文檔更好的呈現,又出現了 springfox-swagger-ui 用來展示和測試生成的 OpenAPI 。這裏的 springfox-swagger-ui 其實就是上面介紹的 Swagger-ui,只是它被通過 webjar 的方式打包到 jar 包內,並通過 maven 的方式引入進來。

上面提到了 Springfox-swagger2 也是通過註解進行信息配置的,那麼是怎麼使用的呢?下面列舉常用的一些註解,這些註解在下面的 Springboot 整合 Swagger 中會用到。

註解 示例 描述
@ApiModel @ApiModel(value = “用戶對象”) 描述一個實體對象
@ApiModelProperty @ApiModelProperty(value = “用戶ID”, required = true, example = “1000”) 描述屬性信息,執行描述,是否必須,給出示例
@Api @Api(value = “用戶操作 API(v1)”, tags = “用戶操作接口”) 用在接口類上,為接口類添加描述
@ApiOperation @ApiOperation(value = “新增用戶”) 描述類的一個方法或者說一個接口
@ApiParam @ApiParam(value = “用戶名”, required = true) 描述單個參數

更多的 Springfox 介紹,可以訪問 Springfox 官方網站。

4. Springboot 整合 Swagger

就目前來說 ,Springboot 框架是非常流行的微服務框架,在微服務框架下,很多時候我們都是直接提供 REST API 的。REST API 如果沒有文檔的話,使用者就很頭疼了。不過不用擔心,上面說了有一位叫 Marrty Pitt 的老外已經創建了一個發展成為 Springfox 的項目,可以方便的提供 JSON 格式的 OpenAPI 規範和文檔支持。且擴展出了 springfox-swagger-ui 用於頁面的展示。

需要注意的是,這裏使用的所謂的 Swagger 其實和真正的 Swagger 並不是一個東西,這裏使用的是 Springfox 提供的 Swagger 實現。它們都是基於 OpenAPI 規範進行 API 構建。所以也都可以 Swagger-ui 進行 API 的頁面呈現。

4.1. 創建項目

如何創建一個 Springboot 項目這裏不提,你可以直接從 下載一個標準項目,也可以使用 idea 快速創建一個 Springboot 項目,也可以順便拷貝一個 Springboot 項目過來測試,總之,方式多種多樣,任你選擇。

下面演示如何在 Springboot 項目中使用 swagger2。

4.2. 引入依賴

這裏主要是引入了 springfox-swagger2,可以通過註解生成 JSON 格式的 OpenAPI 接口文檔,然後由於 Springfox 需要依賴 jackson,所以引入之。springfox-swagger-ui 可以把生成的 OpenAPI 接口文檔显示為頁面。Lombok 的引入可以通過註解為實體類生成 get/set 方法。

<dependencies> 
    <!-- Spring Boot web 開發整合 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-json</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入swagger2的依賴-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- jackson相關依賴 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!-- Lombok 工具 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

4.3. 配置 Springfox-swagger

Springfox-swagger 的配置通過一個 Docket 來包裝,Docket 里的 apiInfo 方法可以傳入關於接口總體的描述信息。而 apis 方法可以指定要掃描的包的具體路徑。在類上添加 @Configuration 聲明這是一個配置類,最後使用 @EnableSwagger2 開啟 Springfox-swagger2。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * <p>
 * Springfox-swagger2 配置
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.codingme.boot.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("未讀代碼 API")
                .description("公眾號:未讀代碼(weidudaima) springboot-swagger2 在線借口文檔")
                .termsOfServiceUrl("https://www.codingme.net")
                .contact("達西呀")
                .version("1.0")
                .build();
    }
}

4.4. 代碼編寫

文章不會把所有代碼一一列出來,這沒有太大意義,所以只貼出主要代碼,完整代碼會上傳到 Github,並在文章底部附上 Github 鏈接。

參數實體類 User.java,使用 @ApiModel@ApiModelProperty 描述參數對象,使用 @NotNull 進行數據校驗,使用 @Data 為參數實體類自動生成 get/set 方法。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * <p>
 * 用戶實體類
 *
 * @Author niujinpeng
 * @Date 2018/12/19 17:13
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用戶對象")
public class User {

    /**
     * 用戶ID
     *
     * @Id 主鍵
     * @GeneratedValue 自增主鍵
     */
    @NotNull(message = "用戶 ID 不能為空")
    @ApiModelProperty(value = "用戶ID", required = true, example = "1000")
    private Integer id;

    /**
     * 用戶名
     */
    @NotNull(message = "用戶名不能為空")
    @ApiModelProperty(value = "用戶名", required = true)
    private String username;
    /**
     * 密碼
     */
    @NotNull(message = "密碼不能為空")
    @ApiModelProperty(value = "用戶密碼", required = true)
    private String password;
    /**
     * 年齡
     */
    @ApiModelProperty(value = "用戶年齡", example = "18")
    private Integer age;
    /**
     * 生日
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    @ApiModelProperty(value = "用戶生日")
    private Date birthday;
    /**
     * 技能
     */
    @ApiModelProperty(value = "用戶技能")
    private String skills;
}

編寫 Controller 層,使用 @Api 描述接口類,使用 @ApiOperation 描述接口,使用 @ApiParam 描述接口參數。代碼中在查詢用戶信息的兩個接口上都添加了 tags = "用戶查詢" 標記,這樣這兩個方法在生成 Swagger 接口文檔時候會分到一個共同的標籤組裡。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.domain.User;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 用戶操作
 *
 * @Author niujinpeng
 * @Date 2019/11/19 23:17
 */

@Slf4j
@RestController(value = "/v1")
@Api(value = "用戶操作 API(v1)", tags = "用戶操作接口")
public class UserController {

    @ApiOperation(value = "新增用戶")
    @PostMapping(value = "/user")
    public Response create(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            // 新增用戶信息 do something
            return ResponseUtill.success("用戶[" + user.getUsername() + "]信息已新增");
        }
    }

    @ApiOperation(value = "刪除用戶")
    @DeleteMapping(value = "/user/{username}")
    public Response delete(@PathVariable("username")
                           @ApiParam(value = "用戶名", required = true) String name) throws Exception {
        // 刪除用戶信息 do something
        return ResponseUtill.success("用戶[" + name + "]信息已刪除");
    }

    @ApiOperation(value = "修改用戶")
    @PutMapping(value = "/user")
    public Response update(@Valid User user, BindingResult bindingResult) throws Exception {
        if (bindingResult.hasErrors()) {
            String message = bindingResult.getFieldError().getDefaultMessage();
            log.info(message);
            return ResponseUtill.error(ResponseEnum.ERROR.getCode(), message);
        } else {
            String username = user.getUsername();
            return ResponseUtill.success("用戶[" + username + "]信息已修改");
        }
    }

    @ApiOperation(value = "獲取單個用戶信息", tags = "用戶查詢")
    @GetMapping(value = "/user/{username}")
    public Response get(@PathVariable("username")
                        @NotNull(message = "用戶名稱不能為空")
                        @ApiParam(value = "用戶名", required = true) String username) throws Exception {
        // 查詢用戶信息 do something
        User user = new User();
        user.setId(10000);
        user.setUsername(username);
        user.setAge(99);
        user.setSkills("cnp");
        return ResponseUtill.success(user);
    }

    @ApiOperation(value = "獲取用戶列表", tags = "用戶查詢")
    @GetMapping(value = "/user")
    public Response selectAll() throws Exception {
        // 查詢用戶信息列表 do something
        User user = new User();
        user.setId(10000);
        user.setUsername("未讀代碼");
        user.setAge(99);
        user.setSkills("cnp");
        List<User> userList = new ArrayList<>();
        userList.add(user);
        return ResponseUtill.success(userList);
    }
}

最後,為了讓代碼變得更加符合規範和好用,使用一個統一的類進行接口響應。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "響應信息")
public class Response {
    /**
     * 響應碼
     */
    @ApiModelProperty(value = "響應碼")
    private String code;
    /**
     * 響應信息
     */
    @ApiModelProperty(value = "響應信息")
    private String message;

    /**
     * 響應數據
     */
    @ApiModelProperty(value = "響應數據")
    private Collection content;
}

4.5. 運行訪問

直接啟動 Springboog 項目,可以看到控制台輸出掃描到的各個接口的訪問路徑,其中就有 /2/api-docs

這個也就是生成的 OpenAPI 規範的描述 JSON 訪問路徑,訪問可以看到。

因為上面我們在引入依賴時,也引入了 springfox-swagger-ui 包,所以還可以訪問 API 的頁面文檔。訪問路徑是 /swagger-ui.html,訪問看到的效果可以看下圖。

也可以看到用戶查詢的兩個方法會歸到了一起,原因就是這兩個方法的註解上使用相同的 tag 屬性。

4.7. 調用測試

springfox-swagger-ui 不僅是生成了 API 文檔,還提供了調用測試功能。下面是在頁面上測試獲取單個用戶信息的過程。

  1. 點擊接口 [/user/{username}] 獲取單個用戶信息。
  2. 點擊 **Try it out** 進入測試傳參頁面。
  3. 輸入參數,點擊 Execute 藍色按鈕執行調用。
  4. 查看返回信息。

下面是測試時的響應截圖。

5. 常見報錯

如果你在程序運行中經常發現像下面這樣的報錯。

java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_111]
    at java.lang.Long.parseLong(Long.java:601) ~[na:1.8.0_111]
    at java.lang.Long.valueOf(Long.java:803) ~[na:1.8.0_111]
    at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412) ~[swagger-models-1.5.20.jar:1.5.20]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:536) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:666) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:156) [jackson-databind-2.5.4.jar:2.5.4]
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:113) [jackson-databind-2.5.4.jar:2.5.4]

那麼你需要檢查使用了 @ApiModelProperty 註解且字段類型為数字類型的屬性上,@ApiModelProperty 註解是否設置了 example 值,如果沒有,那就需要設置一下,像下面這樣。

@NotNull(message = "用戶 ID 不能為空")
@ApiModelProperty(value = "用戶ID", required = true, example = "1000")
private Integer id;

文中代碼都已經上傳到

參考文檔

個人網站:
如果你喜歡這篇文章,可以關注公眾號,一起成長。
關注公眾號回復資源可以沒有套路的獲取全網最火的的 Java 核心知識整理&面試核心資料。

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

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

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

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

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

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

【設計模式】單例模式的八種姿態寫法分析

目錄

前言
網上泛濫流傳單例模式的寫法種類,有說7種的,也有說6種的,當然也不排除說5種的,他們說的有錯嗎?其實沒有對與錯,刨根問底,寫法終究是寫法,其本質精髓大體一致!因此完全沒必要去追究寫法的多少,有這個時間還不如跟着宜春去網吧偷耳機、去田裡抓青蛙得了,一天天的….

言歸正傳…單例模式是最常用到的設計模式之一,熟悉設計模式的朋友對單例模式絕對不會陌生。同時單例模式也是比較簡單易理解的一種設計模式。

@

何謂單例模式?

專業術語

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。

單例模式,簡單的說就是 一個類只能有一個實例,並且在整個項目中都能訪問到這個實例。

單例模式的優點

1、在內存中只有一個對象,節省內存空間。
2、避免頻繁的創建銷毀對象,可以提高性能。
3、避免對共享資源的多重佔用。
4、可以全局訪問。

單例模式實現整體思路流程

首先我們要清楚單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例模式的常規實現思路大致相同為以下三個步驟:

1、私有構造方法
2、指向自己實例的私有靜態引用
3、以自己實例為返回值的靜態的公有的方法

當然也可以理解為
1、私有化構造方法,讓外部不能new。
2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
3、提供一個公有的static靜態方法(一般該方法使用getInstance這個名稱),返回實例對象。

將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。

單例模式的適用場景

由於單例模式有很多獨特的優點,所以是編程中用的比較多的一種設計模式。我總結了一下我所知道的適合使用單例模式的場景:

1、需要頻繁實例化然後銷毀的對象。
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
3、有狀態的工具類對象。
4、頻繁訪問數據庫或文件的對象。

在後面我將會講到JDK中的Runtime類就是使用的餓漢式單例!在Spring MVC框架中的controller 默認是單例模式的!

單例模式的八種姿態寫法

宜春強烈建議:如果是沒有接觸單例模式的讀者朋友強烈建議你們動手敲一遍,不要複製,不然沒效果!

還有一點就是,要真正輕而易舉的理解單例模式,JVM的類加載知識是不能少的,不然你只是會敲的層次,啥?不懂類加載?放心,宜春就是要你會,要你理解透徹。

其實上面的這篇文章特別重要,上面這篇文章的重要性懂的自然懂,不懂的希望能理解宜春的一片好意,去看一下吧,實在看不懂看不下去在回來看這篇文章就好了,再大不了就把博主一起按在馬桶蓋蓋上….

是不是心裏暖暖的?宜春也不多嗶嗶了,直接擼碼走起….

姿態一:餓漢式1(靜態變量)

package singletonPattern;
//餓漢式(靜態變量)

class Singleton{
    //1、私有化構造方法,讓外部不能new
    private Singleton(){

    }
    //2、本類內部創建對象實例【靜態變量目的是為了類加載的時候創建實例】
    private final static Singleton instance=new Singleton();

    //3、提供一個公有的static靜態方法,返回實例對象
    public static Singleton getInstance(){
        return instance;
    }
}
//以下是測試代碼=====================

public class SingletenDemo1 {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        Singleton singleton2=Singleton.getInstance();
//驗證一:
        System.out.println(singleton==singleton2);
//驗證二:
        System.out.println(singleton.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

//運行結果:
//        true
//        460141958
//        460141958

/*
餓漢式(靜態變量)方法

優點:寫法簡單,在類加載的時候就完成了實例化,同時也就避免了線程同步問題,因此線程安全
缺點:由於是在類加載時就完成了實例化,沒有達到懶加載的效果。如果一直沒有使用過這個實例,就造成了內存的浪費!

總結:這種方式基於ClassLoader類加載機制避免了多線程的同步問題,只不過instance屬性在類加載就實例化,在單例模式中大多數都是調用getInstance方法,
     由於getInstance方法是static靜態的,調用它肯定會觸發類加載!但是觸發類加載的原因有很多,我們不能保證這個類會通過其他的方式觸發類加載(比如調用了其他的static方法)
     這個時候初始化instance就沒有達到lazy loading 懶加載的效果,可能造成內存的浪費!

     餓漢式(靜態變量)這種方式可以使用但是會造成內存的浪費!

     */

姿態二:餓漢式2(static靜態代碼塊)

package singletonPattern;
//餓漢式2(static靜態代碼塊)

class Singleton2{
    private Singleton2(){

    }

    private static Singleton2 instance;

    static{ //把創建單例對象的操作放進了static靜態代碼塊中==============
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}
//餓漢式2(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!
//唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中

姿態三:懶漢式1(線程不安全)

package singletonPattern;
//懶漢式1(線程不安全)
class Singleton3{
    private Singleton3(){

    }

    private static Singleton3 instance;

    public static Singleton3 getInstance(){
        if(instance == null){
            instance=new Singleton3();
        }
        return instance;
    }
}
/*
懶漢式(線程不安全)的這種方式起到了懶加載的效果,但只能在單線程下使用。
如果在多線程下,一個線程進入了if(singleton==null)判斷語句塊,還沒執行產生實例的句子,另一個線程
又進來了,這時會產生多個實例,所以不安全。

結語:懶漢式(線程不安全)在實際開發中,不要使用這種方式!!存在潛在危險
*/

姿態四:懶漢式2(線程安全)

package singletonPattern;
//懶漢式2(線程安全)
class Singleton4{
    private Singleton4(){

    }

    private static Singleton4 instance;

    public static synchronized Singleton4 getInstance(){
        if(instance == null){
            instance=new Singleton4();
        }
        return instance;
    }
}

/*
懶漢式2(線程安全)方式

優點:線程安全
缺點:效率太低,每次調用getInstance方法都要進行同步

結語:懶漢式2(線程安全)方式在開發中不推薦使用,主要是效率太低了*/

姿態五:餓漢式2(static靜態代碼塊)

package singletonPattern;
//懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,在多線程下依舊會有多個實例
class Singleton5{
    private Singleton5(){

    }

    private static Singleton5 instance;

    public static  Singleton5 getInstance(){
        if(instance == null){   //多線程情況下可能多個線程進入這個if塊
            synchronized (Singleton5.class){  //到這裏只會一個一個創建實例,雖然安全,但是就不再是單例了
                instance=new Singleton5();
            }
        }
        return instance;
    }
}
/*
懶漢式3 同步代碼塊(線程安全) 但是不滿足單例,依舊會有多個實例

結語:懶漢式3 同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個單例設計的有點搞笑*/

姿態六:雙重檢查單例

package singletonPattern;
//雙重檢查應用實例方式
class Singleton6{
    private Singleton6(){}

    private static volatile Singleton6 singleton;

    public static Singleton6 getInstance(){
        if(singleton==null){
            synchronized(Singleton6.class){
                if(singleton == null){
                    singleton= new Singleton6();
                }
            }
        }
        return singleton;
    }
}
/*
雙重檢查應用實例方式:

線程安全、延遲加載、效率較高

結語:開發中推薦使用!
*/

這個時候博主就得嗶嗶幾句了,細心的童鞋會發現有一個Volatile關鍵字,完了,沒見過,小白童鞋慌了!

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。

這種實現方式既可以實現線程安全地創建實例,而又不會對性能造成太大的影響。它只是第一次創建實例的時候同步,以後就不需要同步了,從而加快了運行速度。

姿態七:靜態內部類單例

package singletonPattern;
//static靜態內部類單例

class Singleton7{
    private Singleton7(){}

    private static volatile Singleton7 instance;

    //寫一個static靜態內部類,給該類添加一個static靜態instance屬性
    private static class SingletonInstance{
        private static final Singleton7 SINGLETON_7=new Singleton7();
    }

    //
    public static synchronized Singleton7 getInstence(){
        return SingletonInstance.SINGLETON_7;
    }
}
/*
靜態內部類單例方式
        1、這種方式採用了類加載機制來保證初始化實例時只有一個線程
        2、巧妙的將實例化Singleton操作放進getInstance方法中,getInstance方法返回靜態內部類中實例化好的Singleton
        3、類的靜態屬性只會在第一次加載類的時候初始化,也就是只會初始化一次,在這裏,JVM幫我們保證了線程的安全,類在初始化時,別的線程無法進入。
       
        優點:線程安全、利用靜態內部類特點實現延遲加載、效率高
        開發中推薦使用這種靜態內部類單例方式!

static靜態內部特點:
1、外部類加載不會導致內部類加載,保證了其懶加載
*/

這個單例,宜春就不得不嗶嗶兩句了,要清楚這個單例,必須要明白static靜態內部特點,也就是外部類加載不會導致內部類加載!

姿態八:餓漢式2(static靜態代碼塊)

package singletonPattern;
//使用枚舉

import com.sun.xml.internal.bind.v2.runtime.unmarshaller.XsiNilLoader;

enum Singleton8{
    INSTANCE;
    public void methodName(){
        System.out.println("測試數據");
    }
}
/*

枚舉方式的枚舉:
推薦寫法,簡單高效。充分利用枚舉類的特性,只定義了一個實例,且枚舉類是天然支持多線程的。
藉助JDK1.5中添加的枚舉來實現單例模式優點:
         1、不僅能避免多線程同步問題 
         2、還能防止反序列化重新創建新的對象

枚舉方式單例是由Effective java作者Josh Bloch提倡的,結語:推薦使用!
*/

當然也可以測試一下

public class SingletonDemo8 {
    public static void main(String[] args) {
        Singleton8 instance = Singleton8.INSTANCE;
        Singleton8 instance2 = Singleton8.INSTANCE;
        System.out.println(instance==instance2);

        System.out.println(instance.hashCode());
        System.out.println(instance2.hashCode());

        instance.methodName();
    }
}

運行結果:

true
460141958
460141958
測試數據

屬實沒毛病!

JDK源碼中單例模式的應用

先來看一段Runtime 的源碼吧,並分析一下其使用的是種單例模式!

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

這應該不難看出吧!如果看不出的話只能說明你真的還沒有理解單例模式,我其實想說單例模式其實是23種設計模式中最簡單的一個,只是寫法比較多而已!同時面試官一般都會問單例模式,它已經是很基礎的了,問的稍微有點水平就是問你單例模式在JDK中哪裡運用到了,顯然JDK中的Runtime其實它使用的就是餓漢式單例!正如註釋所說,每一個java應用程序都有一個Runtime實例。Runtime的單例模式是採用餓漢模式創建的,意思是當你加載這個類文件時,這個實例就已經存在了。

Runtime類可以取得JVM系統信息,或者使用gc()方法釋放掉垃圾空間,還可以使用此類運行本機的程序。

==還有就是spring Mvc 中的controller 默認是單例模式的,解析。==

單例模式總結

1、餓漢式(靜態變量)這種方式可以使用,但是沒有達到 lazy loading 懶加載的效果會造成內存的浪費!開發中不建議使用。
2、餓漢式(static靜態代碼塊)其實和第一種餓漢式(靜態變量)方法差不多,其優缺點一致!唯一不同的就是把創建單例對象的操作放進了static靜態代碼塊中
3、懶漢式(線程不安全)起到了懶加載的效果,但只能在單線程下使用。在實際開發中,不要使用這種方式!!!
4、懶漢式2(線程安全)方式線程安全但是效率太低,每次調用getInstance方法都要進行同步。所以在開發中不推薦使用。 5、懶漢式3
同步代碼塊(線程安全)方式在開發中不使用 ,實際上這個設計有點搞笑哈哈。
6、雙重檢查應用實例方式,線程安全、延遲加載、效率較高。因此開發中推薦使用!
7、靜態內部類單例方式線程安全、利用靜態內部類特點實現延遲加載、效率高。 開發中推薦使用這種靜態內部類單例方式!
8、藉助JDK1.5中添加的枚舉來實現單例模式不僅能避免多線程同步問題還能防止反序列化重新創建新的對象。枚舉方式單例是由Effective java作者Josh Bloch提倡的,開發中推薦使用!

單例模式必須考慮到在多線程的應用場合下的使用,畢竟現在的服務器基本上都是多核的了。

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

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

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

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

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

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

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

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

簡單的學習,實現,領域事件,事件存儲,事件溯源

為什麼寫這篇文章

自己以前都走了彎路,以為學習戰術設計就會DDD了,其實DDD的精華在戰略設計,但是對於我們菜鳥來說,學習一些技術概念也是挺好的
經常看到這些術語,概念太多,也想簡單學習一下,記憶力比較差記錄一下實現的細節

領域事件

1.領域事件是過去發生的與業務有關的事實,一但發生就不可更改,所以存儲事件時只能追加

3.領域事件具有時間點的特徵,所有事件連接起來會形成明顯的時間軸

4.領域事件會導致目標對象狀態的變化,聚合根的行為會產生領域事件,所以會改變聚合的狀態

在聚合根裏面維護一個領域事件的聚合,每一個事件對應一個Handle,通過反射維護一個數據字典,通過事件查找到指定的Handle

領域事件實現的方式:目前看到有3種方式,MediatR,消息隊列 ,發布訂閱模式

eShopOnContainers 中使用的是MediatR

ENode 中使用的是EQueue,EQueue是一個純C#寫的消息隊列

使用已經寫好的消息隊列Rabbitmq ,kafka

事件存儲,事件溯源,事件快照

事件存儲:存儲所有聚合根裏面發生過的事件

1.事件存儲中可以做併發的處理,比如Command 重複,領域事件的重複

2.領域事件的重複通過聚合根Id+版本號判斷,可以在數據庫中建立聯合唯一索引,在存儲事件時檢測重複,記錄重複的事件,根據業務做處理

3.這裏要保證存儲事件與發布領域事件的一致性

如何保證存儲事件與發布領域事件的一致性

先存儲事件然後在發布領域事件,如果發生異常,就一直重試,一直到成功為止,也可以做一定的處理,比如重試到一定的次數,就通知,進行人工處理

我選擇了CAP + Policy + Dapper

事件溯源:在事件存儲中記錄導致狀態變化的一系列領域事件。通過持久化記錄改變狀態的事件,通過重新播放獲得狀態改變的歷史。 事件回放可以返回系統到任何狀態

聚合快照:聚合的生命周期各有長短,有的聚合裏面有大量的事件,,事件越多加載事件以及重建聚合的執行效率就會越來越低,快照裏面存儲的是聚合

1.定時存儲整個聚合根:使用定時器每隔一段時間就存儲聚合到快照表中

2.定量存儲整個聚合根:根據事件存儲中的數量來存儲聚合到快照表中

事件溯源的實現方式

1.首先我們需要實現聚合In Memory,

2.在CommandHandler中訂閱 Command命令,

創建聚合時 ,在內存中維護一個數據字典,key為:聚合根的Id,value為:聚合

修改,刪除,聚合時,根據聚合根的Id,查詢出聚合

如果內存中聚合不存在時:根據聚合根的Id 從聚合快照表中查詢出聚合,然後根據聚合快照存儲的時間,聚合根Id,查詢事件存儲中的所有事件,然後回放事件,得到聚合最終的狀態

記錄遇到的問題

由於基礎非常的差,所以實現的方式都是以最簡單的方式來寫的,存在許多的問題,代碼中有問題的地方希望大家提出來,讓我學習一下

代碼的實現目前還沒有寫快照的部分,也沒有處理EventStorage中的命令重複與聚合根+版本號重複,具體的請看湯總的ENode,裏面有全部的實現

1.怎樣保證存儲事件,發布事件的最終一致性

2.怎麼解析EventStorage中的事件,回放事件

先存儲事件,當事件存儲成功之後,在發布事件

存儲事件失敗:就一直重試,發布事件失敗,使用的是CAP,CAP內部使用的是本地消息表的方式,如果發布事件失敗,也一直重試,如果服務器重啟了,Rabbitmq裏面消息為Ack,消息沒有丟,重連後會繼續執行

存儲事件,發布事件

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage 發布事件
    /// </summary>
    /// <typeparam name="TAggregationRoot"></typeparam>
    /// <param name="event"></param>
    /// <returns></returns>
    public async Task AppendEventStoragePublishEventAsync<TAggregationRoot>(TAggregationRoot @event)
        where TAggregationRoot : IAggregationRoot
    {
        var domainEventList = @event.UncommittedEvents.ToList();
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }

        await TryAppendEventStorageAsync(domainEventList).ContinueWith(async e =>
        {
            if (e.Result == (int)EventStorageStatus.Success)
            {
                await TryPublishDomainEventAsync(domainEventList).ConfigureAwait(false);
                @event.ClearEvents();
            }
        });
    }

    /// <summary>
    /// 發布領域事件
    /// </summary>
    /// <returns></returns>
    public async Task PublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        using (var connection =
            new SqlConnection(ConnectionStr))
        {
            if (connection.State == ConnectionState.Closed)
            {
                await connection.OpenAsync().ConfigureAwait(false);
            }
            using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
            {
                try
                {
                    if (domainEventList.Count > 0)
                    {
                        foreach (var domainEvent in domainEventList)
                        {
                            await _capPublisher.PublishAsync(domainEvent.GetRoutingKey(), domainEvent).ConfigureAwait(false);
                        }
                    }
                    await transaction.CommitAsync().ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    await transaction.RollbackAsync().ConfigureAwait(false);
                    throw;
                }
            }
        }
    }

    /// <summary>
    /// 發布領域事件重試
    /// </summary>
    /// <param name="domainEventList"></param>
    /// <returns></returns>
    public async Task TryPublishDomainEventAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("發布領域事件異常", exception.Message);
                });
            });
        await policy.ExecuteAsync(async () =>
        {
            await PublishDomainEventAsync(domainEventList).ConfigureAwait(false);
        });

    }

    /// <summary>
    /// 存儲聚合根中的事件到EventStorage中
    /// </summary>
    /// <returns></returns>
    public async Task<int> AppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        if (domainEventList.Count == 0)
        {
            throw new Exception("請添加事件!");
        }
        var status = (int)EventStorageStatus.Failure;
        using (var connection = new SqlConnection(ConnectionStr))
        {
            try
            {
                if (connection.State == ConnectionState.Closed)
                {
                    await connection.OpenAsync().ConfigureAwait(false);
                }
                using (var transaction = await connection.BeginTransactionAsync().ConfigureAwait(false))
                {
                    try
                    {
                        if (domainEventList.Count > 0)
                        {
                            foreach (var domainEvent in domainEventList)
                            {
                                EventStorage eventStorage = new EventStorage
                                {
                                    Id = Guid.NewGuid(),
                                    AggregateRootId = domainEvent.AggregateRootId,
                                    AggregateRootType = domainEvent.AggregateRootType,
                                    CreateDateTime = domainEvent.CreateDateTime,
                                    Version = domainEvent.Version,
                                    EventData = Events(domainEvent)
                                };
                                var eventStorageSql =
                                    $"INSERT INTO EventStorageInfo(Id,AggregateRootId,AggregateRootType,CreateDateTime,Version,EventData) VALUES (@Id,@AggregateRootId,@AggregateRootType,@CreateDateTime,@Version,@EventData)";
                                await connection.ExecuteAsync(eventStorageSql, eventStorage, transaction).ConfigureAwait(false);
                            }
                        }
                        await transaction.CommitAsync().ConfigureAwait(false);
                        status = (int)EventStorageStatus.Success;
                    }
                    catch (Exception e)
                    {
                        await transaction.RollbackAsync().ConfigureAwait(false);
                        throw;
                    }
                }

            }
            catch (Exception e)
            {
                connection.Close();
                throw;
            }
        }
        return status;
    }

    /// <summary>
    /// AppendEventStorageAsync異常重試
    /// </summary>
    public async Task<int> TryAppendEventStorageAsync(List<IDomainEvent> domainEventList)
    {
        var policy = Policy.Handle<SocketException>().Or<IOException>().Or<Exception>()
            .RetryForeverAsync(onRetry: exception =>
            {
                Task.Factory.StartNew(() =>
                {
                    //記錄重試的信息
                    _loggerHelper.LogInfo("存儲事件異常", exception.Message);
                });
            });
        var result = await policy.ExecuteAsync(async () =>
          {
              var resulted = await AppendEventStorageAsync(domainEventList).ConfigureAwait(false);
              return resulted;
          });
        return result;
    }

    /// <summary>
    /// 根據DomainEvent序列化事件Json
    /// </summary>
    /// <param name="domainEvent"></param>
    /// <returns></returns>
    public string Events(IDomainEvent domainEvent)
    {
        ConcurrentDictionary<string, string> dictionary = new ConcurrentDictionary<string, string>();
        //獲取領域事件的類型(方便解析Json)
        var domainEventTypeName = domainEvent.GetType().Name;
        var domainEventStr = JsonConvert.SerializeObject(domainEvent);
        dictionary.GetOrAdd(domainEventTypeName, domainEventStr);
        var eventData = JsonConvert.SerializeObject(dictionary);
        return eventData;
    }

解析EventStorage中存儲的事件

    public async Task<List<IDomainEvent>> GetAggregateRootEventStorageById(Guid AggregateRootId)
    {
        try
        {
            using (var connection = new SqlConnection(ConnectionStr))
            {
                var eventStorageList = await connection.QueryAsync<EventStorage>($"SELECT * FROM dbo.EventStorageInfo WHERE AggregateRootId='{AggregateRootId}'");
                List<IDomainEvent> domainEventList = new List<IDomainEvent>();
                foreach (var item in eventStorageList)
                {
                    var dictionaryDomainEvent = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.EventData);
                    foreach (var entry in dictionaryDomainEvent)
                    {
                        var domainEventType = TypeNameProvider.GetType(entry.Key);
                        if (domainEventType != null)
                        {
                            var domainEvent = JsonConvert.DeserializeObject(entry.Value, domainEventType) as IDomainEvent;
                            domainEventList.Add(domainEvent);
                        }
                    }
                }
                return domainEventList;
            }
        }
        catch (Exception ex)
        {
            throw;
        }

注意事項

1.事件沒持久化就代表事件還沒發生成功,事件存儲可能失敗,必須先存儲事件,在發布事件,保證存儲事件與發布事件一致性
1.使用事件驅動,必須要做好冥等的處理
2.如果業務場景中有狀態時:通過狀態來控制
3.新建一張表,用來記錄消費的信息,消費端的代碼裏面,根據唯一的標識,判斷是否處理過該事件
4.Q端的任何更新都應該把聚合根ID和事件版本號作為條件,Q端的更新不用遵循聚合的原則,可以使用最簡單的方式處理
5.倉儲是用來重建聚合的,它的行為和集合一樣只有Get ,Add ,Delete
6.DDD不是技術,是思想,核心在戰略模塊,戰術設計是實現的一種選擇,戰略設計,需要面向對象的分析能力,職責分配,深層次的分析業務

感謝

雖然學習DDD的時間不短了,感覺還是在入門階段,在學習的過程中有許多的不解,經常問ENode群裏面的大佬,也經常@湯總,謝謝大家的幫助與解惑。

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

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

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

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

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

mybatis精講(三)–標籤及TypeHandler使用

目錄

話引

  • 前兩張我們分別介紹了Mybatis環境搭建及其組件的生命周期。這些都是我們Mybatis入門必備技能。有了前兩篇的鋪墊我們今天就來深入下Mybatis, 也為了填下之前埋下的坑。

XML配置標籤

概覽


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration> 
    <!--引入外部配置文件-->
    <properties resource=""/>
    <!--設置-->
    <settings/>
    <!--定義別名-->
    <typeAliases>
        <package name=""/>
    </typeAliases>
    <!--類型處理器-->
    <typeHandlers/>
    <!--對象工廠-->
    <objectFactory/>
    <!--插件-->
    <plugins/>
    <!--定義數據庫信息,默認使用development數據庫構建環境-->
    <environments default="development">
        <environment id="development">
            <!--jdbc事物管理-->
            <transactionManager type="JDBC"/>
            <!--配置數據庫連接信息-->
            <dataSource type="POOLED"/>
        </environment>
    </environments>
    <!--數據庫廠商標識-->
    <databaseIdProvider/>
    <mappers/>
</configuration>

  • 上面模板列出了所有xml可以配置的屬性。這裏plugins是一個讓人哭笑不得的東西。用的好是利器,用的不好就是埋坑。接下來我們來看看各個屬性的作用

properties

  • 該標籤的作用就是引入變量。和maven的properties一樣。在這裏定義的變量或者引入的變量,在下面我們是可以童工${}使用的。

子標籤property


<properties>
  <property name="zxhtom" value="jdbc:mysql://localhost:3306/mybatis"/>
</properties>

<dataSource type="POOLED">
<property name="driver" value="${zxhtom}"/>
<dataSource>
  • 上述的配置就可以直接使用zxhtom這個變量。

resource

  • 除了上述方法我們還可以通過引入其他properties文件,就可以使用文件里的配置變量了。

<properties resource="mybatis.properties"/>

程序注入

  • 最後還有一種我們在構建SqlSessionFactory的時候重新載入我們的Properties對象就可以了。另外三者的優先級是從低到高

settings


configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  • 上面代碼是我們在XMLConfigBuilder解析settings標籤的代碼。從這段代碼中我們了解到settings子標籤。
參數 功能 可選值 默認值
autoMappingBehavior 指定Mybatis應如何自動映射列到字段上。
NONE : 表示取消自動映射
PARTIAL:只會自動映射沒有定義嵌套結果集映射的結果集
FULL : 自動映射任意複雜的結果集
NONE、PARTIAL、FULL PARTIAL
autoMappingUnknownColumnBehavior 指定識別到位置列或屬性的時間
NONE : 什麼都不做
WARNING:日誌會報警(前提是日誌設置了显示權限)
FAILING : 拋出異常。
NONE, WARNING, FAILING NONE
cacheEnabled 該配置影響的所有映射器中配置的緩存的全局開關 true|false true
proxyFactory 指定Mybatis創建具有延遲加載能力的對象所用到的代理工具未指定時將自動查找 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING not set
lazyLoadingEnabled 延時加載全局開關
開啟時:級聯對象會延時加載;級聯標籤中可以通過fetchType來定製覆蓋此選項
true|false false
aggressiveLazyLoading 啟用時:對任意延遲屬性的調用會使帶有延遲加載屬性的對象分層性質完整加載,反之按需加載 true|false true
multipleResultSetsEnabled 是否允許單一語句返回多結果集 true|false true
useColumnLabel 確切的說當映射找不到參數時會使用列標籤(數據庫列名)代替別名去映射 true|false true
useGeneratedKeys 允許 JDBC 支持自動生成主鍵,需要驅動兼容。 如果設置為 true 則這個設置強制使用自動生成主鍵,儘管一些驅動不能兼容但仍可正常工作(比如 Derby) true|false false
defaultExecutorType 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設置超時時間,決定驅動等待數據庫響應的秒數 整數 null
defaultFetchSize 設置數據庫resultSet讀取數據方式,默認全部加載進內存,設置該屬性可以設置一次性讀多少條數據進內存 整數 null
mapUnderscoreToCamelCase 是否開啟自動駝峰命名規則(camel case)映射,即從經典數據庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似映射。 true|false false
safeRowBoundsEnabled -允許在嵌套語句中使用分頁 true|false false
localCacheScope 一級緩存。mybatis默認對同一個sqlsession中數據是共享的。一個sqlsession調用兩次相同查詢實際只會查詢一次。就是因為該屬性為SESSION , STATEMENT則針對的是每一條sql SESSION|STATEMENT SESSION
jdbcTypeForNull 當沒有為參數提供特定的jdbc類型時,為空值則指定JDBC類型。在新增時我們沒有設置參數,這個時候就會根據此參數天長。加入設置VARCHAR,那麼我們新增的數據沒傳參數則為空字符 NULL|VARCHAR|OTHER OTHER
lazyLoadTriggerMethods 指定具體方法延時加載 方法 equals,clone,hashCode,toString
safeResultHandlerEnabled 允許在嵌套語句中使用分頁 true|false true
defaultScriptingLanguage 動態SQL生成的默認語言 org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
defaultEnumTypeHandler mybatis默認的枚舉處理類
callSettersOnNulls 指定當結果集中值為null的時候是否調用映射對象的setter(put)方法。
useActualParamName 允許使用他們的編譯后名稱來映射,3.4.2后默認true.在xml中#{0}則報錯。設置為false,則#{0}代表第一個參數#{n}第n個 true|false true
returnInstanceForEmptyRow 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啟這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集 (如集合或關聯)。(新增於 3.4.2) true|false false
logPrefix 指定 MyBatis 增加到日誌名稱的前綴。

別名

  • 別名是mybatis為我們項目中類起的一個名字,類名往往會很長所以別名就方便我們平時的開發。Mybatis為我們內置了一些類的別名:byte、short、int、long、float、double、boolean、char等基礎類型的別名。還有其的封裝類型、String,Object,Map,List等等常用的類。
    org.apache.ibatis.type.TypeAliasRegistry這個類中幫我們內置了別名。可以看下。自定義別名也是通過這個類進行註冊的。我們可以通過settings中typeAliases配置的方式結合@Alias。或者掃描包也可以的。

TypeHandler

  • 這個接口就四個方法

public interface TypeHandler<T> {

  /**
   * 設置參數是用到的方法
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}
  • 可以理解成攔截器。它主要攔截的是設置參數和獲取結果的兩個節點。這個類的作用就是將Java對象和jdbcType進行相互轉換的一個功能。同樣的在org.apache.ibatis.type.TypeHandlerRegistry這個類中mybatis為我們提供了內置的TypeHandler。基本上是對於基本數據和分裝對象的轉換。
  • 下面我們隨便看一個TypeHandler處理細節
public class BooleanTypeHandler extends BaseTypeHandler<Boolean> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setBoolean(i, parameter);
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    boolean result = rs.getBoolean(columnName);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    boolean result = rs.getBoolean(columnIndex);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    boolean result = cs.getBoolean(columnIndex);
    return !result && cs.wasNull() ? null : result;
  }
}
  • setParameter是PreparedStatement進行設置成boolean類型。getResult分別通過三種不同方式獲取。在這些方法里我們可以根據自己也無需求進行控制。常見的控制是枚舉的轉換。傳遞參數過程可能是枚舉的name,但是傳遞到數據庫中要枚舉的index.這種需求我們就可以在TypeHandler中實現。我們書寫的typeHandler之後並不能被識別,還需要我們在resultMap中的result標籤中通過typeHandler指定我們的自定義Handler.

自定義TypeHandler

  • 承接上文我們說道枚舉的轉換。下面我們還是已學生類為例。學生中性別之前是boolean類型。現在我們採用枚舉類型。但是數據庫中存的還是數據,01.

EnumTypeHandler

  • 在TypeHandlerRegister類中申明了默認的枚舉類處理器是private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
  String s = rs.getString(columnName);
  return s == null ? null : Enum.valueOf(type, s);
}
  • 我們通過這個方法可以看出,這個枚舉處理器適合已枚舉名稱存儲的方式

EnumOrdinalTypeHandler

  • 在Enum中還有一個屬性oridinal。這個表示枚舉中的索引。然後我們通過查看Mybatis提供的處理器發現有個叫EnumOrdinalTypeHandler。我們很容易聯想到的就是這個處理器是通過枚舉的所以作為數據庫內容的。在SexEnum中MALE存儲到數據庫中則為0.注意這個0不是我們的index.而是MALE的索引。如果將MALE和FEMAEL調換。那麼MALE索引則為1.

  • 因為默認的是EnumTypeHandler。所以想用EnumOrdinalTypeHandler的話我們要麼在resultMap中sex字段指定該處理器。要不就通過配置文件typeHandlers註冊進來。(將處理器與Java類進行綁定。mybatis遇到這個Java對象的時候就知道用什麼處理器處理)


<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.github.zxhtom.enums.SexEnum"/>
</typeHandlers>

SexTypeHandler

  • 上面的不管是通過名稱存儲還是通過索引存儲都不太滿足我們的需求。我們想通過我們的index存儲。那麼這時候我們就得自定義處理邏輯了。Mybatis處理器都是繼承BaseTypeHandler。因為BaseTypeHandler實現了TypeHandler.所以我們這裏也就繼承BaseTypeHandler。

public class SexTypeHandler extends BaseTypeHandler<SexEnum> {


    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i,parameter.getIndex());
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int i = rs.getInt(columnName);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int i = rs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int i = cs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }
}

typeHandler注意點

  • 在編寫自定義處理器的時候我們得之處Javatype、jdbctype。兩者不是必填。但至少得有一個。正常我們默認javatype是必填的。
  • 填寫的方式有三種
    • 通過MappedTypes、MappedJdbcTypes分別指定javatype、jdbctype
    • 通過在mybatis-config.xml中配置typeHandlers進行註解。裏面也有這兩個屬性的配置。
    • 通過在mapper.xml的resultmap中再次指定某個字段的typehandler.
  • TypeHandler為我們提供了Java到jdbc數據的轉換橋樑。極大的方便了我們平時的開發。讓我們開發期間忽略數據的轉換這麼糟心的事情。

# 加入戰隊

微信公眾號

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

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

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

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

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

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

結合RBAC模型講解權限管理系統需求及表結構創建

在本號之前的文章中,已經為大家介紹了很多關於Spring Security的使用方法,也介紹了RBAC的基於角色權限控制模型。但是很多朋友雖然已經理解了RBAC控制模型,但是仍有很多的問題阻礙他們進一步開發。比如:

  • RBAC模型的表結構該如何創建?
  • 具體到某個頁面,某個按鈕權限是如何控制的?
  • 為了配合登錄驗證表,用戶表中應該包含哪些核心字段?
  • 這些字段與登錄驗證或權限分配的需求有什麼關係?

那麼本文就希望將這些問題,與大家進行一下分享。

一、回顧RBAC權限模型

  • 用戶與角色之間是多對多的關係,一個用戶有多個角色,一個角色包含多個用戶
  • 角色與權限之間是多對多關係,一個角色有多種權限,一個權限可以屬於多個角色

上圖中:

  • User是用戶表,存儲用戶基本信息
  • Role是角色表,存儲角色相關信息
  • Menu(菜單)是權限表,存儲系統包含哪些菜單及其屬性
  • UserRole是用戶和角色的關係表
  • RoleMenu是角色和權限的關係表

本文講解只將權限控制到菜單的訪問級別,即控制頁面的訪問權限。如果想控制到頁面中按鈕級別的訪問,可以參考Menu與RoleMenu的模式同樣的實現方式。或者乾脆在menu表裡面加上一個字段區別該條記錄是菜單項還是按鈕。

為了有理有據,我們參考一個比較優秀的開源項目:若依後台管理系統。

二、組織部門管理

2.1.需求分析

之所以先將部門管理提出來講一下,是因為部門管理沒有在我們上面的RBAC權限模型中進行提現。但是部門這樣一個實體仍然是,後端管理系統的一個重要組成部分。通常有如下的需求:

  • 部門要能體現出上下級的結構(如上圖中的紅框)。在關係型數據庫中。這就需要使用到部門id及上級部門id,來組合成一個樹形結構。這個知識是SQL學習中必備的知識,如果您還不知道,請自行學習。
  • 如果組織與用戶之間是一對多的關係,就在用戶表中加上一個org_id標識用戶所屬的組織。原則是:實體關係在多的那一邊維護。比如:是讓老師記住自己的學生容易,還是讓學生記住自己的老師更容易?
  • 如果組織與用戶是多對多關係,這種情況現實需求也有可能存在。比如:某人在某單位既是生產部長,又是技術部長。所以他及歸屬於技術部。也歸屬於生產部。對於這種情況有兩種解決方案,把該人員放到公司級別,而不是放到部門級別。另外一種就是從數據庫結構上創建User與Org組織之間的多對多關係。
  • 組織信息包含一些基本信息,如組織名稱、組織狀態、展現排序、創建時間
  • 另外,要有基本的組織的增刪改查功能

2.2 組織部門表的CreateSQL

以下SQL以MySQL為例:

CREATE TABLE `sys_org` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `org_pid` INT(11) NOT NULL COMMENT '上級組織編碼',
    `org_pids` VARCHAR(64) NOT NULL COMMENT '所有的父節點id',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `org_name` VARCHAR(32) NOT NULL COMMENT '組織名',
    `address` VARCHAR(64) NULL DEFAULT NULL COMMENT '地址',
    `phone` VARCHAR(13) NULL DEFAULT NULL COMMENT '電話',
    `email` VARCHAR(32) NULL DEFAULT NULL COMMENT '郵件',
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '組織層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統組織結構表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

注意:mysql沒有oracle中的start with connect by的樹形數據匯總SQL。所以通常需要為了方便管理組織之間的上下級樹形關係,需要加上一些特殊字段,如:org_pids:該組織所有上級組織id逗號分隔,即包括上級的上級;is_leaf是否是恭弘=叶 恭弘子結點;level組織所屬的層級(1,2,3)。

三、菜單權限管理

3.1 需求分析

  • 由上圖可以看出,菜單仍然是樹形結構,所以數據庫表必須有id與menu_pid字段
  • 必要字段:菜單跳轉的url、是否啟用、菜單排序、菜單的icon矢量圖標等
  • 最重要的是菜單要有一個權限標誌,具有唯一性。通常可以使用菜單跳轉的url路徑作為權限標誌。此標誌作為權限管理框架識別用戶是否具有某個頁面查看權限的重要標誌
  • 需要具備菜單的增刪改查基本功能
  • 如果希望將菜單權限和按鈕超鏈接相關權限放到同一個表裡面,可以新增一個字段。用戶標誌該權限記錄是菜單訪問權限還是按鈕訪問權限。

3.2 菜單權限表的CreateSQL

CREATE TABLE `sys_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `menu_pid` INT(11) NOT NULL COMMENT '父菜單ID',
    `menu_pids` VARCHAR(64) NOT NULL COMMENT '當前菜單所有父菜單',
    `is_leaf` TINYINT(4) NOT NULL COMMENT '0:不是恭弘=叶 恭弘子節點,1:是恭弘=叶 恭弘子節點',
    `name` VARCHAR(16) NOT NULL COMMENT '菜單名稱',
    `url` VARCHAR(64) NOT NULL COMMENT '跳轉URL',
    `icon` VARCHAR(45) NULL DEFAULT NULL,
    `icon_color` VARCHAR(16) NULL DEFAULT NULL,
    `sort` TINYINT(4) NULL DEFAULT NULL COMMENT '排序',
    `level` TINYINT(4) NOT NULL COMMENT '菜單層級',
    `status` TINYINT(4) NOT NULL COMMENT '0:啟用,1:禁用',
    PRIMARY KEY (`id`)
)
COMMENT='系統菜單表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

四、角色管理

上圖為角色修改及分配權限的頁面

4.1.需求分析

  • 角色本身的管理需要注意的點非常少,就是簡單的增刪改查。重點在於角色分配該如何做。
  • 角色表包含角色id,角色名稱,備註、排序順序這些基本信息就足夠了
  • 為角色分配權限:以角色為基礎勾選菜單權限或者操作權限,然後先刪除sys_role_menu表內該角色的所有記錄,在將新勾選的權限數據逐條插入sys_role_menu表。
  • sys_role_menu的結構很簡單,記錄role_id與menu_id,一個角色擁有某一個權限就是一條記錄。
  • 角色要有一個全局唯一的標識,因為角色本身也是一種權限。可以通過判斷角色來判斷某用戶的操作是否合法。
  • 通常的需求:不會在角色管理界面為角色添加用戶,而是在用戶管理界面為用戶分配角色。

4.2.角色表與角色菜單權限關聯表的的CreateSQL

CREATE TABLE `sys_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `role_name` VARCHAR(16) NOT NULL COMMENT '角色名',
    `role_flag` VARCHAR(64) NULL DEFAULT NULL COMMENT '角色標識',
    `sort` INT(11) NULL DEFAULT NULL COMMENT '排序',
    PRIMARY KEY (`id`)
)
COMMENT='系統角色表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
CREATE TABLE `sys_role_menu` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NOT NULL COMMENT '角色ID',
    `menu_id` INT(11) NOT NULL COMMENT '菜單ID',
    PRIMARY KEY (`id`)
)
COMMENT='角色菜單多對多關聯表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

五、用戶管理

5.1.需求分析

  • 上圖中點擊左側的組織菜單樹結點,要能显示出該組織下的所有人員(系統用戶)。在組織與用戶是一對多的關係中,需要在用戶表加上org_id字段,用於查詢某個組織下的所有用戶。
  • 用戶表中要保存用戶的用戶名、加密后的密碼。頁面提供密碼修改或重置的功能。
  • 角色分配:實際上為用戶分配角色,與為角色分配權限的設計原則是一樣的。所以可以參考。
  • 實現用戶基本信息的增刪改查功能

5.2.sys_user 用戶信息表及用戶角色關係表的CreateSQL

CREATE TABLE `sys_user` (
        `id` INT(11) NOT NULL AUTO_INCREMENT,
        `org_id` INT(11) NOT NULL,
        `username` VARCHAR(64) NULL DEFAULT NULL COMMENT '用戶名',
        `password` VARCHAR(64) NULL DEFAULT NULL COMMENT '密碼',
        `enabled` INT(11) NULL DEFAULT '1' COMMENT '用戶賬戶是否可用',
        `locked` INT(11) NULL DEFAULT '0' COMMENT '用戶賬戶是否被鎖定',
        `lockrelease_time` TIMESTAMP NULL  '用戶賬戶鎖定到期時間',
        `expired_time` TIMESTAMP NULL  '用戶賬戶過期時間',
        `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '用戶賬戶創建時間',
    PRIMARY KEY (`id`)
)
COMMENT='用戶信息表'
ENGINE=InnoDB
;
CREATE TABLE `sys_user_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `role_id` VARCHAR(16) NULL DEFAULT NULL,
    `user_id` VARCHAR(18) NULL DEFAULT NULL,
    PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;

在用戶的信息表中,體現了一些隱藏的需求。如:多次登錄鎖定與鎖定到期時間的關係。賬號有效期的設定規則等。

當然用戶表中,根據業務的不同還可能加更多的信息,比如:用戶頭像等等。但是通常在比較大型的業務系統開發中,業務模塊中使用的用戶表和在權限管理模塊使用的用戶表通常不是一個,而是根據某些唯一字段弱關聯,分開存放。這樣做的好處在於:經常發生變化的業務需求,不會去影響不經常變化的權限模型。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

前言

在企業項目開發中,對系統的安全和權限控制往往是必需的,常見的安全框架有 Spring Security、Apache Shiro 等。本文主要簡單介紹一下 Spring Security,再通過 Spring Boot 集成開一個簡單的示例。

Spring Security

什麼是 Spring Security?

Spring Security 是一種基於 Spring AOP 和 Servlet 過濾器 Filter 的安全框架,它提供了全面的安全解決方案,提供在 Web 請求和方法調用級別的用戶鑒權和權限控制。

Web 應用的安全性通常包括兩方面:用戶認證(Authentication)和用戶授權(Authorization)。

用戶認證指的是驗證某個用戶是否為系統合法用戶,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼來完成認證。

用戶授權指的是驗證某個用戶是否有權限執行某個操作。

2.原理

Spring Security 功能的實現主要是靠一系列的過濾器鏈相互配合來完成的。以下是項目啟動時打印的默認安全過濾器鏈(集成5.2.0):

[
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546,
    org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6,
    org.springframework.security.web.header.HeaderWriterFilter@4fefa770,
    org.springframework.security.web.csrf.CsrfFilter@6346aba8,
    org.springframework.security.web.authentication.logout.LogoutFilter@677ac054,
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781,
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678,
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6,
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2,
    org.springframework.security.web.session.SessionManagementFilter@471f8a70,
    org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569,
    org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62
]
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

詳細解讀可以參考:https://blog.csdn.net/dushiwodecuo/article/details/78913113

3.核心組件

SecurityContextHolder

用於存儲應用程序安全上下文(Spring Context)的詳細信息,如當前操作的用戶對象信息、認證狀態、角色權限信息等。默認情況下,SecurityContextHolder 會使用 ThreadLocal 來存儲這些信息,意味着安全上下文始終可用於同一執行線程中的方法。

獲取有關當前用戶的信息

因為身份信息與線程是綁定的,所以可以在程序的任何地方使用靜態方法獲取用戶信息。例如獲取當前經過身份驗證的用戶的名稱,代碼如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

其中,getAuthentication() 返回認證信息,getPrincipal() 返回身份信息,UserDetails 是對用戶信息的封裝類。

Authentication

認證信息接口,集成了 Principal 類。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串
getCredentials() 獲取用戶提交的密碼憑證,用戶輸入的密碼字符竄,在認證過後通常會被移除,用於保障安全
getDetails() 獲取用戶詳細信息,用於記錄 ip、sessionid、證書序列號等值
getPrincipal() 獲取用戶身份信息,大部分情況下返回的是 UserDetails 接口的實現類,是框架中最常用的接口之一

AuthenticationManager

認證管理器,負責驗證。認證成功后,AuthenticationManager 返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼通常會被移除)的 Authentication 實例。然後再將 Authentication 設置到 SecurityContextHolder 容器中。

AuthenticationManager 接口是認證相關的核心接口,也是發起認證的入口。但它一般不直接認證,其常用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,存放里多種認證方式,默認情況下,只需要通過一個 AuthenticationProvider 的認證,就可被認為是登錄成功。

UserDetailsService

負責從特定的地方加載用戶信息,通常是通過JdbcDaoImpl從數據庫加載實現,也可以通過內存映射InMemoryDaoImpl實現。

UserDetails

該接口代表了最詳細的用戶信息。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取授予用戶的權限
getPassword() 獲取用戶正確的密碼,這個密碼在驗證時會和 Authentication 中的 getCredentials() 做比對
getUsername() 獲取用於驗證的用戶名
isAccountNonExpired() 指示用戶的帳戶是否已過期,無法驗證過期的用戶
isAccountNonLocked() 指示用戶的賬號是否被鎖定,無法驗證被鎖定的用戶
isCredentialsNonExpired() 指示用戶的憑據(密碼)是否已過期,無法驗證憑證過期的用戶
isEnabled() 指示用戶是否被啟用,無法驗證被禁用的用戶

Spring Security 實戰

1.系統設計

本文主要使用 Spring Security 來實現系統頁面的權限控制和安全認證,本示例不做詳細的數據增刪改查,sql 可以在完整代碼里下載,主要是基於數據庫對頁面 和 ajax 請求做權限控制。

1.1 技術棧

  • 編程語言:Java
  • 編程框架:Spring、Spring MVC、Spring Boot
  • ORM 框架:MyBatis
  • 視圖模板引擎:Thymeleaf
  • 安全框架:Spring Security(5.2.0)
  • 數據庫:MySQL
  • 前端:Layui、JQuery

1.2 功能設計

  1. 實現登錄、退出
  2. 實現菜單 url 跳轉的權限控制
  3. 實現按鈕 ajax 請求的權限控制
  4. 防止跨站請求偽造(CSRF)攻擊

1.3 數據庫層設計

t_user 用戶表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
username varchar 20 用戶名
password varchar 255 密碼

t_role 角色表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_name varchar 20 角色名稱

t_menu 菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
menu_name varchar 20 菜單名稱
menu_url varchar 50 菜單url(Controller 請求路徑)

t_user_roles 用戶權限表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
user_id int 8 用戶表id
role_id int 8 角色表id

t_role_menus 權限菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_id int 8 角色表id
menu_id int 8 菜單表id

實體類這裏不詳細列了。

2.代碼實現

2.0 相關依賴

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- 熱部署模塊 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 -->
        </dependency>
        
            <!-- mysql 數據庫驅動. -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- mybaits -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- alibaba fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

2.1 繼承 WebSecurityConfigurerAdapter 自定義 Spring Security 配置

/**
prePostEnabled :決定Spring Security的前註解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 決定是否Spring Security的保障註解 [@Secured] 是否可用
jsr250Enabled :決定 JSR-250 annotations 註解[@RolesAllowed..] 是否可用.
 */
@Configurable
@EnableWebSecurity
//開啟 Spring Security 方法級安全註解 @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 靜態資源設置
     */
    @Override
    public void configure(WebSecurity webSecurity) {
        //不攔截靜態資源,所有用戶均可訪問的資源
        webSecurity.ignoring().antMatchers(
                "/",
                "/css/**",
                "/js/**",
                "/images/**",
                "/layui/**"
                );
    }
    /**
     * http請求設置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //http.csrf().disable(); //註釋就是使用 csrf 功能       
        http.headers().frameOptions().disable();//解決 in a frame because it set 'X-Frame-Options' to 'DENY' 問題           
        //http.anonymous().disable();
        http.authorizeRequests()
            .antMatchers("/login/**","/initUserData","/main")//不攔截登錄相關方法        
            .permitAll()        
            //.antMatchers("/user").hasRole("ADMIN")  // user接口只有ADMIN角色的可以訪問
//          .anyRequest()
//          .authenticated()// 任何尚未匹配的URL只需要驗證用戶即可訪問
            .anyRequest()
            .access("@rbacPermission.hasPermission(request, authentication)")//根據賬號權限訪問         
            .and()
            .formLogin()
            .loginPage("/")
            .loginPage("/login")   //登錄請求頁
            .loginProcessingUrl("/login")  //登錄POST請求路徑
            .usernameParameter("username") //登錄用戶名參數
            .passwordParameter("password") //登錄密碼參數
            .defaultSuccessUrl("/main")   //默認登錄成功頁面
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler) //無權限處理器
            .and()
            .logout()
            .logoutSuccessUrl("/login?logout");  //退出登錄成功URL
            
    }
    /**
     * 自定義獲取用戶信息接口
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    /**
     * 密碼加密算法
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
 
    }
}

2.2 自定義實現 UserDetails 接口,擴展屬性

public class UserEntity implements UserDetails {

    /**
     * 
     */
    private static final long serialVersionUID = -9005214545793249372L;

    private Long id;// 用戶id
    private String username;// 用戶名
    private String password;// 密碼
    private List<Role> userRoles;// 用戶權限集合
    private List<Menu> roleMenus;// 角色菜單集合

    private Collection<? extends GrantedAuthority> authorities;
    public UserEntity() {
        
    }
    
    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities,
            List<Menu> roleMenus) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.roleMenus = roleMenus;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Role> getUserRoles() {
        return userRoles;
    }

    public void setUserRoles(List<Role> userRoles) {
        this.userRoles = userRoles;
    }

    public List<Menu> getRoleMenus() {
        return roleMenus;
    }

    public void setRoleMenus(List<Menu> roleMenus) {
        this.roleMenus = roleMenus;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

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

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

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

}

2.3 自定義實現 UserDetailsService 接口

/**
 * 獲取用戶相關信息
 * @author charlie
 *
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;
    @Autowired
    private MenuDao menuDao;

    @Override
    public UserEntity loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名查找用戶
        UserEntity user = userDao.getUserByUsername(username);
        System.out.println(user);
        if (user != null) {
            System.out.println("UserDetailsService");
            //根據用戶id獲取用戶角色
            List<Role> roles = roleDao.getUserRoleByUserId(user.getId());
            // 填充權限
            Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //填充權限菜單
            List<Menu> menus=menuDao.getRoleMenuByRoles(roles);
            return new UserEntity(username,user.getPassword(),authorities,menus);
        } else {
            System.out.println(username +" not found");
            throw new UsernameNotFoundException(username +" not found");
        }       
    }

}

2.4 自定義實現 URL 權限控制

/**
 * RBAC數據模型控制權限
 * @author charlie
 *
 */
@Component("rbacPermission")
public class RbacPermission{

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean hasPermission = false;
        // 讀取用戶所擁有的權限菜單
        List<Menu> menus = ((UserEntity) principal).getRoleMenus();
        System.out.println(menus.size());
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {
                hasPermission = true;
                break;
            }
        }
        return hasPermission;
    }
}

2.5 實現 AccessDeniedHandler

自定義處理無權請求

/**
 * 處理無權請求
 * @author charlie
 *
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        boolean isAjax = ControllerTools.isAjaxRequest(request);
        System.out.println("CustomAccessDeniedHandler handle");
        if (!response.isCommitted()) {
            if (isAjax) {
                String msg = accessDeniedException.getMessage();
                log.info("accessDeniedException.message:" + msg);
                String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"沒有權限\"}";
                ControllerTools.print(response, accessDenyMsg);
            } else {
                request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
                response.setStatus(HttpStatus.FORBIDDEN.value());
                RequestDispatcher dispatcher = request.getRequestDispatcher("/403");
                dispatcher.forward(request, response);
            }
        }

    }

    public static class ControllerTools {
        public static boolean isAjaxRequest(HttpServletRequest request) {
            return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        }

        public static void print(HttpServletResponse response, String msg) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(msg);
            writer.flush();
            writer.close();
        }
    }

}

2.6 相關 Controller

登錄/退出跳轉

/**
 * 登錄/退出跳轉
 * @author charlie
 *
 */
@Controller
public class LoginController {
    @GetMapping("/login")
    public ModelAndView login(@RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout) {
        ModelAndView mav = new ModelAndView();
        if (error != null) {
            mav.addObject("error", "用戶名或者密碼不正確");
        }
        if (logout != null) {
            mav.addObject("msg", "退出成功");
        }
        mav.setViewName("login");
        return mav;
    }
}

登錄成功跳轉

@Controller
public class MainController {

    @GetMapping("/main")
    public ModelAndView toMainPage() {
        //獲取登錄的用戶名
        Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username=null;
        if(principal instanceof UserDetails) {
            username=((UserDetails)principal).getUsername();
        }else {
            username=principal.toString();
        }
        ModelAndView mav = new ModelAndView();
        mav.setViewName("main");
        mav.addObject("username", username);
        return mav;
    }
    
}

用於不同權限頁面訪問測試

/**
 * 用於不同權限頁面訪問測試
 * @author charlie
 *
 */
@Controller
public class ResourceController {

    @GetMapping("/publicResource")
    public String toPublicResource() {
        return "resource/public";
    }
    
    @GetMapping("/vipResource")
    public String toVipResource() {
        return "resource/vip";
    }
}

用於不同權限ajax請求測試

/**
 * 用於不同權限ajax請求測試
 * @author charlie
 *
 */
@RestController
@RequestMapping("/test")
public class HttptestController {

    @PostMapping("/public")
    public JSONObject doPublicHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }

    @PostMapping("/vip")
    public JSONObject doVipHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }
}

2.7 相關 html 頁面

登錄頁面

<form class="layui-form" action="/login" method="post">
            <div class="layui-input-inline">
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
                <input type="text" name="username" required
                    placeholder="用戶名" autocomplete="off" class="layui-input">
            </div>
            <div class="layui-input-inline">
                <input type="password" name="password" required  placeholder="密碼" autocomplete="off"
                    class="layui-input">
            </div>
            <div class="layui-input-inline login-btn">
                <button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登錄</button>
            </div>
            <div class="form-message">
                <label th:text="${error}"></label>
                <label th:text="${msg}"></label>
            </div>
        </form>

防止跨站請求偽造(CSRF)攻擊

退出系統

<form id="logoutForm" action="/logout" method="post"
                                style="display: none;">
                                <input type="hidden" th:name="${_csrf.parameterName}"
                                    th:value="${_csrf.token}">
                            </form>
                            <a
                                href="javascript:document.getElementById('logoutForm').submit();">退出系統</a>

ajax 請求頁面

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF">
<button class="layui-btn" id="btnPublic">公共權限請求按鈕</button>
<br>
<br>
<button class="layui-btn" id="btnVip">VIP權限請求按鈕</button>
<script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script>
<script type="text/javascript" th:src="@{/layui/layui.js}"></script>
<script type="text/javascript">
        layui.use('form', function() {
            var form = layui.form;
            $("#btnPublic").click(function(){
                $.ajax({
                    url:"/test/public",
                    type:"POST",
                    data:{id:1},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                
                    }   
                });
            });
            $("#btnVip").click(function(){
                $.ajax({
                    url:"/test/vip",
                    type:"POST",
                    data:{id:2},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                        
                    }
                });
            });
        });
    </script>

2.8 測試

測試提供兩個賬號:user 和 admin (密碼與賬號一樣)

由於 admin 作為管理員權限,設置了全部的訪問權限,這裏只展示 user 的測試結果。

完整代碼

非特殊說明,本文版權歸 所有,轉載請註明出處.

原文標題:Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

原文地址:

如果文章有不足的地方,歡迎提點,後續會完善。

如果文章對您有幫助,請給我點個贊,請掃碼關注下我的公眾號,文章持續更新中…

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※高價收購3C產品,價格不怕你比較

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