SpringBoot系列之i18n集成教程

目錄

SpringBoot系統之i18n國際化語言集成教程
@

1、環境搭建

本博客介紹一下SpringBoot集成i18n,實現系統語言國際化處理,ok,先創建一個SpringBoot項目,具體的參考我的博客專欄:

環境準備:

  • IntelliJ IDEA
  • Maven

項目集成:

  • Thymeleaf(模板引擎,也可以選jsp或者freemark)
  • SpringBoot2.2.1.RELEASE

2、resource bundle資源配置

ok,要實現國際化語言,先要創建resource bundle文件:
在resources文件夾下面創建一個i18n的文件夾,其中:

  • messages.properties是默認的配置
  • messages_zh_CN.properties是(中文/中國)
  • messages_en_US.properties是(英文/美國)
  • etc.

    IDEA工具就提供了很簡便的自動配置功能,如圖,只要點擊新增按鈕,手動輸入,各配置文件都會自動生成屬性

    messages.properties:

messages.loginBtnName=登錄~
messages.password=密碼~
messages.rememberMe=記住我~
messages.tip=請登錄~
messages.username=用戶名~

messages_zh_CN.properties:

messages.loginBtnName=登錄
messages.password=密碼
messages.rememberMe=記住我
messages.tip=請登錄
messages.username=用戶名

messages_en_US.properties:

messages.loginBtnName=login
messages.password=password
messages.rememberMe=Remember me
messages.tip=Please login in
messages.username=userName

在項目的application.properties修改默認配置,讓SpringBoot的自動配置能讀取到resource bundle資源文件

## 配置i18n
# 默認是i18n(中文/中國)
spring.mvc.locale=zh_CN
# 配置resource bundle資源文件的前綴名eg:i18n是文件夾名,messages是資源文件名,支持的符號有.號或者/
spring.messages.basename=i18n.messages
# 設置緩存時間,2.2.1是s為單位,之前版本才是毫秒
spring.messages.cache-duration=1
# 設置資源文件編碼格式為utf8
spring.messages.encoding=utf-8

注意要點:

  • spring.messages.basename必須配置,否則SpringBoot的自動配置將失效
    MessageSourceAutoConfiguration.ResourceBundleCondition 源碼:
protected static class ResourceBundleCondition extends SpringBootCondition {
        //定義一個map緩存池
        private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = cache.get(basename);//緩存拿得到,直接從緩存池讀取
            if (outcome == null) {//緩存拿不到,重新讀取
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }

        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
            ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
            for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(), name)) {
                    if (resource.exists()) {
                    //匹配resource bundle資源
                        return ConditionOutcome.match(message.found("bundle").items(resource));
                    }
                }
            }
            return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
        }
        //解析資源文件
        private Resource[] getResources(ClassLoader classLoader, String name) {
            String target = name.replace('.', '/');//spring.messages.basename參數值的點號換成斜桿
            try {
                return new PathMatchingResourcePatternResolver(classLoader)
                        .getResources("classpath*:" + target + ".properties");
            }
            catch (Exception ex) {
                return NO_RESOURCES;
            }
        }

    }
  • cache-duration在2.2.1版本,指定的是s為單位,找到SpringBoot的MessageSourceAutoConfiguration自動配置類

3、LocaleResolver類

SpringBoot默認採用AcceptHeaderLocaleResolver類作為默認LocaleResolver,LocaleResolver類的作用就是作為i18n的分析器,獲取對應的i18n配置,當然也可以自定義LocaleResolver類


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

/**
 * <pre>
 *  自定義LocaleResolver類
 * </pre>
 * @author nicky
 * <pre>
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2019年11月23日  修改內容:
 * </pre>
 */
public class CustomLocalResolver implements LocaleResolver {

    Logger LOG = LoggerFactory.getLogger(this.getClass());

    @Nullable
    private Locale defaultLocale;

