1. 程式人生 > >利用Slf4j的MDC跟蹤方法呼叫鏈及一個通用的ThreadLocal工具類

利用Slf4j的MDC跟蹤方法呼叫鏈及一個通用的ThreadLocal工具類

一個工程中可能提供很多的資源(或者說提供給外部很多的URL訪問),而工程一般是分層處理,Controller-->Service-->DAO(HTTP請求其他的資源)的處理順序。有時候,我們需要根據日誌列印去看一下某使用者的這次請求到底是發生了什麼錯誤。我們知道系統不可能只有一個人在訪問,假如很多人在訪問的話,日誌列印的是很亂的,想要找到自己需要的資訊非常難,被淹沒在巨量的日誌中了。此時,我們想如果,能通過一個字串就能將一次請求呼叫了哪些方法按照順序搜尋出來就好了。而Slf4j的MDC ( Mapped Diagnostic Contexts )機制就是專門為了此需求而生的。其使用方式非常簡單,配置檔案的pattern中中新增一個{key},在請求方法入口設定一個key=某字串,logger日誌就能輸出此字串。logger的所有日誌方法不需要做任何改動。如下所示。

<pattern>%contextName: %X{METHOD-INVOKE-KEY} : %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'(Caller(.+?)(?=\())|\r|\n|\s*|\t', ''} - %msg%n</pattern>
/**
 * @author xiongshiyan at 2018/7/5
 */
public class LoggerMDCTest {
    private static final Logger logger = LoggerFactory.getLogger(LoggerMDCTest.class);
    @Test
    public void testMDC(){
        for (int i=0 ; i<10 ; i++) {
            new Thread(this::fun).start();
        }
        ThreadUtil.sleeps(1);
    }
    private void fun(){
        String key = "METHOD-INVOKE-KEY";
        //方法攔截器入口處設定,logback日誌配置需要設定 %X{threadUUID}
        MDC.put(key , CommonUtil.getUUID());
            //使用的時候,所有呼叫鏈上的列印都會加上threadUUID
            fun1();
            //子執行緒不會被追蹤
            new Thread(()->logger.error("ccccccccccccccccc")).start();
        //攔截器finally中需要清除
        MDC.remove(key);
        //MDC.clear();
    }

    private void fun1() {
        logger.info("fun1");
        fun2();
    }

    private void fun2() {
        logger.info("fun2");
        fun3();
    }

    private void fun3() {
        logger.error("fun3");
    }
}

其列印如下

