Viterbi(維特比)算法在CRF(條件隨機場)中是如何起作用的?

之前我們介紹過BERT+CRF來進行命名實體識別,並對其中的BERT和CRF的概念和作用做了相關的介紹,然對於CRF中的最優的標籤序列的計算原理,我們只提到了維特比算法,並沒有做進一步的解釋,本文將對維特比算法做一個通俗的講解,以便大家更好的理解CRF為什麼能夠得到最優的標籤序列。

通過閱讀本文你將能回答如下問題:

  • 什麼是維特比算法?
  • 為什麼說維特比算法是一種動態規劃算法?
  • 維特比算法具體怎麼實現?

首先,讓我們簡單回顧一下BERT和CRF在命名實體識別中各自的作用:
命名實體識別中,BERT負責學習輸入句子中每個字和符號到對應的實體標籤的規律,而CRF負責學習相鄰實體標籤之間的轉移規則。詳情可以參考這篇文章。該文章中我們對CRF做了簡單易懂的介紹,其中提到CRF的損失函數計算要用到最優路徑,因為CRF的損失函數是求最優路徑的概率占所有路徑概率和的比例,而我們的目標是最大化這個比例。那麼這裏就涉及到計算最優路徑的問題。這裏的路徑在命名實體識別的例子中,就是最終輸出的與句子中的字或符號一 一對應的標籤序列。不同標籤序列的順序組成了不同的路徑。而CRF就是要找出最正確的那條標籤序列路徑,也就是說這條標籤路徑的概率將是所有路徑中最大的,那麼我們可以窮舉出所有可能的標籤路徑,計算出每條路徑的概率和,然後比較出最大的那條,但是這樣做的代價太大了,所以crf選擇了一種稱為維特比的算法來求解此類問題。

維特比算法(英語:Viterbi algorithm)是一種動態規劃算法。它用於尋找最有可能產生觀測事件序列的維特比路徑。

看看下面這個命名實體識別的例子:

上圖共有5層(觀測序列的長度),每層3個節點(狀態的個數),我們的目標就是找到從第一層到第五層的最優路徑。
首先,我們分別計算紅、黃、藍三個節點的輸入連線的概率,以紅色節點舉例,我們先假設紅色節點在最優路徑上,那麼輸入到該節點的三條連線中,概率最大的那條一定在最優路徑上,同理,我們再分別假設黃色和藍色節點在最優路徑上,我們也能各找到一條概率最大的連線,這樣就得到了下面的圖:

然後,我們接着剛才的思路繼續找後面一層的三條最優的連線:

假設找到的最優連線如下:

然後,接着在後面的層應用這個方法:

此時,看上面最後一張圖,我們有了3條候選最優路徑,分別是棕色、綠色和紫色,用標籤來表達如下:

那麼哪條才是最優路徑呢?
就是看哪條路徑的概率和最大,那條路徑就是最優路徑。
但是在實際實現的時候,一般會在計算各層的最優候選連線的時候,就記錄下前繼連線的概率和,並記錄下對應的狀態節點索引(這裏將已經計算出的結果記錄下來供後續使用的方式,就是維特比算法被稱為動態規劃算法的原因),這樣到最後一層的時候,最後一層各候選連線中概率最大的,就是在最優路徑上的那條連線了,然後從這條連線回溯,找出完整的路徑就是最優路徑了。

一直在說概率最大的路徑,那麼這個概率具體指什麼呢?
還記得上一篇文章介紹條件隨機場(CRF)的時候提到,條件隨機場其實是給定了觀測序列的馬爾可夫隨機場,在一階馬爾可夫模型中,定義了以下三個概念:

  • 狀態集合Q,對應到上面的例子就是:
    {B-P, I-P, O}
  • 初始狀態概率向量Π,對應到上面的例子就是:
    {B-P:0.3, I-P:0.2, O:0.5}
    這裏的概率數值是隨便假設的,僅為了方便舉例說明。
  • 狀態轉移概率矩陣A:

CRF中給定了觀測序列做為先驗條件,對應到上面的例子就是:

其中的概率數值同樣是隨便假設的,為了方便舉例。

下圖中紅色節點的概率(可以看成是一個虛擬的開始節點到該節點的連線的概率)的計算方式如下:
初始狀態為B-P的概率Π(B-P) * 該節點的觀測概率P(小|B-P)

下圖中紅色節點的三條連線概率的計算方式如下:
上一層對應節點的概率 * 上層對應節點到該節點的轉移概率 * 該節點的觀測概率P(明|B-P)

其它層之間的節點連線的概率同理計算可得,然後通過上面介紹的維特比算法過程就可以計算出最優路徑了。

ok,本篇就這麼多內容啦~,感謝閱讀O(∩_∩)O。

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

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

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

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

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

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

Java 數據持久化系列之JDBC

前段時間小冰在工作中遇到了一系列關於數據持久化的問題,在排查問題時發現自己對 Java 後端的數據持久化框架的原理都不太了解,只有不斷試錯,因此走了很多彎路。於是下定決心,集中精力學習了持久化相關框架的原理和實現,總結出這個系列。

上圖是我根據相關源碼和網上資料總結的有關 Java 數據持久化的架構圖(只代表本人想法,如有問題,歡迎留言指出)。最下層就是今天要講的 JDBC,上一層是數據庫連接池層,包括 HikariCP 和 Druid等;再上一層是分庫分表中間件,比如說 ShardingJDBC;再向上是對象關係映射層,也就是 ORM,包括 Mybatis 和 JPA;最上邊是 Spring 的事務管理。

本系列的文章會依次講解圖中各個開源框架的基礎使用,然後描述其原理和代碼實現,最後會着重分析它們之間是如何相互集成和配合的。

廢話不多說,我們先來看 JDBC。

JDBC 定義

JDBC是Java Database Connectivity的簡稱,它定義了一套訪問數據庫的規範和接口。但它自身不參与數據庫訪問的實現。因此對於目前存在的數據庫(譬如Mysql、Oracle)來說,要麼數據庫製造商本身提供這些規範與接口的實現,要麼社區提供這些實現。

如上圖所示,Java 程序只依賴於 JDBC API,通過 DriverManager 來獲取驅動,並且針對不同的數據庫可以使用不同的驅動。這是典型的橋接的設計模式,把抽象 Abstraction 與行為實現Implementation 分離開來,從而可以保持各部分的獨立性以及應對他們的功能擴展。

JDBC 基礎代碼示例

單純使用 JDBC 的代碼邏輯十分簡單,我們就以最為常用的MySQL 為例,展示一下使用 JDBC 來建立數據庫連接、執行查詢語句和遍歷結果的過程。

public static void connectionTest(){

    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;

    try {
        // 1. 加載並註冊 MySQL 的驅動
        Class.forName("com.mysql.cj.jdbc.Driver").newInstance();

        // 2. 根據特定的數據庫連接URL,返回與此URL的所匹配的數據庫驅動對象
        Driver driver = DriverManager.getDriver(URL);
        // 3. 傳入參數,比如說用戶名和密碼
        Properties props = new Properties();
        props.put("user", USER_NAME);
        props.put("password", PASSWORD);

        // 4. 使用數據庫驅動創建數據庫連接 Connection
        connection = driver.connect(URL, props);

        // 5. 從數據庫連接 connection 中獲得 Statement 對象
        statement = connection.createStatement();
        // 6. 執行 sql 語句,返回結果
        resultSet = statement.executeQuery("select * from activity");
        // 7. 處理結果,取出數據
        while(resultSet.next())
        {
            System.out.println(resultSet.getString(2));
        }

        .....
    }finally{
        // 8.關閉鏈接,釋放資源  按照JDBC的規範,使用完成后管理鏈接,
        // 釋放資源,釋放順序應該是: ResultSet ->Statement ->Connection
        resultSet.close();
        statement.close();
        connection.close();
    }
}

代碼中有詳細的註釋描述每一步的過程,相信大家也都對這段代碼十分熟悉。

唯一要提醒的是使用完之後的資源釋放順序。按照 JDBC 規範,應該依次釋放 ResultSet,Statement 和 Connection。當然這隻是規範,很多開源框架都沒有嚴格的執行,但是 HikariCP卻嚴格准守了,它可以帶來很多優勢,這些會在之後的文章中講解。

上圖是 JDBC 中核心的 5 個類或者接口的關係,它們分別是 DriverManager、Driver、Connection、Statement 和 ResultSet。

DriverManager 負責管理數據庫驅動程序,根據 URL 獲取與之匹配的 Driver 具體實現。Driver 則負責處理與具體數據庫的通信細節,根據 URL 創建數據庫連接 Connection。

Connection 表示與數據庫的一個連接會話,可以和數據庫進行數據交互。Statement 是需要執行的 SQL 語句或者存儲過程語句對應的實體,可以執行對應的 SQL 語句。ResultSet 則是 Statement 執行后獲得的結果集對象,可以使用迭代器從中遍曆數據。

不同數據庫的驅動都會實現各自的 Driver、Connection、Statement 和 ResultSet。而更為重要的是,眾多數據庫連接池和分庫分表框架也都是實現了自己的 Connection、Statement 和 ResultSet,比如說 HikariCP、Druid 和 ShardingJDBC。我們接下來會經常看到它們的身影。

接下來,我們依次看一下這幾個類及其涉及的操作的原理和源碼實現。

載入 Driver 實現

可以直接使用 Class#forName的方式來載入驅動實現,或者在 JDBC 4.0 后則基於 SPI 機制來導入驅動實現,通過在 META-INF/services/java.sql.Driver 文件中指定實現類的方式來導入驅動實現,下面我們就來看一下兩種方式的實現原理。

Class#forName 作用是要求 JVM 查找並加載指定的類,如果在類中有靜態初始化器的話,JVM 會執行該類的靜態代碼段。加載具體 Driver 實現時,就會執行 Driver 中的靜態代碼段,將該 Driver 實現註冊到 DriverManager 中。我們來看一下 MySQL 對應 Driver 的具體代碼。它就是直接調用了 DriverManager的 registerDriver 方法將自己註冊到其維護的驅動列表中。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        // 直接調用 DriverManager的 registerDriver 將自己註冊到其中
        DriverManager.registerDriver(new Driver());
    }
}

SPI 機制使用 ServiceLoader 類來提供服務發現機制,動態地為某個接口尋找服務實現。當服務的提供者提供了服務接口的一種實現之後,必須根據 SPI 約定在 META-INF/services 目錄下創建一個以服務接口命名的文件,在該文件中寫入實現該服務接口的具體實現類。當服務調用 ServiceLoader 的 load 方法的時候,ServiceLoader 能夠通過約定的目錄找到指定的文件,並裝載實例化,完成服務的發現。

DriverManager 中的 loadInitialDrivers 方法會使用 ServiceLoader 的 load 方法加載目前項目路徑下的所有 Driver 實現。

public class DriverManager {
    // 程序中已經註冊的Driver具體實現信息列表。registerDriver類就是將Driver加入到這個列表
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    // 使用ServiceLoader 加載具體的jdbc driver實現
    static {
        loadInitialDrivers();
    }
    private static void loadInitialDrivers() {
        // 省略了異常處理
        // 獲得系統屬性 jdbc.drivers 配置的值
        String drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 通過 ServiceLoader 獲取到Driver的具體實現類,然後加載這些類,會調用其靜態代碼塊
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
                return null;
            }
        });

        String[] driversList = drivers.split(":");
        // for 循環加載系統屬性中的Driver類。
        for (String aDriver : driversList) {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        }
    }
}

比如說,項目引用了 MySQL 的 jar包 mysql-connector-java,在這個 jar 包的 META-INF/services 文件夾下有一個叫 java.sql.Driver 的文件,文件的內容為 com.mysql.cj.jdbc.Driver。而 ServiceLoader 的 load 方法找到這個文件夾下的文件,讀取文件的內容,然後加載出文件內容所指定的 Driver 實現。而正如之前所分析的,這個 Driver 類被加載時,會調用 DriverManager 的 registerDriver 方法,從而完成了驅動的加載。