    public void setDefaultLocale(@Nullable Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    @Nullable
    public Locale getDefaultLocale() {
        return this.defaultLocale;
    }

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale defaultLocale = this.getDefaultLocale();//獲取application.properties默認的配置
        if(defaultLocale != null && request.getHeader("Accept-Language") == null) {
            return defaultLocale;//http請求頭沒獲取到Accept-Language才採用默認配置
        } else {//request.getHeader("Accept-Language")獲取得到的情況
            Locale requestLocale = request.getLocale();//獲取request.getHeader("Accept-Language")的值
            String localeFlag = request.getParameter("locale");//從URL獲取的locale值
            //LOG.info("localeFlag:{}",localeFlag);
            //url鏈接有傳locale參數的情況,eg:zh_CN
            if (!StringUtils.isEmpty(localeFlag)) {
                String[] split = localeFlag.split("_");
                requestLocale = new Locale(split[0], split[1]);
            }
            //沒傳的情況,默認返回request.getHeader("Accept-Language")的值
            return requestLocale;
        }
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

4、I18n配置類

I18n還是要繼承WebMvcConfigurer,注意,2.2.1版本才是實現接口就可以,之前1.+版本是要實現WebMvcConfigurerAdapter適配器類的

import com.example.springboot.i18n.component.CustomLocalResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

/**
 * <pre>
 *  I18nConfig配置類
 * </pre>
 * <p>
 * <pre>
 * @author nicky.ma
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2019/11/24 11:15  修改內容:
 * </pre>
 */
 //Configuration必須加上,不然不能加載到Spring容器
@Configuration
//使WebMvcProperties配置類可用,這個可以不加上,本博客例子才用
@EnableConfigurationProperties({ WebMvcProperties.class})
public class I18nConfig implements WebMvcConfigurer{
    
    //裝載WebMvcProperties 屬性
    @Autowired
    WebMvcProperties webMvcProperties;
    /**
     * 定義SessionLocaleResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:52
     * @return org.springframework.web.servlet.LocaleResolver
     */
//    @Bean
//    public LocaleResolver localeResolver() {
//        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
//        // set default locale
//        sessionLocaleResolver.setDefaultLocale(Locale.US);
//        return sessionLocaleResolver;
//    }

    /**
     * 定義CookieLocaleResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:51
     * @return org.springframework.web.servlet.LocaleResolver
     */
//    @Bean
//    public LocaleResolver localeResolver() {
//        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
//        cookieLocaleResolver.setCookieName("Language");
//        cookieLocaleResolver.setCookieMaxAge(1000);
//        return cookieLocaleResolver;
//    }

    /**
     * 自定義LocalResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:45
     * @return org.springframework.web.servlet.LocaleResolver
     */
    @Bean
    public LocaleResolver localeResolver(){
        CustomLocalResolver localResolver = new CustomLocalResolver();
        localResolver.setDefaultLocale(webMvcProperties.getLocale());
        return localResolver;
    }

    /**
     * 定義localeChangeInterceptor
     * @Author nicky.ma
     * @Date 2019/11/24 13:45
     * @return org.springframework.web.servlet.i18n.LocaleChangeInterceptor
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor(){
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        //默認的請求參數為locale,eg: login?locale=zh_CN
        localeChangeInterceptor.setParamName(LocaleChangeInterceptor.DEFAULT_PARAM_NAME);
        return localeChangeInterceptor;
    }

    /**
     * 註冊攔截器
     * @Author nicky.ma
     * @Date 2019/11/24 13:47
     * @Param [registry]
     * @return void
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(localeChangeInterceptor()).addPathPatterns("/**");
    }
}

注意要點:

  • 舊版代碼可以不加LocaleChangeInterceptor 攔截器,2.2.1版本必須通過攔截器
  • 如下代碼,bean的方法名必須為localeResolver,否則會報錯
@Bean
    public LocaleResolver localeResolver(){
        CustomLocalResolver localResolver = new CustomLocalResolver();
        localResolver.setDefaultLocale(webMvcProperties.getLocale());
        return localResolver;
    }

原理:
跟一下源碼,點進LocaleChangeInterceptor類

DispatcherServlet是Spring一個很重要的分發器類,在DispatcherServlet的一個init方法里找到這個LocaleResolver的init方法

這個IOC獲取的bean類名固定為localeResolver,寫例子的時候,我就因為改了bean類名,導致一直報錯,跟了源碼才知道Bean類名要固定為localeResolver

拋異常的時候,也是會獲取默認的LocaleResolver的

找到資源文件,確認,還是默認為AcceptHeaderLocaleResolver

配置了locale屬性的時候,還是選用AcceptHeaderLocaleResolver作為默認的LocaleResolver

spring.mvc.locale=zh_CN

WebMvcAutoConfiguration.localeResolver方法源碼,ConditionalOnMissingBean主鍵的意思是LocaleResolver沒有自定義的時候,才作用,ConditionalOnProperty的意思,有配了屬性才走這裏的邏輯

  • 攔截器攔截的請求參數默認為locale,要使用其它參數,必須通過攔截器設置 ,eg:localeChangeInterceptor.setParamName("lang");
  • LocalResolver種類有:CookieLocaleResolver(Cookie)、SessionLocaleResolver(會話)、FixedLocaleResolver、AcceptHeaderLocaleResolver(默認)、.etc

5、Thymeleaf集成

本博客的模板引擎採用Thymeleaf的,所以新增項目時候就要加上maven相關依賴,沒有的話,自己加上:

<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>

ok,然後去找個bootstrap的登錄頁面,本博客已尚硅谷老師的例子為例,進行拓展,引入靜態資源文件:

Thymeleaf的i18n支持是採用#符號的

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>SpringBoot i18n example</title>
        <!-- Bootstrap core CSS -->
        <link href="asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link href="asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet">
    </head>

    <body class="text-center">
        <form class="form-signin" action="dashboard.html">
            <img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
            <h1 class="h3 mb-3 font-weight-normal" th:text="#{messages.tip}">Please sign in</h1>
            <label class="sr-only" th:text="#{messages.username}">Username</label>
            <input type="text" class="form-control" th:placeholder="#{messages.username}" required="" autofocus="">
            <label class="sr-only" th:text="#{messages.password} ">Password</label>
            <input type="password" class="form-control" th:placeholder="#{messages.password}" required="">
            <div class="checkbox mb-3">
                <label>
          <input type="checkbox" value="remember-me" > [[#{messages.rememberMe}]]
        </label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{messages.loginBtnName}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2019</p>
            <a class="btn btn-sm" th:href="@{/login(locale='zh_CN')} ">中文</a>
            <a class="btn btn-sm" th:href="@{/login(locale='en_US')} ">English</a>
        </form>

    </body>

</html>

切換中文網頁:

切換英文網頁:

當然不點鏈接傳locale的方式也是可以自動切換的,瀏覽器設置語言:

原理localeResolver類會獲取Accept language參數

附錄:
logging manual:
example source:

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

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

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

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

java中的transient關鍵字詳解

目錄

前言
說實話學了一段時間java的朋友對於transient這個關鍵字依舊很陌生基本沒怎麼用過,但是transient關鍵字在java中卻起到了不可或缺的地位!如果要說講到,我覺得最可能出現的地方是IO流中對象流(也叫序列化流)的時候會講到!

相信很多人都是直到自己碰到才會關心這個關鍵字,記得博主第一次碰到transient關鍵字是在閱讀JDK源碼的時候。在學習java的過程中transient關鍵字少見的原因其實離不開它的作用:transient關鍵字的主要作用就是讓某些被transient關鍵字修飾的成員屬性變量不被序列化。實際上也正是因此,在學習過程中很少用得上序列化操作,一般都是在實際開發中!至於序列化,相信有很多小白童鞋一直迷迷糊糊或者沒有具體的概念,這都不是事,下面博主會很清楚的讓你記住啥是序列化,保證你這輩子忘不了(貌似有點誇張,有點裝b,感覺要被打)

@

1、何謂序列化?

說起序列化,隨之而來的另一個概念就是反序列化,小白童鞋不要慌,記住了序列化就相當於記住了反序列化,因為反序列化就是序列化反過來,所以博主建議只記住序列化概念即可,省的搞暈自己。

專業術語定義的序列化:

Java提供了一種對象序列化的機制。用一個字節序列可以表示一個對象,該字節序列包含該對象的數據、對象的類型和對象中存儲的屬性等信息。字節序列寫出到文件之後,相當於文件中持久保存了一個對象的信息。反之,該字節序列還可以從文件中讀取回來,重構對象,對它進行反序列化。對象的數據、對象的類型和對象中存儲的數據信息,都可以用來在內存中創建對象。

宜春的術語定義序列化:

序列化: 字節 ——> 對象

其實,我總結的就是上面的結論,如果不理解,直接參照專業術語的定義,理解之後就記住我的話就行了,記不住,請打死我(我踢m簡直就是個天才)

圖理解序列化:

啥?你不懂啥是字節?其實,我在一篇IO流的文章里就已經介紹了序列化,放心,絕對特別詳細~光看文章名字就知道了~

2、為何要序列化?

從上一節提到序列化的概念,知道概念之後,我們就必須要知道 為何要序列化了。

講為何要序列化原因之前,博主我舉個栗子:

就像你去街上買菜,一般操作都是用塑料袋給包裝起來,直到回家要做菜的時候就把菜給拿出來。而這一系列操作就像極了序列化和反序列化!

Java中對象的序列化指的是將對象轉換成以字節序列的形式來表示,這些字節序列包含了對象的數據和信息,一個序列化后的對象 可以被寫到數據庫或文件中,也可用於 網絡傳輸,一般當我們使用 緩存cache(內存空間不夠有可能會本地存儲到硬盤)或 遠程調用rpc(網絡傳輸)的時候,經常需要讓我們的實體類實現Serializable接口,目的就是為了讓其可序列化。

  • 在開發過程中要使用transient關鍵字修飾的栗子:

如果一個用戶有一些密碼等信息,為了安全起見,不希望在網絡操作中被傳輸,這些信息對應的變量就可以加上transient關鍵字。換句話說,這個字段的生命周期僅存於調用者的內存中而不會寫到磁盤裡持久化。

  • 在開發過程中不需要transient關鍵字修飾的栗子:

1、類中的字段值可以根據其它字段推導出來。
2、看具體業務需求,哪些字段不想被序列化;

不知道各位有木有想過為什麼要不被序列化呢?其實主要是為了節省存儲空間。優化程序!

PS:記得之前看HashMap源碼的時候,發現有個字段是用transient修飾的,我覺得還是有道理的,確實沒必要對這個modCount字段進行序列化,因為沒有意義,modCount主要用於判斷HashMap是否被修改(像put、remove操作的時候,modCount都會自增),對於這種變量,一開始可以為任何值,0當然也是可以(new出來、反序列化出來、或者克隆clone出來的時候都是為0的),沒必要持久化其值。

當然,序列化后的最終目的是為了反序列化,恢復成原先的Java對象,要不然序列化后幹嘛呢,就像買菜一樣,用塑料袋包裹最後還是為了方便安全到家再去掉塑料袋,所以序列化后的字節序列都是可以恢復成Java對象的,這個過程就是反序列化。

3、序列化與transient的使用

 1、需要做序列化的對象的類,必須實現序列化接口:Java.lang.Serializable 接口(一個標誌接口,沒有任何抽象方法),Java 中大多數類都實現了該接口,比如:String,Integer類等,不實現此接口的類將不會使任何狀態序列化或反序列化,會拋NotSerializableException異常 。

  2、底層會判斷,如果當前對象是 Serializable 的實例,才允許做序列化,Java對象 instanceof Serializable 來判斷。

  3、在 Java 中使用對象流ObjectOutputStream來完成序列化以及ObjectInputStream流反序列化   

  1. ==ObjectOutputStream:通過 writeObject()方法做序列化操作== 

  2. ==ObjectInputStream:通過 readObject() 方法做反序列化操作==

4、該類的所有屬性必須是可序列化的。如果有一個屬性不需要可序列化的,則該屬性必須註明是瞬態的,使用transient 關鍵字修飾。

由於字節嘛所以肯定要涉及流的操作,也就是對象流也叫序列化流ObjectOutputstream,下面進行多種情況分析序列化的操作代碼!

在這裏,我真的強烈建議看宜春博客的讀者朋友,請試着去敲,切記一眼帶過或者複製過去運行就完事了,特別是小白童鞋,相信我!你一定會有不一樣的收穫。千萬不要覺得浪費時間,有時候慢就是快,宜春親身體會!

3.1、沒有實現Serializable接口進行序列化情況

package TransientTest;
import java.io.*;

class UserInfo {  //================================注意這裏沒有實現Serializable接口
    private String name;
    private transient String password;

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) {

        UserInfo userInfo=new UserInfo("老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt"));
            output.writeObject(new UserInfo("老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果

3.2、實現Serializable接口序列化情況

當我們加上實現Serializable接口再運行會發現,項目中出現的userinfo.txt文件內容是這樣的:

其實這都不是重點,重點是序列化操作成功了!

3.3、普通序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private String password;//都是普通屬性==============================

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='123'}

3.4、transient序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private transient String password; //特別注意:屬性由transient關鍵字修飾===========

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='null'}

特別注意結果,添加transient修飾的屬性值為默認值null!如果被transient修飾的屬性為int類型,那它被序列化之後值一定是0,當然各位可以去試試,這能說明什麼呢?說明被標記為transient的屬性在對象被序列化的時候不會被保存(或者說變量不會持久化)

3.5、static序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private static String password; //特別注意:屬性由static關鍵字修飾==============

    public UserInfo(String name, String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='123'}

這個時候,你就會錯誤的認為static修飾的也被序列化了,其實不然,實際上這裏很容易被搞暈!明明取出null(默認值)就可以說明不會被序列化,這裏明明沒有變成默認值,為何還要說static不會被序列化呢?

實際上,反序列化后類中static型變量name的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。也就是說被static修飾的變量並沒有參与序列化!但是咱也不能口說無憑啊,是的,那我們就來看兩個程序對比一下就明白了!

第一個程序:這是一個沒有被static修飾的name屬性程序:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

    public  void setName(String name) {
        this.name = name;
    }

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程序員老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設置為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值 =================================注意這裏的代碼
            userInfo.setName("程序員老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取后psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

運行結果:

name=程序員老過, psw=456
name=程序員老過, psw=null

從程序運行結果中可以看出,在反序列化之前試着改變name的值為程序員老改,結果是沒有成功的!

第二個程序:這是一個被static修飾的name屬性程序:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private static final long serialVersionUID = 996890129747019948L;
    private static String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

    public  void setName(String name) {
        this.name = name;
    }

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程序員老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設置為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值
            userInfo.setName("程序員老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取后psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

運行結果:

name=程序員老過, psw=456
name=程序員老改, psw=null

從程序運行結果中可以看出,在反序列化之前試着改變name的值為程序員老改,結果是成功的!現在對比一下兩個程序是不是就很清晰了?

static關鍵字修飾的成員屬性優於非靜態成員屬性加載到內存中,同時靜態也優於對象進入到內存中,被static修飾的成員變量不能被序列化,序列化的都是對象,靜態變量不是對象狀態的一部分,因此它不參与序列化。所以將靜態變量聲明為transient變量是沒有用處的。因此,反序列化后類中static型變量name的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。

如果對static關鍵字還是不太清楚理解的童鞋可以參考這篇文章,應該算是不錯的:

3.6、final序列化情況

對於final關鍵字來講,final變量將直接通過值參与序列化,至於代碼程序我就不再貼出來了,大家可以試着用final修飾驗證一下!

主要注意的是final 和transient可以同時修飾同一個變量,結果也是一樣的,對transient沒有影響,這裏主要提一下,希望各位以後在開發中遇到這些情況不會滿頭霧水!

4、java類中serialVersionUID作用

既然提到了transient關鍵字就不得不提到序列化,既然提到了序列化,就不得不提到serialVersionUID了,它是啥呢?基本上有序列化就會存在這個serialVersionUID。

serialVersionUID適用於Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException,在開發中有時候可寫可不寫,建議最好還是寫上比較好。

5、transient關鍵字小結

1、變量被transient修飾,變量將不會被序列化
2、transient關鍵字只能修飾變量,而不能修飾方法和類。
3、被static關鍵字修飾的變量不參与序列化,一個靜態static變量不管是否被transient修飾,均不能被序列化。
4、final變量值參与序列化,final transient同時修飾變量,final不會影響transient,一樣不會參与序列化

第二點需要注意的是:本地變量是不能被transient關鍵字修飾的。變量如果是用戶自定義類變量,則該類需要實現Serializable接口

第三點需要注意的是:反序列化后類中static型變量的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。

結語:被transient關鍵字修飾導致不被序列化,其優點是可以節省存儲空間。優化程序!隨之而來的是會導致被transient修飾的字段會重新計算,初始化!

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

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

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

  

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

神奇的 SQL 之 MySQL 性能分析神器 → EXPLAIN,SQL 起飛的基石!

前言

  開心一刻

    某人養了一頭豬,煩了想放生,可是豬認識回家的路,放生幾次它都自己回來了。一日,這個人想了個狠辦法,開車帶着豬轉了好多路進山區放生,放生后又各種打轉,然後掏出電話給家裡人打了個電話,問道:“豬回去了嗎?”,家裡人:“早回來了,你在哪了,怎麼還沒回來?”,他大怒道:“讓它來接我,我特么迷路了!!!”

還不如我了

背景

  某一天,樓主打完上班卡,坐在工位逛園子的時候,右下角的 QQ 閃了起來,而且還是個美女頭像!我又驚又喜,腦中閃過我所認識的可能聯繫我的女性,得出個結論:她們這會不可能聯繫我呀,圖像也沒映象,到底是誰了?打開聊天窗口聊了起來

  她:您好,我是公司客服某某某,請問 xxx後台 是您負責的嗎?

  我:您好,是我負責的,有什麼問題嗎?

  她:我發現 xxx 頁面點查詢后,一直是 加載中… ,數據一直出不來,能幫忙看看嗎?

  我:是不是您的姿勢不對?

  她:我就 xxx,然後點查詢

  我:騷等下,我試試,確實有點慢,很長時間才能出來

  她:是的,太慢了,出不來,都急死我了,能快點嗎?

  我:肯定能、必須能!您覺得什麼速度讓您覺得最舒服?

  她:越快越好吧

  我:呃…,是嗎,我先看看是什麼問題,處理好了告訴您,保證讓您覺得舒服!

  她:好的,謝謝!

  公司沒有專門的搜索服務,都是直接從 MySQL 查詢,做簡單的數據處理后返回給頁面,慢的原因肯定就是 SQL 查詢了。找到對應的查詢 SQL ,就是兩個表的聯表查詢,連接鍵也有索引,WHERE 條件也能走索引,怎麼會慢了?然後我用 EXPLAIN 看了下這條 SQL 的執行計劃,找到了慢的原因,具體原因後面揭曉(誰讓你不是豬腳!)

EXPLAIN 是什麼

  它是 MySQL 的一個命令,用來查看 SQL 的執行計劃(SQL 如何執行),根據其輸出結果,我們能夠知道以下信息:表的讀取順序,數據讀取類型,哪些索引可以使用,哪些索引實際使用了,表之間的連接類型,每張表有多少行被優化器查詢等信息,根據這些信息,我們可以找出 SQL 慢的原因,並做針對性的優化

  MySQL 5.6 之前的版本,EXPLAIN 只能用於查看 SELECT 的執行計劃,而從 MySQL 5.6 開始,可以查看 SELECT 、 DELETE 、 INSERT 、 REPLACE 和 UPDATE 的執行計劃,這可不是我瞎掰,不信的可以去 MySQL 的官網查看:

  EXPLAIN 使用方式非常簡單,簡單的你都不敢相信,就是在我們常寫的 SELECT 、 DELETE 、 INSERT 、 REPLACE 和 UPDATE 語句之前加上 EXPLAIN 即可

EXPLAIN SELECT * FROM mysql.`user`;

EXPLAIN DELETE FROM t_user WHERE user_name = '123';

  莫看 EXPLAIN 短,但它胖呀

雖然有點嬰兒肥,但也掩不住我逼人的帥氣!

  雖然 EXPLAIN 使用起來非常簡單,但它的輸出結果中信息量非常大,雖然我胖,但我肚中有貨呀!

環境和數據準備

  MySQL 版本是 5.7.2 ,存儲引擎是 InnoDB 

-- 查看 MySQL 版本
SELECT VERSION();

-- MySQL 提供什麼存儲引擎
SHOW ENGINES;

-- 查看默認存儲引擎
SHOW VARIABLES LIKE '%storage_engine%';

  準備兩張表:用戶表 tbl_user 和用戶登錄記錄表 tbl_user_login_log ,並初始化部分部分數據

-- 表創建與數據初始化
DROP TABLE IF EXISTS tbl_user;
CREATE TABLE tbl_user (
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  user_name VARCHAR(50) NOT NULL COMMENT '用戶名',
  sex TINYINT(1) NOT NULL COMMENT '性別, 1:男,0:女',
  create_time datetime NOT NULL COMMENT '創建時間',
  update_time datetime NOT NULL COMMENT '更新時間',
    remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '備註',
  PRIMARY KEY (id)
) COMMENT='用戶表';

DROP TABLE IF EXISTS tbl_user_login_log;
CREATE TABLE tbl_user_login_log (
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  user_name VARCHAR(50) NOT NULL COMMENT '用戶名',
  ip VARCHAR(15) NOT NULL COMMENT '登錄IP',
  client TINYINT(1) NOT NULL COMMENT '登錄端, 1:android, 2:ios, 3:PC, 4:H5',
  create_time datetime NOT NULL COMMENT '創建時間',
  PRIMARY KEY (id)
) COMMENT='登錄日誌';
INSERT INTO tbl_user(user_name,sex,create_time,update_time,remark) VALUES
('何天香',1,NOW(), NOW(),'朗眉星目,一表人材'),
('薛沉香',0,NOW(), NOW(),'天星樓的總樓主薛搖紅的女兒,也是天星樓的少總樓主,體態豐盈,烏髮飄逸,指若春蔥,袖臂如玉,風姿卓然,高貴典雅,人稱“天星絕香”的武林第一大美女'),
('慕容蘭娟',0,NOW(), NOW(),'武林東南西北四大世家之北世家慕容長明的獨生女兒,生得玲瓏剔透,粉雕玉琢,脾氣卻是剛烈無比,又喜着火紅,所以人送綽號“火鳳凰”,是除天星樓薛沉香之外的武林第二大美女'),
('萇婷',0,NOW(), NOW(),'當今皇上最寵愛的侄女,北王府的郡主,腰肢纖細,遍體羅綺,眉若墨畫,唇點櫻紅;雖無沉香之雅重,蘭娟之熱烈,卻別現出一種空靈'),
('柳含姻',0,NOW(), NOW(),'武林四絕之一的添愁仙子董婉婉的徒弟,體態窈窕,姿容秀麗,真箇是秋水為神玉為骨,芙蓉如面柳如腰,眉若墨畫,唇若點櫻,不弱西子半分,更勝玉環一籌; 搖紅樓、聽雨軒,琵琶一曲值千金!'),
('李凝雪',0,NOW(), NOW(),'李相國的女兒,神采奕奕,英姿颯爽,愛憎分明'),
('周遺夢',0,NOW(), NOW(),'音神傳人,湘妃竹琴的擁有者,雲髻高盤,穿了一身黑色蟬翼紗衫,愈覺得冰肌玉骨,粉面櫻唇,格外嬌艷動人'),
('恭弘=叶 恭弘留痕',0,NOW(), NOW(),'聖域聖女,膚白如雪,白衣飄飄,宛如仙女一般,微笑中帶着說不出的柔和之美'),
('郭疏影',0,NOW(), NOW(),'揚灰右使的徒弟,秀髮細眉,玉肌豐滑,嬌潤脫俗'),
('鍾鈞天',0,NOW(), NOW(),'天界,玄天九部 - 鈞天部的部主,超凡脫俗,仙氣逼人'),
('王雁雲',0,NOW(), NOW(),'塵緣山莊二小姐,刁蠻任性'),
('許侍霜',0,NOW(), NOW(),'藥王穀穀主女兒,醫術高明'),
('馮黯凝',0,NOW(), NOW(),'桃花門門主,嬌艷如火,千嬌百媚');
INSERT INTO tbl_user_login_log(user_name, ip, client, create_time) VALUES
('薛沉香', '10.53.56.78',2, '2019-10-12 12:23:45'),
('萇婷', '10.53.56.78',2, '2019-10-12 22:23:45'),
('慕容蘭娟', '10.53.56.12',1, '2018-08-12 22:23:45'),
('何天香', '10.53.56.12',1, '2019-10-19 10:23:45'),
('柳含姻', '198.11.132.198',2, '2018-05-12 22:23:45'),
('馮黯凝', '198.11.132.198',2, '2018-11-11 22:23:45'),
('周遺夢', '198.11.132.198',2, '2019-06-18 22:23:45'),
('郭疏影', '220.181.38.148',3, '2019-10-21 09:45:56'),
('薛沉香', '220.181.38.148',3, '2019-10-26 22:23:45'),
('萇婷', '104.69.160.60',4, '2019-10-12 10:23:45'),
('王雁雲', '104.69.160.61',4, '2019-10-16 20:23:45'),
('李凝雪', '104.69.160.62',4, '2019-10-17 20:23:45'),
('許侍霜', '104.69.160.63',4, '2019-10-18 20:23:45'),
('恭弘=叶 恭弘留痕', '104.69.160.64',4, '2019-10-19 20:23:45'),
('王雁雲', '104.69.160.65',4, '2019-10-20 20:23:45'),
('恭弘=叶 恭弘留痕', '104.69.160.66',4, '2019-10-21 20:23:45');

SELECT * FROM tbl_user;
SELECT * FROM tbl_user_login_log;

View Code

EXPLAIN 輸出格式概覽

  樓主再不講重點,估計有些看官老爺找他的 2 米長的大砍刀去了

  這麼滴,我們先來看看 EXPLAIN 輸出結果的大概,是不是長得滿臉麻子,讓我們望而生畏 ?

  白白凈凈的,挺好,關鍵長啊! 解釋如下

EXPLAIN 輸出格式詳解

  EXPLAIN 的輸出字段雖然有點多,但常關注的就那麼幾個,但樓主秉着負責的態度,都給大家講一下,需要重點關注的字段,樓主也會標明滴

  EXPLAIN 支持的 SQL 語句有好幾種,但工作中用的最多的還是 SELECT ,所以樓主就偷個懶,以 SELECT 來講解 EXPLAIN,有興趣的老爺去試試其他的

  id

    輸出的是整數,用來標識整個 SQL 的執行順序。id 如果相同,從上往下依次執行id不同;id 值越大,執行優先級越高,越先被執行;如果行引用其他行的並集結果,則該值可以為NULL

    不重要,有所了解就好(其實非常簡單,看一遍基本就能記住了)

  select_type

    查詢的類型,說明如下

    簡單幫大家翻譯一下(有能力的去讀官網,畢竟那是原配,最具權威性)

    SIMPLE:簡單的 SELECT 查詢,沒有 UNION 或者子查詢,包括單表查詢或者多表 JOIN 查詢

    PRIMARY: 最外層的 select 查詢,常見於子查詢或 UNION 查詢 ,最外層的查詢被標識為 PRIMARY

    UNION:UNION 操作的第二個或之後的 SELECT,不依賴於外部查詢的結果集(外部查詢指的就是 PRIMARY 對應的 SELECT)

    DEPENDENT UNION:UNION 操作的第二個或之後的 SELECT,依賴於外部查詢的結果集

    UNION RESULT:UNION 的結果(如果是 UNION ALL 則無此結果)

    SUBQUERY:子查詢中的第一個 SELECT 查詢,不依賴於外部查詢的結果集

    DEPENDENT SUBQUERY:子查詢中的第一個select查詢,依賴於外部查詢的結果集

    DERIVED:派生表(臨時表),常見於 FROM 子句中有子查詢的情況

      注意:MySQL5.7 中對 Derived table 做了一個新特性,該特性允許將符合條件的 Derived table 中的子表與父查詢的表合併進行直接JOIN,從而簡化簡化了執行計劃,同時也提高了執行效率;默認情況下,MySQL5.7 中這個特性是開啟的,所以默認情況下,上面的 SQL 的執行計劃應該是這樣的

      可通過 SET SESSION optimizer_switch=‘derived_merge=on|off‘ 來開啟或關閉當前 SESSION 的該特性。貌似扯的有點遠了(樓主你是不是在隨性發揮?),更多詳情可以去查閱

    MATERIALIZED:被物化的子查詢,MySQL5.6 引入的一種新的 select_type,主要是優化 FROM 或 IN 子句中的子查詢,更多詳情請查看:

    UNCACHEABLE SUBQUERY:對於外層的主表,子查詢不可被緩存,每次都需要計算

    UNCACHEABLE UNION:類似於 UNCACHEABLE SUBQUERY,只是出現在 UNION 操作中

    SIMPLLE、PRIMARY、SUBQUERY、DERIVED 這 4 個在實際工作中碰到的會比較多,看得懂這 4 個就行了,至於其他的,碰到了再去查資料就好了(我也想全部記住,但用的少,太容易忘記了,我也很無賴呀)

  table

    显示了對應行正在訪問哪個表(有別名就显示別名),還會有 <union2,3> 、 <subquery2> 、 <derived2> (這裏的 2,3、2、2 指的是 id 列的值)類似的值,具體可以往上看,這裏就不演示了(再演示就太長了,你們都看不下去了,那我不是白忙乎了 ?)

  partitions

    查詢進行匹配的分區,對於非分區表,該值為NULL。大多數情況下用不到分區,所以這一列我們無需關注

  type

    關聯類型或者訪問類型,它指明了 MySQL 決定如何查找表中符合條件的行,這是我們判斷查詢是否高效的重要依據(type 之於 EXPLAIN,就好比三圍之於女人!),完整介紹請看:

    其值有多種,我們以性能好到性能差的順序一個一個來看     

    system

      該表只有一行(=系統表),是 const 類型的特例
    const

      確定只有一行匹配的時候,mysql 優化器會在查詢前讀取它並且只讀取一次,速度非常快。用於 primary key 或 unique 索引中有常亮值比較的情形

    eq_ref

      對於每個來自於前面的表的行,從該表最多只返回一條符合條件的記錄。當連接使用的索引是 PRIMARY KEY 或 UNIQUE NOT NULL 索引時使用,非常高效

    ref

      索引訪問,也稱索引查找,它返回所有匹配某個單個值的行。此類型通常出現在多表的 JOIN 查詢, 針對於非 UNIQUE 或非 PRIMARY KEY, 或者是使用了最左前綴規則索引的查詢,換句話說,如果 JOIN 不能基於關鍵字選擇單個行的話,則使用ref

    fulltext

      當使用全文索引時會用到,這種索引一般用不到,會用專門的搜索服務(solr、elasticsearch等)來替代
    ref_or_null

      類似ref,但是添加了可以專門搜索 NULL 的行

      這個是有前提條件的,前提為 weapon 列有索引,且 weapon 列存在  NULL 

    index_merge

      該訪問類型使用了索引合併優化方法

      這個同樣也是有條件的, id 列和 weapon 列都有單列索引。如果出現 index_merge,並且這類 SQL 後期使用較頻繁,可以考慮把單列索引換為組合索引,這樣效率更高

    unique_subquery

      類似於兩表連接中被驅動表的 eq_ref 訪問方式,unique_subquery 是針對在一些包含 IN 子查詢的查詢語句中,如果查詢優化器決定將 IN 子查詢轉換為 EXISTS 子查詢,而且子查詢可以使用到主鍵或者唯一索引進行等值匹配時,則會使用 unique_subquery

    index_subquery

      index_subquery 與 unique_subquery類似,只不過訪問子查詢中的表時使用的是普通的索引

    range

      使用索引來檢索給定範圍的行,當使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比較關鍵字列時,則會使用 rang

      前提是必須基於索引,也就是 id 上必須有索引

    index

      當我們可以使用索引覆蓋,但需要掃描全部的索引記錄時,則會使用 index;進行統計時非常常見

    ALL

      我們熟悉的全表掃描

  possible_keys

    展示在這個 SQL 中,可能用到的索引有哪些,但不一定在查詢時使用。若為空則表示沒有可以使用的索引,此時可以通過檢查 WHERE 語句看是否可以引用某些列或者新建索引來提高性能

  key

    展示這個 SQL 實際使用的索引,如果沒有選擇索引,則此列為null,要想強制 MySQL 使用或忽視 possible_keys 列中的索引,在查詢中使用 FORCE INDEX、USE INDEX 或者I GNORE INDEX

  key_len

    展示 MySQL 決定使用的鍵長度(字節數)。如果 key 是 NULL,則長度為 NULL。在不損失精確性的情況下,長度越短越好

  ref

    展示的是與索引列作等值匹配的東東是個啥,比如只是一個常數或者是某個列。它显示的列的名字(或const),此列多數時候為 Null

  rows

    展示的是 mysql 解析器認為執行此 SQL 時預計需要掃描的行數。此數值為一個預估值,不是具體值,通常比實際值小

  filtered

    展示的是返回結果的行數所佔需要讀到的行(rows 的值)的比例,當然是越小越好啦

  extra

    表示不在其他列但也很重要的額外信息。取值有很多,我們挑一些比較常見的過一下

    using index

      表示 SQL 使用了使用覆蓋索引,而不用回表去查詢數據,性能非常不錯

    using where

      表示存儲引擎搜到記錄後進行了後過濾(POST-FILTER),如果查詢未能使用索引,using where 的作用只是提醒我們 mysql 要用 where 條件過濾結果集

    using temporary

      表示 mysql 需要使用臨時表來存儲結果集,常見於排序和分組查詢

    using filesort

      表示 mysql 無法利用索引直接完成排序(排序的字段不是索引字段),此時會用到緩衝空間(內存或者磁盤)來進行排序;一般出現該值,則表示 SQL 要進行優化了,它對 CPU 的消耗是比較大的

    impossible where

      查詢語句的WHERE子句永遠為 FALSE 時將會提示該額外信息

 

 

     當然還有其他的,不常見,等碰到了大家再去查吧(現在凌晨 1 點,我實在是太困了!)

總結

  1、背景疑問

    還記得客服小姐姐的問題嗎,她嫌我們太慢,具體原因下篇再詳細介紹,這裏就提一下:連表查詢的 連接鍵 類型不一致,一個 INT 類型,一個 VARCHAR 類型,導致 type 是 ALL(這誰設計的呀,坑死人呀! 難道是我 ?)

  2、思維導圖

    本來是想自己畫個思維導圖的,可上網一搜,發現了一個人家畫好了的思維導圖,我就偷個懶借用下:,裏面描述的很詳細,同時也包括了各種示例,真香!

  3、肚中精華

    EXPLAIN 的輸出內容很多,我們沒必要全部掌握,重點我已經幫大家划好

    type,就像 RMB 一樣重要

    key,也像 RMB 一樣重要

    extra,還像 RMB 一樣重要

    說白了還是 RMB 最重要,不是,我的意思是 type、key、extra 都很重要,其他的用到了再去買吧

  4、示例代碼

    

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

程序員修神之路–kubernetes是微服務發展的必然產物



菜菜哥,我昨天又請假出去面試了


戰況如何呀?


多數面試題回答的還行,但是最後讓我介紹微服務和kubernetes的時候,掛了


話說微服務和kubernetes內容確實挺多的


那你給我大體介紹一下唄


可以呀,不過要請和coffee哦


◆◆
kubernetes介紹
◆◆

在很多項目的發展初期,都是小型或者大型的單體項目,部署在單台或者多台服務器上,以單個進程的方式來運行。這些項目隨着需求的遞增,發布周期逐漸增長,迭代速度明顯下降。傳統的發布方式是:開發人員將項目打包發給運維人員,運維人員進行部署、資源分配等操作。

隨着軟件行業架構方式的改變,這些大型的單體應用按照業務或者其他維度逐漸被分解為可獨立運行的組件,我們稱之為微服務。微服務彼此之間被獨立開發、部署、升級、擴容,真正實現了大型應用的解耦工作。關於微服務的介紹,大家可以去擼一下菜菜之前的文章:

https://mp.weixin.qq.com/s/b7Bd8giwWVNF1CtkaDaVpw

https://mp.weixin.qq.com/s/BixgyGFrlwZ7wpgDdrmU_g

軟件開發行業就是這樣奇葩,每一個問題被解決之後總是伴隨着另外的問題出現,就像程序員改bug,為什麼總有改不完的bug,真的很令人頭大!!!

微服務雖然解決了一些問題,但是隨着微服務數量的增多,配置、管理、擴容、高可用等要求的實現變的越來越困難,包括運維團隊如何更好的利用硬件資源並降低服務器成本,以及部署的自動化和故障處理等問題變得原來越棘手。

以上問題正是kubernetes要解決並且擅長的領域,它可以讓開發者自主部署應用,自主控制迭代的頻率,完全解放運維團隊。而運維團隊的工作重心從以往的服務器資源管理轉移到了kubernetes的資源管理。kubernetes最厲害之處是對硬件基礎設施進行了封裝和抽象,使得開發人員完全不用去了解硬件的基礎原理,不用去關注底層服務器。kubernetes內部把設置的服務器抽象為資源池,在部署應用的時候,它會自動給應用分配合適合理的服務器資源,並且能夠保證這些應用能正常的和其他應用進行通信。一個kubernetes集群的大體結構如下:

那kubernetes有哪些具體優勢呢?能說下不?


再加一杯coffee?


◆◆
kubernetes優勢
◆◆

微服務雖好,但是數量多了就會有量帶來的問題。隨着系統組件的不斷增長,這些組件的管理問題逐漸浮出水面。首先我們要明白kubernetes是一個軟件系統,它依賴於linux容器的特性來管理組件(kubernetes和容器並非一個概念,請不要混淆)。通過kubernetes部署應用程序時候,你的集群無論包含多少個節點,對於kubernetes來說不會有什麼差異,這完全得益於它對底層基礎設置的抽象,使得數個節點運行的時候表現的好像一個節點一樣。

自動擴容

在kubernetes系統中,它可以對每個應用進行實時的監控,並能根據策略來應對突發的流量做出反應。例如:在流量高峰期間,kubernetes可以根據各個節點的資源利用情況,進行自動的增加節點或者減少節點操作,這在以前的傳統應用部署方式中是不容易做到的。

簡化部署流程

以往的傳統應用發布的時候,需要開發人員把項目打包,並檢查項目的配置文件是否正確,然後發給運維人員,運維人員然後把線上的應用版本備份,然後停止服務進行更新。在kubernetes中,我們多數情況下只需要一條指令或者點擊一個按鈕,就可以把應用升級到最新版本,而且升級的過程中還可做做到不間斷服務。當然整個的流程還涉及到容器的操作,本次這裏不再做過多介紹。

但是這裡有一個意外情況,如果kubernetes集群中存在不同架構CPU的服務器,而你的應用程序是針對特定CPU架構的軟件,可能需要在kubernetes中指定節點去運行你的應用程

提高服務器資源的利用率

傳統應用部署的時候,多數情況下總會把資源留有一定的比例來作為資源的緩衝,來應對流量的峰值,很少有人把單個服務器資源利用率提高到90%以上,從服務器故障的概率來說,服務器資源使用率在90%要比50%高很多,而且服務器一旦出現故障,都是運維人員來解決問題和背鍋,所以傳統的物理機或者虛擬機部署應用的方式,硬件的資源利用率相比較來說是比較低的。

而kubernetes對集群的管理由於抽象了底層硬件設施,所以已經將應用程序和基礎設施分離開來。當你告訴kubernetes運行你 應用程序時,它會根據程序的資源需求和集群內每隔節點的可用資源情況選擇合適的節點來運行。而且通過容器的技術,可以讓應用程序在任何時間遷移到集群中的任何機器上。而對於服務器選擇的最優的組合,kubernetes比人工做的更好,它會根據集群中每台服務器的負載情況來把硬件利用率提高到最高。

自動修復

在傳統的應用架構中,如果一台服務器發生故障,那麼這台服務器上的應用將會全部down掉,多數情況下需要運維人員去處理,這也是為什麼運維人員需要7*24小時隨時待命的一個重要原因。相信你也曾看到過因為半夜故障運維人員罵娘的情景。在kubernetes中,它監視並管理着所有的節點和應用,在節點出現故障的時候,kubernetes可以自動將該節點上的應用遷移到其他健康節點,並將故障節點在資源池中排除。如果你的kubernetes集群基礎設施有足夠的備用資源來支撐系統的正常運行,運維人員完全可以拖延到正常的工作時間再處理故障,讓程序員和運維人員過一下965的工作節奏。

這點有點像Actor模型的設計理論,提倡的是任其崩潰原理。

一致的運行環境

無論你是開發還是運維人員,在傳統的部署方案中,總會有運行環境差異性的煩惱,這樣的差異性大到每個服務器的差異,小到開發環境、仿真環境、生產環境,而且每個環境的服務器都會隨着時間的推移而變化。我相信你一定遇到過開發環境程序運行正常,生產環境卻異常的情況。這種差異性不僅僅是因為生產環境由運維團隊管理,開發環境由開發者管理,更重要的這兩組人對系統的要求是不同的,運維團隊會對線上生產環境定時的打補丁,做安全監測等操作,而開發者可能根本就不會弔這些問題。除此之外,應用系統依賴的第三方庫可能在開發、仿真、生產環境中版本不同,這樣的問題反正我是遇到過。

而kubernetes採用的容器技術,在把應用打包的時候,運行環境也一起被打入包中,這就保證了相同版本的容器包(鏡像)在任何服務器上都有相同的運行環境

kubernetes原來有這麼優勢,那我得好好學學了


雖然kubernetes優勢很多,但是入門門檻比較高,而且在個別情況下反而不合適


kubernetes要求開發人員對容器技術和網絡知識有一定了解,所以是否採用kubernetes要根據團隊的綜合技能和項目斟酌使用,並不是所有項目採用kubernetes都有利

 

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

你必須知道的容器日誌 (2) 開源日誌管理方案 ELK/EFK

本篇已加入《》,可以點擊查看更多容器化技術相關係列文章。上一篇《》中介紹了Docker自帶的logs子命令以及其Logging driver,本篇將會介紹一個流行的開源日誌管理方案ELK。

一、關於ELK

1.1 ELK簡介

  ELK 是Elastic公司提供的一套完整的日誌收集以及展示的解決方案,是三個產品的首字母縮寫,分別是ElasticSearch、Logstash 和 Kibana。

  • Elasticsearch是實時全文搜索和分析引擎,提供搜集、分析、存儲數據三大功能
  • Logstash是一個用來搜集、分析、過濾日誌的工具
  • Kibana是一個基於Web的圖形界面,用於搜索、分析和可視化存儲在 Elasticsearch指標中的日誌數據   

1.2 ELK日誌處理流程

   上圖展示了在Docker環境下,一個典型的ELK方案下的日誌收集處理流程:

  • Logstash從各個Docker容器中提取日誌信息
  • Logstash將日誌轉發到ElasticSearch進行索引和保存
  • Kibana負責分析和可視化日誌信息

  由於Logstash在數據收集上並不出色,而且作為Agent,其性能並不達標。基於此,Elastic發布了beats系列輕量級採集組件。

  這裏我們要實踐的Beat組件是Filebeat,Filebeat是構建於beats之上的,應用於日誌收集場景的實現,用來替代 Logstash Forwarder 的下一代 Logstash 收集器,是為了更快速穩定輕量低耗地進行收集工作,它可以很方便地與 Logstash 還有直接與 Elasticsearch 進行對接。

  本次實驗直接使用Filebeat作為Agent,它會收集我們在第一篇《》中介紹的json-file的log文件中的記錄變動,並直接將日誌發給ElasticSearch進行索引和保存,其處理流程變為下圖,你也可以認為它可以稱作 EFK。

二、ELK套件的安裝

  本次實驗我們採用Docker方式部署一個最小規模的ELK運行環境,當然,實際環境中我們或許需要考慮高可用和負載均衡。

  首先拉取一下sebp/elk這個集成鏡像,這裏選擇的tag版本是640(最新版本已經是7XX了):

docker pull sebp/elk:640

  注:由於其包含了整個ELK方案,所以需要耐心等待一會。

  通過以下命令使用sebp/elk這個集成鏡像啟動運行ELK:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
    sebp/elk:640

  運行完成之後就可以先訪問一下 http://[Your-HostIP]:5601 看看Kibana的效果:  

  Kibana管理界面

Kibana Index Patterns界面

  當然,目前沒有任何可以显示的ES的索引和數據,再訪問一下http://[Your-HostIP]:9200 看看ElasticSearch的API接口是否可用:

ElasticSearch API

  Note:如果啟動過程中發現一些錯誤,導致ELK容器無法啟動,可以參考《》及《》一文。如果你的主機內存低於4G,建議增加配置設置ES內存使用大小,以免啟動不了。例如下面增加的配置,限制ES內存使用最大為1G:

docker run -it -d --name elk \
    -p 5601:5601 \
    -p 9200:9200 \
    -p 5044:5044 \
  -e ES_MIN_MEM=512m \ -e ES_MAX_MEM=1024m \ sebp/elk:640

三、Filebeat配置

3.1 安裝Filebeat

  這裏我們通過rpm的方式下載Filebeat,注意這裏下載和我們ELK對應的版本(ELK是6.4.0,這裏也是下載6.4.0,避免出現錯誤):

wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.4.0-x86_64.rpm
rpm -ivh filebeat-6.4.0-x86_64.rpm

3.2 配置Filebeat  

   這裏我們需要告訴Filebeat要監控哪些日誌文件 及 將日誌發送到哪裡去,因此我們需要修改一下Filebeat的配置:

cd /etc/filebeat
vim filebeat.yml

  要修改的內容為:

  (1)監控哪些日誌?

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /var/lib/docker/containers/*/*.log - /var/log/syslog

  這裏指定paths:/var/lib/docker/containers/*/*.log,另外需要注意的是將 enabled 設為 true。

  (2)將日誌發到哪裡?

#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
  # Array of hosts to connect to.
  hosts: ["192.168.16.190:9200"]

  # Optional protocol and basic auth credentials.
  #protocol: "https"
  #username: "elastic"
  #password: "changeme"

  這裏指定直接發送到ElasticSearch,配置一下ES的接口地址即可。

  Note:如果要發到Logstash,請使用後面這段配置,將其取消註釋進行相關配置即可:

#----------------------------- Logstash output --------------------------------
#output.logstash:
  # The Logstash hosts
  #hosts: ["localhost:5044"]

  # Optional SSL. By default is off.
  # List of root certificates for HTTPS server verifications
  #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]

  # Certificate for SSL client authentication
  #ssl.certificate: "/etc/pki/client/cert.pem"

  # Client Certificate Key
  #ssl.key: "/etc/pki/client/cert.key"

3.3 啟動Filebeat

  由於Filebeat在安裝時已經註冊為systemd的服務,所以只需要直接啟動即可:

systemctl start filebeat.service

  檢查Filebeat啟動狀態:

systemctl status filebeat.service

3.4 驗證Filebeat

  通過訪問ElasticSearch API可以發現以下變化:ES建立了以filebeat-開頭的索引,我們還能夠看到其來源及具體的message。

四、Kibana配置

  接下來我們就要告訴Kibana,要查詢和分析ElasticSearch中的哪些日誌,因此需要配置一個Index Pattern。從Filebeat中我們知道Index是filebeat-timestamp這種格式,因此這裏我們定義Index Pattern為 filebeat-*

  點擊Next Step,這裏我們選擇Time Filter field name為@timestamp:

  單擊Create index pattern按鈕,即可完成配置。

  這時我們單擊Kibana左側的Discover菜單,即可看到容器的日誌信息啦:

  仔細看看細節,我們關注一下message字段:

  可以看到,我們重點要關注的是message,因此我們也可以篩選一下只看這個字段的信息:

  此外,Kibana還提供了搜索關鍵詞的日誌功能,例如這裏我關注一下日誌中包含unhandled exception(未處理異常)的日誌信息:

  這裏只是樸素的展示了導入ELK的日誌信息,實際上ELK還有很多很豐富的玩法,例如分析聚合、炫酷Dashboard等等。筆者在這裏也是初步使用,就介紹到這裏啦。

五、Fluentd引入

5.1 關於Fluentd

  前面我們採用的是Filebeat收集Docker的日誌信息,基於Docker默認的json-file這個logging driver,這裏我們改用Fluentd這個開源項目來替換json-file收集容器的日誌。

  Fluentd是一個開源的數據收集器,專為處理數據流設計,使用JSON作為數據格式。它採用了插件式的架構,具有高可擴展性高可用性,同時還實現了高可靠的信息轉發。Fluentd也是雲原生基金會 (CNCF) 的成員項目之一,遵循Apache 2 License協議,其github地址為:。Fluentd與Logstash相比,比佔用內存更少、社區更活躍,兩者的對比可以參考這篇文章《》。

  因此,整個日誌收集與處理流程變為下圖,我們用 Filebeat 將 Fluentd 收集到的日誌轉發給 Elasticsearch。

   當然,我們也可以使用Fluentd的插件(fluent-plugin-elasticsearch)直接將日誌發送給 Elasticsearch,可以根據自己的需要替換掉Filebeat,從而形成Fluentd => ElasticSearch => Kibana 的架構,也稱作EFK。

5.2 運行Fluentd

  這裏我們通過容器來運行一個Fluentd採集器:

docker run -d -p 24224:24224 -p 24224:24224/udp -v /edc/fluentd/log:/fluentd/log fluent/fluentd

  默認Fluentd會使用24224端口,其日誌會收集在我們映射的路徑下。

  此外,我們還需要修改Filebeat的配置文件,將/edc/fluentd/log加入監控目錄下:

#=========================== Filebeat inputs =============================

filebeat.inputs:

# Each - is an input. Most options can be set at the input level, so
# you can use different inputs for various configurations.
# Below are the input specific configurations.

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /edc/fluentd/log/*.log

  添加監控配置之後,需要重新restart一下filebeat:

systemctl restart filebeat

5.3 運行測試容器

  為了驗證效果,這裏我們Run兩個容器,並分別制定其log-dirver為fluentd:

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-A" \
           busybox sh -c 'while true; do echo "This is a log message from container A"; sleep 10; done;'

docker run -d \
           --log-driver=fluentd \
           --log-opt fluentd-address=localhost:24224 \
           --log-opt tag="test-docker-B" \
           busybox sh -c 'while true; do echo "This is a log message from container B"; sleep 10; done;'

  這裏通過指定容器的log-driver,以及為每個容器設立了tag,方便我們後面驗證查看日誌。

5.4 驗證EFK效果

  這時再次進入Kibana中查看日誌信息,便可以通過剛剛設置的tag信息篩選到剛剛添加的容器的日誌信息了:

六、小結

  本文從ELK的基本組成入手,介紹了ELK的基本處理流程,以及從0開始搭建了一個ELK環境,演示了基於Filebeat收集容器日誌信息的案例。然後,通過引入Fluentd這個開源數據收集器,演示了如何基於EFK的日誌收集案例。當然,ELK/EFK有很多的知識點,筆者也還只是初步使用,希望未來能夠分享更多的實踐總結。

參考資料

CloudMan,《》

一杯甜酒,《》

於老三,《》

zpei0411,《》

曹林華,《》

 

作者:

出處:

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

.NET core3.0 使用Jwt保護api

摘要:

本文演示如何向有效用戶提供jwt,以及如何在webapi中使用該token通過JwtBearerMiddleware中間件對用戶進行身份認證。

認證和授權區別?

首先我們要弄清楚認證(Authentication)和授權(Authorization)的區別,以免混淆了。認證是確認的過程中你是誰,而授權圍繞是你被允許做什麼,即權限。顯然,在確認允許用戶做什麼之前,你需要知道他們是誰,因此,在需要授權時,還必須以某種方式對用戶進行身份驗證。 

什麼是JWT?

根據維基百科的定義,JSON WEB Token(JWT),是一種基於JSON的、用於在網絡上聲明某種主張的令牌(token)。JWT通常由三部分組成:頭信息(header),消息體(payload)和簽名(signature)。

頭信息指定了該JWT使用的簽名算法:

header = '{"alg":"HS256","typ":"JWT"}'

HS256表示使用了HMAC-SHA256來生成簽名。

消息體包含了JWT的意圖:

payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的時間

未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用”.”分隔),簽名則通過私有的key計算而成:

key = 'secretkey'  
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)

最後在未簽名的令牌尾部拼接上base64url編碼的簽名(同樣使用”.”分隔)就是JWT了:

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)

# token看起來像這樣: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

JWT常常被用作保護服務端的資源(resource),客戶端通常將JWT通過HTTP的Authorization header發送給服務端,服務端使用自己保存的key計算、驗證簽名以判斷該JWT是否可信:

Authorization: Bearer eyJhbGci*...<snip>...*yu5CSpyHI

準備工作

使用vs2019創建webapi項目,並且安裝nuget包

Microsoft.AspNetCore.Authentication.JwtBearer

Startup類
  • ConfigureServices 添加認證服務

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = "https://www.cnblogs.com/chengtian",
                    ValidIssuer = "https://www.cnblogs.com/chengtian",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"))
                };
            });
  • Configure 配置認證中間件

 app.UseAuthentication();//認證中間件

創建一個token

  • 添加一個登錄model命名為LoginInput

public class LoginInput
    {

        public string Username { get; set; }

        public string Password { get; set; }
    }
  • 添加一個認證控制器命名為AuthenticateController

[Route("api/[controller]")]
    public class AuthenticateController : Controller
    {
        [HttpPost]
        [Route("login")]
        public IActionResult Login([FromBody]LoginInput input)
        {
            //從數據庫驗證用戶名,密碼 
            //驗證通過 否則 返回Unauthorized

            //創建claim
            var authClaims = new[] {
                new Claim(JwtRegisteredClaimNames.Sub,input.Username),
                new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
            };
            IdentityModelEventSource.ShowPII = true;
            //簽名秘鑰 可以放到json文件中
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"));

            var token = new JwtSecurityToken(
                   issuer: "https://www.cnblogs.com/chengtian",
                   audience: "https://www.cnblogs.com/chengtian",
                   expires: DateTime.Now.AddHours(2),
                   claims: authClaims,
                   signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                   );

            //返回token和過期時間
            return Ok(new
            {
                token = new JwtSecurityTokenHandler().WriteToken(token),
                expiration = token.ValidTo
            });
        }
    }
添加api資源

利用默認的控制器WeatherForecastController

    • 添加個Authorize標籤

    • 路由調整為:[Route(“api/[controller]”)] 代碼如下

 [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase

到此所有的代碼都已經准好了,下面進行運行測試

運行項目

使用postman進行模擬

  • 輸入url:https://localhost:44364/api/weatherforecast

     

     發現返回時401未認證,下面獲取token

  • 通過用戶和密碼獲取token

    如果我們的憑證正確,將會返回一個token和過期日期,然後利用該令牌進行訪問

  • 利用token進行請求

    ok,最後發現請求狀態200!

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Java虛擬機詳解(十)——類加載過程

  在上一篇文章中,我們詳細的介紹了Java,那麼這些Class文件是如何被加載到內存,由虛擬機來直接使用的呢?這就是本篇博客將要介紹的——類加載過程。

1、類的生命周期

  類從被加載到虛擬機內存開始,到卸載出內存為止,其聲明周期流程如下:

  

  上圖中紅色的5個部分(加載、驗證、準備、初始化、卸載)順序是確定的,也就是說,類的加載過程必須按照這種順序按部就班的開始。這裏的“開始”不是按部就班的“進行”或者“完成”,因為這些階段通常是互相交叉混合的進行的,通常會在一個階段執行過程中調用另一個階段。

2、加載

  “加載”階段是“類加載”生命周期的第一個階段。在加載階段,虛擬機要完成下面三件事:

  ①、通過一個類的全限定名來獲取定義此類的二進制字節流。

  ②、將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

  ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。

  PS:類的全限定名可以理解為這個類存放的絕對路徑。方法區是JDK1.7以前定義的運行時數據區,而在JDK1.8以後改為元數據區(Metaspace),主要用於存放被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。詳情可以參考這邊該系列的第二篇文章——。

  另外,我們看第一點——通過類的權限定名來獲取定義此類的二進制流,這裏並沒有明確指明要從哪裡獲取以及怎樣獲取,也就是說並沒有明確規定一定要我們從一個 Class 文件中獲取。基於此,在Java的發展過程中,充滿創造力的開發人員在這個舞台上玩出了各種花樣:

  1、從 ZIP 包中讀取。這稱為後面的 JAR、EAR、WAR 格式的基礎。

  2、從網絡中獲取。比較典型的應用就是 Applet。

  3、運行時計算生成。這就是動態代理技術。

  4、由其它文件生成。比如 JSP 應用。

  5、從數據庫中讀取。

  加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,然後在Java堆中實例化一個 java.lang.Class 類的對象,這個對象將作為程序訪問方法區中這些類型數據的外部接口。

  注意,加載階段與連接階段的部分內容(如一部分字節碼文件的格式校驗)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了。

3、驗證

  驗證是連接階段的第一步,作用是為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  我們說Java語言本身是相對安全,因為編譯器的存在,純粹的Java代碼要訪問數組邊界外的數據、跳轉到不存在的代碼行之類的,是要被編譯器拒絕的。但是前面我們也說過,Class 文件不一定非要從Java源碼編譯過來,可以使用任何途徑,包括你很牛逼,直接用十六進制編輯器來編寫 Class 文件。

  所以,如果虛擬機不檢查輸入的字節流,將會載入有害的字節流而導致系統崩潰。但是虛擬機規範對於檢查哪些方面,何時檢查,怎麼檢查都沒有明確的規定,不同的虛擬機實現方式可能都會有所不同,但是大致都會完成下面四個方面的檢查。

①、文件格式驗證

  校驗字節流是否符合Class文件格式的規範,並且能夠被當前版本的虛擬機處理。

  一、是否以魔數 0xCAFEBABE 開頭。

  二、主、次版本號是否是當前虛擬機處理範圍之內。

  三、常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)

  四、指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。

  五、CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據。

  六、Class 文件中各個部分及文件本身是否有被刪除的或附加的其他信息。

  以上是一部分校驗內容,當然遠不止這些。經過這些校驗后,字節流才會進入內存的方法區中存儲,接下來後面的三個階段校驗都是基於方法區的存儲結構進行的。

②、元數據驗證

  第二個階段主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範要求。

  一、這個類是否有父類(除了java.lang.Object 類之外,所有的類都應當有父類)。

  二、這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。

  三、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有普通方法。

  四、類中的字段、方法是否與父類產生了矛盾(例如覆蓋了父類的final字段、或者出現不符合規則的重載)

③、字節碼驗證

  第三個階段字節碼驗證是整個驗證階段中最複雜的,主要是進行數據流和控制流分析。該階段將對類的方法進行分析,保證被校驗的方法在運行時不會做出危害虛擬機安全的行為。

  一、保證任意時刻操作數棧中的數據類型與指令代碼序列都能配合工作。例如不會出現在操作數棧中放置了一個 int 類型的數據,使用時卻按照 long 類型來加載到本地變量表中。

  二、保證跳轉指令不會跳轉到方法體以外的字節碼指令中。

  三、保證方法體中的類型轉換是有效的。比如把一個子類對象賦值給父類數據類型,這是安全的。但是把父類對象賦值給子類數據類型,甚至賦值給完全不相干的類型,這就是不合法的。

④、符號引用驗證

  符號引用驗證主要是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,通常需要校驗如下內容:

  一、符號引用中通過字符串描述的全限定名是否能夠找到相應的類。

  二、在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。

  三、符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可以被當前類訪問。

4、準備

  準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存是在方法區中進行分配。

  注意:

  一、上面說的是類變量,也就是被 static 修飾的變量,不包括實例變量。實例變量會在對象實例化時隨着對象一起分配在堆中。

  二、初始值,指的是一些數據類型的默認值。基本的數據類型初始值如下(引用類型的初始值為null):

  

 

   比如,定義 public static int value = 123 。那麼在準備階段過後,value 的值是 0 而不是 123,把 value 賦值為123 是在程序被編譯后,存放在類的構造器方法之中,是在初始化階段才會被執行。但是有一種特殊情況,通過final 修飾的屬性,比如 定義 public final static int value = 123,那麼在準備階段過後,value 就被賦值為123了。

5、解析

  解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程。

  符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。

  直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。

  解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用,分別對應於常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANTS_InterfaceMethodref_info四種類型常量。

6、初始化

   初始化階段是類加載階段的最後一步,前面過程中,除第一個加載階段可以通過用戶自定義類加載器參与之外,其餘過程都是完全由虛擬機主導和控制。而到了初始化階段,則開始真正執行類中定義的Java程序代碼(或者說是字節碼)。

  在前面介紹的準備階段中,類變量已經被賦值過初始值了,而初始化階段,則根據程序員的編碼去初始化變量和資源。

  換句話來說,初始化階段是執行類構造器<clinit>() 方法的過程。

  ①、<clinit>() 方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。

  比如如下代碼會報錯:

  

 

   但是你把第 14 行代碼放到 static 靜態代碼塊的上面就不會報錯了。或者不改變代碼順序,將第 11 行代碼移除,也不會報錯。

  ②、<clinit>() 方法與類的構造函數(或者說是實例構造器<init>()方法)不同,它不需要显示的調用父類構造器,虛擬機會保證在子類的<init>()方法執行之前,父類的<init>()方法已經執行完畢。因此虛擬機中第一個被執行的<init>()方法的類肯定是 java.lang.Object。

  ③、由於父類的<clinit>() 方法先執行,所以父類中定義的靜態語句塊要優先於子類的變量賦值操作。

  ④、<clinit>() 方法對於接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>() 方法。

  ⑤、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>() 方法。但接口與類不同的是,執行接口中的<clinit>() 方法不需要先執行父接口的<clinit>() 方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。

  ⑥、接口的實現類在初始化時也一樣不會執行接口的<clinit>() 方法。

  ⑦、虛擬機會保證一個類的<clinit>() 方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>() 方法,其他的線程都需要阻塞等待,直到活動線程執行<clinit>() 方法完畢。如果在一個類的<clinit>() 方法中有很耗時的操作,那麼可能造成多個進程的阻塞。

  比如對於如下代碼:

package com.yb.carton.controller;

/**
 * Create by YSOcean
 */
public class ClassLoadInitTest {


    static class Hello{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName() + "init");
                while(true){}
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h1 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"start");
            Hello h2 = new Hello();
            System.out.println(Thread.currentThread().getName()+"run over");
        }).start();
    }

}

