標籤: 銷售文案

  • 作為一個Java開發你用過Jib嗎

    作為一個Java開發你用過Jib嗎

    1. 前言

    JibGoogle開發的可以直接構建 Java應用的DockerOCI鏡像的類庫,以MavenGradle插件形式提供。它最騷操作的是可以在沒有Docker守護程序的情況下構建,也就是說,您不必在計算機上安裝docker守護程序!儘管Spring Boot 2.3.0.RELEASE已經推出了構建鏡像的功能,胖哥還是忍不住要試試Jib

    其實最騷的還是名字。

    2. Docker構建流程和Jib的構建流程

    沒有對比就沒有傷害。我們還是要對比一下這兩者的構建流程。

    Docker構建流程需要我們先把項目打成Jar然後編寫Dockerfile,然後使用Docker構建功能進行構建鏡像、運行容器。流程如下:

    而Jib是這樣構建的:

    作為一個Java開發者,不用再關心各種無關的命令和操作,只需要專註於Java,而且高效穩定以及可復用的增量構建。為什麼Jib能這麼快而高效?

    傳統上,將Java應用程序與應用程序Jar一起構建為單個圖像層,而Jib的構建策略將Java應用程序分為多層,以進行更細化的增量構建。更改代碼時,僅重建更改,而不重建整個應用程序。

    3. Jib構建Spring Boot應用

    接下來我將演示如何將Spring Boot 應用打成鏡像並上傳到Dockerhub倉庫。

    Maven工程為例,我們只需要在pom.xml中引入Jib Maven 插件。默認情況下Jib會把我們打好的鏡像上傳到Googlegcr.io倉庫,實際中我們會把打好的鏡像上傳到私有倉庫,所以我們要加一些個性化配置。這裏我以dockerhub倉庫為例添加一些個性化配置:

    <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.4.0</version>
        <configuration>
            <!-- 相當於 Dockerfile 中的 FROM -->
            <from>
                <image>amazoncorretto:8</image>
            </from>
            <to>
                <!--構建鏡像名稱,這裏我使用maven中定義的項目名稱-->
                <image>daxus/${project.name}</image>
                <!--私有倉庫的賬號密碼-->
                <auth>
                    <username>felordcn</username>
                    <password>yourpassword</password>
                </auth>
                <!--Docker 鏡像的 tag 這裏使用maven定義的版本號-->
                <tags>
                    <tag>
                        ${project.version}
                    </tag>
                </tags>
            </to>
        </configuration>
    </plugin>
    

    然後在項目根目錄執行mvn clean compile jib:build就可以了。

    其實也可以簡單引入Jib插件:

    <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.4.0</version>
    </plugin>
    

    只不過我們的命令會更複雜一些,需要指定一些必要的參數,例如:

    mvn clean compile jib:build \
        -Djib.to.image=myregistry/myimage:latest \
        -Djib.to.auth.username=$USERNAME \
        -Djib.to.auth.password=$PASSWORD
    

    更多的定製命令可參考官方文檔:

    https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin#extended-usage

    4. 總結

    Jib使用起來非常簡單,讓開發人員以Java的風格來完成Docker鏡像的構建,能夠大大改善編程的體驗。多多關注:碼農小胖哥 獲取更多有用的編程乾貨教程。

    關注公眾號:Felordcn 獲取更多資訊

    個人博客:https://felord.cn

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 與特斯拉 Mddel X 比拼 奧迪 2018 年左右將推純電動車

    國外媒體美國汽車新聞網報導,奧迪將推出全新的純電動 SUV 車型與特斯拉 Model X 車型競爭,但奧迪認為插電混合動力車型才是目前新能源汽車市場最理想的車型。   奧迪將在 2018 年左右發布一款續航里程可達到 498 公里的純電動 SUV,將採用全新的造型設計理念,基於第二代 MLB 平台研發,並藉鑑新一代奧迪 Q5 車型的部分技術與設計。奧迪全新的純電動 SUV 車型將可在 20 分鐘內完成 80% 的充電,續航里程超過特斯拉 Model X 車型,對其構成不小的威脅。   不過,奧迪執行長斯泰德(Rupert Stadler)近日表示在未來的 10 至 15 年內,插電混合動力汽車將是消費者選擇新能源汽車時的首選車型。插電混合動力車型使用一台汽油引擎或柴油引擎和一台電動機聯合驅動,有一定的純電動續航里程,十分適合消費者在市區中駕駛。但是斯泰德同時表示隨著充電網絡建設的繼續,消費者未來對電動車的接受程度將增加。      

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

    【【其他文章推薦】

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

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

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

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

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

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

  • (七) 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  ?

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

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

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

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

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

  • 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...");
        }
    }
    

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 香港環保署:去年及今年首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月19日公視報導

    11月17號適逢法國黃背心運動一週年,去年不滿總統馬克宏調漲油價的民眾發起抗議行動,在各大城市遍地開花,並逐漸演變成反對馬克宏政府政策的社運。直到現在,仍有部分示威者,每星期六都會固定集會,提醒政府他們怒火難平。今年再度出現幾萬人走上街頭的盛大場面。有群眾和鎮暴警察發生衝突,還有人闖進巴黎的老佛爺百貨,讓業者被迫停業一天。

    抗議民眾表示,「我很高興能夠在這向馬克宏宣告:我們就在這裡,我們還在這裡,黃背心運動不死。儘管他們多方試圖摧毀黃背心,但黃背心屹立不搖,我們都是為了法國。」

    近幾個月來,黃背心運動趨於和緩,但週年紀念又讓情勢再度激化,數萬人走上街頭。部分抗議人士推翻路邊車輛,點燃垃圾桶等物,還向鎮暴警察扔擲石頭,而警方也以催淚瓦斯和水柱還擊和驅散人群。

    根據法國內政部的說法,法國全國各地星期六一共逮捕了264人,其中巴黎就佔了六成以上。黃背心支持者表示,目前他們考慮加入其他工會行動,參與12月5號開始反對馬克宏年金改革的無限期大罷工。

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 前端工程師必備:從瀏覽器的渲染到性能優化

    前端工程師必備:從瀏覽器的渲染到性能優化

    摘要:本文主要講談及瀏覽器的渲染原理、流程以及相關的性能問題。

    問題前瞻

    1. 為什麼css需要放在頭部?
    2. js為什麼要放在body後面?
    3. 圖片的加載和渲染會阻塞頁面DOM構建嗎?
    4. dom解析完才出現頁面嗎?
    5. 首屏時間根據什麼來判定?

    瀏覽器渲染

    1.瀏覽器渲染圖解

    [來自google開發者文檔]

    瀏覽器渲染頁面主要經歷了下面的步驟:

    1.處理 HTML 標記並構建 DOM 樹。
    2.處理 CSS 標記並構建 CSSOM 樹。
    3.將 DOM 與 CSSOM 合併成一個渲染樹。
    4.根據渲染樹來布局,以計算每個節點的幾何信息。
    5.將各個節點繪製到屏幕上。

    為構建渲染樹,瀏覽器大體上完成了下列工作:

    從 DOM 樹的根節點開始遍歷每個可見節點。
    
    某些節點不可見(例如腳本標記、元標記等),因為它們不會體現在渲染輸出中,所以會被忽略。
    某些節點通過 CSS 隱藏,因此在渲染樹中也會被忽略,例如,上例中的 span 節點---不會出現在渲染樹中,---因為有一個顯式規則在該節點上設置了“display: none”屬性。
    對於每個可見節點,為其找到適配的 CSSOM 規則並應用它們。
    
    發射可見節點,連同其內容和計算的樣式。

    根據以上解析,DOM樹和CSSOM樹的構建對於頁面性能有非常大的影響,沒有DOM樹,頁面基本的標籤塊都沒有,沒有樣式,頁面也基本是空白的。所以具體css的解析規則是什麼?js是怎麼影響頁面渲染的?了解了這些,我們才能有的放矢,對頁面性能進行優化。

    2.css解析規則

    1
    <div id="div1">
    2
    <div class="a">
    3
    <div class="b">
    4
    ...
    5
    </div>
    6
    <div class="c">
    7
    <div class="d">
    8
    ...
    9
    </div>
    10
    <div class="e">
    11
    ...
    12
    </div>
    13
    </div>
    14
    </div>
    15
    <div class="f">
    16
    <div class="c">
    17
    <div class="d">
    18
    ...
    19
    </div>
    20
    </div>
    21
    </div>
    22
    </div>

     

    1
    #div1 .c .d {}
    2
    .f .c .d {}
    3
    .a .c .e {}
    4
    #div1 .f {}
    5
    .c .d{}

    從左向右的匹配規則

    從右向左的匹配規則

    如果css從左向右解析,意味着我們需要遍歷更多的節點。不管樣式規則寫得多細緻,每一個dom結點仍然需要遍歷,因為整個style rules還會有其它公共樣式影響。如果從右向左解析,因為子元素只有一個父元素,所以能夠很快定位出當前dom符不符合樣式規則。

    3.js加載和執行機制

    首先明確一點,我們可以通過js去修改網頁的內容,樣式和交互等,這一意味着js會影響頁面的dom結構,如果js和dom構建并行執行,那麼很容易會出現衝突,所以js在執行時必然會阻塞dom和cssom的構建過程,不論是外部js還是內聯腳本。

    js的位置是否影響dom解析?

    首先我們為什麼提倡把js放在body標籤的後面去加載,因為從demo上看無論是放在head還是放在body后加載js,頁面domcontentload的時間都是一樣的:

    我們從圖中可以看出js的加載和執行是阻塞dom解析的,但是因為頁面並不是一次就渲染完成,所以我們需要做的是盡量讓用戶看到首屏的部分被渲染出來,js放在頭部,則頁面的內容區域還沒有解析到就被阻塞了,導致用戶看到的是白屏,而js放在body後面,儘管此時頁面dom仍然沒有解析完成,但是已經渲染出一部分樓層了,這也是為什麼我們比較看重頁面的首屏時間。

    只有DOM和CSSOM樹構建好后併合並成渲染樹才能開始繪製頁面圖形,那是不是把整個DOM樹和CSSOM樹構建好后才能開始繪製頁面?這顯然是不符合我們平時訪問頁面的認知的,實際上:

    為達到更好的用戶體驗,呈現引擎會力求儘快將內容显示在屏幕上。它不必等到整個 HTML 文檔解析完畢之後,就會開始構建呈現樹和設置布局。在不斷接收和處理來自網絡的其餘內容的同時,呈現引擎會將部分內容解析並显示出來。

    具體瀏覽器什麼時候進行首次繪製?可以查看本文對瀏覽器首次渲染時間點的探究。

    4.圖片的加載和渲染機制

    首先我們解答一下上面的問題:圖片的加載與渲染會不會阻塞頁面渲染?答案是圖片的加載和渲染不會影響頁面的渲染。

    那麼標籤中的圖片和樣式中的圖片的加載和渲染時間是什麼樣的呢?

    解析HTML【遇到標籤加載圖片】 —> 構建DOM樹
    加載樣式 —> 解析樣式【遇到背景圖片鏈接不加載】 —> 構建樣式規則樹
    加載javascript —> 執行javascript代碼
    把DOM樹和樣式規則樹匹配構建渲染樹【遍歷DOM樹時加載對應樣式規則上的背景圖片】
    計算元素位置進行布局
    繪製【開始渲染圖片】

    當然把DOM樹和樣式規則樹匹配構建渲染樹時,只會把可見元素和它對應的樣式規則結合一起產出到渲染樹,這就意味有不可見元素,當匹配DOM樹和樣式規則樹時,若發現一個元素的對應的樣式規則上有display:none,瀏覽器會認為該元素是不可見的,因此不會把該元素產出到渲染樹上。

    性能優化

    css優化

    1.盡量減少層級

    1
    #div p.class {
    2
    color: red;
    3
    }
    4
    
    5
    .class {
    6
    color: red;
    7
    }

    層級減少,意味者匹配時遍歷的dom就少。
    關於less嵌套的書寫規範也基於這個道理。

    2.使用類選擇器而不是標籤選擇器

    減少匹配次數

    3.按需加載css

    1
    (function(){
    2
    window.gConfig = window.gConfig || {};
    3
    window.gConfig.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    4
    var hClassName;
    5
    if(window.gConfig.isMobile){
    6
    hClassName = ' phone';
    7
    
    8
    document.write('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.css" />');
    9
    document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/m/index.js" crossorigin="anonymous" as="script" />');
    10
    
    11
    }else{
    12
    hClassName = ' pc';
    13
    
    14
    document.write('<link rel="stylesheet" href="https://res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.css" />');
    15
    document.write('<link rel="preload" href="//res.hc-cdn.com/cpage-pep-discount-area-v6/2.0.24/pc/index.js" crossorigin="anonymous" as="script" />');
    16
    
    17
    }
    18
    var root = document.documentElement;
    19
    root.className += hClassName ;
    20
    
    21
    })();

    async 與 defer

    [來自https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html]

    使用

    • 如果腳本是模塊化的並且不依賴於任何腳本,請使用async。
    • 如果該腳本依賴於另一個腳本或由另一個腳本所依賴,則使用defer。

    減少資源請求

    瀏覽器的併發數量有限,所以為了減少瀏覽器因為優先加載很多不必要資源,以及網絡請求和響應時間帶來的頁面渲染阻塞時間,我們首先應該想到的是減少頁面加載的資源,能夠盡量用壓縮合併,懶加載等方法減少頁面的資源請求。

    延遲加載圖像

    儘管圖片的加載和渲染不會影響頁面渲染,但是為了盡可能地優先展示首屏圖片和減少資源請求數量,我們需要對圖片做懶加載。

    1
    document.addEventListener("DOMContentLoaded", function() {
    2
    let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    3
    let active = false;
    4
    
    5
    const lazyLoad = function() {
    6
    if (active === false) {
    7
    active = true;
    8
    
    9
    setTimeout(function() {
    10
    lazyImages.forEach(function(lazyImage) {
    11
    if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
    12
    lazyImage.src = lazyImage.dataset.src;
    13
    lazyImage.srcset = lazyImage.dataset.srcset;
    14
    lazyImage.classList.remove("lazy");
    15
    
    16
    lazyImages = lazyImages.filter(function(image) {
    17
    return image !== lazyImage;
    18
    });
    19
    
    20
    if (lazyImages.length === 0) {
    21
    document.removeEventListener("scroll", lazyLoad);
    22
    window.removeEventListener("resize", lazyLoad);
    23
    window.removeEventListener("orientationchange", lazyLoad);
    24
    }
    25
    }
    26
    });
    27
    
    28
    active = false;
    29
    }, 200);
    30
    }
    31
    };
    32
    
    33
    document.addEventListener("scroll", lazyLoad);
    34
    window.addEventListener("resize", lazyLoad);
    35
    window.addEventListener("orientationchange", lazyLoad);
    36
    });

    詳情參考延遲加載圖像和視頻

    大促活動實踐

    2.1 懶加載與異步加載

    懶加載與異步加載是大促活動性能優化的主要手段,直白的說就是把用戶不需要或者不會立即看到的頁面數據與內容全都挪到頁面首屏渲染完成之後去加載,極限減小頁面首屏渲染的數據加載量與js,css執行帶來的性能損耗。

    2.1.1 導航下拉的異步加載

    導航的下拉內容是一塊結構非常複雜的html片段,如果直接加載,瀏覽器渲染的時間會拖慢頁面整體的加載時間:

    所有我們需要通過異步加載方式來獲取這段html片段,等頁面首屏渲染結束后再添加到頁面上,大致的代碼如下:

    1
    $.ajax({
    2
    url: url, async: false, timeout: 10000,
    3
    success: function (data) {
    4
    container.innerHTML = data;
    5
    var appendHtml = $('<div class="footer-wrapper">' + container.querySelector('#footer').innerHTML + '</div>');
    6
    var tempHtml = '<div style="display:none;">' + '<script type="text/html" id="header-lazyload-html-drop" class="header-lazyload-html" data-holder="#holder-drop">' + appendHtml.find('#header-lazyload-html-drop').html() + '<\/script><script type="text/html" id="header-lazyload-html-mbnav" class="header-lazyload-html" data-holder="#holder-mbnav">' + appendHtml.find('#header-lazyload-html-mbnav').html() + '<\/script></div>';
    7
    $('#footer').append(tempHtml);
    8
    feloader.onLoad(function () {
    9
    feloader.use('@cloud/common-resource/header', function () {
    10
    });
    11
    $('#footer').css('display', 'block');
    12
    });
    13
    },
    14
    error: function (XMLHttpRequest, textStatus, errorThrown) {
    15
    console.log(XMLHttpRequest.status, XMLHttpRequest.readyState, textStatus);
    16
    },
    17
    });

    2.1.2 圖片懶加載

    官網的cui套件中已經有lazyload的插件支持圖片懶加載,使用方法頁非常簡單:

    1
    <div class="list">
    2
    <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/1" src="佔位圖片URL" />
    3
    <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/2" src="佔位圖片URL" />
    4
    <img class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/3" src="佔位圖片URL" />
    5
    <div class="lazyload" data-src="http://www.placehold.it/375x200/eee/444/3"></div>
    6
    ...
    7
    </div>

    從代碼我們差不多可以猜出圖片懶加載的原理,其實就是我們通過覆蓋img標籤src屬性,使得img標籤開始加載時由於沒有src的具體圖片地址而不去加載圖片,等到重要資源加載完之後,通過監聽onload的時間或者滾動條的滾動時機再去重寫對應標籤的src值來達到圖片懶加載:

    1
    /**
    2
    * load image
    3
    * @param {HTMLElement} el - the image element
    4
    * @private
    5
    */
    6
    _load(el) {
    7
    let source = el.getAttribute(ATTR_IMAGE_URL);
    8
    if (source) {
    9
    let processor = this._config.processor;
    10
    if (processor) {
    11
    source = processor(source, el);
    12
    }
    13
    
    14
    el.addEventListener('load', () => {
    15
    el.classList.remove(CLASSNAME);
    16
    });
    17
    // 判斷是否是什麼元素
    18
    if (el.tagName === 'IMG') {
    19
    el.src = source;
    20
    } else {
    21
    // 判斷source是不是一個類名,如果是類名的話,則加到class裏面去
    22
    if (/^[A-Za-z0-9_-]+$/.test(source)) {
    23
    el.classList.add(source);
    24
    } else {
    25
    let styles = el.getAttribute('style') || '';
    26
    styles += `;background-image: url(${source});`;
    27
    el.setAttribute('style', styles);
    28
    el.style.backgroundImage = source; // = `background-image: url(${source});`;
    29
    }
    30
    }
    31
    
    32
    el.removeAttribute(ATTR_IMAGE_URL);
    33
    }
    34
    }

    具體的插件代碼大家可以查看https://git.huawei.com/cnpm/lazyload

    同時官網的頁腳部分也採用了採用其它的加載方式也實現了懶加載的效果,頁腳的圖片都在css中引用,想要延遲加載頁腳圖片就需要延遲加載頁腳的css,但是延遲加載css造成的後果就是頁面加載的一瞬間頁腳會因為樣式確實而显示錯亂,所以我們可以在css樣式加載前強勢隱藏掉頁腳部分,等css加載完成后,頁腳dom自帶的display:block會自動显示頁腳。(==因為頁腳的seo特性沒有對其進行懶加載==)

    2.1.3 樓層內容的懶加載

    基於xtpl自帶的懶加載能力,配合pep定製頁面模板的邏輯,我們可以實現html的懶加載。在頁面初次渲染的時候,只有每個樓層的大體框架和標題等關鍵信息,如果需要的話可以給默認圖片等佔位,或設置最小高度佔位,防止錨點定位失效。
    當頁面滾動到該樓層的位置,js代碼方會執行,在初始化函數中,對該樓層的html進行加載,渲染,實現樓層圖片和html的懶加載,減少了首屏時間。
    具體代碼如下:

    1
    <div class="nov-c6-cards j-content">
    2
    </div>

     

    1
    public render(){
    2
    this.$el.find('.j-content').html(new Xtemplate(tpl).render(mockData))
    3
    ...
    4
    }

    2.1.4 套餐數據懶加載

    套餐數據的加載一直以來都是令人頭疼的,本次雙十一對於套餐腳本也做了優化,不僅對數據進行了緩存,同時也可以在指定的範圍進行套餐數據的渲染——和上述所說的樓層懶加載配合,可以做到未展示的樓層,套餐數據不請求,下拉框不渲染,詢價接口不調用,在首屏不出現大量套餐的情況下,可以大大提升首屏加載的性能。

    2.2.資源整合

    2.2.1.頁頭頁尾資源統一維護

    基礎模板的優化涉及到資源的合併,壓縮與異步加載,dom的延遲加載和圖片的懶加載。首先我們給出官網基礎模板引用的一部分js資源的表格:

    這部分js存在問題是分散在pep的各個資產庫路徑維護,有些壓縮了,有些沒有壓縮,js的加載也基本是順序執行,所以我們對這個部分的js和css資源進行了一個整合,進行的操作是遷移,合併,壓縮。

    建立common-resource倉庫去統一維護管理頁頭頁腳及公共資源代碼。

    2.2.2.合併加載方式相同的基礎功能js並壓縮

    common.js

    1
    import './common/js/AGrid';
    2
    import './common/js/jquery.base64';
    3
    import './common/js/lang-tips';
    4
    import './common/js/setLocaleCookie';
    5
    import './common/js/pepDialog';

    如上面代碼,將官網中用的分散的基礎功能js合併成一個common.js,經過伏羲流水線發布,cui套件會自動將js壓縮,這樣做的效果當然是減少官網頁面請求資源數,減小資源大小。

    2.2.3.資源異步加載

    觀察2.2.1中的表格可以發現,官網大部分js都是放在頭部或者是body后順序加載的,這些資源的加載時間必定是在DOMOnLoad之前

    這些js都是會阻塞頁面的渲染,導致頁面首屏加載變慢,我們需要做的就是通過之前頭尾資源的整理得出哪些資源是可以在onload之後去加載的,這些我們就可以把頁面加載時不需要執行的js和css全部移到頁面渲染完成後去加載,少了這部分的js邏輯執行時的阻塞,頁面首屏渲染的時間也會大大降低。

    通過cui套件中的feloader插件,我們可以比較便捷的控制js和css加載的時機:

    1
    feloader.onLoad(function () {
    2
    feloader.use([
    3
    '@cloud/link-to/index',
    4
    '@cloud/common-resource/uba',
    5
    '@cloud/common-resource/footer',
    6
    '@cloud/common-resource/header',
    7
    '@cloud/common-resource/common',
    8
    '@cloud/common-resource/prompt.css',
    9
    '@cloud/common-resource/footer.css',
    10
    ]);
    11
    });

    下圖可以明顯看到js的加載都轉移到onload之後了:

    2.2.4 圖片壓縮

    除了對設計給出的圖片有壓縮要求外,我們還通過對一部分不常更新的小圖標圖片進行base64編碼來減少頁面的圖片請求數量。

    2.3預解析與預加載

    除了延遲加載外,基礎模板還進行了諸如dns預解析,資源預加載的手段來提前解析dns和加載頁面資源。

    2.3.1 DNS 預解析

    當用戶訪問過官網頁面后,DNS預解析能夠使用戶在訪問雙十一活動頁之前提前進行DNS解析,從而減少雙十一活動頁面的dns解析時間,提高頁面的訪問性能,其實寫法也很簡單:

    1
    <link rel="dns-prefetch" href="//res.hc-cdn.com">
    2
    <link rel="dns-prefetch" href="//res-static1.huaweicloud.com">
    3
    <link rel="dns-prefetch" href="//res-static2.huaweicloud.com">
    4
    <link rel="dns-prefetch" href="//res-static3.huaweicloud.com">

    2.3.2 preload 預加載

    活動頁的部分js還使用了preload預加載的方式來提升頁面加載性能,preload的為什麼可以達到這種效果,我們需要看下面這段摘錄:

    Preloader 簡介

    HTML 解析器在創建 DOM 時如果碰上同步腳本(synchronous script),解析器會停止創建 DOM,轉而去執行腳本。所以,如果資源的獲取只發生在解析器創建 DOM時,同步腳本的介入將使網絡處於空置狀態,尤其是對外部腳本資源來說,當然,頁面內的腳本有時也會導致延遲。

    預加載器(Preloader)的出現就是為了優化這個過程,預加載器通過分析瀏覽器對 HTML 文檔的早期解析結果(這一階段叫做“令牌化(tokenization)”),找到可能包含資源的標籤(tag),並將這些資源的 URL 收集起來。令牌化階段的輸出將會送到真正的 HTML 解析器手中,而收集起來的資源 URLs 會和資源類型一起被送到讀取器(fetcher)手中,讀取器會根據這些資源對頁面加載速度的影響進行有次序地加載。

    基於以上原理,我們對官網相對重要的js資源進行preload預加載,以使得瀏覽器可以儘快地加載頁面所需的重要資源。

    1
    <link rel="preload" href="//res.hc-cdn.com/cnpm-feloader/1.0.6/feloader.js" as="script"/>
    2
    <link rel="preload" href="//polyfill.alicdn.com/polyfill.min.js?features=default,es6" as="script"/>
    3
    <link rel="preload" href="https://res-static3.huaweicloud.com/content/dam/cloudbu-site/archive/commons/3rdlib/jquery/jquery-1.12.4.min.js" as="script"/>
    4
    <link rel="preload" href="//res.hc-cdn.com/cnpm-wpk-reporter/1.0.6/wpk-performance.js" as="script"/>
    5
    
    6
    <link rel="preload" href="//res.hc-cdn.com/cpage-pep-2019nov-promotion/1.1.15/components/activity-banner/images/banner_mb.jpg" as="image" media="(max-width: 767px)">

    優化效果

    3.總結

    前端性能優化的方法手段並不僅限於文章陳述,官網前端團隊還會在前端性能優化的道路上學習更多,探索更多,將華為雲官網頁面的加載性能做到極致!

     

    點擊關注,第一時間了解華為雲新鮮技術~

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

    【【其他文章推薦】

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

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

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

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

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

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

  • 自已做動畫及編寫程序搞清楚最大堆的實現原理

    自已做動畫及編寫程序搞清楚最大堆的實現原理

    目錄

    • 背景
    • 概念
    • 最大堆
      • 最大堆的線性存儲
      • 動畫實現最大堆加入新元素
      • 代碼實現最大堆加入新元素
      • 動畫實現最大堆取出最大元素
      • 代碼實現最大堆取出最大元素
      • 程序測試
    • 最大堆的應用–優先隊列
    • 寫在最後

    背景

    • 二叉樹是數據結構中的重點,也是難點。二叉樹比數組、棧、隊列等線性結構相比複雜度更高,想要做到心中有“樹”,需要自己動手畫圖、觀察、思考,才能領會其真諦。
    • 在上篇文章《自己動手作圖深入理解二叉樹、滿二叉樹及完全二叉樹》中,我們對完全二叉樹有了一定認識,該文將對一種特殊的完全二叉樹”最大堆”進行底層研究。

    概念

    堆(heap)通常是一個可以被看做一棵二叉樹的數組對象。堆總是滿足下列性質:

    • 堆總是一棵完全二叉樹。
    • 堆中某個節點的值總是不大於或不小於其父節點的值;

    最大堆

    • 根節點最大的堆叫做最大堆
    最大堆的線性存儲
    • 由於堆是一種特殊的完全二叉樹,可以利用數組集合形成線性存儲的數據結構。
    /**
     * 最大堆的底層實現--數組集合形成線性存儲的數據結構
     *  * @author zhuhuix
     * @date 2020-06-28
     */
    public class MaxHeap<E extends Comparable<E>> {
    
        // 存放元素的數組集合
        private ArrayList<E> list;
    
        MaxHeap() {
            this.list = new ArrayList<>();
        }
    
        // 得到左孩子索引
        private int getLeftChildIndex(int i) {
            return (2 * i + 1);
        }
    
        // 得到右孩子索引
        private int getRightChildIndex(int i) {
            return (2 * i + 2);
        }
    
        // 得到父結點索引
        private int getParentIndex(int i) {
            if (i == 0) {
                throw new IllegalArgumentException("非法索引值");
            } else {
                return ((i - 1) / 2);
            }
        }
    }
    
    動畫實現最大堆加入新元素
    • 加入到數組集合尾部的元素與父結點進行比較,通過上浮操作,保證所有子結點不能大於父結點。
    代碼實現最大堆加入新元素
    /**
     * 最大堆的底層實現
     *
     * @author zhuhuix
     * @date 2020-06-28
     */
    public class MaxHeap<E extends Comparable<E>> {
    
        // 存放元素的數組集合
        private ArrayList<E> list;
    
        MaxHeap() {
            this.list = new ArrayList<>();
        }
    
        // 得到左孩子索引
        private int getLeftChildIndex(int i) {
            return (2 * i + 1);
        }
    
        // 得到右孩子索引
        private int getRightChildIndex(int i) {
            return (2 * i + 2);
        }
    
        // 得到父結點索引
        private int getParentIndex(int i) {
            if (i == 0) {
                throw new IllegalArgumentException("非法索引值");
            } else {
                return ((i - 1) / 2);
            }
        }
    
        // 添加元素
        public void add(E e) {
            this.list.add(e);
            /**
             * 將加入的結點與父結點進行比較:
             * 如果加入的結點大於父結點,則進行上浮
             * 直至新結點小於或等於父結點為止
             */
    
            // 獲取當前添加元素在數組中的索引
            int i = this.list.size() - 1;
            while (i > 0) {
                E current = this.list.get(i);
                E parent = this.list.get(getParentIndex(i));
                // 如果父結點元素大於當前加入的元素,則進行交換
                if (parent.compareTo(current) < 0) {
                    // 交換新加入的結點與父結點的位置
                    Collections.swap(this.list, i, getParentIndex(i));
                } else {
                    break;
                }
                i = getParentIndex(i);
            }
        }
        
    }
    
    動畫實現最大堆取出最大元素
    • 獲取最大堆中的根結點,即為最大元素;並把尾部結點放置到根結點,並通過下沉操作,把子結點中的最大元素移動根結點。
    代碼實現最大堆取出最大元素
    /**
     * 最大堆的底層實現
     *
     * @author zhuhuix
     * @date 2020-06-28
     */
    public class MaxHeap<E extends Comparable<E>> {
    
        // 存放元素的數組集合
        private ArrayList<E> list;
    
        MaxHeap() {
            this.list = new ArrayList<>();
        }
    
        // 得到左孩子索引
        private int getLeftChildIndex(int i) {
            return (2 * i + 1);
        }
    
        // 得到右孩子索引
        private int getRightChildIndex(int i) {
            return (2 * i + 2);
        }
    
        // 得到父結點索引
        private int getParentIndex(int i) {
            if (i == 0) {
                throw new IllegalArgumentException("非法索引值");
            } else {
                return ((i - 1) / 2);
            }
        }
    
        // 查找最大元素
        public E findMax() {
            if (this.list.size() == 0) {
                return null;
            }
            // 最大堆中的元素永遠在根結點
            return this.list.get(0);
        }
    
        // 取出最大元素
        public E getMax() {
            if (findMax() != null) {
                E e = findMax();
    
                /**
                 * 取出最大元素后,需要把堆中第二大的元素放置在根結點:
                 * 將根結點元素與最後面的元素進行交換,
                 * 讓最後面的元素出現在根結點,並移除最大元素
                 * 將根結點的元素與左右孩子結點比較,直至根結點的元素變成最大值
                 */
                int i = 0;
                Collections.swap(this.list, i, this.list.size() - 1);
                this.list.remove(this.list.size() - 1);
    
                // 通過循環進行當前結點與左右孩子結點的大小比較
                while (getLeftChildIndex(i) < this.list.size() && getRightChildIndex(i) < this.list.size()) {
                    int leftIndex = getLeftChildIndex(i);
                    int rightIndex = getRightChildIndex(i);
    
                    // 通過比較左右孩子的元素哪個較大,確定當前結點與哪個孩子進行交換
                    int index = this.list.get(leftIndex).compareTo(this.list.get(rightIndex)) > 0 ? leftIndex : rightIndex;
                    if (this.list.get(i).compareTo(this.list.get(index)) < 0) {
                        Collections.swap(this.list, i, index);
                    } else {
                        // 如果當前結點都大於左右孩子,則結束比較
                        break;
                    }
                    i = index;
                }
    
                return e;
            } else {
                return null;
            }
        }
    }
    
    
    程序測試
    /**
     * 最大堆的底層實現--測試程序
     *
     * @author zhuhuix
     * @date 2020-06-28
     */
    public class MaxHeapTest {
        public static void main(String[] args) {
            MaxHeap<Integer> maxHeap = new MaxHeap<>();
    
            // 將10個数字加入形成最大堆
            int[] arrays = {19,29,4,2,27,0,38,15,12,31};
            for (int i = 0; i < arrays.length; i++) {
                maxHeap.add(arrays[i]);
            }
    
            // 依次從堆中取出最大值
            for (int i = 0; i < arrays.length; i++) {
                System.out.println("第"+(i+1)+"次取出堆目前的最大值:"+maxHeap.getMax());
            }
        }
    }
    
    

    最大堆的應用–優先隊列

    優先隊列:出隊的和順序與入隊的順序無關,只與優先級相關;
    優先隊列通常可以採用最大堆的數據結構來實現。

    /**
     * 用最大堆的數據結構實現優先隊列
     * 
     * @author zhuhuix
     * @date 2020-06-28
     */
    public class PriorityQueue<E extends Comparable<E>>  {
        private MaxHeap<E> mhp;
        PriorityQueue() {
            mhp=new MaxHeap<>();
        }
    
        // 入隊
        public void enqueue(E e) {
            mhp.add(e);
        }
    
        // 優選級最高的元素出隊
        public E dequeue() {
            return mhp.getMax();
        }
    
        // 查看優先級最高的元素
        public E getFront() {
            return mhp.findMax();
        }
    }
    
    

    寫在最後

    • 以上通過畫圖、動畫演示、代碼編寫對堆與最大堆的概念和底層實現方式,都作了深入分析;作為最大堆的反向結構,最小堆的實現也是一樣,讀者可參考以上動畫和代碼,動手練習。
    • 畫圖、編碼不易,請點贊、收藏、關注三連!!!

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

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

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

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

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

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

    ※超省錢租車方案

  • 日本國土百萬年的惡夢 每日數百噸的福島輻射污染水

    文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

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

    【【其他文章推薦】

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

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

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

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

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

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

  • 馬來西亞最後一頭蘇門答臘犀牛病逝 全球剩80頭

    摘錄自2019年11月24日中央通訊社馬來西亞報導

    馬來西亞最後一隻蘇門答臘犀牛,25歲的母犀牛伊曼自2014年被捕獲後,一直在野生動物保護區接受妥善照顧,牠今天(24日)因癌病逝於婆羅洲(Borneo)沙巴(Sabah)。沙巴野生動物部門(Sabah Wildlife Department)主任奧古斯丁(Augustine Tuuga)說:「伊曼的死亡比預期要快,但我們知道,牠已經開始承受極大痛苦。」

    蘇門答臘犀牛是體型最小的犀牛,曾廣布亞洲各地,野生蘇門答臘犀牛如今已近乎絕種,據保育人士估計,目前全球僅剩約30至80頭,大多棲息在蘇門答臘和印尼所管轄的婆羅洲地區。馬來西亞2015年宣布野生蘇門答臘犀牛絕種,最後一頭公蘇門答臘犀牛今年5月離世。

    保育團體國際犀牛基金會(International Rhino Foundation)說,棲地減少和盜獵導致蘇門答臘犀牛生存於孤立區域,代表牠們繁衍困難,數十年內可能就會滅絕。

    自2011年以來,馬來西亞不斷嘗試以體外受精方式,繁殖人工飼養的蘇門答臘犀牛,但迄今尚未成功。

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

    【其他文章推薦】

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

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

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

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

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