test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.938 [Thread-0] INFO  (LoggerMDCTest.java:35) - fun1
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.938 [Thread-5] INFO  (LoggerMDCTest.java:35) - fun1
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] INFO  (LoggerMDCTest.java:35) - fun1
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] INFO  (LoggerMDCTest.java:35) - fun1
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] INFO  (LoggerMDCTest.java:40) - fun2
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] INFO  (LoggerMDCTest.java:40) - fun2
test: bce26452e73e44bb85f9363dae9f204a : 2018-07-20 17:18:09.938 [Thread-3] ERROR (LoggerMDCTest.java:45) - fun3
test: 1604b3b8cb2c42bbb8d83a509a6f0a5c : 2018-07-20 17:18:09.938 [Thread-4] ERROR (LoggerMDCTest.java:45) - fun3
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.938 [Thread-8] INFO  (LoggerMDCTest.java:35) - fun1
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.938 [Thread-7] INFO  (LoggerMDCTest.java:35) - fun1
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.938 [Thread-6] INFO  (LoggerMDCTest.java:35) - fun1
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.938 [Thread-1] INFO  (LoggerMDCTest.java:35) - fun1
test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.938 [Thread-0] INFO  (LoggerMDCTest.java:40) - fun2
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.954 [Thread-6] INFO  (LoggerMDCTest.java:40) - fun2
test: 03e0a8e0f81c46a9ae4db21745dc69c4 : 2018-07-20 17:18:09.954 [Thread-0] ERROR (LoggerMDCTest.java:45) - fun3
test: 4e929113d21148ba8a649852ffb70ba9 : 2018-07-20 17:18:09.954 [Thread-6] ERROR (LoggerMDCTest.java:45) - fun3
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.938 [Thread-5] INFO  (LoggerMDCTest.java:40) - fun2
test: a3a577094bc4420f8a84b1c6d577ecd6 : 2018-07-20 17:18:09.954 [Thread-5] ERROR (LoggerMDCTest.java:45) - fun3
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.938 [Thread-9] INFO  (LoggerMDCTest.java:35) - fun1
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.938 [Thread-2] INFO  (LoggerMDCTest.java:35) - fun1
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.954 [Thread-9] INFO  (LoggerMDCTest.java:40) - fun2
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.954 [Thread-2] INFO  (LoggerMDCTest.java:40) - fun2
test: 3dca7a817c5542bbb05dde9771011de1 : 2018-07-20 17:18:09.954 [Thread-9] ERROR (LoggerMDCTest.java:45) - fun3
test: 474823856d4e42b5a0314a10607044e0 : 2018-07-20 17:18:09.954 [Thread-2] ERROR (LoggerMDCTest.java:45) - fun3
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.954 [Thread-8] INFO  (LoggerMDCTest.java:40) - fun2
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.954 [Thread-1] INFO  (LoggerMDCTest.java:40) - fun2
test: 1fc9d0c2a4d84d6583f6cead3518079f : 2018-07-20 17:18:09.954 [Thread-8] ERROR (LoggerMDCTest.java:45) - fun3
test: 80d2020be98d487f8b3b3b69b366c30e : 2018-07-20 17:18:09.954 [Thread-1] ERROR (LoggerMDCTest.java:45) - fun3
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.954 [Thread-7] INFO  (LoggerMDCTest.java:40) - fun2
test: 7d5f4ee94a3146909030afdbca89d1d4 : 2018-07-20 17:18:09.954 [Thread-7] ERROR (LoggerMDCTest.java:45) - fun3
test:  : 2018-07-20 17:18:09.954 [Thread-10] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.954 [Thread-11] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.954 [Thread-12] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.954 [Thread-14] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-15] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-16] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-18] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-19] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-13] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc
test:  : 2018-07-20 17:18:09.969 [Thread-17] ERROR (LoggerMDCTest.java:28) - ccccccccccccccccc

由上可知:同一個執行緒的fun1、fun2、fun3列印了相同的一個隨機字串。

MDC的基本原理是:通過一個ThreadLocal儲存設定的key值,在列印的時候從ThreadLocal中獲取到打印出來。由此可知,如果一個請求中的某些方法呼叫是在另外的執行緒中執行,那MDC是獲取不到該值的。vertx-web的handler就是如此。

在我們的工程中,一般都是在所有的請求之前新增一個類似攔截器的元件,在該元件中設定一個隨機字串,然後開始真正的呼叫(這些呼叫中就能打印出此隨機字串),然後finally方法中清除該字串,防止記憶體洩露。一個例子:

try {
            //所有handler之前設定一個tag
            methodInvokeKeySetting(vertxRequest.getHeader(METHOD_INVOKE_KEY));
            
            //方法呼叫鏈開始
            done.handle(null);
        } catch (Exception e) {
            throw e;
        } finally {
            //finally刪除該key
            MDC.remove(METHOD_INVOKE_KEY);
        }

/**
     * 日誌串聯header設定,方法呼叫鏈都加上此tag,並透明傳遞到http
     */
    private void methodInvokeKeySetting(String methodInvokeKey) {
        logger.info("METHOD_INVOKE_KEY header : " + methodInvokeKey);
        String string = methodInvokeKey;
        if(null == string || "".equals(string)){
            string = CommonUtil.randomString(8);
        }
        logger.info("將要設定的隨機串為 : " + string);
        MDC.put(METHOD_INVOKE_KEY , string);
        StringMapInfoHolderUtil.put(METHOD_INVOKE_KEY , string);
    }

Spring環境中新增一個攔截器,並放入配置中。

@Component
public class LogbackInterceptor extends HandlerInterceptorAdapter{
    private static final Logger logger = LoggerFactory.getLogger(LogbackInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String header = request.getHeader(Constant.METHOD_INVOKE_KEY);
        methodInvokeKeySetting(header);
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        clear();
        super.afterCompletion(request, response, handler, ex);
    }