View Code

  運行結果如下:

  

 

   線程1搶到了執行<clinit>() 方法,但是該方法是一個死循環,線程2將一直阻塞等待。

  知道了類的初始化過程,那麼類的初始化何時被觸發呢?JVM大概規定了如下幾種情況:

  ①、當虛擬機啟動時,初始化用戶指定的類。

  ②、當遇到用以新建目標類實例的 new 指令時,初始化 new 指定的目標類。

  ③、當遇到調用靜態方法的指令時,初始化該靜態方法所在的類。

  ④、當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類。

  ⑤、子類的初始化會觸發父類的初始化。

  ⑥、如果一個接口定義了 default 方法,那麼直接實現或間接實現該接口的類的初始化,會觸發該接口的初始化。

  ⑦、使用反射 API 對某個類進行反射調用時,會初始化這個類。

  ⑧、當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

 

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

羞,Spring Bean 初始化/銷毀竟然有這麼多姿勢

文章來源:

一、前言

日常開發過程有時需要在應用啟動之後加載某些資源,或者在應用關閉之前釋放資源。Spring 框架提供相關功能,圍繞 Spring Bean 生命周期,可以在 Bean 創建過程初始化資源,以及銷毀 Bean 過程釋放資源。Spring 提供多種不同的方式初始化/銷毀 Bean,如果同時使用這幾種方式,Spring 如何處理這幾者之間的順序?

