架构师必知必会:Java内置的控制反转机制”Service Provider”
前言
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”的定义可以总结为三句话:
- 一个”Service”(服务)就是一组知名的接口或(通常是抽象)类的集合,“Service Provider“(服务提供者)就是对服务的特定实现;
- 服务提供者,通过jar包中的“META-INF/services/fully-qualified.name.of.service.Interface“文件包含实现类的完全限定名,将实现类发布为提供者;
- 服务查找机制,通过遍历ClassPath中所有的上述文件内容,来查找并创建提供者的实例。
以上是”Service Provider”标准中最重要的三点,请大家务必完全理解并牢记于心,标准中还有其它一些限制条件,只需了解以便问题的快速定位,如:
- 提供者必须包含无参构造函数,以便服务查找机制可以通过反射创建其实例;
- 提供者发布文件中,可以通过”#”开头定义注释行;
- 提供者发布文件中,可以通过换行来分隔多个实现类;
- 须在安全上下文中调用服务查找者…
服务加载机制
JDK中内置了“Service Provider“加载工具类”ServiceLoader”,通过静态方法”ServiceLoader.load()”方法,即可创建指定类型的”Service Provider”迭代器(ServiceLoader本身),通过遍历迭代器即可得到所有Provider的实例。”ServiceLoader”的源代码可以在JDK中找到,实现”服务加载”最关键的是下面几段(以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”都是你有必要掌握的知识。
参考资料
- Jar包标准中关于"Service Provider"的介绍
- JDK源码:脚本引擎ScriptEngine, 字符集Charset, 文件系统FileSystems, 网络通讯NIO
- Tomcat源码中的"ContextConfig", "StandardContext"
- Spring-Web源码中的"SpringServletContainerInitializer"
- Spring-Boot源码中的"SpringBootServletInitializer"
- 最新的logback源码
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
KDE Plasma 5.18 LTS 版本发布
距离 KDE Plasma5.18 LTSBeta 版本发布近一个月时间之后,目前,KDE Plasma 5.18 LTS 版本已完成了发布。LTS 代表“长期支持”,这意味着 KDE 贡献者将在未来两年内更新并维护 5.18(常规版本将维持 4 个月)。 该版本的一些亮点内容如下: 可以更好地与 GTK / GNOME 应用程序集成 新的可选系统/用户反馈功能, 表情符号选择器/选择器已添加到桌面, Plasma天气小部件得到了改进 在 KSysGuard 中支持 NVIDIA GPU 统计信息 各种 X11 和 Wayland 的 KWin 修复程序 通知改进 在 Plasma 桌面周围进行的大量优化工作。 自 5.12 LTS 以来的新功能 对于从先前的长期支持版本(Plasma 5.12)进行升级的人员,以下是过去两年的发展中的一些重点: 完全改写的通知系统 浏览器整合 重新设计的系统设置页面,使用一致的网格视图或经过大修的界面 GTK 应用程序现在尊重更多设置,使用 KDE 配色方案,在 X11 上具有阴影,并支持全局菜单 显示管理方面的改进,包括新的 OSD 和小部件 Fl...
- 下一篇
Prometheus 2.16.0-rc.1 发布,Go 编写的服务监控系统
Prometheus 2.16.0-rc.1 发布了,Prometheus 是一个 Go 语言开发的开源的服务监控系统和时间序列数据库。该版本引入了一些新特性,比如记录其他组件、增强功能和修复 bug,这些都是为了提高可用性。 更新内容如下: FEATURE React UI:在 /graph 上支持本地时区#6692 PromQL:添加 absent_over_time 查询功能#6490 将查询的可选日志记录添加到自己的文件中#6520 BUGFIX React UI:在旧版浏览器上的 fetch() 上发送 cookie#6553 React UI:对堆叠图采用 grafana flot 修复#6603 React UI:图形页面浏览器的历史记录已损坏,因此后退按钮可以按预期工作#6659 TSDB:确保已记录 compactionsSkipped 指标,如果从 head 返回一个错误,则记录正确的错误#6616 …… 更新说明:https://github.com/prometheus/prometheus/releases/tag/v2.16.0-rc.1
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7设置SWAP分区,小内存服务器的救世主