    /**
     * 日誌串聯header設定,方法呼叫鏈都加上此tag,並透明傳遞到http
     */
    private void methodInvokeKeySetting(String methodInvokeKey) {
        logger.info("METHOD_INVOKE_KEY header : " + methodInvokeKey);
        String string = methodInvokeKey;
        if(null == string || "".equals(string)){
            string = CommonUtil.randomString(8);
        }
        logger.info("將要設定的隨機串為 : " + string);
        //logback中可以日誌就能打印出來
        MDC.put(Constant.METHOD_INVOKE_KEY , string);
        //其他可以透明地使用這個設定
        StringMapInfoHolderUtil.put(Constant.METHOD_INVOKE_KEY , string);
    }

    private static void clear(){
        //finally刪除該key
        MDC.remove(Constant.METHOD_INVOKE_KEY);
        // 保守的做法,所有的方法呼叫完事之後,應該清除所有的threadlocal資料
        StringMapInfoHolderUtil.clear();
    }
}

工程與工程之間如何串聯呢?通過header透明傳遞該字串。

如上所示:

StringMapInfoHolderUtil.put(METHOD_INVOKE_KEY , string);方法也是利用ThreadLocal儲存一個值,然後我們在http請求的時候從threadlocal中獲取該值設定到header,在被請求的工程中獲取該header即可。如此兩個工程間就可以透明傳遞一個串聯的字串了。

//在所有的handler執行之前先設定header
    before(RoutingContext routingContext): handlers(routingContext){
        try {
            //所有handler之前設定一個tag
            String header = routingContext.request().getHeader(METHOD_INVOKE_KEY);
            logger.info("METHOD_INVOKE_KEY header : " + header);
            String string = header;
            if(null == string || "".equals(string)){
                string = CommonUtil.randomString(8);
            }
            logger.info("將要設定的隨機串為 : " + string);
            MDC.put(METHOD_INVOKE_KEY , string);
        } catch (Exception e){
            logger.error(e.getMessage() , e);
        }
    }

    //finally 刪除
    after(RoutingContext routingContext): handlers(routingContext){
        try {
            MDC.remove(METHOD_INVOKE_KEY);
        } catch (Exception e){
            logger.error(e.getMessage() , e);
        }
    }

以上也演示了一種解決vertx的兩個handler執行在不同的執行緒上的一種方案,通過aspectj增強handler,這樣兩個handler就能獲取到同一個header。

public class Test1Handler implements AbstractHandler{
    private static final Logger logger = LoggerFactory.getLogger(Test1Handler.class);
    @Override
    public void handle(RoutingContext routingContext) {
        logger.info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        System.out.println("Test1Handler : " + Thread.currentThread().getName() + ":" +routingContext.request().getHeader("METHOD-INVOKE-KEY"));
        routingContext.next();
    }
}
/**
 * @author xiongshiyan at 2018/7/20 , contact me with email [email protected] or phone 15208384257
 */
public class Test2Handler implements AbstractHandler{
    private static final Logger logger = LoggerFactory.getLogger(Test2Handler.class);
    @Override
    public void handle(RoutingContext routingContext) {
        logger.info("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
        System.out.println("Test2Handler : " + Thread.currentThread().getName() + ":" +routingContext.request().getHeader("METHOD-INVOKE-KEY"));

        ccccc();

        routingContext.response().end("結束了");
    }

    private void ccccc() {
        logger.info("ccccccccccccccccccccccccccccccccccccccccccc");
        ddddd();
    }

    private void ddddd() {
        logger.info("dddddddddddddddddddddddddddddddddddddddddddd");
    }
}

列印如下:

local:  2018-07-20 17:38:48.845 [vert.x-worker-thread-0] INFO  (PreHandlerAspectJ.aj:22) - METHOD_INVOKE_KEY header : header_from_other
local:  2018-07-20 17:38:48.847 [vert.x-worker-thread-0] INFO  (PreHandlerAspectJ.aj:27) - 將要設定的隨機串為 : header_from_other
local: header_from_other 2018-07-20 17:38:48.848 [vert.x-worker-thread-0] INFO  (Test1Handler.java:15) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Test1Handler : vert.x-worker-thread-0:header_from_other
local:  2018-07-20 17:38:48.863 [vert.x-worker-thread-1] INFO  (PreHandlerAspectJ.aj:22) - METHOD_INVOKE_KEY header : header_from_other
local:  2018-07-20 17:38:48.867 [vert.x-worker-thread-1] INFO  (PreHandlerAspectJ.aj:27) - 將要設定的隨機串為 : header_from_other
local: header_from_other 2018-07-20 17:38:48.868 [vert.x-worker-thread-1] INFO  (Test2Handler.java:15) - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Test2Handler : vert.x-worker-thread-1:header_from_other
local: header_from_other 2018-07-20 17:38:48.869 [vert.x-worker-thread-1] INFO  (Test2Handler.java:24) - ccccccccccccccccccccccccccccccccccccccccccc
local: header_from_other 2018-07-20 17:38:48.869 [vert.x-worker-thread-1] INFO  (Test2Handler.java:29) - dddddddddddddddddddddddddddddddddddddddddddd

兩個handler執行在不同的執行緒,但是獲取到同一個header。

需要注意的是,MDC只在logback和log4j中得到了比較好的支援。

附:通用的一個threadlocal工具類

/**
 * 在同一個執行緒儲存資訊的工具類
 * @author xiongshiyan at 2017/12/27
 */
public class InfoHolder<T> {