有沒有覺得標題很熟悉,沒錯標題模仿二哥 「@沉默王二」 文章。

二、姿勢剖析

首先我們先來回顧一下 Spring 初始化/銷毀 Bean 幾種方式,分別為:

  • init-method/destroy-method
  • InitializingBean/DisposableBean
  • @PostConstruct/@PreDestroy
  • ContextStartedEvent/ContextClosedEvent

PS: 其實還有一種方式,就是繼承 Spring Lifecycle 接口。不過這種方式比較繁瑣,這裏就不再分析。

2.1、init-method/destroy-method

這種方式在配置文件文件指定初始化/銷毀方法。XML 配置如下

<bean id="demoService" class="com.dubbo.example.provider.DemoServiceImpl"  destroy-method="close"  init-method="initMethod"/>

或者也可以使用註解方式配置:

@Configurable
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public HelloService hello() {
        return new HelloService();
    }
}

還記得剛開始接觸學習 Spring 框架,使用就是這種方式。

2.2、InitializingBean/DisposableBean

這種方式需要繼承 Spring 接口 InitializingBean/DisposableBean,其中 InitializingBean 用於初始化動作,而 DisposableBean 用於銷毀之前清理動作。使用方式如下:

@Service
public class HelloService implements InitializingBean, DisposableBean {
    