Connection、Statement 和 ResultSet

當程序加載完具體驅動實現后,接下來就是建立與數據庫的連接,執行 SQL 語句並且處理返回結果了,其過程如下圖所示。

建立 Connection

創建 Connection 連接對象,可以使用 Driver 的 connect 方法,也可以使用 DriverManager 提供的 getConnection 方法,此方法通過 url 自動匹配對應的驅動 Driver 實例,然後還是調用對應的 connect 方法返回 Connection 對象實例。

建立 Connection 會涉及到與數據庫進行網絡請求等大量費時的操作,為了提升性能,往往都會引入數據庫連接池,也就是說復用 Connection,免去每次都創建 Connection 所消耗的時間和系統資源。

Connection 默認情況下,對於創建的 Statement 執行的 SQL 語句都是自動提交事務的,即在 Statement 語句執行完后,自動執行 commit 操作,將事務提交,結果影響到物理數據庫。為了滿足更好地事務控制需求,我們也可以手動地控制事務,手動地在Statement 的 SQL 語句執行後進行 commit 或者rollback。

connection = driver.connect(URL, props);
// 將自動提交關閉
connection.setAutoCommit(false);

statement = connection.createStatement();
statement.execute("INSERT INTO activity (activity_id, activity_name, product_id, start_time, end_time, total, status, sec_speed, buy_limit, buy_rate) VALUES (1, '香蕉大甩賣', 1, 530871061, 530872061, 20, 0, 1, 1, 0.20);");
// 執行後手動 commit
statement.getConnection().commit();

Statement

Statement 的功能在於根據傳入的 SQL 語句,將傳入 SQL 經過整理組合成數據庫能夠識別的執行語句(對於靜態的 SQL 語句,不需要整理組合;而對於預編譯SQL 語句和批量語句,則需要整理),然後傳遞 SQL 請求,之後會得到返回的結果。對於查詢 SQL,結果會以 ResultSet 的形式返回。

當你創建了一個 Statement 對象之後,你可以用它的三個執行方法的任一方法來執行 SQL 語句。

  • boolean execute(String SQL) : 如果 ResultSet 對象可以被檢索,則返回的布爾值為 true ,否則返回 false 。當你需要使用真正的動態 SQL 時,可以使用這個方法來執行 SQL DDL 語句。
  • int executeUpdate(String SQL) : 返回執行 SQL 語句影響的行的數目。使用該方法來執行 SQL 語句,是希望得到一些受影響的行的數目,例如,INSERT,UPDATE 或 DELETE 語句。
  • ResultSet executeQuery(String SQL) : 返回一個 ResultSet 對象。當你希望得到一個結果集時使用該方法,就像你使用一個 SELECT 語句。

對於不同類型的 SQL 語句,Statement 有不同的接口與其對應。

接口 | 介紹
-|-| Statement | 適合運行靜態 SQL 語句,不接受動態參數 PreparedStatement | 計劃多次使用並且預先編譯的 SQL 語句,接口需要傳入額外的參數 CallableStatement | 用於訪問數據庫存儲過程

Statement 主要用於執行靜態SQL語句,即內容固定不變的SQL語句。Statement每執行一次都要對傳入的SQL語句編譯一次,效率較差。而 PreparedStatement則解決了這個問題,它會對 SQL 進行預編譯,提高了執行效率。

PreparedStatement pstmt = null;
    try {
        String SQL = "Update activity SET activity_name = ? WHERE activity_id = ?";
        pstmt = connection.prepareStatement(SQL);
        pstmt.setString(1, "測試");
        pstmt.setInt(2, 1);
        pstmt.executeUpdate();
    }
    catch (SQLException e) {
    }
    finally {
        pstmt.close();
    }
}

除此之外, PreparedStatement 還可以預防 SQL 注入,因為 PreparedStatement 不允許在插入參數時改變 SQL 語句的邏輯結構。

PreparedStatement 傳入任何數據不會和原 SQL 語句發生匹配關係,無需對輸入的數據做過濾。如果用戶將”or 1 = 1”傳入賦值給佔位符,下述SQL 語句將無法執行:select * from t where username = ? and password = ?。

ResultSet

當 Statement 查詢 SQL 執行后,會得到 ResultSet 對象,ResultSet 對象是 SQL語句查詢的結果集合。ResultSet 對從數據庫返回的結果進行了封裝,使用迭代器的模式可以逐條取出結果集中的記錄。

while(resultSet.next()) {
    System.out.println(resultSet.getString(2));
}

ResultSet 一般也建議使用完畢直接 close 掉,但是需要注意的是關閉 ResultSet 對象不關閉其持有的 Blob、Clob 或 NClob 對象。 Blob、Clob 或 NClob 對象在它們被創建的的事務期間會一直持有效,除非其 free 函數被調用。

參考

    • https://blog.csdn.net/wl044090432/article/details/60768342
    • https://blog.csdn.net/luanlouis/article/details/29850811

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

salesforce lightning零基礎學習(十五) 公用組件之 獲取表字段的Picklist(多語言),salesforce 零基礎學習(六十二)獲取sObject中類型為Picklist的field values(含record type)

此篇參考:

 我們在lightning中在前台會經常碰到獲取picklist的values然後使用select option進行渲染成下拉列表,此篇用於實現針對指定的sObject以及fieldName(Picklist類型)獲取此字段對應的所有可用的values的公用組件。因為不同的record type可能設置不同的picklist values,所以還有另外的參數設置針對指定的record type developer name去獲取指定的record type對應的Picklist values.

一. PicklistService公用組件聲明實現

Common_PicklistController.cls:有三個形參,其中objectName以及fieldName是必填參數,recordTypeDeveloperName為可選參數。

 1 public without sharing class Common_PicklistController {
 2     
 3     @AuraEnabled(cacheable=true)
 4     public static List<Map<String,String>> getPicklistValues(String objectName, String fieldName,String recordTypeDeveloperName) {
 5         //1. use object and field name get DescribeFieldResult and also get all the picklist values
 6         List<Schema.PicklistEntry> allPicklistValuesByField;
 7         try {
 8             List<Schema.DescribeSobjectResult> objDescriptions = Schema.describeSObjects(new List<String>{objectName});
 9             Schema.SObjectField field = objDescriptions[0].fields.getMap().get(fieldName);
10             Schema.DescribeFieldResult fieldDescribeResult = field.getDescribe();
11             allPicklistValuesByField = fieldDescribeResult.getPicklistValues();
12         } catch (Exception e) {
13             throw new AuraHandledException('Failed to retrieve values : '+ objectName +'.'+ fieldName +': '+ e.getMessage());
14         }
15         
16         //2. get all active field name -> label map
17         List<Map<String,String>> activeOptionMapList = new List<Map<String,String>>();
18         Map<String,String> optionValue2LabelMap = new Map<String,String>();
19         List<String> optionValueList;
20         for(Schema.PicklistEntry entry : allPicklistValuesByField) {
21             if (entry.isActive()) {
22                 System.debug(LoggingLevel.INFO, '*** entry: ' + JSON.serialize(entry));
23                 optionValue2LabelMap.put(entry.getValue(), entry.getLabel());
24             }
25         }
26 
27         //3. generate list with option value(with/without record type)
28         if(String.isNotBlank(recordTypeDeveloperName)) {
29             optionValueList = PicklistDescriber.describe(objectName,recordTypeDeveloperName,fieldName);
30         } else {
31             optionValueList = new List<String>(optionValue2LabelMap.keySet());
32         }
33 
34         //4. generate and format result
35         if(optionValueList != null) {
36             for(String option : optionValueList) {
37                 String optionLabel = optionValue2LabelMap.get(option);
38                 Map<String,String> optionDataMap = new Map<String,String>();
39                 optionDataMap.put('value',option);
40                 optionDataMap.put('label', optionLabel);
41                 activeOptionMapList.add(optionDataMap);
42             }
43         }
44 
45         return activeOptionMapList;
46     }
47 }

 Common_PicklistService.cmp:聲明了getPicklistInfo方法,有以下三個主要參數.objectName對應sObject的API名字,fieldName對應的此sObject中的Picklist類型的字段,recordTypeDeveloperName對應這個sObject的record type的developer name

1 <aura:component access="global" controller="Common_PicklistController">
2     <aura:method access="global" name="getPicklistInfo"  description="Retrieve active picklist values and labels mapping with(without) record type" action="{!c.getPicklistInfoAction}">
3         <aura:attribute type="String" name="objectName" required="true" description="Object name"/>
4         <aura:attribute type="String" name="fieldName" required="true" description="Field name"/>
5         <aura:attribute type="String" name="recordTypeDeveloperName" description="record type developer name"/>
6         <aura:attribute type="Function" name="callback" required="true" description="Callback function that returns the picklist values and labels mapping as [{value: String, label: String}]"/>
7     </aura:method>
8 </aura:component>

 Common_PicklistServiceController.js: 獲取傳遞過來的參數,調用後台方法並對結果放在callback中。

 1 ({
 2     getPicklistInfoAction : function(component, event, helper) {
 3         const params = event.getParam('arguments');
 4         const action = component.get('c.getPicklistValueList');
 5         action.setParams({
 6             objectName : params.objectName,
 7             fieldName : params.fieldName,
 8             recordTypeDeveloperName : params.recordTypeDeveloperName
 9         });
10         action.setCallback(this, function(response) {
11             const state = response.getState();
12             if (state === 'SUCCESS') {
13                 params.callback(response.getReturnValue());
14             } else if (state === 'ERROR') {
15                 console.error('failed to retrieve picklist values for '+ params.objectName +'.'+ params.fieldName);
16                 const errors = response.getError();
17                 if (errors) {
18                     console.error(JSON.stringify(errors));
19                 } else {
20                     console.error('Unknown error');
21                 }
22             }
23         });
24 
25         $A.enqueueAction(action);
26     }
27 })

二. 公用組件調用

 上面介紹了公用組件以後,下面的demo是如何調用。

SimplePicklistDemo引入Common_PicklistService,設置aura:id用於後期獲取到此component,從而調用方法

 1 <aura:component implements="flexipage:availableForAllPageTypes">
 2     <!-- include common picklist service component -->
 3     <c:Common_PicklistService aura:id="service"/>
 4     <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
 5 
 6     <aura:attribute name="accountTypeList" type="List"/>
 7     <aura:attribute name="accountTypeListByRecordType" type="List"/>
 8 
 9     <lightning:layout verticalAlign="center" class="x-large">
10         <lightning:layoutItem flexibility="auto" padding="around-small">
11             <lightning:select label="account type">
12                 <aura:iteration items="{!v.accountTypeList}" var="type">
13                     <option value="{!type.value}" text="{!type.label}"></option>
14                 </aura:iteration>
15             </lightning:select>
16         </lightning:layoutItem>
17 
18         <lightning:layoutItem flexibility="auto" padding="around-small">
19             <lightning:select label="account type with record type">
20                 <aura:iteration items="{!v.accountTypeListByRecordType}" var="type">
21                     <option value="{!type.value}" text="{!type.label}"></option>
22                 </aura:iteration>
23             </lightning:select>
24         </lightning:layoutItem>
25     </lightning:layout>
26     
27 </aura:component>

SimplePicklistDemoController.js:初始化方法用於獲取到公用組件component然後獲取Account的type的values,第一個是獲取所有的values/labels,第二個是獲取指定record type的values/labels。

 1 ({
 2     doInit : function(component, event, helper) {
 3         const service = component.find('service');
 4         service.getPicklistInfo('Account','type','',function(result) {
 5             component.set('v.accountTypeList', result);
 6         });
 7 
 8         service.getPicklistInfo('Account','type','Business_Account',function(result) {
 9             component.set('v.accountTypeListByRecordType',result);
10         });
11     }
12 })

三.效果展示:

1. account type的展示方式

 

 2. account type with record type的展示方式。

 

 總結:篇中介紹了Picklist values針對with/without record type的公用組件的使用,感興趣的可以進行優化,篇中有錯誤的歡迎指出,有不懂的歡迎留言。

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

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

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

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

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

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

