架構師必知必會:Java內置的控制反起色制」Service Provider」

前言

    Java統治服務器編程領域多年還未有退位趨勢,以IoC(控制反轉)思想爲核心的Spring功不可沒。大多數時候,咱們均可以使用Spring框架來實現咱們的依賴注入,但仍有不少場景,咱們指望本身的代碼有更少的依賴、適應更多的場景,好比跨Android和服務端、跨JVM語言的組件拼裝。javascript

    其實從Java6開始已經提供了一套依賴注入標準「Service Provider」和相應的工具」ServiceLoader」來實現咱們本身的控制反轉,且其已經普遍應用在JDK的擴展性設計之中(如:腳本引擎ScriptEngine, 字符集Charset,  文件系統FileSystems, 網絡通信NIO),並愈來愈多地被其它開源組件所使用(如:Web標準Servlet3.0, 通用日誌接口slf4j-api:1.3),Java9進一步對「Service Provider」進行擴展實現了Java的模塊化。因此」Service Provider」機制是Java愈來愈重要的基礎知識之一。html

    容我帶領各位,經過JDK文檔和源碼來一步步瞭解」Service Provider」,進而掌握經過Java自帶能力零依賴實現本身的動態依賴注入的方法,或按Java標準來擴展JDK、日誌、Http服務的能力。java

 

「Service Provider「標準

」Service Provider」首先是做爲一個標準被Javase所吸納:python

https://docs.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Service_Providergit

標準中對」Service Provider」的定義能夠總結爲三句話:github

  1. 一個」Service」(服務)就是一組知名的接口或(一般是抽象)類的集合,「Service Provider「(服務提供者)就是對服務的特定實現;
  2. 服務提供者,經過jar包中的「META-INF/services/fully-qualified.name.of.service.Interface「文件包含實現類的徹底限定名,將實現類發佈爲提供者;
  3. 服務查找機制,經過遍歷ClassPath中全部的上述文件內容,來查找並建立提供者的實例。

以上是」Service Provider」標準中最重要的三點,請你們務必徹底理解並牢記於心,標準中還有其它一些限制條件,只需瞭解以便問題的快速定位,如:web

  1. 提供者必須包含無參構造函數,以便服務查找機制能夠經過反射建立其實例;
  2. 提供者發佈文件中,能夠經過」#」開頭定義註釋行;
  3. 提供者發佈文件中,能夠經過換行來分隔多個實現類;
  4. 須在安全上下文中調用服務查找者…

 

服務加載機制

JDK中內置了「Service Provider「加載工具類」ServiceLoader」,經過靜態方法」ServiceLoader.load()」方法,便可建立指定類型的」Service Provider」迭代器(ServiceLoader自己),經過遍歷迭代器便可獲得全部Provider的實例。」ServiceLoader」的源代碼能夠在JDK中找到,實現」服務加載」最關鍵的是下面幾段(以JDK8源碼爲例):spring

一、」Service Provider」標準定義的服務發佈文件路徑前綴:apache

二、使用(系統或用戶)ClassLoader找到指定」Service」的」Provider」全部發布文件編程

三、根據發佈文件中的類名加載Provider的Class

四、經過Provider的Class建立Provider實例

咱們看到ServiceLoader經過調用提供者Class的newInstance方法,建立了服務提供者的實例,這也是爲何服務提供者須要有一個無參構造函數的緣由。

    出於Web應用安全隔離的須要,Tomcat在實現Servlet3.0標準的」 ServletContainerInitializer」應用自啓動機制中,使用了遵循」Service Provider」標準的另外一套服務查找實現」WebappServiceLoader」,主要區別在於去」WEB-INF/lib」下而不是直接經過類加載器查找」Service Provider」文件,感興趣的同窗能夠去看Tomcat的源碼:org.apache.catalina.startup.WebappServiceLoader

 

服務使用者

    羅馬不是一天建成的,JDK也是。從MessageDigest、Charset、ScriptEngine等誕生於Java不一樣時期的API來看,」Service Provider」標準和」ServiceLoader」工具類的使用從無到有,從選擇之一到惟一選擇,咱們能夠看出」Service Provider」和」ServiceLoader」機制將是擴展JDK已有服務(Charset、ScriptEngine、NIO等)的標準。

    讓咱們一塊兒經過腳本引擎ScriptEngine這一與」Service Provider」一塊兒誕生的API,來學習如何經過」Service Provider」擴展JDK服務,以及玩轉可擴展服務的設計。

    從Java6開始,JDK內置了一套javascript腳本引擎」NashornScriptEngine」,能夠很方便地在java裏面直接解析、運行js腳本,爲代碼提供動態執行能力,而不用依賴任何第三方庫:

public static void callJavascript() {
        //建立腳本引擎管理器
        ScriptEngineManager m = new ScriptEngineManager();
        //經過腳本引擎管理器查找並建立javascript引擎
        ScriptEngine jsEngine = m.getEngineByName("javascript");
        try {
            //綁定預約義參數
            Bindings bindings = jsEngine.getBindings(ScriptContext.ENGINE_SCOPE);
            bindings.put("a", 2);
            bindings.put("b", 3);
            //調用javascript的pow函數
            Object result = jsEngine.eval("Math.pow(a,b)", bindings);
            //打印返回值」8.0」
            System.out.println(result);
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }

變量、函數、與java互調用等更復雜的功能,你們能夠一試,就不在本文的討論範圍以內。

    除了自帶的JavaScript引擎,經過」Service Provider」機制,很容易擴展ScriptEngine,支持其它腳本,好比引入」org.python:jython:2.7.0」後,ScriptEngine就具有了執行Python腳本的能力:

package org.ctstudio;

import org.python.util.PythonInterpreter;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.Properties;

public class PyScriptEngineDemo {
    //打招呼函數,供Python腳本調用
    public static void sayHello(String name) {
        System.out.format("Hello %s!\n", name);
    }

public static void callPython() {
    //Jython引擎使用前須要提早作一些初始化工做
        Properties props = new Properties();
        props.setProperty("python.import.site", "false");
        PythonInterpreter.initialize(System.getProperties(), props, new String[]{});
        //建立腳本引擎管理器
        ScriptEngineManager m = new ScriptEngineManager();
        //經過腳本引擎管理器查找並建立jython引擎
        ScriptEngine pyEngine = m.getEngineByName("jython");
        try {
            //執行Python腳本,在其中調用java函數
            pyEngine.eval("from org.ctstudio import PyScriptEngineDemo\n" +
                    "PyScriptEngineDemo.sayHello('Jack')");
            //Hello Jack!
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        callPython();
    }
}

ScriptEngineManager是如何使用」Service Provider」機制的呢?經過JDK源代碼,咱們能夠看到,ScriptEngineManager正是經過」ServiceLoader.load()」方法來發現全部腳本引擎:

 

    因爲腳本引擎實例的建立是一件開銷比較大的事情,而」ServiceLoader」在迭代過程當中就會直接建立提供者的實例,因此ScriptEngineManager沒有直接返回ScriptEngine,而是使用抽象工廠模式發現並保存ScriptEngineFactory實例,只在真正須要的時候才經過具體工廠建立ScriptEngine的實例:

 

    Servlet3.0的設計則較直接,」ServletContainerInitializer」的提供者被發現並建立實例,隨後全部提供者的」 onStartup」被調用,以觸發用戶自定義的初始化過程,具體可參考Tomcat的源碼:

org.apache.catalina.startup.ContextConfig:

org.apache.catalina.core.StandardContext:

 

Java最有名的日誌框架之一logback就是利用Servlet3.0的這一機制來初始化:

 

而Spring框架則利用Servlet3.0的這一機制實現了將Bean與服務提供者集成起來的WebApplicationInitializer,Spring-Boot則進一步在WebApplicationInitializer的基礎之上實現了Web應用拉起工具基類SpringBootServletInitializer,使得咱們能夠在Servlet容器中輕鬆拉起咱們的Spring應用:

 

最新版本的日誌外觀slf4j2.0(未發佈)的LoggerFactory已改由」Service Provider」標準來加載日誌實現:

Logback1.3(未發佈,其做者也是log4j、slf4j的做者)也改用」Service Provider」機制來實現與日誌外觀的集成:

 

結語

    如此多舉足輕重的開源軟件的重視,足以體現」Service Provider」做爲Java標準的控制反起色制的重要影響,並可預見其將會愈來愈重要。無論你是想要爲開源軟件做貢獻,或是設計本身的可擴展組件,」Service Provider」都是你有必要掌握的知識。

參考資料

  1. Jar包標準中關於"Service Provider"的介紹
  2. JDK源碼:腳本引擎ScriptEngine, 字符集Charset,  文件系統FileSystems, 網絡通信NIO
  3. Tomcat源碼中的"ContextConfig", "StandardContext"
  4. Spring-Web源碼中的"SpringServletContainerInitializer"
  5. Spring-Boot源碼中的"SpringBootServletInitializer"
  6. 最新的logback源碼
相關文章
相關標籤/搜索