    @Override
    public void destroy() throws Exception {
        System.out.println("hello destroy...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello init....");
    }
}

2.3、@PostConstruct/@PreDestroy

這種方式相對於上面兩種方式來說,使用方式最簡單,只需要在相應的方法上使用註解即可。使用方式如下:

@Service
public class HelloService {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }
}

這裏踩過一個坑,如果使用 JDK9 之後版本 ,@PostConstruct/@PreDestroy 需要使用 maven 單獨引入 javax.annotation-api,否者註解不會生效。

2.4、ContextStartedEvent/ContextClosedEvent

這種方式使用 Spring 事件機制,日常業務開發比較少見,常用與框架集成中。Spring 啟動之後將會發送 ContextStartedEvent 事件,而關閉之前將會發送 ContextClosedEvent 事件。我們需要繼承 Spring ApplicationListener 才能監聽以上兩種事件。

@Service
public class HelloListener implements ApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof ContextClosedEvent){
            System.out.println("hello ContextClosedEvent");
        }else if(event instanceof ContextStartedEvent){
            System.out.println("hello ContextStartedEvent");
        }

    }
}

也可以使用 @EventListener註解,使用方式如下:

public class HelloListenerV2 {
    
    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("hello ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }
}

PS:只有調用 ApplicationContext#start 才會發送 ContextStartedEvent。若不想這麼麻煩,可以監聽 ContextRefreshedEvent 事件代替。一旦 Spring 容器初始化完成,就會發送 ContextRefreshedEvent。

三、綜合使用

回顧完上面幾種方式,這裏我們綜合使用上面的四種方式,來看下 Spring 內部的處理順序。在看結果之前,各位讀者大人可以猜測下這幾種方式的執行順序。

public class HelloService implements InitializingBean, DisposableBean {