drf組件之jwt認證

drf組件之jwt認證模塊

一、認證規則

全稱:json web token
解釋:加密字符串的原始數據是json,後台產生,通過web傳輸給前台存儲
格式:三段式 – 頭.載荷.簽名 – 頭和載荷用的是base64可逆加密,簽名用md5不可逆加密
內容:
頭(基礎信息,也可以為空):加密方式、公司信息、項目組信息、…
載荷(核心信息):用戶信息、過期時間、…
簽名(安全保障):頭加密結果+載荷加密結果+服務器秘鑰 的md5加密結果

認證規則:
後台一定要保障 服務器秘鑰 的安全性(它是jwt的唯一安全保障)
後台簽發token(login接口 ) -> 前台存儲 -> 發送需要認證的請求帶着token -> 後台校驗得到合法的用戶 -> 權限管理

為什麼要有jwt認證:
1)服務器壓力小, 後台不需要存儲token,只需要存儲簽發與校驗token的算法,效率遠遠大於後台存儲和取出token完成校驗
2) jwt算法認證,更適合服務器集群部署

二、認證模塊

安裝:pip install djangorestframework-jwt
模塊包:rest_framework_jwt

採用drf-jwt框架,後期任務只需要書寫登錄
為什麼要重寫登錄:drf-jwt只完成了賬號密碼登錄,我們還需要手機登錄,郵箱登錄
為什麼不需要重寫認證類:因為認證規則已經完成且固定不變,變得只有認證字符串的前綴,前綴可以在配置文件中配置

三、JWT使用

jwt配置;

在settings.py文件中配置,如果不配置,默認走jwt默認的

jwt插件的三個接口:

在urls.py中配置

在postman中測試一下籤發token

注意:上面三個接口都是發送POST請求

四、利用JWT實現多方式登錄

注:APIResponse 為自定義Response對象

# views.py
from rest_framework.views import APIView
from . import models,serializers
from utils.response import APIResponse

class LoginAPIView(APIView):
    # 登錄接口應該禁用所有的認證和、權限,因為不管是誰都應該能進來
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        # 將數據傳到序列化組件進行校驗
        user_ser = serializers.LoginSerializer(data=request.data)
        user_ser.is_valid(raise_exception=True)

        return APIResponse(msg='login success', data={
            'username': user_ser.user.username,
            'token': user_ser.token
        })

注意:

通過user對象生成payload載荷
payload = jwt_payload_handler(user)

通過payload簽發token
token = jwt_encode_handler(payload)

