文档章节

架构师必知必会:Java内置的控制反转机制”Service Provider”

chentao106
 chentao106
发布于 02/12 22:24
字数 2040
阅读 6.3K
收藏 8

前言

    Java统治服务器编程领域多年还未有退位趋势,以IoC(控制反转)思想为核心的Spring功不可没。大多数时候,我们都可以使用Spring框架来实现我们的依赖注入,但仍有很多场景,我们期望自己的代码有更少的依赖、适应更多的场景,比如跨Android和服务端、跨JVM语言的组件拼装。

    其实从Java6开始已经提供了一套依赖注入标准“Service Provider”和相应的工具”ServiceLoader”来实现我们自己的控制反转,且其已经广泛应用在JDK的扩展性设计之中(如:脚本引擎ScriptEngine, 字符集Charset,  文件系统FileSystems, 网络通讯NIO),并越来越多地被其它开源组件所使用(如:Web标准Servlet3.0, 通用日志接口slf4j-api:1.3),Java9进一步对“Service Provider”进行扩展实现了Java的模块化。所以”Service Provider”机制是Java越来越重要的基础知识之一。

    容我带领各位,通过JDK文档和源码来一步步了解”Service Provider”,进而掌握通过Java自带能力零依赖实现自己的动态依赖注入的方法,或按Java标准来扩展JDK、日志、Http服务的能力。

 

“Service Provider“标准

”Service Provider”首先是作为一个标准被Javase所吸纳:

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

标准中对”Service Provider”的定义可以总结为三句话:

  1. 一个”Service”(服务)就是一组知名的接口或(通常是抽象)类的集合,“Service Provider“(服务提供者)就是对服务的特定实现;
  2. 服务提供者,通过jar包中的“META-INF/services/fully-qualified.name.of.service.Interface“文件包含实现类的完全限定名,将实现类发布为提供者;
  3. 服务查找机制,通过遍历ClassPath中所有的上述文件内容,来查找并创建提供者的实例。

以上是”Service Provider”标准中最重要的三点,请大家务必完全理解并牢记于心,标准中还有其它一些限制条件,只需了解以便问题的快速定位,如:

  1. 提供者必须包含无参构造函数,以便服务查找机制可以通过反射创建其实例;
  2. 提供者发布文件中,可以通过”#”开头定义注释行;
  3. 提供者发布文件中,可以通过换行来分隔多个实现类;
  4. 须在安全上下文中调用服务查找者…

 

服务加载机制

JDK中内置了“Service Provider“加载工具类”ServiceLoader”,通过静态方法”ServiceLoader.load()”方法,即可创建指定类型的”Service Provider”迭代器(ServiceLoader本身),通过遍历迭代器即可得到所有Provider的实例。”ServiceLoader”的源代码可以在JDK中找到,实现”服务加载”最关键的是下面几段(以Oracle JDK8源码为例):

1、”Service Provider”标准定义的服务发布文件路径前缀:

2、使用(系统或用户)ClassLoader找到指定”Service”的”Provider”所有发布文件

3、根据发布文件中的类名加载Provider的Class

4、通过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源码

 

© 著作权归作者所有

chentao106

chentao106

粉丝 7
博文 3
码字总数 7868
作品 0
深圳
私信 提问
加载中

评论(2)

邓磊磊
邓磊磊
写的好哇参考很全面
yong9981
yong9981
写的真好,学习了。
【项目管理】软件项目经理须知的 Java 8 安全知识

【译者按】作为软件研发项目的项目经理,只懂项目管理知识是不够的,需要对软件技术本身有基本的了解。Java 是一种主流的系统开发语言,其安全设计对于构建安全的信息系统有至关重要的意义。...

军雷
2017/06/08
0
0
组件化框架设计之Java SPI机制(三)

11/12号文档资料已全面更新!;《【阿里P7】移动互联网架构师高级教程+BAT面试题》,点击下方链接前往领取: 【阿里P7】移动互联网架构师进阶高级教程+BAT面试题 本篇文章将从深入理解java ...

Android进阶开发
2019/11/14
0
0
资深架构师带你详细了解,Spring之IoC容器

一、 IoC概述 IoC(Inverse of Control,控制反转)是Spring容器的内核,AOP、声明式事务等功能在此基础上开花结构。不过IoC确实包括很多内涵,涉及代码解耦、设计模式、代码优化等问题的考量...

Java-飞鱼
2019/07/01
45
0
跟面试官聊到JVM,他99%会让你谈谈这个问题!

但凡问到 JVM(Java 虚拟机)通常有 99% 的概率一定会问: 在 JVM 中如何判断一个对象的生死状态? 本文就来聊聊这个问题,判断对象的生死状态的算法有以下几个: 1、引用计数器算法 引用计算...

别打我会飞
2019/05/16
55
0
阿里P7架构师:这些技术点没搞懂,我劝你不要跳槽!

阿里P7架构师架构师:这些技术点没搞懂,我劝你不要跳槽! 哪些技术点呢? 废话不多说,技术点全在下面这6张图里面了! 1.怎么看源码? 2.分布式 3.微服务 4.性能优化 5.工程化 粉丝福利:一...

别打我会飞
2019/05/24
175
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周一乱弹 —— 提前接受社会的毒打教育

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @爱吃蛋挞的kk:分享Buddy Guy的单曲《I Need You Tonight》 《I Need You Tonight》- Buddy Guy 手机党少年们想听歌,请使劲儿戳(这里) 凌...

小小编辑
29分钟前
46
0
成都哪里可以开租赁费发票-成都新闻网

成都哪里可以开租赁费发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用了...

提供格
31分钟前
45
0
成都哪里可以开钢材发票-成都新闻网

成都哪里可以开钢材发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用了该...

多徐重
33分钟前
29
0
成都哪里可以开医疗器械发票-成都新闻网

成都哪里可以开医疗器械发票【1.3.2 - 2.9.3.0 - 0.5.6.8.】李生,adb的全称为Android Debug Bridge,是Android手机通用的一个USB端口。百度CarLife的部分车机采用...

识过人石
35分钟前
75
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部