    @PostConstruct
    public void init() {
        System.out.println("hello @PostConstruct");
    }

    @PreDestroy
    public void PreDestroy() {
        System.out.println("hello @PreDestroy");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bye DisposableBean...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("hello InitializingBean....");
    }

    public void xmlinit(){
        System.out.println("hello xml-init...");
    }

    public void xmlDestory(){
        System.out.println("bye xmlDestory...");
    }

    @EventListener(value = {ContextClosedEvent.class, ContextStartedEvent.class})
    public void receiveEvents(ApplicationEvent event) {
        if (event instanceof ContextClosedEvent) {
            System.out.println("bye ContextClosedEvent");
        } else if (event instanceof ContextStartedEvent) {
            System.out.println("hello ContextStartedEvent");
        }
    }

}

xml 配置方式如下:

    <context:annotation-config />
    <context:component-scan base-package="com.dubbo.example.demo"/>
    
    <bean class="com.dubbo.example.demo.HelloService" init-method="xmlinit" destroy-method="xmlDestory"/>

應用啟動方法如下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
context.start();
context.close();

程序輸出結果如下所示:

最後採用圖示說明總結以上結果:

四、源碼解析

不知道各位讀者有沒有猜對這幾種方式的執行順序,下面我們就從源碼角度解析 Spring 內部處理的順序。

4.1、初始化過程

使用 ClassPathXmlApplicationContext 啟動 Spring 容器,將會調用 refresh 方法初始化容器。初始化過程將會創建 Bean 。最後當一切準備完畢,將會發送 ContextRefreshedEvent。當容器初始化完畢,調用 context.start() 就發送 ContextStartedEvent 事件。

refresh 方法源碼如下:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
            //... 忽略無關代碼

