我是SPI,我讓框架更加優雅了!


文章首發於【陳樹義的博客】,點擊跳轉到原文《我是 SPI,我讓框架更加優雅了!》

自從上次小黑進入公司的架構組之后,小黑就承擔起整個公司底層框架的開發工作。就在剛剛,小黑又接到一個任務:做一個通用的歌曲信息解析框架。即輸入歌曲數據,之后返回該歌曲的名稱、作者、時長等時間。

接到項目的小黑經過兩天的奮戰,終於把第一個版本的歌曲解析框架完成了。第一版的歌曲解析框架是這樣的:

public class ParseUtil{
    public static Song parseMp3Song(byte[] data){
        //parse song according to mp3 data format
    }
}

使用的人只需要引入工具類,之后調用 parseMp3Song() 方法即可,非常方便。

ParseUtil.parseMp3Song(data);	  //song stored with mp3 format

過了幾天領導又找上門來了,說有些歌曲是用 mp4 格式存儲的,你這個方法就用不了啊。你今天下班之前趕緊把這個功能加上,其他項目急着用呢。苦逼的小黑加班加點在 ParseUtil 中加上了 parseMp4Song 這個方法,於是第二版的歌曲解析框架是這樣的:

public class ParseUtil{
    public static Song parseMp4Song(byte[] data){
        //parse song according to mp4 data format
    }
}

寫完之后小黑趕緊將框架版本升級到 2.0.0,並通知使用框架的兄弟們升級框架,並修改相關代碼。這時候使用框架是這樣的:

ParseUtil.parseMp3Song(data);	  //song stored with mp3 format
ParseUtil.parseMp4Song(data);	  //song stored with mp4 format

但第二版本的歌曲解析框架上線之后,小黑覺得這樣的設計並不好,要是后面又有新的歌曲格式,那我豈不是還得修改框架。而且對於使用框架的人來說,這種使用方式並不友好。因為每次調用框架之前,都需要知道解析的歌曲是什么格式,如果是 mp3 格式的歌曲,那么調用 ParseUtil.parseMp3Song(data) 方法。,如果是 mp4 格式的歌曲,那么調用 ParseUtil.parseMp4Song(data) 方法。這未免太笨了吧!

小黑想:無論對於什么樣歌曲,都不應該讓框架使用者去關心它的格式。框架使用者只需要將數據傳給我,我再將結果告訴他就好了。

就在小黑冥思苦想的時候,站在一旁的樹義同學說:你想一想,這種情況是不是有點像我們使用 JDBC 連接數據庫?

當我們想使用 MySQL 數據庫的時候,我們需要引入 mysql 的驅動包。

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.6</version>
</dependency>

而當我們使用 SQLServer 數據庫的時候,我們需要引入 SQLServer 的驅動包。

<dependency>
  <groupId>com.microsoft.sqlserver</groupId>
  <artifactId>mssql-jdbc</artifactId>
  <version>6.4.0.jre8</version>
</dependency>

但是我們在獲取數據庫連接的時候,卻都是用同樣的代碼:

Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
Statement stmt = conn.createStatement();
String sql = "SELECT id, name, url, comment FROM blog";
ResultSet rs = stmt.executeQuery(sql);

那我們能不能也參考 JDBC 的設計方法,把歌曲解析器這個單獨抽離出來,當需要增加一個新的歌曲解析器時,直接引入相關的解析器 Jar 包就好了。這樣在增加歌曲格式解析器時,我們就不需要修改框架代碼,只需要新增一個特定格式解析器的 Jar 包就可以。

按着這種實現思路,小黑立即着手開始第三版歌曲解析框架的開發。經過三天三夜的開發,框架終於開發完成,這時候的框架分成了三個部分:

  • song-parser 項目。負責定義通用的歌曲解析接口,並不提供任何具體的歌曲解析器實現。
  • song-parser-mp3 項目。實現了 song-parser 項目的歌曲解析接口,實現了 mp3 格式歌曲的解析。
  • song-parser-mp4 項目。實現了 song-parser 項目的歌曲解析接口,實現了 mp4 格式歌曲的解析。

