分類: 3C資訊

  • (七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、內存)

    (七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、內存)

    興趣的朋友可以去了解一下前五篇,你的贊就是對我最大的支持,感謝大家!

    (一) SpringBoot起飛之路-HelloWorld

    (二) SpringBoot起飛之路-入門原理分析

    (三) SpringBoot起飛之路-YAML配置小結(入門必知必會)

    (四) SpringBoot起飛之路-靜態資源處理

    (五) SpringBoot起飛之路-Thymeleaf模板引擎

    (六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatis

    說明:

    • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻醜了

    • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有需要的小夥伴,隨意去 down

      • https://github.com/ideal-20/Springboot-Study-Code
    • 才疏學淺,就會點淺薄的知識,大家權當一篇工具文來看啦,不喜勿憤哈 ~

    (一) 初識 Spring Security

    (1) 引言

    權限以及安全問題,雖然並不是一個影響到程序、項目運行的必須條件,但是卻是開發中的一項重要考慮因素,例如某些資源我們不想被訪問到或者我們某些方法想要滿足指定身份才可以訪問,我們可以使用 AOP 或者過濾器來實現要求,但是實際上,如果代碼涉及的邏輯比較多以後,代碼是極其繁瑣,冗餘的,而有很多開發框架,例如 Spring Security,Shiro,已經為我們提供了這種功能,我們只需要知道如何正確配置以及使用它了

    (2) 基本介紹

    先看一下官網的介紹

    Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

    Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它是保護基於spring的應用程序的實際標準。

    Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

    Spring Security是一個框架,側重於為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring安全性的真正強大之處在於它很容易擴展以滿足定製需求

    簡單的說,Spring Security 就是一個控制訪問權限,強大且完善的框架

    Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分,同時它們也是 Spring Security 提供的核心功能

    用戶認證:用戶認證就是指這個用戶身份是否合法,一般我們的用戶認證就是通過校驗用戶名密碼,來判斷用戶身份的合法性,確定身份合法后,用戶就可以訪問該系統

    用戶授權:如果不同的用戶需要有不同等級的權限,就涉及到用戶授權,用戶授權就是對用戶能訪問的資源,所能執行的操作進行控制,根據不同用戶角色來劃分不同的權限

    (二) 靜態頁面導入 And 環境搭建

    (1) 關於靜態頁面

    A:頁面介紹

    頁面是我自己臨時弄得,有需要的朋友可以去我 GitHub:ideal-20 下載源碼,簡單說明一下這個頁面

    做一個靜態頁面如果嫌麻煩,也可以單純的自己創建一些簡單的頁面,寫幾個標題文字,能體現出當前是哪個頁面就好了

    我代碼中用的這些頁面,就是拿開源的前端組件框架進行了一點的美化,然後方便講解一些功能,頁面模板主要是配合 Thymeleaf

    1、目錄結構

    ├── index.html                        // 首頁
    ├── images                            // 首頁圖片,僅美觀,無實際作用
    ├── css                               // 上線項目文件,放在服務器即可正常訪問
    ├── js                                // 項目截圖
    ├── views                             // 總子頁面文件夾,權限驗證的關鍵頁面
    │   ├── login.html					  // 自製登錄頁面(用來替代 Spring Security 默認的 )
    │   ├── L-A							  // L-A 子頁面文件夾,下含 a b c 三個子頁面
    │   │   ├── a.html
    │   │   ├── b.html
    │   │   ├── c.html
    |	├── L-B							  // L-B 子頁面文件夾,下含 a b c 三個子頁面
    │   │   ├── a.html
    │   │   ├── b.html
    │   │   ├── c.html
    |	├── L-C							  // L-C 子頁面文件夾,下含 a b c 三個子頁面
    │   │   ├── a.html
    │   │   ├── b.html
    │   │   ├── c.html
    

    B:導入到項目

    主要就是把基本一些鏈接,引入什麼的先替換成 Thymeleaf 的標籤格式,這裏語法用的不是特別多,即使對於 Thymeleaf 不是很熟悉也是很容易看懂的,當然如果仍然感覺有點吃力,可以單純的做成 html,將就一下,或者去看一下我以前的文章哈,裏面有關於 Thymeleaf 入門的講解

    css、image、js 放到 resources –> static 下 ,views 和 index.html 放到 resources –> templates下

    (2) 環境搭建

    A:引入依賴

    這一部分引入也好,初始化項目的時候,勾選好自動生成也好,只要依賴正常導入了即可

    • 引入 Spring Security 模塊
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

    關鍵的依賴主要就是上面這個啟動器,但是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools

    thymeleaf-extras-springsecurity5 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Spring Security 的

    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    

    B:頁面跳轉 Controller

    因為我們用了模板,頁面的跳轉就需要交給 Controller 了,很簡單,首先是首頁的,當然關於頁面這個就無所謂了,我隨便跳轉到了我的博客,接着還有一個登錄頁面的跳轉

    有一個小 Tip 需要提一下,因為 L-A、L-B、L-C 文件夾下都有3個頁面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 寫一個較為通用的跳轉方法

    @Controller
    public class PageController {
    
        @RequestMapping({"/", "index"})
        public String index() {
            return "index";
        }
    
        @RequestMapping("/about")
        public String toAboutPage() {
            return "redirect:http://www.ideal-20.cn";
        }
    
        @RequestMapping("/toLoginPage")
        public String toLoginPage() {
            return "views/login";
        }
    
        @RequestMapping("/levelA/{name}")
        public String toLevelAPage(@PathVariable("name") String name) {
            return "views/L-A/" + name;
        }
    
        @RequestMapping("/levelB/{name}")
        public String toLevelBPage(@PathVariable("name") String name) {
            return "views/L-B/" + name;
        }
    
        @RequestMapping("/levelC/{name}")
        public String toLevelCPage(@PathVariable("name") String name) {
            return "views/L-C/" + name;
        }
    }
    

    C:環境搭建最終效果

    • 為了貼圖方便,我把頁面拉窄了一點
    • 首頁右上角應該為登錄的鏈接,這裡是因為,我運行的是已經寫好的代碼,不登錄頁面例如 L-A-a 等模塊就显示不出來,所以拿一個定義好的管理員身份登陸了
    • 關於如何使其自動切換显示登陸還是登錄后信息,在後面會講解

    1、首頁

    2、子頁面

    L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一樣的,只是文字有一點變化

    3、登陸頁面

    (三) 整合 Spring Security (內存中)

    這一部分,為了簡化一些,容易理解一些,沒有從帶數據的場景出發(因為涉及代碼少一些,所以講解會多一點),而是直接將一些身份等等寫死了,寫到了內存中,方便理解,接着會在下一個標題中給出含有數據庫的寫法(講解會少一些,重點只說一些與前一種的不同點)

    (1) 配置授權內容

    A:源碼了解用戶授權方式

    可以去官網看一下,官網有提供給我們一些樣例,其中有一個關於配置類的小樣例,也就是下面這個,我們通過這個例子,展開分析

    https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

    @EnableWebSecurity
    public class Config extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .apply(customDsl())
                    .flag(true)
                    .and()
                ...;
        }
    }
    

    1、創建 config –> SecurityConfig 配置類

    • 創建一個配置類,像官網中一樣,繼承 WebSecurityConfigurerAdapter
    • 類上添加 @EnableWebSecurity 註解,代表開啟WebSecurity模式
    • 重寫 configure(HttpSecurity http) 方法
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
        }
    }
    

    既然是重寫,那麼我們可以點進去,看一下父類中關於 configure(HttpSecurity http) 方法的源碼註釋,它有很多有用的信息

    我摘選出這麼兩小段,第一段的意思就是 ,我們想要使用 HttpSecurity ,要通過重寫,不能通過 super 調用,否則會有覆蓋問題,第二段就是給出了一個默認的配置方式

    * Override this method to configure the {@link HttpSecurity}. Typically subclasses
    * should not invoke this method by calling super as it may override their
    
    * configuration. The default configuration is:
    * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
    

    2、按照源碼的註釋分析

    我們先按照剛才看到的註釋寫出來,首先能看到,它是支持一個鏈式調用的

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().httpBasic();
    }
    
    • 通過字面意思也很好理解,authorizeRequests 是關於請求授權的,所以要涉及到關於請求授權(允許指定身份用戶訪問不同權限的資源)的問題就需要調用了

    • 其次,anyRequest().authenticated() 也就是說所有HTTP請求都需要被認證

    • 接着看,通過 and() 連接了一些新的內容,例如選擇表單登錄還是 HTTPBasic 的方式(這裏認證的過程就是讓你輸入用戶名密碼,檢測你的身份,兩種方式表單或者那種彈窗)

    Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全

    給大家演示一下:

    • 如果不指定一種認證方式 .and().formLogin() 或者 .and().httpBasic() 訪問任何頁面都會提示 403 禁止訪問的錯誤
    • 指定 .and().formLogin() 認證,彈出一個表單頁面(自帶的,和自己創建的沒關係)
    • 指定 .and().httpBasic(); 認證,彈出一個窗口進行 HTTPBasic 認證

    B:自定製用戶授權

    1、先看源碼註釋

    默認配置,設定了所有 HTTP 請求 都需要進行認證,所以我們在訪問首頁等的時候也會被攔截,但是實際情況下,有一些頁面是可以被任何人訪問的,例如首頁,或者自定義的登陸的等頁面,這時候需要用自己定義一些用戶授權的規則

    在 WebSecurityConfigurerAdapter 的 formLogin() 註釋附近,又看到了一個有意思的內容

    注:&quot 代表引號

    * 		http
    * 			.authorizeRequests(authorizeRequests ->
    * 				authorizeRequests
    * 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
    * 			)
    

    這就是我們想要找的,自定義的配置,通過一個一個 antMatchers 進行匹配,通過 hasRole 來規定其合法的身份,也就是說只有滿足這個身份的用戶才能訪問前面規定的路徑資源

    Matchers 前面的 ant 前綴代表着,他可以用 ant 風格的路徑表達式(舉例的時候就能看懂了)

    通配符 說明
    ? 匹配任何單字符
    * 匹配0或者任意數量的字符
    ** 匹配0或者更多的目錄

    補充: 如果想用正則表達式的方式,可以用這個方法 .regexMatchers()

    當然了,有很多情況下,你想要讓任何人都可以訪問某個路徑,例如首頁,permitAll() 方法 就可以達到這種效果,在這裏補充一些常用的方法

    • permitAll() :允許任何訪問

    • denyAll():拒絕所有訪問

    • anonymous():允許匿名用戶訪問

    • authenticated() :允許認證的用戶進行訪問

    • hasRole(String) :如果用戶具備給定角色(用戶組)的話,就允許訪問/

    • hasAnyRole(String…) :如果用戶具有給定角色(用戶組)中的一個的話,允許訪問.

    • rememberMe() :如果用戶是通過Remember-me功能認證的,就允許訪問

    • fullyAuthenticated():如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問

    • hasAuthority(String):如果用戶具備給定權限的話就允許訪問

    • hasAnyAuthority(String…) :如果用戶具備給定權限中的某一個的話,就允許訪問

    • hasIpAddress(String) :如果請求來自給定ip地址的話,就允許訪問.

    • not() :對其他訪問結果求反

    說明:hasAnyAuthority(“ROLE_ADMIN”) 和 hasRole(“ADMIN”) 的區別就是,後者會自動使用 它會自動使用 “ROLE_” 前綴

    2、我們來定製一下用戶授權

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http.authorizeRequests()
            	.antMatchers("/").permitAll()
            	.antMatchers("/levelA/**").hasRole("vip1")
            	.antMatchers("/levelB/**").hasRole("vip2")
            	.antMatchers("/levelC/**").hasRole("vip3")
            	.and().formLogin();
    }
    

    我們上面代碼的意思就是,當訪問 /levelA/ /levelB/ /levelC/ 這三個路徑下面的任意文件(這裡有 a/b/c.html)都需要認證,身份分別是對應 vip1、vip2、vip3,而其他頁面,就可以隨便訪問了

    很顯然,雖然說規定了授權的內容,也就是哪些權限的用戶,可以訪問哪些資源,但是我們由於並沒有配置用戶的信息(合法的或者非法的),所以自然,前面的登錄頁面,都是會直接報錯的,下面我們來分析一下,如何進行認證

    (2) 配置認證內容

    A:源碼了解用戶認證方式

    剛才的授權部分,我們重寫了 configure(HttpSecurity http) 方法,我們繼續看看重寫方法中,有沒有可能幫助我們驗證身份,進行用戶認證的方法,我們首先來看這個方法 configure(AuthenticationManagerBuilder auth)

    先去看一下源碼的註釋(此部分的格式,我稍微修改了一下,方便觀看):

    這是其中他局舉的一個例子,其實這個就是我們想要的,看註釋也可以看出來,他就是用來在內存中啟用基於用戶名的身份驗證的

    * protected void configure(AuthenticationManagerBuilder auth) {
    *  auth
    *  // enable in memory based authentication with a user named
    *  // &quot;user&quot; and &quot;admin&quot;
    *  		.inMemoryAuthentication()
    *   		.withUser(&quot;user&quot;)
    *    			.password(&quot;password&quot;)
    *    			.roles(&quot;USER&quot;).and()
    *        	.withUser(&quot;admin&quot;)
    *    			.password(&quot;password&quot;)
    *    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
    * }
    

    照貓畫虎,我們也先這麼做

    B:自定製用戶認證

    代碼如下:

    //定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin")
            		.password(new BCryptPasswordEncoder().encode("666"))
            		.roles("vip1", "vip2", "vip3")
                .and()
                .withUser("ideal-20")
            		.password(new BCryptPasswordEncoder().encode("666"))
            		.roles("vip1", "vip2")
                .and()
                .withUser("jack")
            		.password(new BCryptPasswordEncoder().encode("666"))
            		.roles("vip1");
    }
    

    我們就是照着例子打的,但是,其中我們又加入了編碼的問題,它要求必須進行編碼,否則會報錯,官方推薦的是bcrypt加密方式,我們這裏就用這種,當然自己用常見的 MD5 等等都是可以的,可以自己寫一個工具類

    到這裏,測試一下,實際上就可以按照身份的不同,從而擁有訪問不同路徑資源你的權限了,主要的功能已經實現了,下面補充一些,更加友好的功能,例如登錄註銷按鈕的显示,以及記住密碼等等

    (3) 註銷問題

    1、註銷配置

    當然了,前面因為已經有很多配置了,所以可以通過 .and() 進行連接,例如 .and().xxx,或者像下面給出的,單獨再寫一個 http.xxx

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       ......
        // 註銷配置
    	http.logout().logoutSuccessUrl("/")
    }
    

    上面短短一句的代碼, logout() 代表開啟了註銷的配置,logoutSuccessUrl(“/”),代表註銷成功后,返回的頁面,我們令其註銷后回到首頁

    前台的頁面中,我已經給出了註銷的按鈕的代碼,當然這不是固定的,不同的 ui 框架,不同的模板引擎都是不一樣的,但是路徑是 /logout

    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 註銷
    </a>
    

    (4) 根據身份權限显示組件

    A:登錄、註銷的显示

    還有這樣一種問題,右上角,未登錄的時候,應該显示登陸按鈕,登錄后,應該显示用戶信息,以及註銷等等,這一部分,主要是頁面這邊的問題

    显示的條件其實很簡單,就是判斷是否認證了,認證了就取出一些值,沒認證就显示登陸

    1、這時,我們就需要引入一個 Thymeleaf 配合 Spring Security 的一個依賴 (當然了如果是別的技術,就不一樣了)

    地址如下:

    https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    2、導入命名空間

    引入這個文件的目的,就是為了在頁面寫權限判斷等相關的內容的時候可以有提示

    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    

    3、修改導航欄邏輯

    <!--登錄註銷-->
    <div class="right menu">
    
      <!--如果未登錄-->
      <div sec:authorize="!isAuthenticated()">
        <a class="item" th:href="@{/toLoginPage}">
          <i class="address card icon"></i> 登錄
        </a>
      </div>
    
      <!--如果已登錄-->
      <div sec:authorize="isAuthenticated()">
        <a class="item">
          <i class="address card icon"></i>
          用戶名:<span sec:authentication="principal.username"></span>
          <!--角色:<span sec:authentication="principal.authorities"></span>-->
        </a>
      </div>
    
      <div sec:authorize="isAuthenticated()">
        <a class="item" th:href="@{/logout}">
          <i class="address card icon"></i> 註銷
        </a>
      </div>
    </div>
    

    B:組件面板的显示

    上面的代碼,解決了導航欄的問題,但是例如我們首頁中,一些板塊,對於不同的用戶的显示也是不同的嗎

    正如上面的例子,沒有登錄的用戶,是不能訪問了 /levelA/、 /levelB/、 /levelC/ 下面的任何文件的,只有登錄的用戶,根據權限的大小,才能訪問某一個,或者所有

    而我們首頁部分的三個面板就是用來显示這三塊的鏈接,對於沒有足夠身份的人,實際上显示這個面板就已經是多餘了,當然,你可以選擇显示,但是如果想要根據身份显示面板怎麼做呢?

    關鍵就是在 div 中添加了這樣一句權限的代碼,沒有這個指定的身份,這個面板就不會显示sec:authorize="hasRole('vip1')"

    <div class="column" sec:authorize="hasRole('vip1')">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelA/a}">L-A-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/b}">L-A-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/c}">L-A-c</a>
        </div>
      </div>
    </div>
    <div class="column" sec:authorize="hasRole('vip2')">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelB/a}">L-B-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/b}">L-B-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/c}">L-B-c</a>
        </div>
      </div>
    </div>
    <div class="column" sec:authorize="hasRole('vip3')">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelC/a}">L-A-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/b}">L-C-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/c}">L-C-c</a>
        </div>
      </div>
    </div>
    

    演示一下:

    (5) 記住用戶

    如果重啟瀏覽器后,就需要重新登錄,對於一部分用戶來說,他們認為是麻煩的,所以很多網站登錄時都提供記住用戶這種選項

    1、一個簡單的配置就可以達到目的,這種情況下,默認的登陸頁面,就會多出一個記住用戶的單選框

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	......
    	//記住用戶
        http.rememberMe();
    }
    

    2、但是如果,登陸頁面是自定義(下面講)的怎麼辦呢?,其實只要修改為如下配置即可,

    //定製記住我的參數!
    http.rememberMe().rememberMeParameter("remember");
    

    上面的 remember 對應 input 中的 name 屬性值

    <input type="checkbox" name="remember"/>
    <label>記住密碼</label>
    

    3、它做了哪些事情呢?

    可以打開頁面的控制台看一下,實際上配置后,用戶選擇記住密碼后,會自動幫我們增加一個 cookie 叫做 remember-me,過期時間為 14 天,當註銷的時候,這個 cookie 就會被刪除了

    (6) 定製登錄頁面

    1、配置

    自帶的登陸頁面確實,還是比較丑的,版本更低一些的,更是不美觀,如果想要使用自己定製的登陸頁面,可以加入下面的配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	......
    	// 登陸表單提交請求
        http.formLogin()
    	.usernameParameter("username")
    	.passwordParameter("password")
    	.loginPage("/toLoginPage")
    	.loginProcessingUrl("/login")
    }
    
    • .loginPage("/toLoginPage") 就是說,當你訪問一些需要用戶權限認證的頁面時,就會發起這個請求,到你的登錄頁面
    • .loginProcessingUrl("/login") 就是表單中,真正要提交請求的一個路徑
    • 其餘兩個就是關於用戶名和密碼的一個獲取,其值和頁面中用戶名密碼的 name 屬性值一致

    2、頁面跳轉

    前面我們就提過這個,回顧一下

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }
    

    3、自定義登錄頁面的表單提交 action 設置

    <form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
    	......
    </form>
    

    (7) 關閉csrf

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	......
    	//關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
    	http.csrf().disable();
    }
    

    (四) 整合 Spring Security (JDBC)

    因為配置內存中的用戶還是相對簡單一些的,所以一些細節也都說了一下,基於上面的基礎,來看一下 如何用 JDBC 實現上面的功能,當然了這部分只能算補充,基本不會這麼用的,下面的整合 MyBatis 才是常用的()

    (1) 創建表以及數據

    這裏創建了三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

    這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `uid` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
      `password` varchar(255) DEFAULT NULL COMMENT '密碼',
      `roles` varchar(255) DEFAULT NULL COMMENT '角色',
      PRIMARY KEY (`uid`)
    )
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
    INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
    INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');
    

    (2) 創建實體

    我使用了 lombok,不過自己寫 get set 構造方法 也是一樣的

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Integer uid;
        private String username;
        private String password;
        private String roles;
    }
    

    (3) 配置授權內容

    這部分沒什麼區別

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
                .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
                .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
                .and().formLogin()
    
                // 登陸表單提交請求
                .and().formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/toLoginPage")
                .loginProcessingUrl("/login")
                //註銷
                .and().logout().logoutSuccessUrl("/")
                //記住我
                .and().rememberMe().rememberMeParameter("remember")
                //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
                .and().csrf().disable();
    }
    

    (4) 配置認證內容

    A:配置數據庫

    spring:
      datasource:
        username: root
        password: root99
        url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    server:
      port: 8082
    

    B:具體配置

    以幾個注意的地方:

    • 查詢語句都是通過 username 查詢

    • usersByUsernameQuery()方法里的參數一定要有一個 true 的查詢結果,所以我直接在查詢語句中寫了一個 true

    • MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候簡單點

    • DataSource dataSource 要在前面注入進來(選擇 sql 的)

    //定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery("select username,password,true from user where username = ?")
                .authoritiesByUsernameQuery("select username,roles from user where username = ?")
                .passwordEncoder(new PasswordEncoder() {
                    @Override
                    public String encode(CharSequence rawPassword) {
                        return MD5Util.MD5EncodeUtf8((String) rawPassword);
                    }
    
                    @Override
                    public boolean matches(CharSequence rawPassword, String encodedPassword) {
                        return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                    }
                });
    }
    

    C:MD5工具類

    package cn.ideal.utils;
    
    import java.security.MessageDigest;
    
    /**
     * @ClassName: MD5Util
     * @Description: MD5 加密工具類
     * @Author: BWH_Steven
     * @Date: 2020/4/27 16:46
     * @Version: 1.0
     */
    public class MD5Util {
    
        private static String byteArrayToHexString(byte b[]) {
            StringBuffer resultSb = new StringBuffer();
            for (int i = 0; i < b.length; i++)
                resultSb.append(byteToHexString(b[i]));
    
            return resultSb.toString();
        }
    
        private static String byteToHexString(byte b) {
            int n = b;
            if (n < 0)
                n += 256;
            int d1 = n / 16;
            int d2 = n % 16;
            return hexDigits[d1] + hexDigits[d2];
        }
    
        /**
         * 返回大寫MD5
         *
         * @param origin
         * @param charsetname
         * @return
         */
        private static String MD5Encode(String origin, String charsetname) {
            String resultString = null;
            try {
                resultString = new String(origin);
                MessageDigest md = MessageDigest.getInstance("MD5");
                if (charsetname == null || "".equals(charsetname))
                    resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
                else
                    resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
            } catch (Exception exception) {
            }
            return resultString.toUpperCase();
        }
    
        public static String MD5EncodeUtf8(String origin) {
    //        origin = origin + PropertiesUtil.getProperty("password.salt", "");
            return MD5Encode(origin, "utf-8");
        }
    
        private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
                "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
    }
    

    D:修改頁面

    到這裏,JDBC 的整合方式就成功了,至於前面的頁面只需要根據我們自己設計的權限進行修改,別的地方和前面內存中的方式是一樣的

    <div class="ui stackable three column grid">
      <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
        <div class="ui raised segments">
          <div class="ui segment">
            <a th:href="@{/levelA/a}">L-A-a</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelA/b}">L-A-b</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelA/c}">L-A-c</a>
          </div>
        </div>
      </div>
      <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
        <div class="ui raised segments">
          <div class="ui segment">
            <a th:href="@{/levelB/a}">L-B-a</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelB/b}">L-B-b</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelB/c}">L-B-c</a>
          </div>
        </div>
      </div>
      <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
        <div class="ui raised segments">
          <div class="ui segment">
            <a th:href="@{/levelC/a}">L-C-a</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelC/b}">L-C-b</a>
          </div>
          <div class="ui segment">
            <a th:href="@{/levelC/c}">L-C-c</a>
          </div>
        </div>
      </div>
      <!-- <div class="column"></div> -->
    </div>
    

    (五) 整合 Spring Security (MyBatis)

    因為這部分內容是比較常用的,所以,我盡可能給的完善一些

    (1) 添加依賴

    像 lombok、commons-lang3 都不是必須的,都是可以使用原生的一些手段替代的,寫到那裡我會提的

    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    

    (2) 創建表

    和 JDBC 部分用同樣的表

    三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

    這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `uid` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
      `password` varchar(255) DEFAULT NULL COMMENT '密碼',
      `roles` varchar(255) DEFAULT NULL COMMENT '角色',
      PRIMARY KEY (`uid`)
    )
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
    INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
    INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');
    

    (3) 整合 MyBatis

    在進行 Spring Security 的配置前,最好先把 MyBatis 先整合好,這樣等會只考慮 Spring Security 的問題就可以了

    說明:這部分我盡可能簡化了,例如連接池就用默認的,如果這部分感覺還是有點問題,可以參考一下我前幾篇,關於整合 MyBatis 的文章

    A:配置數據庫

    spring:
      datasource:
        username: root
        password: root99
        url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    mybatis:
      mapper-locations: classpath:mapper/*Mapper.xml
      type-aliases-package: cn.ideal.pojo
    
    server:
      port: 8081
    

    B:配置 Mapper 以及 XML

    UserMapper

    @Mapper
    public interface UserMapper {
        User queryUserByUserName(String username);
    }
    

    mapper/UserMapper.xml

    <?xml version="1.0" encoding="utf-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="cn.ideal.mapper.UserMapper">
        <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
             select * from user where username = #{username}
        </select>
    </mapper>
    

    這裏就不演示測試了,是沒有問題的

    (4) 配置授權內容

    這部分沒什麼好說的,和前面的都一樣,解釋在內存中配置用戶時已經詳細說過了

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
                .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
                .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
                .and().formLogin()
    
                // 登陸表單提交請求
                .and().formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/toLoginPage")
                .loginProcessingUrl("/login")
                //註銷
                .and().logout().logoutSuccessUrl("/")
                //記住我
                .and().rememberMe().rememberMeParameter("remember")
                //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
                .and().csrf().disable();
    }
    

    (5) 配置認證內容

    A:創建 UserService

    創建一個類,實現 UserDetailsService,其實主要就是為了 loadUserByname 方法,在這個類中,我們可以注入 mapper 等等,去查用戶,如果查不到,就還留在這個頁面,如果查到了,做出一定邏輯后(例如判空等等),就會把用戶信息封裝到 Spring Security 自己的的 User類中去,Spring Security 拿前台的數據和它比較,做出操作,例如認證成功或者錯誤

    注意:

    • StringUtils 是 commons.lang3 下的,使用需要導包,我們用了一個判空功能,不想用的話,用原生的是一個道理,這不是重點
    • 注意區分自己的 User 和 Spring Security 的 User
    @Service
    public class UserService<T extends User> implements UserDetailsService{
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userMapper.queryUserByUserName(username);
            if (username == null){
                throw  new UsernameNotFoundException("用戶名不存在");
            }
    
            List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
            String role = user.getRoles();
            if (StringUtils.isNotBlank(role)){
                authorityList.add(new SimpleGrantedAuthority(role.trim()));
            }
            return new org.springframework.security.core.userdetails
                .User(user.getUsername(),user.getPassword(),authorityList);
        }
    }
    

    B:修改配置類

    這裏也很熟悉,我們調用就可以調用 userDetailsService 了,同樣還需要指定編碼相關的內容 實例化 PasswordEncoder,就需要重寫 encode、 matches

    //定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return MD5Util.MD5EncodeUtf8((String) rawPassword);
            }
    
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
            }
        });
    }
    

    C:MD5 工具類補充

    其實上面已經給出了,但是怕大家看起來不方便,這裏再貼一下

    MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候可以簡單點

    package cn.ideal.utils;
    
    import java.security.MessageDigest;
    
    /**
     * @ClassName: MD5Util
     * @Description: MD5 加密工具類
     * @Author: BWH_Steven
     * @Date: 2020/4/27 16:46
     * @Version: 1.0
     */
    public class MD5Util {
    
        private static String byteArrayToHexString(byte b[]) {
            StringBuffer resultSb = new StringBuffer();
            for (int i = 0; i < b.length; i++)
                resultSb.append(byteToHexString(b[i]));
    
            return resultSb.toString();
        }
    
        private static String byteToHexString(byte b) {
            int n = b;
            if (n < 0)
                n += 256;
            int d1 = n / 16;
            int d2 = n % 16;
            return hexDigits[d1] + hexDigits[d2];
        }
    
        /**
         * 返回大寫MD5
         *
         * @param origin
         * @param charsetname
         * @return
         */
        private static String MD5Encode(String origin, String charsetname) {
            String resultString = null;
            try {
                resultString = new String(origin);
                MessageDigest md = MessageDigest.getInstance("MD5");
                if (charsetname == null || "".equals(charsetname))
                    resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
                else
                    resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
            } catch (Exception exception) {
            }
            return resultString.toUpperCase();
        }
    
        public static String MD5EncodeUtf8(String origin) {
    //        origin = origin + PropertiesUtil.getProperty("password.salt", "");
            return MD5Encode(origin, "utf-8");
        }
    
        private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
                "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
    }
    

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

    【【其他文章推薦】

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

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

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

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

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

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

  • 最新的一波Vue實戰技巧,不用則已,一用驚人

    Vue中,不同的選項有不同的合併策略,比如 data,props,methods是同名屬性覆蓋合併,其他直接合併,而生命周期鈎子函數則是將同名的函數放到一個數組中,在調用的時候依次調用

    Vue中,提供了一個api, Vue.config.optionMergeStrategies,可以通過這個api去自定義選項的合併策略。

    在代碼中打印

    console.log(Vue.config.optionMergeStrategies)
    

      

     通過合併策略自定義生命周期函數

    背景

    最近客戶給領導反饋,我們的系統用一段時間,瀏覽器就變得有點卡,不知道為什麼。問題出來了,本來想甩鍋到後端,但是瀏覽器問題,沒法甩鍋啊,那就排查吧。

    後來發現頁面有許多定時器,ajax輪詢還有動畫,打開一個瀏覽器頁簽沒法問題,打開多了,瀏覽器就變得卡了,這時候我就想如果能在用戶切換頁簽時候將這些都停掉,不久解決了。百度裏面上下檢索,找到了一個事件visibilitychange,可以用來判斷瀏覽器頁簽是否显示。

    有方法了,就寫唄

    export default {
      created() {
        window.addEventListener('visibilitychange', this.$_hanldeVisiblityChange)
        // 此處用了hookEvent,可以參考小編前一篇文章
        this.$on('hook:beforeDestroy', () => {
          window.removeEventListener(
            'visibilitychange',
            this.$_hanldeVisiblityChange
          )
        })
      },
      methods: {
        $_hanldeVisiblityChange() {
          if (document.visibilityState === 'hidden') {
            // 停掉那一堆東西
          }
          if (document.visibilityState === 'visible') {
            // 開啟那一堆東西
          }
        }
      }
    }
    

    通過上面的代碼,可以看到在每一個需要監聽處理的文件都要寫一堆事件監聽,判斷頁面是否显示的代碼,一處兩處還可以,文件多了就頭疼了,這時候小編突發奇想,定義一個頁面显示隱藏的生命周期鈎子,把這些判斷都封裝起來

    自定義生命周期鈎子函數

    定義生命周期函數 pageHidden 與 pageVisible

    import Vue from 'vue'
    
    // 通知所有組件頁面狀態發生了變化
    const notifyVisibilityChange = (lifeCycleName, vm) => {
      // 生命周期函數會存在$options中,通過$options[lifeCycleName]獲取生命周期
      const lifeCycles = vm.$options[lifeCycleName]
      // 因為使用了created的合併策略,所以是一個數組
      if (lifeCycles && lifeCycles.length) {
        // 遍歷 lifeCycleName對應的生命周期函數列表,依次執行
        lifeCycles.forEach(lifecycle => {
          lifecycle.call(vm)
        })
      }
      // 遍歷所有的子組件,然後依次遞歸執行
      if (vm.$children && vm.$children.length) {
        vm.$children.forEach(child => {
          notifyVisibilityChange(lifeCycleName, child)
        })
      }
    }
    
    // 添加生命周期函數
    export function init() {
      const optionMergeStrategies = Vue.config.optionMergeStrategies
      // 定義了兩個生命周期函數 pageVisible, pageHidden
      // 為什麼要賦值為 optionMergeStrategies.created呢
      // 這個相當於指定 pageVisible, pageHidden 的合併策略與 created的相同(其他生命周期函數都一樣)
      optionMergeStrategies.pageVisible = optionMergeStrategies.beforeCreate
      optionMergeStrategies.pageHidden = optionMergeStrategies.created
    }
    
    
    // 將事件變化綁定到根節點上面
    // rootVm vue根節點實例
    export function bind(rootVm) {
      window.addEventListener('visibilitychange', () => {
        // 判斷調用哪個生命周期函數
        let lifeCycleName = undefined
        if (document.visibilityState === 'hidden') {
          lifeCycleName = 'pageHidden'
        } else if (document.visibilityState === 'visible') {
          lifeCycleName = 'pageVisible'
        }
        if (lifeCycleName) {
          // 通過所有組件生命周期發生變化了
          notifyVisibilityChange(lifeCycleName, rootVm)
        }
      })
    }
    

    應用

    1. main.js主入口文件引入
    import { init, bind } from './utils/custom-life-cycle'
    
    // 初始化生命周期函數, 必須在Vue實例化之前確定合併策略
    init()
    
    const vm = new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
    // 將rootVm 綁定到生命周期函數監聽裏面
    bind(vm)
    

      2. 在需要的地方監聽生命周期函數

    export default {
      pageVisible() {
        console.log('頁面显示出來了')
      },
      pageHidden() {
        console.log('頁面隱藏了')
      }
    }
    

      

    provideinject,不止父子傳值,祖宗傳值也可以

    Vue相關的面試經常會被面試官問道,Vue父子之間傳值的方式有哪些,通常我們會回答,props傳值,$emit事件傳值,vuex傳值,還有eventbus傳值等等,今天再加一種provideinject傳值,離offer又近了一步。(對了,下一節還有一種)

    使用過React的同學都知道,在React中有一個上下文Context,組件可以通過Context向任意後代傳值,而Vueprovideinject的作用於Context的作用基本一樣

    先舉一個例子

    使用過elemment-ui的同學一定對下面的代碼感到熟悉

    <template>
      <el-form :model="formData" size="small">
        <el-form-item label="姓名" prop="name">
          <el-input v-model="formData.name" />
        </el-form-item>
        <el-form-item label="年齡" prop="age">
          <el-input-number v-model="formData.age" />
        </el-form-item>
        <el-button>提交</el-button>
      </el-form>
    </template>
    <script>
    export default {
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      

    看了上面的代碼,貌似沒啥特殊的,天天寫啊。在el-form上面我們指定了一個屬性size="small",然後有沒有發現表單裏面的所有表單元素以及按鈕的 size都變成了small,這個是怎麼做到的?接下來我們自己手寫一個表單模擬一下

    自己手寫一個表單

    自定義表單custom-form.vue

    <template>
      <form class="custom-form">
        <slot></slot>
      </form>
    </template>
    <script>
    export default {
      props: {
        // 控製表單元素的大小
        size: {
          type: String,
          default: 'default',
          // size 只能是下面的四個值
          validator(value) {
            return ['default', 'large', 'small', 'mini'].includes(value)
          }
        },
        // 控製表單元素的禁用狀態
        disabled: {
          type: Boolean,
          default: false
        }
      },
      // 通過provide將當前表單實例傳遞到所有後代組件中
      provide() {
        return {
          customForm: this
        }
      }
    }
    </script>
    

      

    在上面代碼中,我們通過provide將當前組件的實例傳遞到後代組件中,provide是一個函數,函數返回的是一個對象

    自定義表單項custom-form-item.vue

    沒有什麼特殊的,只是加了一個label,element-ui更複雜一些

    <template>
      <div class="custom-form-item">
        <label class="custom-form-item__label">{{ label }}</label>
        <div class="custom-form-item__content">
          <slot></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      props: {
        label: {
          type: String,
          default: ''
        }
      }
    }
    </script>

    自定義輸入框 custom-input.vue

    <template>
      <div
        class="custom-input"
        :class="[
          `custom-input--${getSize}`,
          getDisabled && `custom-input--disabled`
        ]"
      >
        <input class="custom-input__input" :value="value" @input="$_handleChange" />
      </div>
    </template>
    <script>
    /* eslint-disable vue/require-default-prop */
    export default {
      props: {
        // 這裏用了自定義v-model
        value: {
          type: String,
          default: ''
        },
        size: {
          type: String
        },
        disabled: {
          type: Boolean
        }
      },
      // 通過inject 將form組件注入的實例添加進來
      inject: ['customForm'],
      computed: {
        // 通過計算組件獲取組件的size, 如果當前組件傳入,則使用當前組件的,否則是否form組件的
        getSize() {
          return this.size || this.customForm.size
        },
        // 組件是否禁用
        getDisabled() {
          const { disabled } = this
          if (disabled !== undefined) {
            return disabled
          }
          return this.customForm.disabled
        }
      },
      methods: {
        // 自定義v-model
        $_handleChange(e) {
          this.$emit('input', e.target.value)
        }
      }
    }
    </script>
    

      


    form中,我們通過
    provide返回了一個對象,在
    input中,我們可以通過
    inject獲取
    form中返回對象中的項,如上代碼
    inject:['customForm']所示,然後就可以在組件內通過
    this.customForm調用
    form實例上面的屬性與方法了

    在項目中使用

    <template>
      <custom-form size="small">
        <custom-form-item label="姓名">
          <custom-input v-model="formData.name" />
        </custom-form-item>
      </custom-form>
    </template>
    <script>
    import CustomForm from '../components/custom-form'
    import CustomFormItem from '../components/custom-form-item'
    import CustomInput from '../components/custom-input'
    export default {
      components: {
        CustomForm,
        CustomFormItem,
        CustomInput
      },
      data() {
        return {
          formData: {
            name: '',
            age: 0
          }
        }
      }
    }
    </script>
    

      執行上面代碼,運行結果為:

    <form class="custom-form">
      <div class="custom-form-item">
        <label class="custom-form-item__label">姓名</label>
        <div class="custom-form-item__content">
          <!--size=small已經添加到指定的位置了-->
          <div class="custom-input custom-input--small">
            <input class="custom-input__input">
          </div>
        </div>
      </div>
    </form>
    

      

    通過上面的代碼可以看到,input組件已經設置組件樣式為custom-input--small

    inject格式說明

    除了上面代碼中所使用的inject:['customForm']寫法之外,inject還可以是一個對象。且可以指定默認值

    修改上例,如果custom-input外部沒有custom-form,則不會注入customForm,此時為customForm指定默認值

    {
      inject: {
        customForm: {
          // 對於非原始值,和props一樣,需要提供一個工廠方法
          default: () => ({
            size: 'default'
          })
        }
      }
    }
    

      

    使用限制

    1.provideinject的綁定不是可響應式的。但是,如果你傳入的是一個可監聽的對象,如上面的customForm: this,那麼其對象的屬性還是可響應的。

    2.Vue官網建議provideinject 主要在開發高階插件/組件庫時使用。不推薦用於普通應用程序代碼中。因為provideinject在代碼中是不可追溯的(ctrl + f可以搜),建議可以使用Vuex代替。 但是,也不是說不能用,在局部功能有時候用了作用還是比較大的。

     

    插槽,我要鑽到你的懷裡

    插槽,相信每一位Vue都有使用過,但是如何更好的去理解插槽,如何去自定義插槽,今天小編為你帶來更形象的說明。

    默認插槽

    <template>
      <!--這是一個一居室-->
      <div class="one-bedroom">
        <!--添加一個默認插槽,用戶可以在外部隨意定義這個一居室的內容-->
        <slot></slot>
      </div>
    </template>
    

      

    <template>
      <!--這裏一居室-->
      <one-bedroom>
        <!--將傢具放到房間裏面,組件內部就是上面提供的默認插槽的空間-->
        <span>先放一個小床,反正沒有女朋友</span>
        <span>再放一個電腦桌,在家還要加班寫bug</span>
      </one-bedroom>
    </template>
    <script>
    import OneBedroom from '../components/one-bedroom'
    export default {
      components: {
        OneBedroom
      }
    }
    </script>

    具名插槽

    <template>
      <div class="two-bedroom">
        <!--這是主卧-->
        <div class="master-bedroom">
          <!---主卧使用默認插槽-->
          <slot></slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--主卧使用默認插槽-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
        <!--次卧,通過v-slot:secondard 可以指定使用哪一個具名插槽, v-slot:secondard 也可以簡寫為 #secondard-->
        <template v-slot:secondard>
          <div>
            <span>父母要住,放一個硬一點的床,軟床對腰不好</span>
            <span>放一個衣櫃</span>
          </div>
        </template>
      </two-bedroom>
    </template>
    <script>
    import TwoBedroom from '../components/slot/two-bedroom'
    export default {
      components: {
        TwoBedroom
      }
    }
    </script>

    作用域插槽

    <template>
      <div class="two-bedroom">
        <!--其他內容省略-->
        <div class="toilet">
          <!--通過v-bind 可以向外傳遞參數, 告訴外面衛生間可以放洗衣機-->
          <slot name="toilet" v-bind="{ washer: true }"></slot>
        </div>
      </div>
    </template>
    

      

    <template>
      <two-bedroom>
        <!--其他省略-->
        <!--衛生間插槽,通過v-slot="scope"可以獲取組件內部通過v-bind傳的值-->
        <template v-slot:toilet="scope">
          <!--判斷是否可以放洗衣機-->
          <span v-if="scope.washer">這裏放洗衣機</span>
        </template>
      </two-bedroom>
    </template>  

    插槽默認值

    <template>
      <div class="second-hand-house">
        <div class="master-bedroom">
          <!--插槽可以指定默認值,如果外部調用組件時沒有修改插槽內容,則使用默認插槽-->
          <slot>
            <span>這裡有一張水床,玩的夠嗨</span>
            <span>還有一個衣櫃,有點舊了</span>
          </slot>
        </div>
        <!--這是次卧-->
        <div class="secondary-bedroom">
          <!--次卧使用具名插槽-->
          <slot name="secondard">
            <span>這裡有一張嬰兒床</span>
          </slot>
        </div>
      </div>
    </template>
    

      

    <second-hand-house>
        <!--主卧使用默認插槽,只裝修主卧-->
        <div>
          <span>放一個大床,要結婚了,嘿嘿嘿</span>
          <span>放一個衣櫃,老婆的衣服太多了</span>
          <span>算了,還是放一個電腦桌吧,還要寫bug</span>
        </div>
      </second-hand-house>
    

    dispatchbroadcast,這是一種有歷史的組件通信方式

    dispatch
    broadcast是一種有歷史的組件通信方式,為什麼是有歷史的,因為他們是
    Vue1.0提供的一種方式,在
    Vue2.0中廢棄了。但是廢棄了不代表我們不能自己手動實現,像許多UI庫內部都有實現。本文以
    element-ui實現為基礎進行介紹。同時看完本節,你會對組件的
    $parent,
    $children,
    $options有所了解。

    方法介紹

    $dispatch: $dispatch會向上觸發一個事件,同時傳遞要觸發的祖先組件的名稱與參數,當事件向上傳遞到對應的組件上時會觸發組件上的事件偵聽器,同時傳播會停止。

    $broadcast: $broadcast會向所有的後代組件傳播一個事件,同時傳遞要觸發的後代組件的名稱與參數,當事件傳遞到對應的後代組件時,會觸發組件上的事件偵聽器,同時傳播會停止(因為向下傳遞是樹形的,所以只會停止其中一個恭弘=叶 恭弘子分支的傳遞)。

    $dispatch實現與應用

    1. 代碼實現

     // 向上傳播事件
     // @param {*} eventName 事件名稱
     // @param {*} componentName 接收事件的組件名稱
     // @param {...any} params 傳遞的參數,可以有多個
     
    function dispatch(eventName, componentName, ...params) {
      // 如果沒有$parent, 則取$root
      let parent = this.$parent || this.$root
      while (parent) {
        // 組件的name存儲在組件的$options.componentName 上面
        const name = parent.$options.name
        // 如果接收事件的組件是當前組件
        if (name === componentName) {
          // 通過當前組件上面的$emit觸發事件,同事傳遞事件名稱與參數
          parent.$emit.apply(parent, [eventName, ...params])
          break
        } else {
          // 否則繼續向上判斷
          parent = parent.$parent
        }
      }
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $dispatch: dispatch
      }
    }  

    2. 代碼應用

    • 在子組件中通過$dispatch向上觸發事件

      import emitter from '../mixins/emitter'
      export default {
        name: 'Chart',
        // 通過混入將$dispatch加入進來
        mixins: [emitter],
         mounted() {
           // 在組件渲染完之後,將組件通過$dispatch將自己註冊到Board組件上
          this.$dispatch('register', 'Board', this)
        }
      }
    • Board組件上通過$on監聽要註冊的事件

    $broadcast實現與應用

    1. 代碼實現

      //向下傳播事件
      // @param {*} eventName 事件名稱
      // @param {*} componentName 要觸發組件的名稱
      // @param  {...any} params 傳遞的參數
     
    function broadcast(eventName, componentName, ...params) {
      this.$children.forEach(child => {
        const name = child.$options.name
        if (name === componentName) {
          child.$emit.apply(child, [eventName, ...params])
        } else {
          broadcast.apply(child, [eventName, componentName, ...params])
        }
      })
    }
    
    // 導出一個對象,然後在需要用到的地方通過混入添加
    export default {
      methods: {
        $broadcast: broadcast
      }
    }  

    2. 代碼應用

    在父組件中通過$broadcast向下觸發事件

    import emitter from '../mixins/emitter'
    export default {
      name: 'Board',
      // 通過混入將$dispatch加入進來
      mixins: [emitter],
      methods:{
      	//在需要的時候,刷新組件
      	$_refreshChildren(params) {
      		this.$broadcast('refresh', 'Chart', params)
      	}
      }
    }
    

    在後代組件中通過$on監聽刷新事件

    export default {
      name: 'Chart',
      created() {
        this.$on('refresh',(params) => {
          // 刷新事件
        })
      }
    }
    

    總結

    通過上面的例子,同學們應該都能對$dispatch$broadcast有所了解,但是為什麼Vue2.0要放棄這兩個方法呢?官方給出的解釋是:”因為基於組件樹結構的事件流方式實在是讓人難以理解,並且在組件結構擴展的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟組件間的通信問題。“

    確實如官網所說,這種事件流的方式確實不容易讓人理解,而且後期維護成本比較高。但是在小編看來,不管黑貓白貓,能抓老鼠的都是好貓,在許多特定的業務場景中,因為業務的複雜性,很有可能使用到這樣的通信方式。但是使用歸使用,但是不能濫用,小編一直就在項目中有使用。

     

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

    【其他文章推薦】

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

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

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

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

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

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

  • 深入解讀Dictionary

    深入解讀Dictionary

    Dictionary<TKey,TValue>是日常.net開發中最常用的數據類型之一,基本上遇到鍵值對類型的數據時第一反應就是使用這種散列表。散列表特別適合快速查找操作,查找的效率是常數階O(1)。那麼為什麼這種數據類型的查找效率能夠這麼高效?它背後的數據類型是如何支撐這種查找效率的?它在使用過程中有沒有什麼局限性?一起來探究下這個數據類型的奧秘吧。

    本文內容針對的是.Net Framework 4.5.1的代碼實現,在其他.Net版本中或多或少都會有些差異,但是基本的原理還是相同的。

    本文的內容主要分為三個部分,第一部分是從代碼的角度來分析並以圖文並茂的方式通俗的解釋Dictionary如何解決的散列衝突並實現高效的數據插入和查找。第二部分名為“眼見為實”,由於第一部分是從代碼層面分析Dictionary的實現,側重於理論分析,因此第二部分使用windbg直接分析內存結構,跟第一部分的理論分析相互印證,加深對於這種數據類型的深入理解。最後是從數據結構的時間複雜度的角度進行分析並提出了幾條實踐建議。

    本文內容:

    • 第一部分 代碼分析
      • 散列衝突
      • Dictionary圖文解析
      • Dictionary的初始化
      • 添加第四個元素
    • 第二部分 眼見為實
      • 添加第一個元素后的內存結構
      • 添加第四個元素后的內存結構
    • 第三部分
      • 時間複雜度分析
      • 實踐建議

    散列衝突

    提到散列表,就不能不提散列衝突。由於哈希算法被計算的數據是無限的,而計算后的結果範圍有限,因此總會存在不同的數據經過計算后得到的值相同,這就是哈希衝突。(兩個不同的數據計算后的結果一樣)。散列衝突的解決方案有好幾個,比如開放尋址法、鏈式尋址法。

    Dictionary使用的是鏈式尋址法,也叫做拉鏈法。拉鏈法的基本思想是將散列值相同的數據存在同一個鏈表中,如果有散列值相同的元素,則加到鏈表的頭部。同樣道理,在查找元素的時候,先計算散列值,然後在對應散列值的鏈表中查找目標元素。

    用圖來表達鏈式尋址法的思想:

    Dictionary<TKey,TValue>的內部數據結構

    Dictionary的內部存儲數據主要是依賴了兩個數組,分別是int[] bucketsEntry[] entries。其中buckets是Dictionary所有操作的入口,類似於上文中解說拉鏈法所用的圖中的那個豎著的數據結構。Entry是一個結構體,用於封裝所有的元素並且增加了next字段用於構建鏈表數據結構。為了便於理解,下文中截取了一段相關的源代碼並增加了註釋。

    //數據在Dictionary<TKey,TValue>的存儲形式,所有的鍵值對在Dictionary<TKey,TValue>都是被包裝成一個個的Entry保存在內存中
    private struct Entry {
        public int hashCode;    // 散列計算結果的低31位數,默認值為-1
        public int next;        // 下一個Entry的索引值,鏈表的最後一個Entry的next為-1
        public TKey key;        // Entry對象的Key對應於傳入的TKey
        public TValue value;    // Entry對象的Value對應與傳入的TValue
    }
    
    private int[] buckets;      //hashCode的桶,是查找所有Entry的第一級數據結構
    private Entry[] entries;    //保存真正的數據
    

    下文中以Dictionary<int,string>為例,分析Dictionary在使用過程中內部數據的組織方式。

    Dictionary初始化

    初始化代碼:

    Dictionary<int, string> dictionary = new Dictionary<int, string>();
    

    Dictionary的初始化時,bucketsentries的長度都是0。

    添加一個元素

    dictionary.Add(1, "xyxy");
    

    向Dictionary中添加這個新元素大致經過了7個步驟:

    1. 首先判斷數組長度是否需要擴容(原長度為0,需要擴容);
    2. 對於數組進行擴容,分別創建長度為3的bucket數組和entries數組(使用大於原數組長度2倍的第一個素數作為新的數組長度);
    3. 整數1的hashcode為1;
    4. 取低31位數值為1(計算公式:hashcode & 0x7FFFFFFF=1);
    5. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
    6. 將hashcode、key、value包裝起來(封裝到entries數組下標為0的結構體中);
    7. 設置bucket[1]的值為0(因為新元素被封裝到了entries數組下標為0的位置);

    當向Dictionary中添加一個元素后,內部數據結構如下圖(為了便於理解,圖上將bucket和entries中各個鏈表頭結點用線標出了關聯關係):

    添加第二個元素

    dictionary.Add(7, "xyxy");
    

    向Dictionary中添加這個元素大致經過了6個步驟:

    1. 計算7的hashcode是7;
    2. 取低31位數值為7(計算公式:hashcode & 0x7FFFFFFF=1);
    3. 該key的hashcode落到bucket下標為1的位置(計算公式:hashCode % buckets.Length=1);
    4. 將hashcode、key、value包裝起來(封裝到entries數組下標為1的結構體中,跟步驟3計算得到的1沒有關係,只是因為entries數組下標為1的元素是空着的所以放在這裏);
    5. 原bucket[1]為0,所以設置當前結構體的Entry.next為0;
    6. 設置bucket[1]為1(因為鏈表的頭部節點位於entries數組下標為1的位置)

    當向Dictionary中添加第二個元素后,內部數據結構是這樣的:

    添加第三個元素

    dictionary.Add(2, "xyxy");
    

    向Dictionary添加這個元素經過了如下5個步驟:

    1. 整數2計算的hashcode是2;
    2. hashcode取低31位數值為2(計算公式:hashcode & 0x7FFFFFFF=2);
    3. 該key的hashcode落到bucket下標為2的位置(計算公式:hashCode % buckets.Length=2);
    4. 將hashcode、key、value包裝起來(封裝到entries數組下標為2的結構體中,到此entries的數組就存滿了);
    5. 原bucket[2]上為-1,所以bucket[2]節點下並沒有對應鏈表,設置當前結構體的Entry.next為-1;
    6. 設置bucket[2]為2(因為鏈表的頭部節點位於entries數組下標為2的位置)

    當向Dictionary中添加第三個元素后,內部數據結構:

    添加第四個元素

    dictionary.Add(4, "xyxy");
    

    通過前面幾個操作可以看出,當前數據結構中entries數組中的元素已滿,如果再添加元素的話,會發生怎樣的變化呢?

    假如再對於dictionary添加一個元素,原來申請的內存空間肯定是不夠用的,必須對於當前數據結構進行擴容,然後在擴容的基礎上再執行添加元素的操作。那麼在解釋這個Add方法原理的時候,分為兩個場景分別進行:數組擴容和元素添加。

    數組擴容

    在發現數組容量不夠的時候,Dictionary首先執行擴容操作,擴容的規則與該數據類型首次初始化的規則相同,即使用大於原數組長度2倍的第一個素數7作為新數組的長度(3*2=6,大於6的第一個素數是7)。

    擴容步驟:

    1. 新申請一個容量為7的數組,並將原數組的元素拷貝至新數組(代碼:Array.Copy(entries, 0, newEntries, 0, count);
    2. 重新計算原Dictionary中的元素的hashCode在bucket中的位置(注意新的bucket數組中數值的變化);
    3. 重新計算鏈表(注意entries數組中結構體的next值的變化);

    擴容完成后Dictionary的內容數據結構:

    添加元素

    當前已經完成了entriesbucket數組的擴容,有了充裕的空間來存儲新的元素,所以可以在新的數據結構的基礎上繼續添加元素。

    當向Dictionary中添加第四個元素后,內部數據結構是這樣的:

    添加這個新的元素的步驟:

    1. 整數4計算的hashcode是4;
    2. hashcode取低31位數值為4(計算公式:hashcode & 0x7FFFFFFF=4);
    3. 該key的hashcode落到bucket下標為4的位置(計算公式:hashCode % buckets.Length=4);
    4. 將hashcode、key、value包裝起來;(封裝到entries數組下標為3的結構體中);
    5. 原bucket[4]上為-1,所以當前節點下並沒有鏈表,設置當前結構體的Entry.next為-1;
    6. 設置bucket[4]為3(因為鏈表的頭部節點位於entries數組下標為3的位置)

    眼見為實

    畢竟本文的主題是圖文並茂分析Dictionary<Tkey,Tvalue>的原理,雖然已經從代碼層面和理論層面分析了Dictionary<Tkey,Tvalue>的實現,但是如果能夠分析這個數據類型的實際內存數據結果,可以獲得更直觀的感受並且對於這個數據類型能夠有更加深入的認識。由於篇幅的限制,無法將Dictionary<Tkey,Tvalue>的所有操作場景結果都進行內存分析,那麼本文中精選有代表性的兩個場景進行分析:一是該數據類型初始化后添加第一個元素的內存結構,二是該數據類型進行第一次擴容后的數據結構。

    Dictionary添加第一個元素后的內存結構

    執行代碼:

    Dictionary<int, string> dic = new Dictionary<int, string>();
    dic.Add(1, "xyxy");
    Console.Read();
    

    打開windbg附加到該進程(由於使用的是控制台應用程序,當前線程是0號線程,因此如果附加進程后默認的不是0號線程時執行~0s切換到0號線程),執行!clrstack -l查看當前線程及線程上使用的所有變量:

    0:000> !clrstack -l
    OS Thread Id: 0x48b8 (0)
            Child SP               IP Call Site
    0000006de697e998 00007ffab577c134 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    0000006de697e998 00007ffa96abc9c8 [InlinedCallFrame: 0000006de697e998] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    0000006de697e960 00007ffa96abc9c8 *** ERROR: Module load completed but symbols could not be loaded for C:\WINDOWS\assembly\NativeImages_v4.0.30319_64\mscorlib\5c1b7b73113a6f079ae59ad2eb210951\mscorlib.ni.dll
    DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
    
    0000006de697ea40 00007ffa972d39ec System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
        LOCALS:
            <no data>
            <no data>
            <no data>
            <no data>
            <no data>
            <no data>
    
    0000006de697ead0 00007ffa972d38f5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
        LOCALS:
            <no data>
            <no data>
    
    0000006de697eb30 00007ffa96a882d4 System.IO.StreamReader.ReadBuffer()
        LOCALS:
            <no data>
            <no data>
    
    0000006de697eb80 00007ffa97275f23 System.IO.StreamReader.Read()
        LOCALS:
            <no data>
    
    0000006de697ebb0 00007ffa9747a2fd System.IO.TextReader+SyncTextReader.Read()
    
    0000006de697ec10 00007ffa97272698 System.Console.Read()
    
    0000006de697ec40 00007ffa38670909 ConsoleTest.DictionaryDebug.Main(System.String[])
        LOCALS:
            0x0000006de697ec70 = 0x00000215680d2dd8
    
    0000006de697ee88 00007ffa97ba6913 [GCFrame: 0000006de697ee88] 
    
    

    通過對於線程堆棧的分析很容易看出當前線程上使用了一個局部變量,地址為:0x000001d86c972dd8,使用!do命令查看該變量的內容:

    0:000> !do 0x00000215680d2dd8
    Name:        System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.String, mscorlib]]
    MethodTable: 00007ffa96513328
    EEClass:     00007ffa9662f610
    Size:        80(0x50) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    00007ffa964a8538  4001887        8       System.Int32[]  0 instance 00000215680d2ee8 buckets
    00007ffa976c4dc0  4001888       10 ...non, mscorlib]][]  0 instance 00000215680d2f10 entries
    00007ffa964a85a0  4001889       38         System.Int32  1 instance                1 count
    00007ffa964a85a0  400188a       3c         System.Int32  1 instance                1 version
    00007ffa964a85a0  400188b       40         System.Int32  1 instance               -1 freeList
    00007ffa964a85a0  400188c       44         System.Int32  1 instance                0 freeCount
    00007ffa96519630  400188d       18 ...Int32, mscorlib]]  0 instance 00000215680d2ed0 comparer
    00007ffa964c6ad0  400188e       20 ...Canon, mscorlib]]  0 instance 0000000000000000 keys
    00007ffa977214e0  400188f       28 ...Canon, mscorlib]]  0 instance 0000000000000000 values
    00007ffa964a5dd8  4001890       30        System.Object  0 instance 0000000000000000 _syncRoot
    
    

    從內存結構來看,該變量中就是我們查找的Dic存在buckets、entries、count、version等字段,其中buckets和entries在上文中已經有多次提及,也是本文的分析重點。既然要眼見為實,那麼buckets和entries這兩個數組的內容到底是什麼樣的呢?這兩個都是數組,一個是int數組,另一個是結構體數組,對於這兩個內容分別使用!da命令查看其內容:

    首先是buckets的內容:

    0:000> !da -start 0 -details 00000215680d2ee8 
    Name:        System.Int32[]
    MethodTable: 00007ffa964a8538
    EEClass:     00007ffa966160e8
    Size:        36(0x24) bytes
    Array:       Rank 1, Number of elements 3, Type Int32
    Element Methodtable: 00007ffa964a85a0
    [0] 00000215680d2ef8
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [1] 00000215680d2efc
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                    0     m_value
    [2] 00000215680d2f00
        Name:        System.Int32
        MethodTable: 00007ffa964a85a0
        EEClass:     00007ffa96616078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    
    
    

    當前buckets中有三個值,分別是:-1、0和-1,其中-1是數組初始化后的默認值,而下錶為1的位置的值0則是上文中添加dic.Add(1, "xyxy");這個指令的結果,代表其對應的鏈表首節點在entries數組中下標為0的位置,那麼entries數組中的數值是什麼樣子的呢?

    0:000> !da -start 0 -details 00000215680d2f10 
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
    MethodTable: 00007ffa965135b8
    EEClass:     00007ffa9662d1f0
    Size:        96(0x60) bytes
    Array:       Rank 1, Number of elements 3, Type VALUETYPE
    Element Methodtable: 00007ffa96513558
    [0] 00000215680d2f20
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000215680d2db0     value
    [1] 00000215680d2f38
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [2] 00000215680d2f50
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    
    

    通過對於entries數組的分析可以看出,這個數組也有三個值,其中下標為0的位置已經填入相關內容,比如hashCode為1,key為1,其中value的內容是一個內存地址:000001d86c972db0,這個地址指向的就是字符串對象,它的內容是xyxy,使用!do指令來看下具體內容:

    0:000> !do  00000215680d2db0
    Name:        System.String
    MethodTable: 00007ffcc6b359c0
    EEClass:     00007ffcc6b12ec0
    Size:        34(0x22) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      xyxy
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    00007ffcc6b385a0  4000283        8         System.Int32  1 instance                4 m_stringLength
    00007ffcc6b36838  4000284        c          System.Char  1 instance               78 m_firstChar
    00007ffcc6b359c0  4000288       e0        System.String  0   shared           static Empty
    
    

    簡要分析擴容后的內存結構

    此次執行的代碼為:

    Dictionary<int, string> dic = new Dictionary<int, string>();
    dic.Add(1, "xyxy");
    dic.Add(7, "xyxy");
    dic.Add(2, "xyxy");
    dic.Add(4, "xyxy");
    Console.Read();
    

    同樣採取附加進程的方式分析這段代碼執行后的內存結構,本章節中忽略掉如何查找Dictionary變量地址的部分,直接分析buckets數組和entries數組的內容。

    首先是buckets數組的內存結構:

    0:000> !da -start 0 -details 0000019a471a54f8 
    Name:        System.Int32[]
    MethodTable: 00007ffcc6b38538
    EEClass:     00007ffcc6ca60e8
    Size:        52(0x34) bytes
    Array:       Rank 1, Number of elements 7, Type Int32
    Element Methodtable: 00007ffcc6b385a0
    [0] 0000019a471a5508
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    1     m_value
    [1] 0000019a471a550c
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    0     m_value
    [2] 0000019a471a5510
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    2     m_value
    [3] 0000019a471a5514
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [4] 0000019a471a5518
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                    3     m_value
    [5] 0000019a471a551c
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    [6] 0000019a471a5520
        Name:        System.Int32
        MethodTable: 00007ffcc6b385a0
        EEClass:     00007ffcc6ca6078
        Size:        24(0x18) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffcc6b385a0  40005a2        0             System.Int32      1     instance                   -1     m_value
    
    

    然後是entries的內存結構:

    0:000> !da -start 0 -details 00000237effb2fa8 
    Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]][]
    MethodTable: 00007ffa965135b8
    EEClass:     00007ffa9662d1f0
    Size:        192(0xc0) bytes
    Array:       Rank 1, Number of elements 7, Type VALUETYPE
    Element Methodtable: 00007ffa96513558
    [0] 00000237effb2fb8
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    1     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    1     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [1] 00000237effb2fd0
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    7     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    7     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [2] 00000237effb2fe8
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    2     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    2     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [3] 00000237effb3000
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    4     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                   -1     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    4     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     00000237effb2db0     value
    [4] 00000237effb3018
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [5] 00000237effb3030
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    [6] 00000237effb3048
        Name:        System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.String, mscorlib]]
        MethodTable: 00007ffa96513558
        EEClass:     00007ffa966304e8
        Size:        40(0x28) bytes
        File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
        Fields:
                          MT    Field   Offset                 Type VT     Attr            Value Name
            00007ffa964a85a0  4003502        8             System.Int32      1     instance                    0     hashCode
            00007ffa964a85a0  4003503        c             System.Int32      1     instance                    0     next
            00007ffa964a85a0  4003504       10             System.Int32      1     instance                    0     key
            00007ffa964aa238  4003505        0           System.__Canon      0     instance     0000000000000000     value
    
    

    從內存的結構來看,擴容后bucket數組中使用了下標為0、1、2和4這四個位置,entries中使用了0~3存儲了示例中添加的數據,符合前文中理論分析的結果,兩者相互之間具有良好的印證關係。

    時間複雜度分析

    時間複雜度表達的是數據結構操作數據的時候所消耗的時間隨着數據集規模的增長的變化趨勢。常用的指標有最好情況時間複雜度、最壞情況時間複雜度和均攤時間複雜度。那麼對於Dictionary<Tkey,TValue>來說,插入和查找過程中這些時間複雜度分別是什麼樣的呢?

    最好情況時間複雜度:對於查找來說最好的是元素處於鏈表的頭部,查找效率不會隨着數據規模的增加而增加,因此該複雜度為常量階複雜度,即O(1);插入操作最理想的情況是數組中有空餘的空間,不需要進行擴容操作,此時時間複雜度也是常量階的,即O(1);

    最壞情況時間複雜度:對於插入來說,比較耗時的操作場景是需要順着鏈表查找符合條件的元素,鏈表越長,查找時間越長(下文稱為場景一);而對於插入來說最壞的情況是數組長度不足,需要動態擴容並重新組織鏈表結構(下文稱為場景二);

    場景一中時間複雜度隨着鏈表長度的增加而增加,但是Dictionary中規定鏈表的最大長度為100,如果有長度超過100的鏈表就需要擴容並調整鏈表結構,所以順着鏈表查找數據不會隨着數據規模的增長而增長,最大時間複雜度是固定的,因此時間複雜度還是常量階複雜度,即O(1);

    場景二中時間複雜度隨着數組中元素的數量增加而增加,如果原來的數組元素為n個,那麼擴容時需要將這n個元素拷貝到新的數組中並計算其在新鏈表中的位置,因此該操作的時間複雜度是隨着數組的長度n的增加而增加的,屬於線性階時間複雜度,即O(n)。

    綜合場景一和場景二的分析結果得出最壞情況時間複雜度出現在數據擴容過程中,時間複雜度為O(n)。

    最好情況時間複雜度和最壞情況時間複雜度都過於極端,只能描述最好的情況和最壞的情況,那麼在使用過程中如何評價數據結構在大部分情況下的時間複雜度?通常對於複雜的數據結構可以使用均攤時間複雜度來評價這個指標。均攤時間複雜度適用於對於一個數據進行連續操作,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度較高的場景。這些操作存在前後連貫性,這種情況下將較高的複雜度攤派到之前的操作中,一般均攤時間複雜度就相當於最好情況時間複雜度。

    通過前面的分析可以看出Dictionary恰好就符合使用均攤時間複雜度分析的場景。以插入操作為例,假設預申請的entries數組長度為n,在第n+1次插入數據時必然會遇到一次數組擴容導致的時間消耗較高的場景。將這次較為複雜的操作的時間均攤到之前的n次操作后,每次操作時間的複雜度也是常量階的,因此Dictionary插入數據時均攤時間複雜度也是O(1)。

    實踐建議

    首先,Dictionary這種類型適合用於對於數據檢索有明顯目的性的場景,比如讀寫比例比較高的場景。其次,如果有大數據量的場景,最好能夠提前聲明容量,避免多次分配內存帶來的額外的時間和空間上的消耗。因為不進行擴容的場景,插入和查找效率都是常量階O(1),在引起擴容的情況下,時間複雜度是線性階O(n)。如果僅僅是為了存儲數據,使用Dictionary並不合適,因為它相對於List<T>具有更加複雜的數據結構,這樣會帶來額外的空間上面的消耗。雖然Dictionary<TKey,TValue>的TKey是個任意類型的,但是除非是對於判斷對象是否相等有特殊的要求,否則不建議直接使用自定義類作為Tkey。

    總結

    C#中的Dictionary<TKey,TValue>是藉助於散列函數構建的高性能數據結構,Dictionary解決散列衝突的方式是使用鏈表來保存散列值存在衝突的節點,俗稱拉鏈法。本文中通過圖文並茂的方式幫助理解Dictionary添加元素和擴容過程這兩個典型的應用場景,在理論分析之後使用windbg分析了內存中的實際結構,以此加深對於這種數據類型的深入理解。隨後在此基礎上分析了Dictionary的時間複雜度。Dictionary的最好情況時間複雜度是O(1),最壞情況複雜度是O(n),均攤時間複雜度是O(1),Dictionary在大多數情況下都是常量階時間複雜度,在內部數組自動擴容過程中會產生明顯的性能下降,因此在實際實踐過程中最好在聲明新對象的時候能夠預估容量,儘力避免數組自動擴容導致的性能下降。

    參考資料

    • Dictionary<TKey,TValue>源代碼(.net framework4.8版本)
    • .NET中Dictionary<TKey, TValue>淺析
    • 解決hash衝突的三個方法
    • 算法複雜度分析(下):最好、最壞、平均、均攤等時間複雜度概述

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

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

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

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

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

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

    ※回頭車貨運收費標準

  • Java 反射簡介

    本文部分內容參考博客。點擊鏈接可以查看原文。

    1. 反射的概念

    反射是指在運行時將類的屬性、構造函數和方法等元素動態地映射成一個個對象。通過這些對象我們可以動態地生成對象實例,調用類的方法和更改類的屬性值。

    2. 使用場景

    什麼情況下運用JAVA反射呢?如果編譯時根本無法預知對象和類可能屬於哪些類,程序只依靠運行時信息來發現該對象和類的真實信息,此時就必須使用反射。

    使用反射可以實現下面的功能:

    • 在運行時判斷任意一個對象所屬的類
    • 在運行時構造任意一個類的對象
    • 在運行時判斷任意一個類所具有的方法和屬性
    • 在運行時調用任意一個對象的方法
    • 生成動態代理

    3. 獲得Class對象的幾種方式

    前面已經介紹過了,每個類被加載之後,系統就會為該類生成一個對應的Class對象,通過該Class對象就可以訪問到JVM中的這個類。在Java程序中獲得Class對象通常有如下3種方式。

    • 使用Class類的forName(String clazzName)靜態方法。該方法需要傳入字符串參數,該字符串參數的值是某個類的全限定類名(必須添加完整包名)。
    • 調用某個類的class屬性來獲取該類對應的Class對象。例如,Person.class將會返回Person類對應的Class對象。
    • 調用某個對象的getClass()方法。該方法是java.lang.Object類中的一個方法,所以所有的Java對象都可以調用該方法,該方法將會返回該對象所屬類對應的Class對象。

    對於第一種方式和第二種方式都是直接根據類來取得該類的Class對象,相比之下,第二種方式有如下兩種優勢。

    • 代碼更安全。程序在編譯階段就可以檢查需要訪問的Class對象是否存在。
    • 程序性能更好。因為這種方式無須調用方法,所以性能更好。

    也就是說,大部分時候我們都應該使用第二種方式來獲取指定類的Class對象。但如果我們只有一個字符串,例如“java.lang.String”,若需要獲取該字符串對應的Class對象,則只能使用第一種方式,使用Class的forName(String clazzName)方法獲取Class對象時,該方法可能拋出一個ClassNotFoundException異常。一旦獲得了某個類所對應的Class對象之後,程序就可以調用Class對象的方法來獲得該對象和該類的真實信息了。

    4. Class類 API介紹

    通過class類我們能夠獲取大量的信息:

    1. 獲取構造函數
    • Connstructor getConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定public構造器。
    • Constructor<?>[] getConstructors():返回此Class對象對應類的所有public構造器。
    • Constructor getDeclaredConstructor(Class<?>… parameterTypes):返回此Class對象對應類的指定構造器,與構造器的訪問權限無關。
    • Constructor<?>[] getDeclaredConstructors():返回此Class對象對應類的所有構造器,與構造器的訪問權限無關。
    1. 獲取方法
    • Method getDeclaredMethod(String name, Class<?>… parameterTypes):返回此Class對象對應類的指定方法,與方法的訪問權限無關。
    • Method[] getDeclaredMethods():返回此Class對象對應類的全部方法,與方法的訪問權限無關。
    1. 獲取屬性
    • Field getField(String name):返回此Class對象對應類的指定public Field。
    • Field[] getFields():返回此Class對象對應類的所有public Field。
    • Field getDeclaredField(String name):返回此Class對象對應類的指定Field,與Field的訪問權限無關。
    • Field[] getDeclaredFields():返回此Class對象對應類的全部Field,與Field的訪問權限無關。
    1. 獲取Class對應類上所包含的Annotation。
    • A getAnnotation(Class annotationClass):試圖獲取該Class對象對應類上指定類型的Annotation;如果該類型的註釋不存在,則返回null。
    • Annotation[] getAnnotations():返回該Class對象對應類上的所有Annotation。
    • Annotation[] getDeclaredAnnotations():返回直接修飾該Class對應類的所有Annotation。
    1. 獲取Class對象對應類包含的內部類。
    • Class<?>[] getDeclaredClasses():返回該Class對象對應類里包含的全部內部類。
      如下方法用於訪問該Class對象對應類所在的外部類。
    • Class<?> getDeclaringClass():返回該Class對象對應類所在的外部類。
      如下方法用於訪問該Class對象對應類所繼承的父類、所實現的接口等。
    • Class<?>[] getInterfaces():返回該Class對象對應類所實現的全部接口。
    1. 獲取Class對象對應類所繼承的父類
    • Class<? super T> getSuperclass():返回該Class對象對應類的超類的Class對象。
    1. 獲取Class對象對應類的修飾符、所在包、類名等基本信息。
    • int getModifiers():返回此類或接口的所有修飾符。修飾符由public、protected、private、final、static、abstract等對應的常量組成,返回的整數應使用Modifier工具類的方法來解碼,才可以獲取真實的修飾符。
    • Package getPackage():獲取此類的包。
    • String getName():以字符串形式返回此Class對象所表示的類的名稱。
    • String getSimpleName():以字符串形式返回此Class對象所表示的類的簡稱。
    1. 判斷該類是否為接口、枚舉、註釋類型等
    • boolean isAnnotation():返回此Class對象是否表示一個註釋類型(由@interface定義)。
    • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判斷此Class對象是否使用了Annotation註釋修飾。
    • boolean isAnonymousClass():返回此Class對象是否是一個匿名類。
    • boolean isArray():返回此Class對象是否表示一個數組類。
    • boolean isEnum():返回此Class對象是否表示一個枚舉(由enum關鍵字定義)。
    • boolean isInterface():返回此Class對象是否表示一個接口(使用interface定義)。
    • boolean isInstance(Object obj):判斷obj是否是此Class對象的實例,該方法可以完全代替instanceof操作符。

    上面的多個getMethod()方法和getConstructor()方法中,都需要傳入多個類型為Class<?>的參數,用於獲取指定的方法或指定的構造器。關於這個參數的作用,假設某個類內包含如下3個info方法簽名:

    • public void info()
    • public void info(String str)
    • public void info(String str , Integer num)

    這3個同名方法屬於重載,它們的方法名相同,但參數列表不同。在Java語言中要確定一個方法光有方法名是不行的,例如,我們指定info方法——實際上可以是上面3個方法中的任意一個!如果需要確定一個方法,則應該由方法名和形參列表來確定,但形參名沒有任何實際意義,所以只能由形參類型來確定。例如,我們想要確定第二個info方法,則必須指定方法名為info,形參列表為String.class——因此在程序中獲取該方法使用如下代碼:

    clazz.getMethod("info",String.class);
    

    使用反射生成對象

    1. 利用構造函數生成對象
    • 使用Class對象的newInstance()方法來創建該Class對象對應類的實例,這種方式要求該Class對象的對應類有默認構造器,而執行newInstance()方法時實際上是利用默認構造器來創建該類的實例。
    • 先使用Class對象獲取指定的Constructor對象,再調用Constructor對象的newInstance()方法來創建該Class對象對應類的實例。通過這種方式可以選擇使用指定的構造器來創建實例。
    Constructor c = clazz.getConstructor(String.class);
    c.newInstance("xx");
    

    調用方法

    Method setProName = aClass.getDeclaredMethod("setProName",String.class);
    setProName.setAccessible(true);
    etProName.invoke(product,"我是一個產品");
    

    操作屬性

    Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
            }
            Field proName = aClass.getDeclaredField("proName");
            proName.setAccessible(true);
            proName.set(product,"我是一個產品");
            System.out.println("修稿屬性:"+product);
    

    操作數組

    //使用反射動態地創建數組
    //創建一個元素類型為String,長度為3的數組
    Object arr = Array.newInstance(String.class, 3);
    //依次為arr數組中index為0,1,2的元素賦值
    Array.set(arr, 0, "榮耀盒子");
    Array.set(arr, 1, "榮耀8手機");
    Array.set(arr, 2, "華為mate9保時捷版");
    Object o1= Array.get(arr, 0);
    Object o2= Array.get(arr, 1);
    Object o3= Array.get(arr, 2);
    System.out.println(o1);
    System.out.println(o2);
    System.out.println(o3);
    

    5. 使用Demo

    public class ReflectDemo {
        public static void main(String[] args) throws Exception {
            Class<Product> aClass = Product.class;
            Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
            for (Constructor<?> declaredConstructor : declaredConstructors) {
                System.out.println(declaredConstructor.getName());
            }
            Constructor<Product> constructor = aClass.getConstructor(int.class, String.class);
            Product product = constructor.newInstance(10, "ds");
            System.out.println("創建對象:"+product);
    
            //獲取方法並調用
            Method setProName = aClass.getDeclaredMethod("setProName", String.class);
            setProName.setAccessible(true);
            setProName.invoke(product,"我是一個產品");
            System.out.println("調用方法:"+product);
    
            //獲取屬性,並設置屬性的值
            Field[] declaredFields = aClass.getDeclaredFields();
            for (Field declaredField : declaredFields) {
                System.out.println("fieldName:"+declaredField.getName()+" filedType:"+declaredField.getType());
            }
            Field proName = aClass.getDeclaredField("proName");
            proName.setAccessible(true);
            proName.set(product,"我是一個產品");
            System.out.println("修稿屬性:"+product);
    
            //使用反射動態地創建數組
            //創建一個元素類型為String,長度為3的數組
            Object arr = Array.newInstance(String.class, 3);
            //依次為arr數組中index為0,1,2的元素賦值
            Array.set(arr, 0, "榮耀盒子");
            Array.set(arr, 1, "榮耀8手機");
            Array.set(arr, 2, "華為mate9保時捷版");
            Object o1= Array.get(arr, 0);
            Object o2= Array.get(arr, 1);
            Object o3= Array.get(arr, 2);
            System.out.println(o1);
            System.out.println(o2);
            System.out.println(o3);
            
            System.out.println("end...");
        }
    }
    

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 可拖拽圓形進度條組件(支持移動端)

    可拖拽圓形進度條組件(支持移動端)

    好久之前寫過一個可拖拽圓形進度條的dome,中間有網友反饋過一些問題,最近比較閑有時間修改了一些問題也做了一些優化,並封裝成組件,基於canvas實現,只需傳入放置組件dom容器,任何框架均可直接使用;

    codepen 示例如下:https://codepen.io/pangyongsheng/pen/XRmNRK

     

    一、如何使用

    npm下載

    執行 npm i drag-arc -S 或 cnpm i drag-arc -S

     
    import DragArc from 'drag-arc';
     new DragArc({
        el: dom,
        value: 10,
        change: (v) => {
            console.log(v)
        },
        ...
    })
    或者 也可從項目下載dist/dist/drag-arc.min.js,直接通過srcipt標籤引入

    其中dom為放置組件HTML容器,可通過ref獲取;

    主要屬性方法(詳見github/npm)

    項目地址:https://github.com/pangyongsheng/canvas-arc-draw
    npm地址:https://www.npmjs.com/package/drag-arc

    Name Description Type Default Required
    el 放置組件的DOM元素 Element none Y
    change 當前值變化時觸發的事件,回調參數為當前進度值Number(0-100) Function ()=>{} N
    startDeg 滑動圓弧的起始弧度 Number  0 N
    endDeg 滑動圓弧的結束弧度 Number 1 N
    value 默認值 Number (0-100) 0 N
    textShow 显示文字 Boolean true N
    color 外側圓弧顏色 String,Array [“#06dabc”, “#33aaff”] N
    slider 滑塊半徑 Number #FFF N
    innerColor 內側弧度的顏色 String #ccc N
    outColor 外側圓弧背景顏色 String,Array #ccc N
    innerLineWidth 內側弧線寬 Number 1 N
    outLineWidth 外側弧線寬 Number 20 N
    counterclockwise 逆時針方向 Boolean true N
    sliderColor 滑塊顏色 String #CCC N
    sliderBorderColor 滑塊邊框顏色 String #fff N

    二、實現方法簡介

    1、繪製位置幾何關係

    如圖所示,以canvas畫布中心點建立坐標系,則有:

    滑塊位置與弧度關係:

    由圓的參數方程得出
    x=rcosφ
    y=rsinφ

    鼠標移動位置與弧度關係:

    通過事件回調參數 我們可以獲得 鼠標mousemove事件或者移動端touchmove事件的x,y坐標,可計算tan值為
    tanφ = y/x;
    再通過反三角函數有可得:
    φ=arctan(tanφ)

    以上基本的位置關係已經得出;

    2、js實現中的幾個問題

    (1)坐標的轉化方法

    由於上述位置關係是基於中心坐標實現的,而canvas繪製坐標是以左上角為原點實現的,故需要實現兩種坐標的轉化關係;

    (2)canvas弧度位置與正常弧度位置的轉化

    下圖是canvas的弧度位置恰好與我們正常計算的方向是相反的,同樣需考慮弧度的轉換;

    (3)Math.atan方法返回值與實際弧度的關係

    由於Math.atan() 函數返回一個數值的反正切[- π/2 , π/2 ],
    而實際中我們需要獲得到[0-2π]直接的值,所以在通過鼠標位置獲取弧度值時需要通過Math.atan(y/x)和xy在中心坐標的正負綜合判斷其所在象限從何獲取實際的獲取弧度值;

    (4)弧度與進度條值得關係

    由於鼠標移動觸發繪圖方法是較為連續的動畫效果,而進度是間隔的,
    這裏我們需要實現個類似d3js中domain和range的比例關係。
    這裏我們將值[0,100]對應弧度比例為[startDeg, endDeg]

    (5)終點的判斷

    由於鼠標移動的位置是任意的,可能導致滑塊到達終點後由於鼠標移動到了起點時,滑塊也直接從終點移動到起點,故需對起點終點做判斷,到達起點后不可再向後滑動,到達終點后不可再向前滑動;

    3、詳細實現方法可以參考這篇文章

     https://www.cnblogs.com/pangys/p/6837344.html

     

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

    【其他文章推薦】

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

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

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

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

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

  • 解Bug之路-中間件”SQL重複執行”

    解Bug之路-中間件”SQL重複執行”

    前言

    我們的分庫分表中間件在線上運行了兩年多,到目前為止還算穩定。在筆者將精力放在處理各種災難性事件(例如中間件物理機宕機/數據庫宕機/網絡隔離等突發事件)時。竟然發現還有一些奇怪的corner case。現在就將排查思路寫成文章分享出來。

    Bug現場

    應用拓撲

    應用通過中間件連後端多個數據庫,sql會根據路由規則路由到指定的節點,如下圖所示:

    錯誤現象

    應用在做某些數據庫操作時,會發現有比較大的概率失敗。他們的代碼邏輯是這樣:

    	int count = updateSql(sql1);
    	...
    	// 偽代碼
    	int count = updateSql("update test set value =1 where id in ("100","200") and status = 1;
    	if( 0 == count ){
    		throw new RuntimeException("更新失敗");
    	}
    	......
    	int count = updateSql(sql3);
    	...
    

    即每做一次update之後都檢查下是否更新成功,如果不成功則回滾並拋異常。
    在實際測試的過程中,發現經常報錯,更新為0。而實際那條sql確實是可以更新到的(即報錯回滾后,我們手動執行sql可以執行並update count>0)。

    中間件日誌

    筆者根據sql去中間件日誌裏面搜索。發現了非常奇怪的結果,日誌如下:

    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>0;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24266;
    2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>2;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24591;
    

    由於中間件對每條sql都標識了唯一的一個sqlID,在日誌表現看來就好像sql執行了兩遍!由於sql中有一個in,很容易想到是否被拆成了兩條執行了。如下圖所示:

    這條思路很快被筆者否決了,因為筆者explain並手動執行了一下,這條sql確實只路由到了一個節點。真正完全否決掉這條思路的是筆者在日誌裏面還發現,同樣的SQL會打印三遍!即看上去像執行了三次,這就和僅僅只in了兩個id的sql在思路上相矛盾了。

    數據庫日誌

    那到底數據真正執行了多少條呢?找DBA去撈一下其中的sql日誌,由於線下環境沒有日誌切割,日誌量巨大,搜索時間太慢。沒辦法,就按照現有的數據進行分析吧。

    日誌如何被觸發

    由於當前沒有任何思路,於是筆者翻看中間件的代碼,發現在update語句執行后,中間件會在收到mysql okay包后打印上述日誌。如下圖所示:

    注意到所有出問題的update出問題的時候都是同一個NIOREACTOR線程先後打印了兩條日誌,所以筆者推斷這兩個okay大概率是同一個後端連接返回的。

    什麼情況會返回多個okay?

    這個問題筆者思索了很久,因為在筆者的實際重新執行出問題的sql並debug時,永遠只有一個okay返回。於是筆者聯想到,我們中間件有個狀態同步的部分,而這些狀態同步是將set auto_commit=0等sql拼接到應用發送的sql前面。即變成如下所示:

    sql可能為
    set auto_commit=0;set charset=gbk;>update test set value =1 where id in ("1","2") and status = 1;
    

    於是筆者細細讀了這部分的代碼,發現處理的很好。其通過計算出前面拼接出的sql數量,再在接收okay包的時候進行遞減,最後將真正執行的那條sql處理返回。其處理如下圖所示:

    但這裏確給了筆者一個靈感,即一條sql文本確實是有可能返回多個okay包的。

    真相大白

    在筆者發現(sql1;sql2;)這樣的拼接sql會返回多個okay包后,就立刻聯想到,該不會業務自己寫了這樣的sql發給中間件,造成中間件的sql處理邏輯錯亂吧。因為我們的中間件只有在對自己拼接(同步狀態)的sql做處理,明顯是無法處理應用傳過來即為拼接sql的情況。
    由於看上去有問題的那條sql並沒有拼接,於是筆者憑藉這條sql打印所在的reactor線程往上搜索,發現其上面真的有拼接sql!

    2020-03-1311:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;
    sql=>update test_2 set value =1 where id=1 and status = 1;update test_2 set value =1 where id=2 and status = 1;
    

    如上圖所示,(update1;update2)中update1的okay返回被驅動認為是所有的返回。然後應用立即發送了update3。前腳剛發送,update2的okay返回就回來了而其剛好是0,應用就報錯了(要不是0,這個錯亂邏輯還不會提前暴露)。那三條”重複執行”也很好解釋了,就是之前的拼接sql會有三條。

    為何是概率出現

    但奇怪的是,並不是每次拼接sql都會造成update3″重複執行”的現象,按照筆者的推斷應該前面只要是多條拼接sql就會必現才對。於是筆者翻了下jdbc驅動源碼,發現其在發送命令之前會清理下接收buffer,如下所示:

    MysqlIO.java
    final Buffer sendCommand(......){
    	......
    	// 清理接收buffer,會將殘存的okay包清除掉
    	clearInputStream();
    	......
    	send(this.sendPacket, this.sendPacket.getPosition());
    	......
    }
    

    正是由於clearInputStream()使得錯誤非必現(暴露),如果okay(update2)在應用發送第三條sql前先到jdbc驅動會被驅動忽略!
    讓我們再看一下不會讓update3″重複執行”的時序圖:

    即根據okay(update2)返回的快慢來決定是否暴露這個問題,如下圖所示:

    同時筆者觀察日誌,確實這種情況下”update1;update2″這條語句在中間件裏面日誌有兩條。

    臨時解決方案

    讓業務開發不用這些拼接sql的寫法后,再也沒出過問題。

    為什麼不連中間件是okay的

    業務開發這些sql是就在線上運行了好久,用了中間件后才出現問題。
    既然不連中間件是okay的,那麼jdbc必然有這方面的完善處理,筆者去翻了下mysql-connect-java(5.1.46)。由於jdbc裏面存在大量的兼容細節處理,筆者這邊只列出一些關鍵代碼路徑:

    MySQL JDBC 源碼
    MySQLIO
    stack;
    executeUpdate
    	|->executeUpdateInternel
    		|->executeInternal
    			|->execSQL
    				|->sqlQueryDirect
    					|->readAllResults (MysqlIO.java)
    readAllResults: //核心在這個函數的處理裏面
    ResultSetImpl readAllResults(......){
    		......
           while (moreRowSetsExist) {
    			  ......
    			  // 在返回okay包的保中其serverStatus字段中如果SERVER_MORE_RESULTS_EXISTS置位
    			  // 表明還有更多的okay packet
                moreRowSetsExist = (this.serverStatus & SERVER_MORE_RESULTS_EXISTS) != 0;
            }
            ......
    }
    

    正確的處理流程如下圖所示:

    而我們中間件的源碼確實這麼處理的:

    @Override
    public void okResponse(byte[] data, BackendConnection conn) {
    	......
    	// 這邊僅僅處理了autocommit的狀態,沒有處理SERVER_MORE_RESULTS_EXISTS
    	// 所以導致了不兼容拼接sql的現象
    	ok.serverStatus = source.isAutocommit() ? 2 : 1;
    	ok.write(source);
    	......
    }
    

    select也”重複執行”了

    解決完上面的問題后,筆者在日誌里竟然發現select盡然也有重複的,這邊並不會牽涉到okay包的處理,難道還有問題?日誌如下所示:

    2020-03-13 12:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;select abc;
    2020-03-13 12:21:01:045[NIOREACTOR-21RW]frontIP=>ip2;sqlID=>12345678;rows=>1;select abc;
    

    從不同的REACTOR線程號(20RW/21RW)和不同的frontIP(ip1,ip2)來看是兩個連接執行了同樣的sql,但為何sqlID是一樣的?任何一個詭異的現象都必須一查到底。於是筆者登錄到應用上看了下應用日誌,確實應用有兩個不同的線程運行了同一條sql。
    那肯定是中間件日誌打印的問題了,筆者很快就想通了其中的關竅,我們中間件有個對同樣sql緩存其路由節點結構體的功能(這樣下一次同樣sql就不必解析,降低了CPU),而sqlID信息正好也在那個路由節點結構體裏面。如下圖所示:

    這個緩存功能感覺沒啥用(因為線上基本是沒有相同sql的),於是筆者在筆者優化的閃電模式下(大幅度提高中間件性能)將這個功能禁用掉了,沒想到為了排查問題而開啟的詳細日誌碰巧將這個功能開啟了。

    總結

    任何系統都不能說百分之百穩定可靠,尤其是不能立flag。在線上運行了好幾年的系統也是如此。只有對所有預料外的現象進行細緻的追查與深入的分析並解決,才能讓我們的系統越來越可靠。

    公眾號

    關注筆者公眾號,獲取更多乾貨文章:

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

    【其他文章推薦】

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

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

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

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

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

  • 多圖解釋Redis的整數集合intset升級過程

    多圖解釋Redis的整數集合intset升級過程

    redis源碼分析系列文章

    [Redis源碼系列]在Liunx安裝和常見API 

    為什麼要從Redis源碼分析 

    String底層實現——動態字符串SDS 

    雙向鏈表都不懂,還說懂Redis?

    面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

    Redis的跳躍表確定不了解下

     

    前言

    大噶好,今天仍然是元氣滿滿的一天,拋開永遠寫不完的需求,拒絕要求賊變態的客戶,單純的學習技術,感受技術的魅力。(哈哈哈,皮一下很開森)

    前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS雙向鏈表Adlist字典Dict跳躍表,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

    今天來說下set的底層實現整數集合,如果有對set不明白的,常見的API使用這篇就不講了,看上面的傳送門哈。

    整數集合概念

    整數集合是Redis設計的一種底層結構,是set的底層實現,當集合中只包含整數值元素,並且這個集合元素數據不多時,會使用這種結構。但是如果不滿足剛才的條件,會使用其他結構,這邊暫時不講哈。

    下圖為整數集合的實際組成,包括三個部分,分別是編碼格式encoding,包含元素數量length,保存元素的數組contents。(這邊只需要簡單看下,下面針對每個模塊詳細說明哈)

    整數集合的實現

    我們看下intset.h裏面關於整數集合的定義,上代碼哈:

    //整數集合結構體
    typedef struct intset {
        uint32_t encoding;  //編碼格式,有如下三種格式,初始值默認為INTSET_ENC_INT16
        uint32_t length;    //集合元素數量
        int8_t contents[];  //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
    } intset;               
    
    #define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2個字節,表示範圍-32,768~32,767
    #define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
    #define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807

     

     

    編碼格式encoding

    包括INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64三種類型,其分別對應着不同的範圍,具體看上面代碼的註釋信息。

    因為插入的數據的大小是不一樣的,為了盡可能的節約內存(畢竟都是錢,平時要省着點用),所以我們需要使用不同的類型來存儲數據。

    集合元素數量length

    記錄了保存數據contents的長度,即有多少個元素。

    保存元素的數組contents

    真正存儲數據的地方,數組是按照從小到大有序排序的,並且不包含任何重複項(因為set是不含重複項,所以其底層實現也是不含包含項的)。

    整數集合升級過程(重點,手動標星)

    上面的圖我們重新看下,編碼格式encoding為INTSET_ENC_INT16,即每個數據佔16位。長度length為4,即數組content裏面有四個元素,分別是1,2,3,4。如果我們要添加一個数字位40000,很明顯超過編碼格式為INTSET_ENC_INT16的範圍-32,768~32,767,應該是編碼格式為INTSET_ENC_INT32。那麼他是如何升級的呢,從INTSET_ENC_INT16升級到INTSET_ENC_INT32的呢?

    1.了解舊的存儲格式

    首先我們看下1,2,3,4這四個元素是如何存儲的。首先要知道一共有多少位,計算規則為length*編碼格式的位數,即4*16=64。所以每個元素佔用了16位。

    2.確定新的編碼格式

    新的元素為40000,已經超過了INTSET_ENC_INT16的範圍-32,768~32,767,所以新的編碼格式為INTSET_ENC_INT32。

    3.根據新的編碼格式新增內存

    上面已經說明了編碼格式為INTSET_ENC_INT32,計算規則為length*編碼格式的位數,即5*32=160。所以新增的位數為64-159。

    4.根據編碼格式設置對應的值

    從上面知道按照新的編碼格式,每個數據應該佔用32位,但是舊的編碼格式,每個數據佔用16位。所以我們從後面開始,每次獲取32位用來存儲數據。

    這樣說太難懂了,看下圖。

    首先,那最後32位,即128-159存儲40000。那麼第49-127是空着的。

    接着,取空着的49-127最後的32位,即96到127這32位,用來存儲4。那麼之前4存儲的位置48-6349-127剩下的64-95這兩部分組成了一個大部分,即48-95,現在空着啦。

    在接着在48-95這個大部分,再取后32位,即64-95,用來存儲3。那麼之前3存儲位置32-4748-95剩下的48-63這兩部分組成了一個大部分,即32-63,現在空着啦。

    再接着,將32-63這個大部分,再取后32位,即還是32-63,用來存儲2。那麼之前2存儲位置16-31空着啦。

    最後,將16-31和原來0-31合起來,存儲1。

    至此,整個升級過程結束。整體來說,分為3步,確定新的編碼格式,新增需要的內存空間,從后往前調整數據。

    這邊有個小問題,為啥要從后往前調整數據呢?

    原因是如果從前往後,數據可能會覆蓋。也拿上面個例子來說,數據1在0-15位,數據2在16-31位,如果從前往後,我們知道新的編碼格式INTSET_ENC_INT32要求每個元素佔用32位,那麼數據1應該佔用0-31,這個時候數據2就被覆蓋了,以後就不知道數據2啦。

    但是從后往前,因為後面新增了一些內存,所以不會發生覆蓋現象。

    升級的優點

     節約內存

    整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。 

    不支持降級

    一旦對數組進行升級,編碼就會一直保存升級后的狀態。即使後面把40000刪掉了,編碼格式還是不會將會INTSET_ENC_INT16。

    整數集合的源碼分析

    創建一個空集合 intsetnew

    這個方法比較簡單,是初始化整數集合的步驟,即下圖部分。

    主要的步驟是分配內存空間,設置默認編碼格式,以及初始化數組長度length。

    intset *intsetNew(void) {
        intset *is = zmalloc(sizeof(intset));//分配內存空間 
        is->encoding = intrev32ifbe(INTSET_ENC_INT16);//設置默認編碼格式INTSET_ENC_INT16 
        is->length = 0;//初始化length 
        return is;
    }

    添加元素並升級insetAdd流程圖(重點)

    添加元素並升級insetAdd源碼分析

    可以根據上面的流程圖,對照着下面的源碼分析,這邊就不寫啦哈。

    //添加元素
    //輸入參數*is為原整數集合
    //value為要添加的元素
    //*success為是否添加成功的標誌量 ,1表示成功,0表示失敗 
    intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
        //確定要添加的元素的編碼格式 
        uint8_t valenc = _intsetValueEncoding(value);
        
        uint32_t pos;
        //如果success沒有初始值,則初始化為1 
        if (success) *success = 1;
    
       //如果新的編碼格式大於現在的編碼格式,則升級並添加元素 
        if (valenc > intrev32ifbe(is->encoding)) {
            //調用另一個方法 
            return intsetUpgradeAndAdd(is,value);
        } else {
            //如果編碼格式不變,則調用查詢方法 
            //輸入參數is為原整數集合 
            //value為要添加的數據
            //pos為位置 
            if (intsetSearch(is,value,&pos)) {//如果找到了,則直接返回,因為數據是不可重複的。 
                if (success) *success = 0;
                return is;
            }
    
            //設置length 
            is = intsetResize(is,intrev32ifbe(is->length)+1);
            if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
        }
        //設置數據 
        _intsetSet(is,pos,value);
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //#define INT8_MAX 127
    //#define INT16_MAX 32767
    //#define INT32_MAX 2147483647
    //#define INT64_MAX 9223372036854775807LL 
    static uint8_t _intsetValueEncoding(int64_t v) {
        if (v < INT32_MIN || v > INT32_MAX)
            return INTSET_ENC_INT64;
        else if (v < INT16_MIN || v > INT16_MAX)
            return INTSET_ENC_INT32;
        else
            return INTSET_ENC_INT16;
    }
    
    
    //根據輸入參數value的編碼格式,對整數集合is的編碼格式升級 
    static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
        //當前集合的編碼格式 
        uint8_t curenc = intrev32ifbe(is->encoding);
        //根據對value解析獲取新的編碼格式 
        uint8_t newenc = _intsetValueEncoding(value);
        //獲取集合元素數量 
        int length = intrev32ifbe(is->length);
        //如果要添加的數據小於0,則prepend為1,否則為0 
        int prepend = value < 0 ? 1 : 0;
    
       //設置集合為新的編碼格式,並根據編碼格式重新設置內存 
        is->encoding = intrev32ifbe(newenc);
        is = intsetResize(is,intrev32ifbe(is->length)+1);
    
        //逐步循環,直到length小於0,挨個重新設置每個值,從后往前 
        while(length--)
            _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    
        //如果value為負數,則放在最前面 
        if (prepend)
            _intsetSet(is,0,value);
        else//如果value為整數,設置最末尾的元素為value 
            _intsetSet(is,intrev32ifbe(is->length),value);
        //重新設置length 
        is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
        return is;
    }
    
    
    //找到is集合中值為value的下標,返回1,並保存在pos中,沒有找到返回0,並將pos設置為value可以插入到數組的位置
    static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
        int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
        int64_t cur = -1;
    
        //如果集合為空,那麼位置pos為0 
        if (intrev32ifbe(is->length) == 0) { 
            if (pos) *pos = 0;
            return 0;
        } else {
            //因為數據是有序集合,如果要添加的數據大於最後一個数字,那麼直接把要添加的值放在最後即可,返回最大值下標 
            if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
                if (pos) *pos = intrev32ifbe(is->length);
                return 0;
            } else if (value < _intsetGet(is,0)) { //如果這個數據小於數組下標為0的數據,即為最小值 ,返回0 
                if (pos) *pos = 0;
                return 0;
            }
        }
        //有序集合採用二分法 
        while(max >= min) {
            mid = ((unsigned int)min + (unsigned int)max) >> 1;
            cur = _intsetGet(is,mid);
            if (value > cur) {
                min = mid+1;
            } else if (value < cur) {
                max = mid-1;
            } else {
                break;
            }
        }
    
        //確定找到 
        if (value == cur) {
            if (pos) *pos = mid;//設置參數pos,返回1,即找到位置 
            return 1;
        } else {//如果沒找到,則min和max相鄰,隨便設置都行,並返回0 
            if (pos) *pos = min; 
            return 0;
        }
    }

     

    結語

    該篇主要講了Redis的SET數據類型的底層實現整數集合,先從整數集合是什麼,,剖析了其主要組成部分,進而通過多幅過程圖解釋了intset是如何升級的,最後結合源碼對整數集合進行描述,如創建過程,升級過程,中間穿插例子和過程圖。

    如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

    如果覺得有說的不對的地方,歡迎評論指出。

    好了,拜拜咯。

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

    【其他文章推薦】

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

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

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

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

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

  • 香港環保署:去年及今年首9月 空氣中二噁英濃度沒異常

    摘錄自2019年11月17日香港電台網站、頭條日報報導

    港警近月大量使用催淚彈,其中催淚彈不時出現大量火光,有民眾關注高溫會產生二噁英。二噁英是常見的持久性環境污染物,能在環境中長存數十年,不受破壞,毒性極高,會導致生育和發育問題、破壞免疫系統、干擾荷爾蒙分泌以及致癌。

    香港環保署在中西區和荃灣設監測站,監測空氣中二噁英的濃度,但上月數字至今未更新。香港環保署表示,去年及今年至9月的數據顯示,二噁英日均濃度是每立方米0.009至0.086皮克,並沒發現有異常情況出現。香港環保署表示,現在沒有空氣中二噁英的濃度指引,但過去3年香港取得的二噁英濃度,遠低於加拿大安大略省的二噁英每立方米0.1皮克,和日本每立方米0.6皮克的指標。

    參選「啟德中及南」選區候選人梁咏欣表示,近日收到居民反映,指有大量懷疑含有「二噁英」的雜物,被棄置在距離啟德約400米的宏照道路政署地盤內。她要求當局提供環境調查報告及實地分析數據,以確保附近環境不被有害物質污染。

    香港環保署稱已將上月份收集的二噁英樣本,送交政府化驗所作化學分析,一般需數星期,預期於本月(11月)底會完成,收到報告後會盡快公布。

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

    【【其他文章推薦】

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

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

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

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

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

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

  • 印尼蘇門答臘象身首異處慘死 象牙遭盜採

    摘錄自2019年11月8日自由時報屏東報導

    印尼保育官員今天表示,1頭列為極危(critically endangered)物種的蘇門答臘象屍體被發現,牠的頭被砍下且象牙被拔走,顯然是一宗盜獵案件。

    蘇門答臘島廖內省(Riau)一名農園工人昨天發現這頭40歲公象腐爛的屍體。當地保育機關主管蘇哈尤諾(Suharyono)在聲明中說:「大象的頭被砍下,切斷的象鼻落在距離象身1公尺處。」當局正在追查犯案人士。

    森林濫伐造成蘇門答臘象的天然棲地縮減,導致牠們和人類的衝突加劇。另一方面,蘇門答臘象象牙在野生動物黑市交易中價值連城。

    去年在印尼亞齊省(Aceh)也發現一具顯然被毒死的蘇門答臘象屍體,當時牠的象牙也不見。印尼環境部估計,境內野生蘇門答臘象只剩不到2000頭。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 無視巴黎氣候協定 全球化石燃料產量遠高於限制

    摘錄自2019年11月20日中央通訊社報導

    聯合國和頂尖研究團體今天(20日)發布報告警告,全球已規劃或準備進行的石油、天然氣和煤炭產量,將遠遠超越抑制全球暖化讓地球維持適合人居所需的產量目標。

    聯合國環境規劃署(UN Environment Programme)和4個氣候變遷研究中心聯合發布報告指出,全球預計生產的化石燃料總量,較為了讓地表溫度較工業革命前水準升高不超過攝氏2度所容許的燃燒量,超出達50%。

    若將地表升溫幅度限制在攝氏1.5度,則計劃中的化石燃料產量將較容許數量超出1倍多。2015年達成的巴黎氣候協定,要求將全球暖化限制在遠低於攝氏2度水準,可能的話僅升溫攝氏1.5度。

    儘管截至目前全球僅升溫攝氏1度,但全世界已出現逐漸增強的致命熱浪、洪災和超級風暴,而超級風暴因海平面加速上升而破壞力更強大。研究人員警告,煤炭、石油和天然氣供應的「過度投資」,與未來數十年必須大幅縮減溫室氣體排放的目標,兩者直接相衝突。

    聯合國去年發布的報告斷定,若要抑制地表升溫僅攝氏1.5度,則全球二氧化碳排放必須在2030年底前減少45%,並於2050年底前達到「淨零排放」。

    斯德哥爾摩環境研究所(Stockholm Environment Institute)美國中心主任賴薩魯斯(Michael Lazarus)表示:「我們首次展現,巴黎(氣候協定的)溫度目標,和各國煤炭、石油和天然氣的生產計畫及政策,兩者間落差有多巨大。」

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

    【其他文章推薦】

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

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

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

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

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

    ※回頭車貨運收費標準