            // 初始化所有非延遲初始化的 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 發送 ContextRefreshedEvent
            finishRefresh();

            //... 忽略無關代碼
    }
}

一路跟蹤 finishBeanFactoryInitialization 源碼,直到 AbstractAutowireCapableBeanFactory#initializeBean,源碼如下:

protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 調用 BeanPostProcessor#postProcessBeforeInitialization 方法
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
        // 初始化 Bean
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
        throw new BeanCreationException(
                (mbd != null ? mbd.getResourceDescription() : null),
                beanName, "Invocation of init method failed", ex);
    }
}

BeanPostProcessor 將會起着攔截器的作用,一旦 Bean 符合條件,將會執行一些處理。這裏帶有 @PostConstruct 註解的 Bean 都將會被 CommonAnnotationBeanPostProcessor 類攔截,內部將會觸發 @PostConstruct 標註的方法。

接着執行 invokeInitMethods ,方法如下:

protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
        throws Throwable {

    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        // 省略無關代碼
        // 如果是 Bean 繼承 InitializingBean,將會執行  afterPropertiesSet 方法
        ((InitializingBean) bean).afterPropertiesSet();
    }

    if (mbd != null) {
        String initMethodName = mbd.getInitMethodName();
        if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
                !mbd.isExternallyManagedInitMethod(initMethodName)) {
            // 執行 XML 定義 init-method
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}

如果 Bean 繼承 InitializingBean 接口,將會執行 afterPropertiesSet 方法,另外如果在 XML 中指定了 init-method ,也將會觸發。

上面源碼其實都是圍繞着 Bean 創建的過程,當所有 Bean 創建完成之後,調用 context#start 將會發送 ContextStartedEvent 。這裏源碼比較簡單,如下:

public void start() {
    getLifecycleProcessor().start();
    publishEvent(new ContextStartedEvent(this));
}

4.2、銷毀過程

調用 ClassPathXmlApplicationContext#close 方法將會關閉容器,具體邏輯將會在 doClose 方法執行。

doClose 這個方法首先發送 ContextClosedEvent,然再后開始銷毀 Bean。

靈魂拷問:如果我們顛倒上面兩者順序,結果會一樣嗎?

doClose 源碼如下:

protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 省略無關代碼

        try {
            // Publish shutdown event.
            publishEvent(new ContextClosedEvent(this));
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }


        // 銷毀 Bean
        destroyBeans();

        // 省略無關代碼
    }
}

destroyBeans 最終將會執行 DisposableBeanAdapter#destroy,@PreDestroy、DisposableBean、destroy-method 三者定義的方法都將會在內部被執行。

首先執行 DestructionAwareBeanPostProcessor#postProcessBeforeDestruction,這裏方法類似與上面 BeanPostProcessor。

@PreDestroy 註解將會被 CommonAnnotationBeanPostProcessor 攔截,這裏類同時也繼承了 DestructionAwareBeanPostProcessor。

最後如果 Bean 為 DisposableBean 的子類,將會執行 destroy 方法,如果在 xml 定義了 destroy-method 方法,該方法也會被執行。

public void destroy() {
    if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
        for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
            processor.postProcessBeforeDestruction(this.bean, this.beanName);
        }
    }

    if (this.invokeDisposableBean) {
        // 省略無關代碼
        // 如果 Bean 繼承 DisposableBean,執行 destroy 方法
        ((DisposableBean) bean).destroy();
        
    }

    if (this.destroyMethod != null) {
        // 執行 xml 指定的  destroy-method 方法
        invokeCustomDestroyMethod(this.destroyMethod);
    }
    else if (this.destroyMethodName != null) {
        Method methodToCall = determineDestroyMethod();
        if (methodToCall != null) {
            invokeCustomDestroyMethod(methodToCall);
        }
    }
}

五、總結