這時候使用歌曲解析框架的流程是這樣的:

首先,在項目中引入 song-parser 項目以及具體的解析實現,例如這里我引入 song-parser-mp3、song-parser-mp4 項目。

//歌曲解析框架
<dependency>
    <groupId>com.chenshuyi.demo</groupId>
    <artifactId>song-parser</artifactId>
    <version>1.0.0</version>
</dependency>
//引入MP3歌曲解析器
<dependency>
    <groupId>com.xiaohei.demo</groupId>
    <artifactId>song-parser-mp3</artifactId>
    <version>1.0.0</version>
</dependency> 

這里引入了 mp3 歌曲解析器,那么我們就可以在項目中解析 mp3 格式的歌曲。

//parse mp3 song
Song song = ParserManager.getSong(mockSongData("MP3")); 

如果需要解析 mp4 格式的歌曲,那我們引入 mp4 歌曲解析器:

<dependency>
    <groupId>com.chenshuyi.demo</groupId>
    <artifactId>song-parser</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>com.xiaohei.demo</groupId>
    <artifactId>song-parser-mp3</artifactId>
    <version>1.0.0</version>
</dependency>
//引入MP4歌曲解析器
<dependency>
    <groupId>com.xiaoshu.demo</groupId>
    <artifactId>song-parser-mp4</artifactId>
    <version>1.0.0</version>
</dependency>

之后還是使用 ParserManager.getSong(byte[] data) 方法進行歌曲信息解析:

//parse mp4 song
Song song = ParserManager.getSong(mockSongData("MP4")); 

經過這樣的一個設計,我們發現升級之后,使用的人並不需要修改原有的代碼,也不需要升級原有的框架版本,只需要將新的歌曲解析器 Jar 包引入即可。

看着最新完成的第三版歌曲解析框架,小黑暗暗得意自己的架構設計,覺得這絕對是一個划時代的創造。於是趕緊跟樹義分享自己的設計思路,沒想到樹義卻淡定地說:其實這個就是 Java 的 SPI 機制,英文全稱是 Service Provider Interface,常用於框架的可擴展實現。Java 語言的 JDBC、JDNI 就使用了這種技術,甚至我們常用的 dubbo 也是在 Java SPI 機制基礎上做的改進。

小黑怪不好意思地摸摸頭,原來 Java 的創造者早就想到了,我還以為自己創造了一種新的開發方式呢!雖然樹義知道是用 SPI 機制實現的,但樹義還是對小黑怎么做出這個框架感到好奇,於是問小黑:你這個框架到底是咋做的叻,說出來讓我們學習學習唄!

小黑得意地打開 IDE 編輯器,滔滔不絕地說起來。其實這個「歌曲解析框架」分為兩個部分:

  • song-parser 項目。負責定義通用的歌曲解析接口,並不提供任何具體的歌曲解析器實現。
  • song-parser-xxx 項目。實現了 song-parser 項目的歌曲解析接口,實現了 xxx 格式歌曲的解析。例如上面說的,song-parser-mp3 實現了 mp3 格式歌曲的解析,song-parser-mp4 實現了 mp4 格式歌曲的解析,等等。

song-parser 項目

song-parser 項目定義了通用的歌曲解析接口,並不提供具體的解析實現。在 song-parser 項目定義了下面兩個關鍵的接口和類:Parser 接口、ParserManager 類。

  • Parser 接口

定義了抽象的解析方法,傳入歌曲的數據,返回歌曲的信息。

public interface Parser {
    Song parse(byte[] data) throws Exception;
}
  • ParseManager 類

主要包括兩個三個部分:

loadInitialParsers() 用於程序啟動時初始化所有的歌曲解析器。

ParserManager.registerParser()用於歌曲解析器的注冊。

ParserManager.getSong()提供了獲取歌曲信息的方法。

public class ParserManager {

    private final static CopyOnWriteArrayList<ParserInfo> registeredParsers = new CopyOnWriteArrayList<>();

    static {
        loadInitialParsers();
        System.out.println("SongParser initialized");
    }