    protected ThreadLocal<T> holder = new ThreadLocal<>();

    /**
     * 往當前執行緒新增
     * @param t t
     */
    public void set(T t){
        holder.set(t);
    }


    /**
     * 獲取當前執行緒的
     */
    public T get(){
        return holder.get();
    }

    /**
     * 清空
     */
    public void clear(){
        holder.remove();
    }
}

map型別的子類

/**
 * Map型別值的工具類
 * @author xiongshiyan at 2017/12/27
 */
public class MapInfoHolder<K,V> extends InfoHolder<Map<K,V>> {

    /**
     * 往當前執行緒儲存一對值
     * @param key key
     * @param value value
     */
    public void add(K key ,V value){
        Map<K, V> map = holder.get();
        if(null == map){
            map = new HashMap<>();
        }
        map.put(key,value);
        holder.set(map);
    }

    /**
     * 根據key獲取當前執行緒的值
     * @param key key
     */
    public V get(K key){
        Map<K, V> map = holder.get();
        if (null == map){
            return null;
        }
        return map.get(key);
    }

    public void remove(K key){
        Map<K, V> map = holder.get();
        if (null == map){
            return ;
        }
        map.remove(key);
    }
}

設計了兩個工具類:一個是可以設定任意型別值的通用InfoHolder,一個是可以設定key-value值的。

/**
 * 任意型別值的工具類
 * @author xiongshiyan at 2017/12/27
 */
public class NormalInfoHolderUtil {

    private static final InfoHolder INFO_HOLDER = new InfoHolder();
    @SuppressWarnings("unchecked")
    public static <T> void set(T object){
        INFO_HOLDER.set(object);
    }
    @SuppressWarnings("unchecked")
    public static <T>  T get(){
        return (T) INFO_HOLDER.get();
    }
    public static void clear(){
        INFO_HOLDER.clear();
    }
}
/**
 * Map<String,String></>型別值的工具類
 * @author xiongshiyan at 2017/12/27
 */
public class StringMapInfoHolderUtil {