init-method/destroy-method 這種方式需要使用 XML 配置文件或單獨註解配置類,相對來說比較繁瑣。而InitializingBean/DisposableBean 這種方式需要單獨繼承 Spring 的接口實現相關方法。@PostConstruct/@PreDestroy 這種註解方式使用方式簡單,代碼清晰,比較推薦使用這種方式。

另外 ContextStartedEvent/ContextClosedEvent 這種方式比較適合在一些集成框架使用,比如 Dubbo 2.6.X 優雅停機就是用改機制。

六、Spring 歷史文章推薦

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Java描述設計模式(23):訪問者模式

本文源碼: ||

一、生活場景

1、場景描述

電競是遊戲比賽達到“競技”層面的體育項目。利用电子設備作為運動器械進行的、人與人之間的智力對抗運動。通過電競,可以提高人的反應能力、協調能力、團隊精神等。但是不同人群的對電競的持有的觀念不一樣,有的人認為電競就是沉迷網絡,持反對態度,而有的人就比較贊同。下面基於訪問者模式來描述該場景。

2、場景圖解

3、代碼實現

public class C01_InScene {
    public static void main(String[] args) {
        DataSet dataSet = new DataSet() ;
        dataSet.addCrowd(new Youth());
        dataSet.addCrowd(new MiddleAge());
        CrowdView crowdView = new Against() ;
        dataSet.display(crowdView);
        crowdView = new Approve() ;
        dataSet.display(crowdView);
    }
}
/**
 * 雙分派,不同人群管理
 */
abstract class Crowd {
    abstract void accept(CrowdView action);
}
class Youth extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getYouthView(this);
    }
}
class MiddleAge extends Crowd {
    @Override
    public void accept(CrowdView view) {
        view.getMiddleAgeView (this);
    }
}
/**
 * 不同人群觀念的管理
 */
abstract class CrowdView {
    // 青年人觀念
    abstract void getYouthView (Youth youth);
    // 中年人觀念
    abstract void getMiddleAgeView (MiddleAge middleAge);
}
class Approve extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人贊同電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人贊同電競");
    }
}
class Against extends CrowdView {
    @Override
    public void getYouthView(Youth youth) {
        System.out.println("青年人反對電競");
    }
    @Override
    public void getMiddleAgeView(MiddleAge middleAge) {
        System.out.println("中年人反對電競");
    }
}
/**
 * 提供一個數據集合
 */
class DataSet {
    private List<Crowd> crowdList = new ArrayList<>();
    public void addCrowd (Crowd crowd) {
        crowdList.add(crowd);
    }
    public void display(CrowdView crowdView) {
        for(Crowd crowd : crowdList) {
            crowd.accept(crowdView);
        }
    }
}

二、訪問者模式

1、基礎概念

訪問者模式是對象的行為模式,把作用於數據結構的各元素的操作封裝,操作之間沒有關聯。可以在不改變數據結構的前提下定義作用於這些元素的不同的操作。主要將數據結構與數據操作分離,解決數據結構和操作耦合問題核心原理:被訪問的類裏面加對外提供接待訪問者的接口。

2、模式圖解

3、核心角色

  • 抽象訪問者角色

聲明多個方法操作,具體訪問者角色需要實現的接口。

  • 具體訪問者角色

實現抽象訪問者所聲明的接口,就是各個訪問操作。

  • 抽象節點角色

聲明接受操作,接受訪問者對象作為參數。

  • 具體節點角色

實現抽象節點所規定的具體操作。

  • 結構對象角色

能枚舉結構中的所有元素,可以提供一個高層的接口,用來允許訪問者對象訪問每一個元素。

4、源碼實現

public class C02_Visitor {
    public static void main(String[] args) {
        ObjectStructure obs = new ObjectStructure();
        obs.add(new NodeA());
        obs.add(new NodeB());
        Visitor visitor = new VisitorA();
        obs.doAccept(visitor);
    }
}
/**
 * 抽象訪問者角色
 */
interface Visitor {
    /**
     * NodeA的訪問操作
     */
    void visit(NodeA node);
    /**
     * NodeB的訪問操作
     */
    void visit(NodeB node);
}
/**
 * 具體訪問者角色
 */
class VisitorA implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
class VisitorB implements Visitor {
    @Override
    public void visit(NodeA node) {
        node.operationA() ;
    }
    @Override
    public void visit(NodeB node) {
        node.operationB() ;
    }
}
/**
 * 抽象節點角色
 */
abstract class Node {
    /**
     * 接收訪問者
     */
    abstract void accept(Visitor visitor);
}
/**
 * 具體節點角色
 */
class NodeA extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationA(){
        System.out.println("NodeA.operationA");
    }
}
class NodeB extends Node{
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    public void operationB(){
        System.out.println("NodeB.operationB");
    }
}
/**
 * 結構對象角色類
 */
class ObjectStructure {
    private List<Node> nodes = new ArrayList<>();
    public void detach(Node node) {
        nodes.remove(node);
    }
    public void add(Node node){
        nodes.add(node);
    }
    public void doAccept(Visitor visitor){
        for(Node node : nodes) {
            node.accept(visitor);
        }
    }
}

三、Spring框架應用

1、Bean結構的訪問

BeanDefinitionVisitor類,遍歷bean的各個屬性;接口 BeanDefinition,定義Bean的各樣信息,比如屬性值、構造方法、參數等等。這裏封裝操作bean結構的相關方法,但卻沒有改變bean的結構。

2、核心代碼塊

public class BeanDefinitionVisitor {
    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        this.visitParentName(beanDefinition);
        this.visitBeanClassName(beanDefinition);
        this.visitFactoryBeanName(beanDefinition);
        this.visitFactoryMethodName(beanDefinition);
        this.visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            this.visitPropertyValues(beanDefinition.getPropertyValues());
        }
        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            this.visitGenericArgumentValues(cas.getGenericArgumentValues());
        }
    }
}

四、模式總結

1、優點描述

(1) 訪問者模式符合單一職責原則、使程序具有良好的擴展性、靈活性;

(2) 訪問者模式適用與攔截器與過濾器等常見功能,數據結構相對穩定的場景;

2、缺點描述

(1) 訪問者關注其他類的內部細節,依賴性強,違反迪米特法則,這樣導致具體元素更新麻煩;

(2) 訪問者依賴具體元素,不是抽象元素,面向細節編程,違背依賴倒轉原則;

五、源代碼地址

GitHub·地址
https://github.com/cicadasmile/model-arithmetic-parent
GitEE·地址
https://gitee.com/cicadasmile/model-arithmetic-parent

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

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

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

十年風雨,一個普通程序員的成長之路(九)一眼望到頭,一眼望不到頭

還有十幾天就是我的32歲生日,然後,33了,要過年了。

古人三十而立,我卻在這狹窄的圈子里兜兜轉轉。

多年前的喊的一句創業口號,現在還是口號。

焦慮、迷茫。

這两天一場網易的暴力裁員事件,犹如一盆涼水當頭澆下。

讓我又陷入了一年前的時刻。

渾身提不起勁。什麼都不想做。

不知前路在哪裡?

 

回過頭來看,對於當事人來說曲折圓轉的半生,之於他人,不過又是一個復讀機的普通人生而已。

上學、畢業、工作、買房、結婚、生子、還貸。

沒有家庭是形單影只,凄凄涼涼。

有了家庭卻只能蠅營狗苟,負重而行。

一眼望到頭的路罷了。

 

我們都只是這庸碌的銅爐里,亂糟糟破爛爛的一塊廢銅爛鐵而已。

所以財富神話才那麼多捧臭腳的雇從。

因為世間大多的煩惱,便是沒錢的煩惱。

所以我們信努力改變命運、知識改變命令、堅持改變命運。

其實是金錢改變命運。

 

滿是一些交錢的APP,讓你堅持下去,給你鼓勵。永遠溫馨對你。

你會改變命運的,你會財富自由的。

我們如飢似渴,似乎覺得比身邊的人多get了一門技能,升職加薪也是指日可待了。

如果你沒有升職加薪、財富自由、改變命運,那不過是你還不夠努力罷了。

不過是,交的錢還不夠多罷了。

一眼望不到頭。

 

什麼時候是個頭?

寫博客、開公眾號、寫小說。尋找出路。

我們永遠相信自己是天命之子。

堂吉訶德騎着馬,夾着騎士長槍,無知無畏地沖向了風車。

大風車吱喲喲地轉,這裏的風景呀真好看……哦,畫風跑偏了。

大風車吱喲喲地轉,不為堂吉訶德所動,不為騎士長槍所動。似能流轉萬世。

 

天地不仁,以萬物為芻狗。

你又憑什麼跳出世間這個熔爐呢?

只能在時間的鐵鎚下越來越彎曲自己的身子。

在夕陽里傴僂着身子,苟延殘喘。

一眼望到頭,又一眼望不到頭。

 

在這條短暫卻又無盡的路上,那麼多的大V與培訓機構告訴你:

劉強東曾跟你一樣賣過盜版盤跟電腦。

馬雲還沒你強,曾被肯德基拒絕臨時工。

比爾蓋茨中途就退了學,你跟惠普也就差個車庫而已了。

遺憾的是,給你上課的老師,可能正兒八經的資金來源還沒有你多。

你以為打開了得到,便真能得道。

你以為買了極客時間,便真成了極客。

你以為加了大V,便算是有了人脈。

你以為入了知識星球,便真學到了知識。

遺憾的是,大多時候,它們與書架上落灰的書籍沒什麼兩樣。

 

家庭的壓力也讓你學習的時間慢慢變少。

有了一點獨處的時間,你卻又想打兩把遊戲,松一松這命運壓迫的喉嚨,大口地喘息兩聲。

在遊戲中,孩子的哭鬧、老婆的絮絮叨叨,都已變成遙遠的過去。

可是玩了一會,你卻又充滿了負罪感。空虛與寂寞隨之而來。

因為普通人改變命運的機會太少了。

所以只能讀書改變命運。

 

在兩千年的中華文明史中,知識改變命運的箴言已印刻在了基因里。

犹如稻草。

給溺水的人,最後一點光芒與希望。

因為你這一生,我這一生。一眼便已能看到頭。

所以佛度來生。

所以道修逍遙。

若有仙人撫我頂,怎可結髮受長生?

我願化為北冥之鯤,潛於九淵,扶搖九天,逍遙星河之外。

只是一聲“爸爸,我要尿尿”。夢,便醒了。

 

你心裏嘆了一口氣,便把兒子從你跟你老婆身邊抱下了床。

穿衣、洗漱,照了照鏡子,才刮的鬍子又長了出來。

算了,反正是非單身的程序員,也沒什麼可講究的。

關上門,復讀機的一天又開始了。

可是我還是渾身提不起勁,總感覺失去了什麼。

於是,寫下此文。

一眼望到頭,一眼望不到頭。

 

——————————————————–
歡迎關注我的公眾號:姚毛毛的博客

這裡有我的編程生涯感悟與總結,有Java、Linux、Oracle、mysql的相關技術,有工作中進行的架構設計實踐和讀書理論,有JVM、Linux、數據庫的性能調優,有……

有技術,有情懷,有溫度

歡迎關注我:姚毛毛& 妖生

 

 

 

 

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享

※收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選