# serializer.py
from rest_framework.serializers import ModelSerializer, CharField, ValidationError, SerializerMethodField
from . import models
from django.contrib.auth import authenticate
import re
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class LoginSerializer(ModelSerializer):
    username = CharField(write_only=True)
    password = CharField(write_only=True)
    class Meta:
        model = models.User
        fields = ('username', 'password')

    def validate(self, attrs):
        # user_obj = authenticate(**attrs)
        # if not user_obj:
        #     raise ValidationError('用戶名或密碼錯誤')

        # 賬號密碼登錄 ==》 多方式登錄
        user = self._many_method_login(**attrs)

        # 通過user對象生成payload載荷
        payload = jwt_payload_handler(user)
        # 通過payload簽發token
        token = jwt_encode_handler(payload)

        # 將user和token存放在序列化對象中,方便返回到前端去
        self.user = user
        self.token = token

        return attrs

    # 多方式登錄 (用戶名、郵箱、手機號三種方式登錄)
    def _many_method_login(self, **attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 利用正則匹配判斷用戶輸入的信息
        # 1.判斷郵箱登錄
        if re.match(r'.*@.*', username):
            user = models.User.objects.filter(email=username).first()  # type: models.User
        # 2.判斷手機號登錄
        elif re.match(r'^1[3-9][0-9]{9}$',username):
            user = models.User.objects.filter(mobile=username).first()
        # 3.用戶名登錄
        else:
            user = models.User.objects.filter(username=username).first()

        if not user:
            raise ValidationError({'username': '賬號有誤'})

        if not user.check_password(password):
            raise ValidationError({'password': '密碼錯誤'})

        return user

使用postman測試代碼:

五、前後台分離模式下信息交互規則

"""
1)任何人都能直接訪問的接口
    請求不是是get、還是post等,不需要做任何校驗

2)必須登錄后才能訪問的接口
    任何請求方式都可能做該方式的限制,請求必須在請求頭中攜帶認證信息 - authorization
    
3)前台的認證信息獲取只能通過登錄接口
    前台提供賬號密碼等信息,去後台換認證信息token
    
4)前台如何完成登錄註銷
    前台登錄成功一般在cookie中保存認證信息token,分離註銷就是前台主動清除保存的token信息
"""

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

ASP.NET Aries 高級開發教程:如何寫WebAPI接口

前提:

最近,有不少同學又問到,Aries里如何提供WebAPI接口?

針對這個問題,今天給順路寫個教程,其實呢,很簡單的。

方式一:直接用WebService提供接口。

用這種方式,直接添加接口就可以了,Aries只是默認處理了.html後綴的請求。對於WS的asmx後綴是沒有影響的,所以傳統怎麼添加就怎麼添加。

 

方式二:單獨用Taurus.MVC寫一個接口項目。

用這種方式,就是把接口獨立成一個項目,然後通過IIS部署成子應用程序就可以了。

只是部署成子應用程序的時候,需要注意一下子目錄和根目錄的web.config,出現重複的只能留根目錄的那個。

(一般都會建議用戶用這種方式,好處是可以在IIS里學會一下怎麼部署子應用程序。)

方式三:在Aries引入Taurus.MVC即可。

這個方式,其實也很簡單,下面介紹一下簡單的部署:

1、引用Taurus.MVC用於寫WebAPI:

在Web.UI項目添加引用Taurus.Core.dll(可以在Nuget上引用,也可以引用源碼項目再引用項目)

2、配置Taurus.MVC的兩個必備項:

A、在HttpModule中添加URL攔截。

//這是原來有的: 
<add name="Aries.Core" type="Aries.Core.UrlRewrite,Aries.Core" />
//這是新添加的:
 <add name="Taurus.Core" type="Taurus.Core.UrlRewrite,Taurus.Core"/>

B、在AppSetting中設置路徑模式:

<!--配置模式【值為0,1或2】[默認為1]
      值為0:匹配{Action}/{Para}
      值為1:匹配{Controller}/{Action}/{Para}
      值為2:匹配{Module}/{Controller}/{Action}/{Para}-->
    <add key="Taurus.RouteMode" value="1"/>

C、在AppSetting中設置接口代碼所在的項目:

<!--指定控制器所在的項目(Dll)名稱(可改,項目編繹的dll叫什麼名就寫什麼)
    <add key="Taurus.Controllers" value="Taurus.Controllers"/>-->

 

如果是用Nuget上引用的,默認都會有上面的兩個,其它默認生的,可以註釋掉。

3、開始寫應用接口代碼:

接口代碼寫在哪裡呢?放在哪個項目都可以,只要上面C點的配置指向就可以了,如果接口代碼分散在多個項目中,配置的value可以用“逗號”分隔。

按Taurus.MVC的方式寫接口,繼承自Taurus.Core.Controller即可:

如:

  /// <summary>
    /// API 接口
    /// </summary>
    public  class APIController : Taurus.Core.Controller
    {
        public void Hello()
        {
            Write("hello Controllers.API");
        }
    }

接口訪問:http://…/api/hello

總結說明:

Aries中默認處理的是.html後綴。

Taurus默認處理的是無後綴。

所以兩者並無衝突,直接引用,加配置就可以了,沒你想的複雜。

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

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

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

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

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

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

深入理解static關鍵字

在開始講static之前,我想讓各位看一段有意思的代碼:

public class Test {
     
    static{
        System.out.println("test static 1");
    }
  
    static{
        System.out.println("test static 2");
    }
    
    public static void main(String[] args) {
         
    }
}

看完程序,小白童鞋發話了:啥玩意?main方法中啥都沒有,能運行啥?博主你個星星星…

運行結果:
test static 1
test static 2

小白童鞋:那啥…那啥…博主我說啥了,我啥都沒說…

其實,上面的代碼懂的自然懂,不懂的自然就不懂了,因為上面的代碼涉及到JVM的類加載了!當然不在本篇博客文章的範疇內,如果有興趣理解上面的程序,這篇文章可能會對你有所幫助

1、static存在的主要意義

static的主要意義是在於創建獨立於具體對象的域變量或者方法。以致於即使沒有創建對象,也能使用屬性和調用方法

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態代碼塊以優化程序性能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程序性能,是因為它的特性:只會在類加載的時候執行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。

2、static的獨特之處

1、被static修飾的變量或者方法是獨立於該類的任何對象,也就是說,這些變量和方法不屬於任何一個實例對象,而是被類的實例對象所共享

怎麼理解 “被類的實例對象所共享” 這句話呢?就是說,一個類的靜態成員,它是屬於大夥的【大夥指的是這個類的多個對象實例,我們都知道一個類可以創建多個實例!】,所有的類對象共享的,不像成員變量是自個的【自個指的是這個類的單個實例對象】…我覺得我已經講的很通俗了,你明白了咩?

2、在該類被第一次加載的時候,就會去加載被static修飾的部分,而且只在類第一次使用時加載並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。

3、static變量值在類加載的時候分配空間,以後創建類對象的時候不會重新分配。賦值的話,是可以任意賦值的!

4、被static修飾的變量或者方法是優先於對象存在的,也就是說當一個類加載完畢之後,即便沒有創建對象,也可以去訪問。

3、static應用場景

因為static是被類的實例對象所共享,因此如果某個成員變量是被所有對象所共享的,那麼這個成員變量就應該定義為靜態變量

因此比較常見的static應用場景有:

1、修飾成員變量
2、修飾成員方法
3、靜態代碼塊
4、修飾類【只能修飾內部類也就是靜態內部類】
5、靜態導包

以上的應用場景將會在下文陸續講到…

4、靜態變量和實例變量的概念

靜態變量:
static修飾的成員變量叫做靜態變量【也叫做類變量】,靜態變量是屬於這個類,而不是屬於是對象。

實例變量:
沒有被static修飾的成員變量叫做實例變量,實例變量是屬於這個類的實例對象。

還有一點需要注意的是:static是不允許用來修飾局部變量,不要問我問什麼,因為java規定的!

5、靜態變量和實例變量區別【重點常用】

靜態變量:
靜態變量由於不屬於任何實例對象,屬於類的,所以在內存中只會有一份,在類的加載過程中,JVM只為靜態變量分配一次內存空間。

實例變量:
每次創建對象,都會為每個對象分配成員變量內存空間,實例變量是屬於實例對象的,在內存中,創建幾次對象,就有幾份成員變量。

我相信各位智商都比宜春智商要高,應該都能理解上面的話。下面舉了例子完全出於娛樂,理解了大可不必看,下面的例子僅供參考,僅供娛樂一下下氣氛,趕時間的熊dei大可略過!

怎麼理解呢?打個比喻吧…就比方說程序員小王是一個比較溫柔陽光的男孩子,這1024的這一天,老闆閑的沒事,非要拉着程序員小王來玩耍,怎麼個玩法呢?老闆和小王一人拿着一把菜刀,規則很簡單,互相傷害,一人一刀,你一刀,我一刀….遊戲一開始,老闆二話不說,跳起來就是一刀,程序員小王二話也沒說反手就是一菜刀回去,這個時候老闆發飆了,雙眼瞪得忒大,跳起來又是一刀,這個時候程序員小王不敢還手了,就沒動手。沒想到老闆越來越生猛,左一刀右一刀全程下來差不多砍個半個時….程序員小王一直沒有還過手,因為小王知道他是老闆…

這個程序員小王只會在老闆第一次揮刀的時候,回老闆一刀,之後就不還手了,這個時候我們把程序員小王看做是靜態變量,把老闆第一次向小王揮刀看做是類加載,把小王回老闆一刀看出是分配內存空間,而一人一刀這個回合過程看成是類加載的過程,之後老闆的每一刀都看成是創建一次對象。

連貫起來就是static變量值在類第一次加載的時候分配空間,以後創建類對象的時候不會重新分配

之後這個老闆挨了一刀之後躺醫院了一年,一出院回到公司第一件事就是拉程序員宜春出來玩耍,老闆殊不知其然,這個博主程序員宜春性格異常暴躁,老闆遞給程序員宜春一把菜刀,博主宜春一接過菜刀,猝不及防的被老闆跳起來就是一刀,程序員宜春痛的嗷了一聲,暴躁的程序員宜春還沒嗷完,在嗷的同時跳起來就是給老闆一刀,接着老闆跳起來又是一刀,程序員宜春嗷的一聲又是回一刀,老闆跳起來又一刀,程序員宜春嗷的一聲又是回一刀,只要老闆沒停程序員宜春就沒停,因為程序員宜春知道,就自己這曝脾氣,暴躁起來si都敢摸,肯定有幾個老鐵知道….

程序員宜春就類似實例變量,每次創建對象,都會為每個對象分配成員變量內存空間,就像老闆來一刀,程序員宜春都會回一刀這樣子的…

6、訪問靜態變量和實例變量的兩種方式

我們都知道靜態變量是屬於這個類,而不是屬於是對象,static獨立於對象。

但是各位有木有想過:靜態成員變量雖然獨立於對象,但是不代表不可以通過對象去訪問,所有的靜態方法和靜態變量都可以通過對象訪問【只要訪問權限足夠允許就行】,不理解沒關係,來個代碼就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

猜想一下結果,我猜你的結果是123,哈哈是咩?其實

運行結果: 666

回過頭再去品味一下上面的那段話,你就能非常客觀明了了,這個思想概念要有隻是這種用法不推薦!

因此小結一下訪問靜態變量和實例變量的兩種方法:

靜態變量:

類名.靜態變量

對象.靜態變量(不推薦)

靜態方法:

類名.靜態方法

對象.靜態方法(不推薦)

7、static靜態方法

static修飾的方法也叫做靜態方法,不知道各位發現咩有,其實我們最熟悉的static靜態方法就是main方法了~小白童鞋:喔好像真的是哦~。由於對於靜態方法來說是不屬於任何實例對象的,this指的是當前對象,因為static靜態方法不屬於任何對象,所以就談不上this了。

還有一點就是:構造方法不是靜態方法

8、static靜態代碼塊

先看個程序吧,看看自個是否掌握了static代碼塊,下面程序代碼繼承關係為 BaseThree——> BaseTwo——> BaseOne

BaseOne類

package com.gx.initializationblock;

public class BaseOne {

    public BaseOne() {
        System.out.println("BaseOne構造器");
    }

    {
        System.out.println("BaseOne初始化塊");
        System.out.println();
    }

    static {
        System.out.println("BaseOne靜態初始化塊");

    }

}

BaseTwo類

package com.gx.initializationblock;

public class BaseTwo extends BaseOne {
    public BaseTwo() {
        System.out.println("BaseTwo構造器");
    }

    {
        System.out.println("BaseTwo初始化塊");
    }

    static {
        System.out.println("BaseTwo靜態初始化塊");
    }
}

BaseThree 類

package com.gx.initializationblock;

public class BaseThree extends BaseTwo {
    public BaseThree() {
        System.out.println("BaseThree構造器");
    }

    {
        System.out.println("BaseThree初始化塊");
    }

    static {
        System.out.println("BaseThree靜態初始化塊");
    }
}

測試demo2類

package com.gx.initializationblock;

/*
     注:這裏的ABC對應BaseOne、BaseTwo、BaseThree 
 * 多個類的繼承中初始化塊、靜態初始化塊、構造器的執行順序
     在繼承中,先後執行父類A的靜態塊,父類B的靜態塊,最後子類的靜態塊,
     然後再執行父類A的非靜態塊和構造器,然後是B類的非靜態塊和構造器,最後執行子類的非靜態塊和構造器
 */
public class Demo2 {
    public static void main(String[] args) {
        BaseThree baseThree = new BaseThree();
        System.out.println("-----");
        BaseThree baseThree2 = new BaseThree();

    }
}

運行結果

BaseOne靜態初始化塊
BaseTwo靜態初始化塊
BaseThree靜態初始化塊
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器
-----
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器

至於static代碼塊運行結果不是很清晰的童鞋,詳細講解請看這篇

以上僅僅是讓各位明確代碼塊之間的運行順序,顯然還是不夠的,靜態代碼塊通常用來對靜態變量進行一些初始化操作,比如定義枚舉類,代碼如下:

public enum WeekDayEnum {
    MONDAY(1,"周一"),
    TUESDAY(2, "周二"),
    WEDNESDAY(3, "周三"),
    THURSDAY(4, "周四"),
    FRIDAY(5, "周五"),
    SATURDAY(6, "周六"),
    SUNDAY(7, "周日");
 
    private int code;
    private String desc;
 
    WeekDayEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
 
    private static final Map<Integer, WeekDayEnum> WEEK_ENUM_MAP = new HashMap<Integer, WeekDayEnum>();
 
    // 對map進行初始化
    static {
        for (WeekDayEnum weekDay : WeekDayEnum.values()) {
            WEEK_ENUM_MAP.put(weekDay.getCode(), weekDay);
        }
    }
 
    public static WeekDayEnum findByCode(int code) {
        return WEEK_ENUM_MAP.get(code);
    }
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getDesc() {
        return desc;
    }
 
    public void setDesc(String desc) {
        this.desc = desc;
    }
} 

當然不僅僅是枚舉這一方面,還有我們熟悉的單例模式同樣也用到了靜態代碼塊,如下:

public class Singleton {
    private static Singleton instance;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

9、static變量與普通變量區別

static變量也稱作靜態變量,靜態變量和非靜態變量的區別是:靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。而非靜態變量是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。

還有一點就是static成員變量的初始化順序按照定義的順序進行初始化。

10、靜態內部類

靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地保存着一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味着:

1、它的創建是不需要依賴外圍類的創建。
2、它不能使用任何外圍類的非static成員變量和方法。

代碼舉例(靜態內部類實現單例模式)

public class Singleton {
    
   // 聲明為 private 避免調用默認構造方法創建對象
    private Singleton() {
    }
    
   // 聲明為 private 表明靜態內部該類只能在該 Singleton 類中被訪問
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCESingletonHolder 才會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

11、靜態導包

靜態導包格式:import static

這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法

//  Math. --- 將Math中的所有靜態資源導入,這時候可以直接使用裏面的靜態方法,而不用通過類名進行調用
//  如果只想導入單一某個靜態方法,只需要將換成對應的方法名即可
 
import static java.lang.Math.;
//  換成import static java.lang.Math.max;具有一樣的效果
 
public class Demo {
    public static void main(String[] args) {
 
        int max = max(1,2);
        System.out.println(max);
    }
}

靜態導包在書寫代碼的時候確實能省一點代碼,可以直接調用裏面的靜態成員,但是會影響代碼可讀性,所以開發中一般情況下不建議這麼使用。

12、static注意事項

1、靜態只能訪問靜態。
2、非靜態既可以訪問非靜態的,也可以訪問靜態的。

13、final與static的藕斷絲連

到這裏文章本該結束了的,但是static的使用始終離不開final字眼,二者可謂藕斷絲連,常常繁見,我覺得還是很有必要講講,那麼一起來看看下面這個程序吧。

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

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

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修飾的  i=" + demo1.i);
        System.out.println("static修飾的 t=" + demo1.t);
        System.out.println("final修飾的  i=" + demo2.i);
        System.out.println("static修飾的 t=" + demo2.t);

        System.out.println("t+1= "+ ++demo2.t );
//      System.out.println( ++demo2.i );//編譯失敗
      }
}
運行結果:
    final修飾的  i=0.7282093281367935
    static修飾的 t=0.30720545678577604
    final修飾的  i=0.8106990945706758
    static修飾的 t=0.30720545678577604
    t+1= 1.307205456785776

static修飾的變量沒有發生變化是因為static作用於成員變量只是用來表示保存一份副本,其不會發生變化。怎麼理解這個副本呢?其實static修飾的在類加載的時候就加載完成了(初始化),而且只會加載一次也就是說初始化一次,所以不會發生變化!

至於final修飾的反而發生變化了?是不是巔覆你對final的看法?關於final詳細講解博主也準備好了一篇文章

ok,文章就先到這裏了,希望這篇文章能夠幫助到你對static的認識,若有不足或者不正之處,希望諒解並歡迎批評指正!

如果本文章對你有幫助,哪怕是一點點,那就請點一個讚唄,謝謝~

參考:
《java編程思想》

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

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

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

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

京東物流出問題了?褥了30塊羊毛 & 淺析系統架構

本人親身經歷,但後續的流程分析都是個人猜測的,畢竟沒有實際做過這塊的業務。

訂單物流阻塞經過

火熱的雙11剛剛退去,截止今日,我在京東購買的礦泉水終於到貨啦,下單兩箱還只收到了一箱 🙁 ,從下單到收到貨過去了14天,足足兩周的時間。

我從11-20號開始與京東客服聯繫,直到11-25整個購物體驗才完成,也因為京東沒有按照約定重新發貨,算是補償了我3000個京豆。

朋友們,不會不知道京豆是幹啥的吧,100個京豆相當於一塊錢,1000個京豆相當於10塊錢,3000個京豆就是30塊錢。

可那不是現金有啥卵用,你不會不在京東購物吧,下單的時候就可以選擇用京豆來抵用一部分下單金額了。

所以一般購買商品后鼓勵你去評價,文字超過一定字數且上傳了購買商品的圖片,就能得到比如20個京豆。京豆積少成多,就可以下單抵用現金了。

廢話不多說,回到正題!

雙11我在京東下單,自營商品的訂單一般都是次日達,因為雙11物流緊張,所以下單后提示11-13日送達。

11-13日:

遺憾的是,11-13日並沒有如期送達,查看訂單物流,增加了一段溫馨提示:「由於銷售火爆,根據目前情況,訂單預計11月16日送達到您的手中」,額~,當然大家都能理解,原來京東商品這麼「火爆」,畢竟雙十一累積銷量2000億呢。

11-20日 周三:

問題是到了11-16日並沒有送達,我把這個訂單差點忘記了,11-20號突然想起來了這件事,上京東確認了訂單,才發現還是那個「銷售火爆…」的提示呀!竟然沒有給我送貨。。。

然後在線聯繫人工客服,說了一下情況,客服態度很好,兩箱水拆分下單后因已經拆分為了兩個訂單,有兩個訂單號,為了表示歉意,每個訂單號補了500個京豆,1000個京豆到手了。

然後,客服跟我說,已經給我催促倉儲發貨了,讓耐心等待一下,預計第二天就能到了。

11-21號 周四:

可惜到了第二天,並沒有像客服MM所說的那樣如期送達,反正已經晚了,心想也不差這一天兒,還贈送了京豆,再等等了…… 。

11-22號 周五:

然而,到了11-22號還是沒有配送物流通知,訂單中的分揀流程沒有完成,這是什麼操作??

當天繼續聯繫客服,問了是什麼原因,又來「話術」:小妹已為您催促正在發貨中,此時我有點懷疑了,可能這個流程本身就中斷了,需要人工來協助處理補單流程。

此刻,「客服的嘴,騙人的鬼」終於用到這了~

同時,京東客服升級來了個電話溝通,誠摯的表示歉意,說是倉儲這邊發貨有點問題,正在重新補貨中… ,預計明日就能送到,請注意查收!

11-23日 周六:

「客服的嘴,騙人的鬼」再一次用到這了~

周六仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

11-23日 周日:

周日仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

看來沒很好的注重用戶體驗嘛,再次在線聯繫客服,每次接線的不是同一個客服,所以每次都要求提供一下訂單號,很煩,此時很無語了,本用戶表示很生氣啊,自己查!

然後呢,客服又說已經重新補發貨了,並且這次竟然不給我大概的送貨時間點了,因為他不相信到底有沒有真的去補發貨操作了,補發貨這個操作多半客服是沒有權限的。

另外,解釋到因訂單延遲時間過長,又一次非常的抱歉,給申請了1000京豆,不過這次京豆並不是實時到賬的,需要經過審核流程。

11-25日 周一:

早上已經在地鐵上了,收到了京東快遞小哥的來電,但是只到了一個訂單的貨。查了一下另外一個訂單物流狀態仍然一動沒動 :(。

最後,客服專員再次電話聯繫,解釋到這個訂單給疏忽了,建議我重新下單,然後這個訂單走退款流程。並且再一次給予了1000京豆的補償 :)。

物流系統異常分析:

上述物流配送異常流程中,想了解故障原因,電話中我也有意識的去問一下客服,是不是某個環節有這樣的問題,但是從客服那裡只能給到說倉配流程有問題 ,具體他們也不是很清楚了,全都是針對用戶的話術,避免說錯話。

作為個技術人,通常得思考一下問題背後的原因:

  • 到底是哪個環節出現的問題
  • 出現這樣問題的原因
  • 對用戶的影響及應對方案
  • 如何能避免類似的問題發生
物流系統介紹

由此次問題引出,我還特意去查資料看了一下京東物流的系統架構演進過程。記得當時京東物流招人非常猛,作為一個內部非常重量級的項目投入了很多研發人力。

在2012年的時候京東內部開始對物流系統進行設計改造,那時訪問量應該還不算高,最初的系統還沒那麼複雜。新改造的物流系統:「青龍系統

青龍系統演進過程如上圖所示 ,它的系統發展至今,已經包含了分揀中心,運輸路由,終端,對外拓展,運營支持等五十餘個核心子系統,構建了完善的電商物流體系。

並且青龍系統中總結了一些最佳的實戰原則,如下所示:

這些系統設計原則我認為對任何系統都是通用的,值得我們一起學習的:

  • 高可用

選擇合適的架構方案;大系統小做,服務拆分;併發控制,服務隔離;灰度發布;全方位監控報警;核心服務,平滑降級。

  • 高性能

緩存和異步化,同步接口異步化設計;接口數據緩存化。

接口數據緩存化是非常重要的手段,對Redis緩存系統的很好的利用,構建了具有自己特色的緩存體系,很好的支撐了業務發展。同時,還發展了基於Redis的分佈式調度系統

  • 數據一致性

高實時性/高一致性,高實時性/低一致性,低實時性/高一致性,低實時性/低一致性。

針對具體的業務,可以匹配到具體的數據場景,找到對應的解決方案。要客觀的結合業務分析,選擇最適合的一致性方案,並不是高實時性/高一致性就好,成本是很更貴的。

  • 用戶體驗

東哥要求過任何人不能對用戶體驗提升的建議說No。用戶體驗主要遵循MVP原則和動態運營的原則。

MVP原則:也就是敏捷開發中的迭代思路。即快速迭代,核心需求線上,及時的反饋和改進。

動態運營:跟MVP原則強關聯,上線后收集並分析用戶數據,使得產品落地的設計符合用戶的需求,不符合設計要求的就要不斷的持續調整,是一個動態持續的過程。

物流分揀系統

簡單介紹完了物流系統的演進過程及架構原則,還是回到主題,到底是哪個環節出現了問題?

需要了解整個購物鏈路的各個環節:

用戶整個購物流程經過以上幾個關鍵的流程,已經生成訂單號並且已經支付了,流程到了訂單中心。

各個系統都是分佈式部署的,訂單中心會發送一個MQ消息給各個下游系統,積分系統增加積分京豆等,促銷系統發放優惠券等,倉儲系統接收到MQ消息進行處理,調用物流系統生成物流單,通知到配送站,由配送員送貨。

結合一個火爆的訂單,看一下訂單跟蹤過程:

該訂單在倉庫處理中已經打包完成,訂單在京東【北京李橋接貨倉庫】分揀完成。注意到了「分揀」二字,順便看了一下正常的訂單流程,會經過多個貨倉的分揀過程,最終會分揀到離用戶最近的貨倉。

所以,猜測,這筆訂單的問題就是在配送前的分揀系統處理過程中出現了異常情況。

青龍物流系統其中就包括了預分揀流程,如下所示:

當用戶下單后,首先必須是經過預分揀環節,但是根據最新的訂單跟蹤過程看,是先進行了倉庫打包處理,然後進入分揀流程。

分揀系統接收到訂單,根據不同的訂單進行規則匹配,分配站點,處理成功後生產包裹打印標籤。

訂單無法被正常分揀完成,將無法生成訂單:

想必我的訂單大概率就是在分揀環節出現了問題 ~

分揀系統的目標:

其中可用性要求是達到99.99%,4個9的可用率呢,看來很不幸啊,不可用的0.01%小概率事件偶發在了我的訂單上。

預分揀算法:

1、經驗值

只適用於同一個地址多次購買,依賴於第一妥投地址。

2、特徵值

需要提前人工維護關鍵字,依賴於關鍵字的準確性。

3、特殊配置

需要提前人工配置,依賴於該區域是否有特殊配置。

4、GIS

通過GIS技術精準的匹配地理位置。

上述都沒有匹配到,那麼只能走人工處理流程了。

預分揀系統架構:

訂單系統下發服務,默認會進入到預分揀系統,不同的訂單有不同的匹配規則,匹配規則使用開源的Drools來實現的,規則匹配完成,會按照預分揀算法匹配,優先匹配到離用戶最近的地址,返回自動預分揀的結果。

一旦回傳失敗,應該會有預警,需要人工介入來協助完成預分揀,將結果返回給訂單服務。

預分揀服務系統交互流程:

分揀服務使用Tomcat分佈式部署的Worker進程,完成后,將結果寫入到任務庫,回傳服務從人物庫抓取分揀結果回傳站點。

下圖來源於網絡,不是很清晰了:

其中預分揀服務接受訂單服務都是分佈式部署的,並且針對不同的訂單做了服務隔離,使用應用服務器是Tomcat;全文檢索使用的Solr,可能目前已改進為流行的ElasticSerach架構了;分佈式緩存使用了Redis集群;預分揀算法中的地址庫、特徵值、配置都對應了自己的Worker集群,也是做了服務隔離,每個服務分佈式部署,最終將結果寫入到MySQL數據庫中;預分揀回傳站點單獨的Worker集群,用來從數據庫抓取分揀數據,返回給用戶站點。

小結

經過以上過程猜測性分析,基本就清楚了自己的訂單問題出現的位置了。

大概率就是預分揀服務在某一個站點因為流量洪峰或異常出現了故障,可能服務恢復后沒有及時完成自動分揀數據校對。

與客服的溝通結果來看,當分揀過程出現問題后,可能並沒有及時預警並人工及時的去干預處理,導致分揀流程被阻塞,遲遲無法進入到分揀恢復階段。或許也是考慮到這種小概率事件,就由用戶來直接反饋,然後由人工介入處理。

但是,很明顯,客服用話術告知用戶結果,讓用戶耐心等待的同時。在後續的分揀系統訂單恢複流程並不是那麼順暢的,不一定是那麼簡單的人工直接快速處理,會經過一些校驗核對、人工審核等一系列流程,又或者讓技術人員協助恢復的,導致分揀流程流轉下去很慢,也進而影響了用戶體驗。

在線話術告知用戶結果算是A方案。

人工處理的第一筆訂單跟蹤:

而第二個訂單,客服根據情況執行了B方案,將問題升級到專員,電話聯繫用戶,建議用戶重新下單,並給予一定的補償。當你重新下單,分揀系統接收到新的訂單,就是進入了自動預分揀訂單處理過程了,自動化流程當然是很快的,無需人工干預。

總體來說,京東客服的做法可圈可點,整體售後服務流程較以前值得肯定,越來越完善。

同時,系統架構在未來方向上,肯定更趨向於更加的智能化,使用機器學習、人工智能等手段持續不斷優化物流的各環節,減少或避免小概率的事件發生。

ps:文章前半段真實發生,後半段僅作為問題分析參考。

歡迎關注我的公眾號,掃二維碼關注獲得更多精彩文章,與你一同成長~

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

ASP.NET Core gRPC 使用 Consul 服務註冊發現

一. 前言

gRPC 在當前最常見的應用就是在微服務場景中,所以不可避免的會有服務註冊與發現問題,我們使用gRPC實現的服務可以使用 Consul 或者 etcd 作為服務註冊與發現中心,本文主要介紹Consul。

二. Consul 介紹

Consul是一種服務網絡解決方案,可跨任何運行平台以及公共或私有雲來連接和保護服務。它可以讓你發現服務並保護網絡流量。它可以在Kubernetes中使用,實現服務發現和服務網格功能(k8s默認etcd)。 提供安全服務通訊,保護和觀察服務之間的通信,而無需修改其代碼。 提供動態負載平衡, 使用Consul和HAProxy,Nginx或F5自動執行負載均衡器配置。 Consul 可以用於服務發現和服務網格。

翻譯自官網

三. Consul 安裝配置

安裝

Consul 下載地址:

根據自己的系統來選擇,我這裏選擇的是 Windows 版本的,直接解壓即可運行。

啟動

consul agent -dev -ui

本文不詳細介紹Consul使用,如需請自行查看相關資料

四. .NET Core Consul 客戶端的選擇

Consul 提供了 HTTP API 的方式來進行通訊,我們可以直接調用API或者是使用第三方封裝好的客戶端組件,通過Nuget搜索可以發現許多。

這裏面我沒有一一測試,但是目前使用量最多的 Consul 組件是不支持設置 GRPC 健康檢查的,而且 github 也停止了更新。

所以我 Fork 了這個倉庫,然後添加了 GRPC 的健康檢查支持,本文也將使用這個庫,歡迎大家使用:

因為原倉庫已經 Archived 了,所以我才 Fork 了自己改一下,改動很小,不影響原來的穩定性。

Nuget:

Github:

求個star

關於支持 GPRC 健康檢查的好處:

偷個懶,不翻譯了,摘自GRPC官方文檔

五. 註冊GRPC服務與健康檢查

基於前文()的Demo

1.為服務端項目安裝 NConsul.AspNetCore ( )

這裏面對 AspNetCore 做了適配,使用簡單。

2.在 Startup 的 ConfigureServices方法內進行配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();

    services.AddConsul("http://localhost:8500")
        .AddGRPCHealthCheck("localhost:5000")
        .RegisterService("grpctest","localhost",5000,new []{"xc/grpc/test"});
}