    private static void loadInitialParsers() {
        ServiceLoader<Parser> loadedParsers = ServiceLoader.load(Parser.class);
        Iterator<Parser> driversIterator = loadedParsers.iterator();
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {
            // Do nothing
        }
    }

    public static synchronized void registerParser(Parser parser) {
        registeredParsers.add(new ParserInfo(parser));
    }

    public static Song getSong(byte[] data) {
        for (ParserInfo parserInfo : registeredParsers) {
            try {
                Song song = parserInfo.parser.parse(data);
                if (song != null) {
                    return song;
                }
            } catch (Exception e) {
                //wrong parser, ignored it.
            }
        }
        throw new ParserNotFoundException("10001", "Can not find corresponding data:" + new String(data));
    }
}

其實上面的幾個方法對應了 Service Provider Framework 的四個概念:

  • Service Interface 服務接口,這里對應 Song 接口。
  • Provider Registration API 用戶注冊接口,這里對應 ParserManager.registerParser() 方法。
  • Service Access API 獲取服務實例方法,這里對應 ParserManager.getSong() 方法。
  • Service Provider Interface 創建服務實現的接口,這里對應 Parser 接口。

所有借助 Java SPI 機制實現的框架,除了 Service Interface 服務接口不是必須的之外,其他三個都是必須要有的。

這里我們用 mp3 歌曲解析器為例,來看看到底是如何實現的插件式的歌曲解析的。

在 song-parse-mp3 項目中有兩個類和一個描述文件,分別是:com.chenshuyi.demo.Parser 文件、Parser 類和 Mp3Parser 類。

  • com.chenshuyi.demo.Parser 文件

該文件位於/resources/META-INF/services目錄下,包含如下地址:com.xiaohei.demo.Parser,表示歌曲解析的具體實現類。

  • Parser 類

在 Parser 類中,其調用 ParserManager.registerParser() 類將解析器注冊到一個 List 集合中。

public class Parser extends Mp3Parser implements com.chenshuyi.demo.Parser {
    static
    {
        try
        {
            ParserManager.registerParser(new Parser());
        }
        catch (Exception e)
        {
            throw new RuntimeException("Can't register parser!");
        }
    }
}
  • Mp3Parser 類

而在 Parser.parse() 方法中,則實現了具體的解析業務邏輯。

public class Mp3Parser implements Parser {

    public final byte[] FORMAT = "MP3".getBytes();

    public final int FORMAT_LENGTH = FORMAT.length;

    @Override
    public Song parse(byte[] data) throws Exception{
        if (!isDataCompatible(data)) {
            throw new Exception("data format is wrong.");
        }
        //parse data by mp3 format type
        return new Song("劉千楚", "mp3", "《北京東路的日子》", 220L);
    }

    private boolean isDataCompatible(byte[] data) {
        byte[] format = Arrays.copyOfRange(data, 0, FORMAT_LENGTH);
        return Arrays.equals(format, FORMAT);
    }
}

當我們調用以下語句去獲取歌曲信息時,因為 ParserManager.getSong() 是靜態方法,所以會先初始化 ParserManager 類。

Song song = ParserManager.getSong(mockSongData("MP3"));

在 ParserManager 類中有下面這段代碼:

static {
    loadInitialParsers();
    System.out.println("SongParser initialized");
}

在 loadInitialParsers() 方法中,調用了 Java 的 ServiceLoader 類獲取 Parser 接口的所有實現。

private static void loadInitialParsers() {
    ServiceLoader<Parser> loadedParsers = ServiceLoader.load(Parser.class);
    Iterator<Parser> driversIterator = loadedParsers.iterator();
    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
        // Do nothing
    }
}

當 ParserManager 初始化完成之后,就調用 getSong() 靜態方法。

public static Song getSong(byte[] data) {
    for (ParserInfo parserInfo : registeredParsers) {
        try {
            Song song = parserInfo.parser.parse(data);
            if (song != null) {
                return song;
            }
        } catch (Exception e) {
            //wrong parser, ignored it.
        }
    }
    throw new ParserNotFoundException("10001", "Can not find corresponding data:" + new String(data));
}