    private static final MapInfoHolder<String , String> MAP_INFO_HOLDER = new MapInfoHolder<>();
    public static void put(String key , String value){
        MAP_INFO_HOLDER.add(key , value);
    }
    public static String get(String key){
        return MAP_INFO_HOLDER.get(key);
    }
    public static void remove(String key){
        MAP_INFO_HOLDER.remove(key);
    }
    public static void clear(){
        MAP_INFO_HOLDER.clear();
    }
}

以上兩個工具類基本就能滿足常用的threadlocal相關需求了。

相關推薦

利用Slf4j的MDC跟蹤方法呼叫一個通用ThreadLocal工具

一個工程中可能提供很多的資源(或者說提供給外部很多的URL訪問),而工程一般是分層處理,Controller-->Service-->DAO(HTTP請求其他的資源)的處理順序。有時候,我們需要根據日誌列印去看一下某使用者的這次請求到底是發生了什麼錯誤。我們知道系

UIViewController 中各方法呼叫順序功能詳解

UIViewController 中有很多關於載入和解除安裝的方法,如:loadView, viewDidLoad, viewWillAppear, viewDidAppear, viewWillLayoutSubviews,viewDidLayoutSubvi

PHP封裝一個通用的CURL方法(設定、獲取請求頭響應頭並處理)

通用的一個CURL類方法,設定請求頭、獲取響應頭等! 包括將格式處理成陣列格式,方便直接輸出 /** * 傳送https post請求,也支援http請求,包括header請求 * @param string $url 請求域名 * @

利用Android反射與泛型機制寫一個通用的Adapter

注意點:Android的反射機制有一個問題,就是Class.getDeclaredFields()返回的變數陣列與我們定義的類的變數順序是不一致的.Android是經過了字母順序排序的.所以我們需要將變數名傳入adapter //定義一個帶泛型的抽象類作為基類

一個通用分頁

ref num hello 實現 col rom 還需要 一個 能夠 1、功能   這個通用分頁類實現的功能是輸入頁數(第幾頁)和每頁的數目,就能獲得相應的數據。 2、實現原理   分頁的實現通常分為兩種,一種是先把數據全查詢出來再分頁,一種是需要多少查詢多少,這裏使用第二

使用計算BigDecimal寫一個精確計算工具

在日常開放當中需要我們計算數字,利率。通常Java的做法是使用Math相關的API。但是,這樣做是不夠精確的,由於float和double不能進行計算,如果強行進行計算會使得計算不準確。造成難以挽回的損失。為了彌補這一個缺點Java提供了BigDecimal這個類來解決。在使用這個類的時候需要將do

properties 配置檔案自定義 JDBCUtils 工具

一、properties 配置檔案 相關介紹:   開發中獲得連線的4個引數(驅動、URL、使用者名稱、密碼)通常都存在配置檔案中,方便後期維護,程式如果需要更換資料庫,只需要修改配置檔案即可。 通常情況下,我們習慣使用properties檔案,此檔案我們將做如下要求:   1、檔案位置:任意,建議s

使用POI匯出Word(含表格)的實現方式操作Word的工具

轉載請註明出處:https://www.cnblogs.com/sun-flower1314/p/10128796.html  本篇是關於利用Apache 的POI匯出Word的實現步驟。採用XWPFDocument匯出Word,結構和樣式完全由程式碼控制,操作起來還是非常的不太方便,只能夠建立簡

共享一個SharedPreferences儲存工具

package ***.utils; import android.content.Context; import android.content.SharedPreferences; /** * date : 2017/8/7 * author : bianyaoy

簡單封裝了一個OKHttp的工具 非同步get請求和post請求

package com.example.okhttp.OkHttp; import android.os.Handler; import android.os.Looper; import android.util.Log; import com.example.okhttp.Con

JSON基礎Java的JSON工具

一.JSON基礎 定義:JSON(JavaScript Object Notation, JS 物件簡譜) 是一種輕量級的資料交換格式。它基於 ECMAScript (歐洲計算機協會制定的js規範)的一個子集,採用完全獨立於程式語言的文字格式來儲存和表示資料。

JAVA獲得一個唯一性UUID工具

專案中有時候我們沒有設定主鍵時,  那就需要一個唯一性的uuid來唯一性識別.程式碼如下package util; import java.util.UUID; public class UUIDG

一個Map的工具

[code]package com.huanglq.util;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;/** * 這是一個用1.5

寫的一個簡單的工具,可以做物件型別的判斷和迭代出一個物件所有屬性的值

import java.lang.reflect.Field; /** * @author songzheng */ public class TypeUtil { /** * 得到某個物件型別物件 */ public static Cl

java List Map 轉換成一個List T 工具

List<Object[]> 轉換成一個List<T> 工具類,程式碼如下: ListMapToBeanUtils.java檔案: package com.map.utils; import java.lang.reflect.Field; i

一個圖片處理工具

/** * 圖片處理工具類 */ public class BitMapUtils { /** * 對指定路徑圖片壓縮改變其檔案大小 * @param file * @param bitmap */ public

一個Java Jenkins工具,支援建立,構建,帶引數構建,刪除JenkinsJob,停止Jenkins Job任務等

Jenkins是一個很強大的持續整合的工具,除了在Jenkins的頁面上我們可以去構建我們的job,我們也可以通過java程式碼來通過呼叫jenkins的api來做一些事情,使得我們的java web專案更加便捷,下面是我的一個工具類。 package com.vip.w

一個優雅的threadLocal工具

import java.util.*; public final class ThreadLocalUtil { private static final ThreadLocal<Map<String, Object>> th

php 利用debug backtrace方法跟蹤程式碼呼叫

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!