AddConsul 添加 Consul Server 地址。

AddGRPCHealthCheck 添加 GRPC 健康檢查,即健康檢查走的是 GRPC 協議,該值為 GRPC 服務的地址,不需要path不需要提供 http/https

RegisterService 註冊服務

到這步,還不能啟動運行,如果運行健康檢查是會失敗的。

3.編寫 Health Check 服務 **

對於 GRPC 的健康檢查,官方有標準的定義,新建一個 proto 文件,命名為 HealthCheck.proto

syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
    string service = 1;
}

message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0;
        SERVING = 1;
        NOT_SERVING = 2;
    }
    ServingStatus status = 1;
}

service Health {
    rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

    rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

這裏面的內容不得更改,是官方標準,資料見後文

這裏編譯一下項目,以便自動生成代碼。

然後,添加一個服務的實現類 HealthCheckService

public class HealthCheckService:Health.HealthBase
{
    public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
    {
        //TODO:檢查邏輯
        return Task.FromResult(new HealthCheckResponse(){Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }

    public override async Task Watch(HealthCheckRequest request, IServerStreamWriter<HealthCheckResponse> responseStream, ServerCallContext context)
    {
        //TODO:檢查邏輯
        await responseStream.WriteAsync(new HealthCheckResponse()
            {Status = HealthCheckResponse.Types.ServingStatus.Serving});
    }
}

示例代碼直接返回了檢查結果,實際使用中應該在這裏編寫檢查邏輯,然後根據情況返回相應的檢查結果。檢查結果有3種情況:

結果類型 說明
Unknown 未知狀態
Serving 正常
NotServing 異常,不能提供服務

最後別忘了註冊服務:

4.測試運行

啟動 GRPC 服務

然後訪問 http://localhost:8500/ui 訪問 Consul 控制台

可以看到服務成功註冊,並且健康檢查也是通過了的。通過控制台日誌,還可以看到健康檢查的請求:

六. 客戶端使用服務發現

客戶端項目安裝 Consul 組件,然後改造下代碼:

static async Task Main(string[] args)
{
    var serviceName = "grpctest";
    var consulClient = new ConsulClient(c => c.Address = new Uri("http://localhost:8500"));
    var services = await consulClient.Catalog.Service(serviceName);
    if (services.Response.Length == 0)
    {
        throw new Exception($"未發現服務 {serviceName}");
    }

    var service = services.Response[0];
    var address = $"http://{service.ServiceAddress}:{service.ServicePort}";

    Console.WriteLine($"獲取服務地址成功:{address}");

    //啟用通過http使用http2.0
    AppContext.SetSwitch(
        "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    var channel = GrpcChannel.ForAddress(address);
    var catClient = new LuCat.LuCatClient(channel);
    var catReply = await catClient.SuckingCatAsync(new Empty());
    Console.WriteLine("調用擼貓服務:"+ catReply.Message);
    Console.ReadKey();
}

通過服務名稱獲取服務地址,然後來進行訪問。

運行測試:

可以看到,成功的從Consul獲取了我們的服務地址,然後調用。

六. 參考資料

  • gRPC in Asp.Net Core :

  • GPRC Health Check Doc:

  • 本文 Demo:

  • 本系列文章目錄:

  • NConsul:

  • by Edison Zhou

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

MySQL權限管理

目錄

設置用戶與權限

一個MySQL系統可能有許多用戶。為了安全起見,root用戶通常只用管理目的。對於每個需要使用該系統的用戶,應該為他們創建一個賬號和密碼。這些用戶名和密碼不必與MySQL之外的用戶名稱和密碼(例如,Linux或NT用戶名和密碼)相同。同樣的原則也適合於root用戶。對於系統用戶和MySQL用戶最好使用不同的密碼,這一點對root用戶尤其應該這樣。
為用戶設置密碼不是必須的,但是強烈建議為所有創建的用戶設定密碼。要建立一個Web數據庫,最好為每個網站應用程序建立一個用戶。你可能會問,“為什麼要這麼做呢?”—-答案在於權限。

用戶管理

我們的MySQL的用戶,都被記錄在了mysql數據庫的user表中,如下圖:

在MySQL數據庫中,一個完整的用戶賬號應該包含用戶名登錄主機,也就是說 用戶名@主機名才是一個完整的用戶賬號。

創建用戶基本語法:

CREATE USER 用戶名@主機名 IDENTIFIED BY '密碼';

刪除用戶基本語法:

DROP USER 用戶名@主機名;

修改用戶密碼:

- 修改自己的密碼
set password = password('新密碼');
- 修改別人的密碼(需要有該權限)
set password for 用戶名@主機名 = password('新密碼');

MySQL權限系統介紹

MySQL的最好特性之一是支持複雜的權限系統權限是對特定對象執行特定操作的權力,它與特定用戶相關。其概念非常類似於文件的權限。擋在MySQL中創建一個用戶時,就賦予了該用戶一定的權限,這些權限指定了用戶在本系統中可以做什麼和不可以做什麼。

最少權限原則

最少權限原則可以用來提高任何計算機系統的安全性。它是一個基本的、但又是非常重要的而且容易為我們忽略的原則。該原則包含如下內容:

一個用戶(或一個進程)應該擁有能夠執行分配給他的任務的最低級別的權限。

該原則同樣適用於MySQL,就像它應用於其他地方一樣。例如,要在網站上運行查詢,用戶並不需要root用戶所擁有的所有權限。因此,我們應該創建另一個用戶,這個用戶只有訪問我們剛剛建立的數據庫的必要權限。

創建用戶:GRANT命令

GRANT和REVOKE命令分別用來授予取消MySQL用戶的權限,這些權限分4個級別。它們分別是:

  • 全局
  • 數據庫

GRANT命令用來創建用戶並賦予他們權限。GRANT命令的常見形式是:

GRANT privileges [columns]
ON item
TO user_name [IDENTIFIED BY 'password']
[REQUIRE ssl_options]
[WITH [GRANT OPTION | limit_options]]

普通寫法可以簡略如下:

GRANT 權限列表 ON 數據庫.表 TO 用戶名@主機名 [IDENTIFIED BY '密碼'];

identified by可以省略,也可以寫出
(1)如果寫了,用戶存在,就是修改用戶的密碼
(2)如果寫了,該用戶不存在,就是創建用戶,同時指定密碼

方括號內的子句是可選的。在本語法中,出現了許多佔位符。第一個佔位符是 privileges,應該是由逗號分開的一組權限。MySQL已經有一組已定義的權限。它們在下一節詳細介紹。
佔位符 columns是可選的。可以用它對每一個列指定權限。也可以使用單列的名稱或者逗號分開的一組列的名稱。

佔位符 item是新權限的所應用於的數據庫或表。可以將項目指定為“.”,而將權限應用於所有數據庫。這叫做賦予全局權限。如果沒有使用在特定的數據庫,也可以通過只指定*完成賦予全局權限。更常見的是,以dbname.*的形式指定數據庫中所有的表,以dbname.tablename的形式指定單個表,或者通過指定tablename來指定特定的列。這些分別表示其他3個可以利用的權限:數據庫、表、列。如果在輸入命令的時候正在使用一個數據庫,tablename本身將被解釋成當前數據庫中的一個表。

user_name應該是用戶登錄MySQL所使用的用戶名。請注意,它不必與登錄系統時所使用的用戶名相同。MySQL中的user_name也可以包含一個主機名。可以用它來區分如itbsl(解釋成itbsl@localhost)和itbsl@somewhere.com。這是非常有用的一項能力,因為來自不同域的用戶經常可能使用同一個名字。這也提高了安全性能,因為可以指定用戶從什麼地方連接到本機,甚至可以指定它們在特定的地方可以訪問那些表和數據庫。

password應該是用戶登錄時使用的密碼。常見的密碼選擇規則在這裏都適用。我們後面將更詳細地講述安全問題,但是密碼應該不容易被猜出來。這意味着,密碼不應該是一個字段單詞或與用戶名相同。理想的密碼應該是大、小寫字母和非字母的組合。

REQUIRE子句允許指定用戶是否必須通過加密套接字連接,或者指定其它的SSL選項,關於SSL到MySQL連接的更多信息,請參閱MySQL手冊。

WITH GRANT OPTION選項,如果指定,表示允許指定的用戶向別人授予自己所擁有的權限。
我們也可以指定如下所示的WITH子句:
MAX_QUERIES_PER_HOUR n
或者
MAX_UPDATES_PER_HOUR n
或者
MAX_CONNECTIONS_PER_HOUR n
這些子句可以指定每個用戶每小時執行的查詢、更新和鏈接的數量。在共享的系統上限制單個用戶的負載時,這些子句是非常有用的。
權限存儲在名為mysql的數據庫中的5個系統中。這些表分別是mysql.user、mysql.db、mysql.host、mysql.tables_priv和mysql.columns_priv。作為GRANT命令的替代,可以直接修改這些表。

權限的類型和級別

MySQL中存在3個基本類型的權限:適用於賦予一般用戶的權限、適用於賦予管理員的權限和幾個特定的權限。任何用戶都可以被賦予這3類權限,但是根據最少權限原則,最好嚴格限定只將管理員類型的權限賦予管理員。

我們應該只賦予用戶訪問他們必須使用的數據庫和表的權限。而不應該將訪問mysql的權限賦予不是管理員的人。mysql數據庫是所有用戶名、密碼等信息存儲的地方。

常規用戶的權限直接與特定的SQL命令類型以及用戶是否被允許運行它們相關。下錶所示的是基本用戶權限。“應用於”列下面的對象給出了該類型權限可以授予的對象。

用戶的權限

權限 應用於 描述
SELECT 表、列 允許用戶從表中查詢行(記錄)
INSERT 表、列 允許用戶在表中插入新行
UPDATE 表、列 允許用戶修改現存表裡行中的值
DELETE 允許用戶刪除現存表的行
INDEX 允許用戶創建和拖動特定表索引
ALTER 允許用戶改變現存表的結構,例如,可添加列、重命名列或表、修改列的數據類型
CREATE 數據庫、表 允許用戶創建新數據庫或表。如果在GRANT中指定了一個特定的數據庫或表,它們只能夠創建該數據庫或表,即它們必須首先刪除(drop)它
DROP 數據庫、表 允許用戶拖動(刪除)數據庫或表

從系統的安全性方面考慮,適於常規用戶的權限大多數是相對無害的。ALTER權限通過重命名表可能會影響權限系統,但是大多數用戶需要它。安全性常常是可用性與保險性的折中。遇到ALTER的時候,應當做出自己的選擇,但是通常還是會將這個權限授予用戶。

下面的表給出了適用於管理員用戶使用的權限。
可以將這些權限授予非管理員用戶,這樣做的時候要非常小心。
FILE權限有些不同,它對普通用戶非常有用,因為它可以將數據從文件載入數據庫,從而可以節省許多時間,否則,每次將數據輸入數據庫都需要重新輸入,這很浪費時間。
然而,文件載入可以用來載入MySQL可識別的任何文件,包括屬於其他用戶的數據庫和潛在的密碼文件。授予該權限的時候需要小心,或者自己為用戶載入數據。

管理員權限

權限 描述
CREATE TEMPORARY TABLES 允許管理員在CREATE TABLE語句中使用TEMPORARY關鍵字
FILE 允許將數據從文件讀入表,或從表讀入文件
LOCK TABLES 允許使用LOCK TABLES語句
PROCESS 允許管理員查看屬於所有用戶的服務器進程
RELOAD 允許管理員重新載入授權表、清空授權、主機、日誌和表
REPLICATION CLIENT 允許管理員重新載入授權表、和從機(Slave)上使用SHOW STATUS
REPLICATION SLAVE 允許複製從服務器連接到主服務器
SHOW DATABASES 允許使用SHOW DATABASES語句查看所有數據庫列表。沒有這個權限,用戶只能看到他們能夠看到的數據庫
SHUTDOWN 允許管理員關閉MySQL服務器
SUPER 允許管理員關閉屬於任何用戶的的線程

特別的權限

權限 描述
ALL 授予上面兩個表列表的所有權限。也可以將ALL寫成ALL PRIVILEGES
USAGE 不授予權限。這創建一個用戶並允許他登錄,但是不允許進行任何操作。通常會在以後授予該用戶更多的權限

REVOKE命令

與GRANT相反的命令是REVOKE。它用來從一個用戶收回權限。在語法上與GRANT非常相似:

REVOKE privileges [(columns)]
ON item
FROM user_name

中文翻譯:

REVOKE 權限列表 ON 數據庫.對象 FROM 用戶名@主機名;

如果已經給出了WITH GRANT OPTION子句,可以按如下方式撤銷它(以及所有其他權限):

REVOKE ALL PRIVILEGES, GRANT
FROM user_name

權限立即生效

當我們修改用戶權限之後,如果想不重啟立即生效,需要執行以下flush privileges,這樣能快速刷新權限

FULSH PRIVILEGES;

使用GRANT和REVOKE的例子

要創建一個管理員,可以輸入如下所示的命令:

grant all on * to fred identified by 'mnb123' with grant option;

以上命令授予了用戶名為fred、密碼為mnb123的用戶使用所有數據庫的所有權限,並允許他向其他人授予這些權限。

如果不希望用戶在系統中存在,可以按如下方式撤銷:

revoke all privileges, grant from red;

現在,我們可以按如下方式創建一個沒有任何權限的常規用戶:

grant usage on books.* to sally identified by 'magic123';

在與sally交談后,我們對她需要進行的操作有了一定的了解,因此按如下方式可以授予她適當的權限:

grant select, insert, update, delete, index, alter, create, drop on books.* to sally;

請注意,要完成這些,並不需要指定sally密碼。

如果我們認為sally權限過高,可能會決定按如下方式減少一些權限:

revoke alter, create, drop on books.* from sally;

後來,當她不再需要使用數據庫時,可以按如下方式撤銷所有的權限:

revoke all on books.* from sally;

參考資料:

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

dom4j的測試例子和源碼詳解(重點對比和DOM、SAX的區別)

目錄

簡介

dom4j用於創建和解析XML文件,不是純粹的DOMSAX,而是兩者的結合和改進,另外,dom4j支持Xpath來獲取節點。目前,由於其出色的性能和易用性,目前dom4j已經得到廣泛使用,例如SpringHibernate就是使用dom4j來解析xml配置。

注意,dom4j使用Xpath需要額外引入jaxen的包。

DOM、SAX、JAXP和DOM4J

其實,JDK已經帶有可以解析xml的api,如DOMSAXJAXP,但為什麼dom4j會更受歡迎呢?它們有什麼區別呢?在學習dom4j之前,需要先理解下DOMSAX等概念,因為dom4j就是在此基礎上改進而來。

xerces解釋器

先介紹下xerces解釋器,下面介紹的SAXDOMJAXP都只是接口,而xerces解釋器就是它們的具體實現,在com.sun.org.apache.xerces.internal包。xerces被稱為性能最好的解釋器,除了xerces外,還有其他的第三方解釋器,如crimson

SAX

JDK針對解析xml提供的接口,不是具體實現,在org.xml.sax包。SAX基於事件處理,解析過程中根據當前的XML元素類型,調用用戶自己實現的回調方法,如:startDocument();,startElement()。下面以例子說明,通過SAX解析xml並打印節點名:

    /*這裏解釋下四個的接口:
    EntityResolver:需要實現resolveEntity方法。當解析xml需要引入外部數據源時觸發,通過這個方法可以重定向到本地數據源或進行其他操作。
    DTDHandler:需要實現notationDecl和unparsedEntityDecl方法。當解析到"NOTATION", "ENTITY"或 "ENTITIES"時觸發。
    ContentHandler:最常用的一個接口,需要實現startDocument、endDocument、startElement、endElement等方法。當解析到指定元素類型時觸發。
    ErrorHandler:需要實現warning、error或fatalError方法。當解析出現異常時會觸發。
    */
    @Test
    public void test04() throws Exception {
        //DefaultHandler實現了EntityResolver, DTDHandler, ContentHandler, ErrorHandler四個接口     
        DefaultHandler handler = new DefaultHandler() {
            @Override
            //當解析到Element時,觸發打印該節點名
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                System.out.println(qName);
            }
        };
        //獲取解析器實例
        XMLReader xr = XMLReaderFactory.createXMLReader();
        //設置處理類
        xr.setContentHandler(handler);
        /*
         * xr.setErrorHandler(handler); 
         * xr.setDTDHandler(handler); 
         * xr.setEntityResolver(handler);
         */
        xr.parse(new InputSource("members.xml"));
    }

因為SAX是基於事件處理的,不需要等到整個xml文件都解析完才執行我們的操作,所以效率較高。但SAX存在一個較大缺點,就是不能隨機訪問節點,因為SAX不會主動地去保存處理過的元素(優點就是內存佔用小、效率高),如果想要保存讀取的元素,開發人員先構建出一個xml樹形結構,再手動往裡面放入元素,非常麻煩(其實dom4j就是通過SAX來構建xml樹)。

DOM

JDK針對解析xml提供的接口,不是具體實現,在org.w3c.dom包。DOM採用了解析方式是一次性加載整個XML文檔,在內存中形成一個樹形的數據結構,開發人員可以隨機地操作元素。見以下例子:

    @SuppressWarnings("restriction")
    @Test
    public void test05() throws Exception {
        //獲得DOMParser對象
        com.sun.org.apache.xerces.internal.parsers.DOMParser domParser = new com.sun.org.apache.xerces.internal.parsers.DOMParser();
        //解析文件
        domParser.parse(new InputSource("members.xml"));
        //獲得Document對象
        Document document=domParser.getDocument();
        // 遍歷節點
        printNodeList(document.getChildNodes());        
    }

通過DOM解析,我們可以獲取任意節點進行操作。但是,DOM有兩個缺點:

  1. 由於一次性加載整個XML文件到內存,當處理較大文件時,容易出現內存溢出。
  2. 節點的操作還是比較繁瑣。

以上兩點,dom4j都進行了相應優化。

JAXP

封裝了SAXDOM兩種接口,它並沒有為JAVA解析XML提供任何新功能,只是對外提供更解耦、簡便操作的API。如下:

DOM解析器

    @Test
    public void test02() throws Exception {
        // 獲得DocumentBuilder對象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        // 解析xml文件,獲得Document對象
        Document document = builder.parse("members.xml");
        // 遍歷節點
        printNodeList(document.getChildNodes());
    }

獲取SAX解析器

    @Test
    public void test03() throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxParser = factory.newSAXParser();
        saxParser.parse("members.xml", new DefaultHandler() {
            @Override
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                System.out.println(qName);
            }
        });
    }

其實,JAXP並沒有很大程度提高DOM和SAX的易用性,更多地體現在獲取解析器時實現解耦。完全沒有解決SAXDOM的缺點。

DOM4j

對比過dom4jJAXP就會發現,JAXP本質上還是將SAXDOM當成兩套API來看待,而dom4j就不是,它將SAXDOM結合在一起使用,取長補短,並對原有的api進行了改造,在使用簡便性、性能、面向接口編程等方面都要優於JDK自帶的SAXDOM

以下通過使用例子和源碼分析將作出說明。

項目環境

工程環境

JDK:1.8

maven:3.6.1

IDE:sts4

dom4j:2.1.1

創建項目

項目類型Maven Project,打包方式jar。

引入依賴

注意:dom4j使用XPath,必須引入jaxen的jar包。

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- dom4j的jar包 -->
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.1</version>
</dependency>
<!-- dom4j使用XPath需要的jar包 -->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>
<!-- 配置BeanUtils的包,這個我自定義工具類用的,如果只是簡單使用dom4j可以不引入 -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.3</version>
</dependency>

使用例子–生成xml文件

本例子將分別使用dom4j和JDK的DOM接口生成xml文件(使用JDK的DOM接口時會使用JAXP的API)。

需求

構建xml樹,添加節點,並生成xml文件。格式如下:

<?xml version="1.0" encoding="UTF-8"?>
<members>
  <students>
    <student name="張三" location="河南" age="18"/>
    <student name="李四" location="新疆" age="26"/>
    <student name="王五" location="北京" age="20"/>
  </students>
  <teachers>
    <teacher name="zzs" location="河南" age="18"/>
    <teacher name="zzf" location="新疆" age="26"/>
    <teacher name="lt" location="北京" age="20"/>
  </teachers>
</members>

生成xml文件–使用w3c的DOM接口

主要步驟

  1. 通過JAXP的API獲得Document對象,這個對象可以看成xml的樹;

  2. 將對象轉化為節點,並添加在Document這棵樹上;

  3. 通過Transformer對象將樹輸出到文件中。

編寫測試類

路徑:test目錄下的cn.zzs.dom4j

注意:因為使用的是w3cDOM接口,所以節點對象導的是org.w3c.dom包,而不是org.dom4j包。

    @Test
    public void test02() throws Exception {
        // 創建工廠對象
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        // 創建DocumentBuilder對象
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        // 創建Document對象
        Document document = documentBuilder.newDocument();

        // 創建根節點
        Element root = document.createElement("members");
        document.appendChild(root);

        // 添加一級節點
        Element studentsElement = (Element)root.appendChild(document.createElement("students"));
        Element teachersElement = (Element)root.appendChild(document.createElement("teachers"));

        // 添加二級節點並設置屬性
        Element studentElement1 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement1.setAttribute("name", "張三");
        studentElement1.setAttribute("age", "18");
        studentElement1.setAttribute("location", "河南");
        Element studentElement2 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement2.setAttribute("name", "李四");
        studentElement2.setAttribute("age", "26");
        studentElement2.setAttribute("location", "新疆"); 
        Element studentElement3 = (Element)studentsElement.appendChild(document.createElement("student"));
        studentElement3.setAttribute("name", "王五");
        studentElement3.setAttribute("age", "20");
        studentElement3.setAttribute("location", "北京");     
        Element teacherElement1 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement1.setAttribute("name", "zzs");
        teacherElement1.setAttribute("age", "18");
        teacherElement1.setAttribute("location", "河南"); 
        Element teacherElement2 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement2.setAttribute("name", "zzf");
        teacherElement2.setAttribute("age", "26");
        teacherElement2.setAttribute("location", "新疆");     
        Element teacherElement3 = (Element)teachersElement.appendChild(document.createElement("teacher"));
        teacherElement3.setAttribute("name", "lt");
        teacherElement3.setAttribute("age", "20");
        teacherElement3.setAttribute("location", "北京"); 
            
        // 獲取文件對象
        File file = new File("members.xml");
        if(!file.exists()) {
            file.createNewFile();
        }
        // 獲取Transformer對象
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        // 設置編碼、美化格式
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        // 創建DOMSource對象
        DOMSource domSource = new DOMSource(document);
        // 將document寫出
        transformer.transform(domSource, new StreamResult(new PrintWriter(new FileOutputStream(file))));    
    }   

測試結果

此時,在項目路徑下會生成members.xml,文件內容如下,可以看到,使用w3cDOM接口輸出的內容沒有縮進格式。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<members>
<students>
<student age="18" location="河南" name="張三"/>
<student age="26" location="新疆" name="李四"/>
<student age="20" location="北京" name="王五"/>
</students>
<teachers>
<teacher age="18" location="河南" name="zzs"/>
<teacher age="26" location="新疆" name="zzf"/>
<teacher age="20" location="北京" name="lt"/>
</teachers>
</members>

生成xml文件–使用dom4j的DOM接口

主要步驟

  1. 通過DocumentHelper獲得Document對象,這個對象可以看成xml的樹;

  2. 將對象轉化為節點,並添加在Document這棵樹上;

  3. 通過XMLWriter對象將樹輸出到文件中。

編寫測試類

路徑:test目錄下的cn.zzs.dom4j。通過對比,可以看出,dom4j的API相比JDK的還是要方便很多。

注意:因為使用的是dom4jDOM接口,所以節點對象導的是org.dom4j包,而不是org.w3c.dom包(dom4j一個很大的特點就是改造了w3cDOM接口,極大地簡化了我們對節點的操作)。

    @Test
    public void test02() throws Exception {
        // 創建Document對象
        Document document = DocumentHelper.createDocument();

        // 添加根節點
        Element root = document.addElement("members");

        // 添加一級節點
        Element studentsElement = root.addElement("students");
        Element teachersElement = root.addElement("teachers");

        // 添加二級節點並設置屬性,dom4j改造了w3c的DOM接口,極大地簡化了我們對節點的操作
        studentsElement.addElement("student").addAttribute("name", "張三").addAttribute("age", "18").addAttribute("location", "河南");
        studentsElement.addElement("student").addAttribute("name", "李四").addAttribute("age", "26").addAttribute("location", "新疆");
        studentsElement.addElement("student").addAttribute("name", "王五").addAttribute("age", "20").addAttribute("location", "北京");
        teachersElement.addElement("teacher").addAttribute("name", "zzs").addAttribute("age", "18").addAttribute("location", "河南");
        teachersElement.addElement("teacher").addAttribute("name", "zzf").addAttribute("age", "26").addAttribute("location", "新疆");
        teachersElement.addElement("teacher").addAttribute("name", "lt").addAttribute("age", "20").addAttribute("location", "北京");

        // 獲取文件對象
        File file = new File("members.xml");
        if(!file.exists()) {
            file.createNewFile();
        }
        // 創建輸出格式,不設置的話不會有縮進效果
        OutputFormat format = OutputFormat.createPrettyPrint();
        format.setEncoding("UTF-8");
        // 獲得XMLWriter
        XMLWriter writer = new XMLWriter(new FileWriter(file), format);
        // 打印Document
        writer.write(document);
        // 釋放資源
        writer.close();
    }

測試結果

此時,在項目路徑下會生成members.xml,文件內容如下,可以看出dom4j輸出文件會進行縮進處理,而JDK的不會:

<?xml version="1.0" encoding="UTF-8"?>

<members>
  <students>
    <student name="張三" age="18" location="河南"/>
    <student name="李四" age="26" location="新疆"/>
    <student name="王五" age="20" location="北京"/>
  </students>
  <teachers>
    <teacher name="zzs" age="18" location="河南"/>
    <teacher name="zzf" age="26" location="新疆"/>
    <teacher name="lt" age="20" location="北京"/>
  </teachers>
</members>

使用例子–解析xml文件

需求

  1. 解析xml:解析上面生成的xml文件,將學生和老師節點按以下格式遍歷打印出來(當然也可以再封裝成對象返回給調用者,這裏就不擴展了)。
student:name=張三,location=河南,age=18
student:name=李四,location=新疆,age=26
student:name=王五,location=北京,age=20
teacher:name=zzs,location=河南,age=18
teacher:name=zzf,location=新疆,age=26
teacher:name=lt,location=北京,age=20
  1. dom4j結合XPath查找指定節點

主要步驟

  1. 通過SAXReader對象讀取和解析xml文件,獲得Document對象,即xml樹;

  2. 調用Node的方法遍歷打印xml樹的節點;

  3. 使用XPath查詢指定節點。

測試遍歷節點

考慮篇幅,這裏僅給出一種節點遍歷方式,項目源碼中還給出了其他的幾種。

    /**
     *  測試解析xml
     */
    @Test
    public void test03() throws Exception {
        // 創建指定文件的File對象
        File file = new File("members.xml");
        // 創建SAXReader
        SAXReader saxReader = new SAXReader();
        // 將xml文件讀入成document
        Document document = saxReader.read(file);
        // 獲得根元素
        Element root = document.getRootElement();
        // 遞歸遍歷節點
        list1(root);
    }

    /**
     * 遞歸遍歷節點
     */
    private void list1(Element parent) {
        if(parent == null) {
            return;
        }
        // 遍歷當前節點屬性並輸出
        printAttr(parent);
        // 遞歸打印子節點
        Iterator<Element> iterator2 = parent.elementIterator();
        while(iterator2.hasNext()) {
            Element son = (Element)iterator2.next();
            list1(son);
        }
    }

測試結果如下:

-------第一種遍歷方式:Iterator+遞歸--------
student:name=張三,location=河南,age=18
student:name=李四,location=新疆,age=26
student:name=王五,location=北京,age=20
teacher:name=zzs,location=河南,age=18
teacher:name=zzf,location=新疆,age=26
teacher:name=lt,location=北京,age=20

測試XPath獲取指定節點

    @Test
    public void test04() throws Exception {
        // 創建指定文件的File對象
        File file = new File("members.xml");
        // 創建SAXReader
        SAXReader saxReader = new SAXReader();
        // 將xml文件讀入成document
        Document document = saxReader.read(file);
        // 使用xpath隨機獲取節點
        List<Node> list = document.selectNodes("//members//students/student");
        // List<Node> list = xmlParser.getDocument().selectSingleNode("students");
        // 遍歷節點
        Iterator<Node> iterator = list.iterator();
        while(iterator.hasNext()) {
            Element element = (Element)iterator.next();
            printAttr(element);
        }
    }

測試結果如下:

student:age=18,location=河南,name=張三
student:age=26,location=新疆,name=李四
student:age=20,location=北京,name=王五

XPath語法

利用XPath獲取指定節點,平時用的比較多,這裏列舉下基本語法。

表達式 結果
/members 選取根節點下的所有members子節點
//members 選取根節點下的所有members節點
//students/student[1] 選取students下第一個student子節點
//students/student[last()] 選取students下的最後一個student子節點
//students/student[position()<3] 選取students下前兩個student子節點
//student[@age] 選取所有具有age屬性的student節點
//student[@age=’18’] 選取所有age屬性為18的student節點
//students/* 選取students下的所有節點
//* 選取文檔中所有節點
//student[@*] 選取所有具有屬性的節點
//members/students\ //members/teachers

源碼分析

本文會先介紹dom4j如何將xml元素抽象成具體的對象,再去分析dom4j解析xml文件的過程(注意,閱讀以下內容前需要了解和使用過JDK自帶的DOMSAX)。

dom4j節點的類結構

先來看下一個完整xml的元素組成,可以看出,一個xml文件包含了DocumentElementCommentAttributeDocumentTypeText等等。

DOM的思想就是將xml元素解析為具體對象,並構建樹形數據結構。基於此,w3c提供了xml元素的接口規範,dom4j基本借用了這套規範(如下圖),只是改造了接口的方法,使得我們操作時更加簡便。

SAXReader.read(File file)

通過使用例子可知,我們解析xml文件的入口是SAXReader對象的read方法,入參可以是文件路徑、url、字節流、字符流等,這裏以傳入文件路徑為例。

注意:考慮篇幅和可讀性,以下代碼經過刪減,僅保留所需部分。

    public Document read(File file) throws DocumentException {
        //不管是URI,path,character stream還是byte stream,都會包裝成InputSource對象
        InputSource source = new InputSource(new FileInputStream(file));
        if (this.encoding != null) {
            source.setEncoding(this.encoding);
        }
        
        //下面這段代碼是為了設置systemId,當傳入URI且沒有指定字符流和字節流時,可以通過systemId去連接URL並解析
        //如果一開始傳入了字符流或字節流,這個systemId就是可選的
        String path = file.getAbsolutePath();
        if (path != null) {
            StringBuffer sb = new StringBuffer("file://");
            if (!path.startsWith(File.separator)) {
                sb.append("/");
            }
            path = path.replace('\\', '/');
            sb.append(path);
            source.setSystemId(sb.toString());
        }

        //這裏調用重載方法解析InputSource對象
        return read(source);
    }

SAXReader.read(InputSource in)

看到這個方法的代碼時,使用過JDK的SAX的朋友應該很熟悉,沒錯,dom4j也是採用事件處理的機制來解析xml。其實,只是這裏設置的SAXContentHandler已經實現好了相關的方法,這些方法共同完成一件事情:構建xml樹。明白這一點,應該就能理解dom4j是如何解決SAXDOM的缺點了。

注意:考慮篇幅和可讀性,以下代碼經過刪減,僅保留所需部分。

    public Document read(InputSource in) throws DocumentException {
        // 這裡會調用JAXP接口獲取XMLReader實現類對象
        XMLReader reader = getXMLReader();
        reader = installXMLFilter(reader);
        
        // 下面這些操作,是不是和使用JDK的SAX差不多,dom4j也是使用了事件處理機制。

        // EntityResolver:通過實現resolveEntity方法,當解析xml需要引入外部數據源時觸發,可以重定向到本地數據源或進行其他操作。
        EntityResolver thatEntityResolver = this.entityResolver;
        if (thatEntityResolver == null) {
            thatEntityResolver = createDefaultEntityResolver(in
                    .getSystemId());
            this.entityResolver = thatEntityResolver;
        }
        reader.setEntityResolver(thatEntityResolver);
        
        // 下面的SAXContentHandler繼承了DefaultHandler,即實現了EntityResolver, DTDHandler, ContentHandler, ErrorHandler等接口
        // 其中最重要的是ContentHandler接口,通過實現startDocument、endDocument、startElement、endElement等方法,當dom4j解析xml文件到指定元素類型時,可以觸發我們自定義的方法。
        // 當然,dom4j已經實現了ContentHandler的方法。具體實現的方法內容為:在解析xml時構建xml樹
        SAXContentHandler contentHandler = createContentHandler(reader);
        contentHandler.setEntityResolver(thatEntityResolver);
        contentHandler.setInputSource(in);
        boolean internal = isIncludeInternalDTDDeclarations();
        boolean external = isIncludeExternalDTDDeclarations();
        contentHandler.setIncludeInternalDTDDeclarations(internal);
        contentHandler.setIncludeExternalDTDDeclarations(external);
        contentHandler.setMergeAdjacentText(isMergeAdjacentText());
        contentHandler.setStripWhitespaceText(isStripWhitespaceText());
        contentHandler.setIgnoreComments(isIgnoreComments());
        reader.setContentHandler(contentHandler);

        configureReader(reader, contentHandler);
        
        // 使用事件處理機制解析xml,處理過程會構建xml樹
        reader.parse(in);
        // 返回構建好的xml樹
        return contentHandler.getDocument();
    }

SAXContentHandler

通過上面的分析,可知SAXContentHandlerdom4j構建xml樹的關鍵。這裏看下它的幾個重要方法和屬性。

startDocument()

    // xml樹
    private Document document;

    // 節點棧,棧頂存放當前解析節點(節點解析結束)、或當前解析節點的父節點(節點解析開始)
    private ElementStack elementStack;

    // 節點處理器,可以看成節點開始解析或結束解析的標誌
    private ElementHandler elementHandler;
    
    // 當前解析節點(節點解析結束)、或當前解析節點的父節點(節點解析開始)
    private Element currentElement;
    public void startDocument() throws SAXException {
        document = null;
        currentElement = null;
        
        // 清空節點棧
        elementStack.clear();
        // 初始化節點處理器
        if ((elementHandler != null)
                && (elementHandler instanceof DispatchHandler)) {
            elementStack.setDispatchHandler((DispatchHandler) elementHandler);
        }

        namespaceStack.clear();
        declaredNamespaceIndex = 0;

        if (mergeAdjacentText && (textBuffer == null)) {
            textBuffer = new StringBuffer();
        }

        textInTextBuffer = false;
    }

startElement(String,String,String,Attributes)

    public void startElement(String namespaceURI, String localName,
            String qualifiedName, Attributes attributes) throws SAXException {
        if (mergeAdjacentText && textInTextBuffer) {
            completeCurrentTextNode();
        }

        QName qName = namespaceStack.getQName(namespaceURI, localName,
                qualifiedName);
        // 獲取當前解析節點的父節點
        Branch branch = currentElement;

        if (branch == null) {
            branch = getDocument();
        }
        // 創建當前解析節點
        Element element = branch.addElement(qName);
        addDeclaredNamespaces(element);

        // 添加節點屬性
        addAttributes(element, attributes);
        
        //將當前節點壓入節點棧
        elementStack.pushElement(element);
        currentElement = element;
        entity = null; // fixes bug527062

        //標記節點解析開始
        if (elementHandler != null) {
            elementHandler.onStart(elementStack);
        }
    }

endElement(String, String, String)

    public void endElement(String namespaceURI, String localName, String qName)
            throws SAXException {
        if (mergeAdjacentText && textInTextBuffer) {
            completeCurrentTextNode();
        }
        // 標記節點解析結束
        if ((elementHandler != null) && (currentElement != null)) {
            elementHandler.onEnd(elementStack);
        }
        // 當前解析節點從節點棧中彈出
        elementStack.popElement();
        // 指定為棧頂節點
        currentElement = elementStack.peekElement();
    }

endDocument()

    public void endDocument() throws SAXException {
        namespaceStack.clear();
        // 清空節點棧
        elementStack.clear();
        currentElement = null;
        textBuffer = null;
    }

以上,dom4j的源碼分析基本已經分析完,其他具體細節後續再做補充。

參考以下資料:

本文為原創文章,轉載請附上原文出處鏈接:https://github.com/ZhangZiSheng001/dom4j-demo

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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