從下圖我們可以得知,其實 loadInitialParsers() 方法運行之后,是將所有 Parser 接口的所有視線都放到了 ParserManager.registeredParsers 這個 List 中。

ParserManager.getSong 方法循環遍歷所有歌曲解析器,一旦獲得正確的解析結果便返回。如果全部遍歷結束,還找不到正確的解析器,那么就返回 null。

在一旁的樹義聽着雖然有點懵,但是還是大概聽懂了。這不就是,但是還是覺得小黑很厲害。但說了這么多,我還不知道怎么用這個框架呢。如果我要新增一種來解析 rmvb 格式歌曲,那應該怎么做呢?小黑淡定地擺出 OK 的手勢說:10 分鍾搞定。

小黑首先創建了一個項目 song-parser-rmvb:

<groupId>com.anonymous.demo</groupId>
<artifactId>song-parser-rmvb</artifactId>
<version>1.0.0</version>

接着創建了一個 RmvbParser 類,用於實現具體的歌曲信息解析:

public class RmvbParser implements com.chenshuyi.demo.Parser {

    public final byte[] FORMAT = "RMVB".getBytes();

    public final int FORMAT_LENGTH = FORMAT.length;

    @Override
    public Song parse(byte[] data) throws Exception{
        if (!isDataCompatible(data)) {
            throw new Exception("data format is wrong.");
        }
        //parse data by rmvb format type
        return new Song("AGA", "rmvb", "《Wonderful U》", 240L);
    }

    private boolean isDataCompatible(byte[] data) {
        byte[] format = Arrays.copyOfRange(data, 0, FORMAT_LENGTH);
        return Arrays.equals(format, FORMAT);
    }
}

之后創建了一個 Parser 類,用於在啟動的時候向 ParserManager 類注冊解析器:

public class Parser extends RmvbParser implements com.chenshuyi.demo.Parser {
    static
    {
        try
        {
            ParserManager.registerParser(new Parser());
        }
        catch (Exception e)
        {
            throw new RuntimeException("Can't register parser!");
        }
    }
}

最后在創建了一個描述文件resources/META-INF/services/com.chenshuyi.demo.Parser,並填上了下面的內容:

com.anonymous.demo.Parser

改造完成之后,小黑將新的 RMVB 解析器信息告訴了開發兄弟。開發兄弟在項目中引入了新的歌曲解析器依賴:

//新增rmvb歌曲解析器
<dependency>
    <groupId>com.anonymous.demo</groupId>
    <artifactId>song-parser-rmvb</artifactId>
    <version>1.0.0</version>
</dependency> 

之后使用 ParserManager.getSong(byte[] data) 方法進行歌曲信息解析:

Song song = ParserManager.getSong(mockSongData("RMVB"));
System.out.println("Name:" + song.getName());
System.out.println("Author:" + song.getAuthor());
System.out.println("Time:" + song.getTime());
System.out.println("Format:" + song.getFormat());

代碼成功運行,輸出:

Name:《Wonderful U》
Author:AGA
Time:240
Format:rmvb

站在一旁的樹義看得眼睛都呆了,這樣的開發效率真的很快,而且又很優雅!

樹義有話說

Java SPI 無處不在,通過使用 SPI 能夠讓框架的實現更加優雅,實現可插拔的插件開發。本文中的歌曲解析框架就是借鑒這種方式進行開發的,雖然只是一個簡化版的實現,但是其能讓你更快了解 SPI 機制的實現原理。

「歌曲解析框架」代碼已經上傳到 Github 上,感興趣的朋友可以下載代碼:chenyurong/song-parser-spi-demo。如果想進一步掌握 Java SPI 的應用,建議下載項目並自行擴展一個歌曲解析器,這樣可以最大程度上理解 Java SPI 機制。

很多朋友看到了這篇文章都說我好厲害啊,怎么能想出這么巧妙的方法,其實這些都是模仿 Java JDBC 的源碼的。有興趣的同學可以到我的博客看看這篇文章:帶你一行行深入解析JDBC源碼

文章首發於【陳樹義的博客】,點擊跳轉到原文《我是 SPI,我讓框架更加優雅了!》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM