zoukankan      html  css  js  c++  java
  • 探索Java9 模块系统和反应流

    Java9 新特性 ,Java 模块化,Java 反应流 Reactive,Jigsaw

    模块系统

    Java平台模块系统(JPMS)是Java9中的特性,它是Jigsaw项目的产物。简而言之,它以更简单和易于维护的方式来组织包和类型。

    直到Java8,系统仍面临与类型系统相关的两个问题:

    1.所有的组件(大多是Jar包)都处在classpath中,没有任何的显式依赖申明。诸如Maven之类的构建工具可以在开发过程中帮助组织这些构件。然而,在运行时却没有这样的支持工具。你最终可能会遇到calsspath中缺少某个类,或者更严重的是存在同个类的两个版本,向这样的错误很难诊断。

    2.在API级别上不支持封装。所有的public的类在整个应用中都可以访问,经管事实上这些类只是想供一部分其他类调用。另一方面,私有的类和私有的成员也不是私有的,因为你可以使用反射来绕过访问限制。

    这些就是Java 模块系统要应对的地方。Oralce的Java平台首席架构师Mark Reinhold描述了Java模块系统的目标:

    1.可靠的配置 - 用程序组件相互声明显式依赖的方法替换脆弱,容易出错的类路径机制。

    2.强大的封装 - 允许组件声明其中哪些公共类型可供其他组件访问,哪些不可以。

    Java9 允许你使用模块描述符来定义模块。

    模块描述符

    模块描述符是模块系统的核心,模块描述的声明是在模块目录层次结构的根目录中名为module-info.java的文件中指定的。

    模块描述的声明是以module关键字开始的,其后紧跟的是模块的名字。声明结束标记是一对大括号,里边包含零个或者多个模块。你可以像这样声明一个空模块:

    module com.stackify { }

    在模块声明中你可以列出的指令有这些:

    • require 表示它所依赖的模块,也称作dependency。
    • transitive 仅与require指令一起使用,表明指明的依赖项也可以供此模块的依赖项访问。
    • exports 声明一个可以被其他模块访问的包
    • opens 在运行时曝光一个包,供反射API自省。
    • uses 指定此模块消费的服务的全限定名。
    • provides with – denotes an implementation, specified by the with keyword, for a service, indicated by provides

    模块化应用程序示例

    • dist
    • src
      • client
        • com
          • stackify
            • client
              • Main.java
        • module-info.java
      • impl
        • com
          • stackify
            • impl
              • AccessImpl.java
        • module-info.java
      • model
        • com
          • stackify
            • model
              • Person.java
        • module-info.java
      • service
        • com
          • stackify
            • service
              • AccessService.java
        • module-info.java

    示例应用程序有四个模块组成——model,service,impl,client。在实际项目中应该使用反向域名模式对模块命名,以避免名称冲突。本文例子中使用了简单的名称,便于掌握。每个模块的代码都在src根目录中,编译之后的文件会放在dist中。

    我们先从model模块开始,这个模块只有一个包,包里边有一个class。

    package com.stackify.model;
    
    public class Person {
        private int id;
        private String name;
    
        public Person(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }
    

    模块的声明是这样的:

    module model {
        exports com.stackify.model;
        opens com.stackify.model;
    }
    

    这个模块导出了com.stackify.model包,并为次包开启了内省。

    service模块中定义了一个接口:

    package com.stackify.service;
    
    import com.stackify.model.Person;
    
    public interface AccessService {
        public String getName(Person person);
    }
    

    鉴于service模块使用了com.stackify.model包,所以它必须要依赖model模块,配置如下:

    module service {
        requires transitive model;
        exports com.stackify.service;
    }
    

    注意声明中的 transitive 关键字,这个关键字的存在表明依赖了service模块的所有模块都自动获得了model模块的访问许可。为了使AccessService能被其他模块访问,你必须使用exports到处它所在的包。

    impl模块为访问服务提供了一个实现:

    package com.stackify.impl;
    
    import com.stackify.service.AccessService;
    import com.stackify.model.Person;
    import java.lang.reflect.Field;
    
    public class AccessImpl implements AccessService {
        public String getName(Person person) {
            try {
                return extract(person);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        private String extract(Person person) throws Exception {
            Field field = person.getClass().getDeclaredField("name");
            field.setAccessible(true);
            return (String) field.get(person);
        }
    }
    

    由于model模块中的opens声明使得AccessImpl可以反射Person类。impl模块的模块声明如下:

    module impl {
        requires service;
        provides com.stackify.service.AccessService with com.stackify.impl.AccessImpl;
    }
    

    service模块中对model模块的导入是transitive的,因此,impl模块只需要引用service模块就可以获得这两个模块的访问了。(service,model)

    provides 声明表明impl模块为AccessService接口提供了实现,这个实现是AccessImpl类。

    client模块消费这个AccessService服务,需要进行如下声明:

    module client {
        requires service;
        uses com.stackify.service.AccessService;
    }
    

    client使用这个服务的示例如下:

    package com.stackify.client;
    
    import com.stackify.service.AccessService;
    import com.stackify.model.Person;
    import java.util.ServiceLoader;
    
    public class Main {
        
        public static void main(String[] args) throws Exception {
            AccessService service = ServiceLoader.load(AccessService.class).findFirst().get();
            Person person = new Person(1, "John Doe");
            String name = service.getName(person);
            assert name.equals("John Doe");
        }
    }
    

    你可以看到main函数中并没有使用AccessImpl实现类,事实上,模块系统在运行时基于模块定义中的users,provides...指令自动定位到了AccessService类的具体实现。

    编译和执行

    本小节介绍编译和执行刚才看到的模块化应用程序的步骤。 请注意,您必须按顺序运行项目根目录中的所有命令(src的父目录),然后显示出来。

    编译model模块,并将生成的类文件放入dist目录中的命令为:

    javac -d dist/model src/model/module-info.java src/model/com/stackify/model/Person.java
    

    鉴于service模块依赖model模块,当你编译service模块的时候,你需要使用-p指令指定依赖的模块的路径。

    javac -d dist/service -p dist src/service/module-info.java src/service/com/stackify/service/AccessService.java
    

    同样的,下面的命令展示了如何编译impl和client模块:

    javac -d dist/impl -p dist src/impl/module-info.java src/impl/com/stackify/impl/AccessImpl.java 
    javac -d dist/client -p dist src/client/module-info.java src/client/com/stackify/client/Main.java
    

    Main类中使用了断言声明,于是,你在执行Main程序的时候,需要启用断言:

    java -ea -p dist -m client/com.stackify.client.Main
    

    注意,你需在Main 类之前加上模块名称,然后传递给 - m 选项。

    向后兼容

    在java9之前,所有包的声明都不存在“模块”的理念。但是,这并不会妨碍你将这些包部署在新的模块化系统上。你只需要将它添加在类路径中,就像你在java8中做的那样,这个包会成为“未命名”模块的一部分。

    “未命名”模块会读取其他所有模块,无论这些模块处在classpath中还是模块路径中。于是乎,在java8上编译运行的程序也可以一样的在java9上嘚瑟。不过,具有显式声明的模块无法访问“未命名”模块,这里你需要另外一个模块了——自动模块。

    你可以将祖传的不具备模块声明的老jar包转换成自动模块,方法是将其放入模块路径中。这将定义一个名称来源于jar文件名的模块。这样的一个自动模块可以访问模块路径中的其他所有模块,并公开自己的包。从而实现了包之间的无缝互操作,无论有没有明确的模块。

    反应流

    反应流是一种编程范例——允许以背压的非阻塞的方式处理异步数据流。实质上,这种机制将接收器置于控制之下,使其能够确定要传输的数据量,而不必在每次请求之后等待响应。

    Java平台将反应流作为Java9的一部分集成了进来。该集成允许您以标准方式利用Reactive Streams,从而各种实现可以协同工作。

    Flow类

    Java api将反应流的接口封装在Flow类中——包括,Publisher,Subscriber,Subscription和Processor。

    Publisher提供了条目和相关的控制信息。这个接口只定义了一个方法,即subscribe方法,这个方法添加了一个subscriber(订阅者),改订阅者监听数据和发布者传输的数据。

    Subscriber 从一个Publisher接收数据,这个接口定义了四个方法:

    • onSubscribe
    • onNext
    • onError
    • onComplete

    Subscription 用来控制发布者,订阅者之间的通信。这个接口定义了两个方法:request和cancel。request方法请求发布者发布特定数量的条目,cancel会导致订阅者取消订阅。

    有时候,你可能希望在数据条目从发布者传输到订阅者时对其进行操作。这个时候你可以使用Processor。这个接口拓展了Subscriber和Publisher,使其可以从发布者的角度充当发布者,从订阅者的角度充当订阅者。

    内部实现

    Java平台为Publisher和Subscription提供了开箱即用的实现。Publisher接口的实现类是SubmissionPublisher.除了Publisher接口中定义的方法,这个类还具有其他方法,包括:

    • submit 发布一个条目给每个subscriber
    • close 给每一个subscriber发送一个onComplete信号,并禁止后续的订阅

    Subscription的实现类是一个私有的类,意图仅供SubmissionPublisher使用。当你调用SubmissionPublisher 的带有Subscriber 参数的subscribe方法时,一个Subscription对象被创建并传递给那个subscriber的onSubscribe 方法。

    一个简单的应用

    有了PublisherSubscription 的现成实现,你仅需要声明一个Subscriber接口的实现类,就可以创建一个反应流的应用。如下,这个类需要String类型的消息:

    public class StringSubscriber implements Subscriber<String> {
        private Subscription subscription;
        private StringBuilder buffer;
    
        @Override
        public void onSubscribe(Subscription subscription) {
            this.subscription = subscription;
            this.buffer = new StringBuilder();
            subscription.request(1);
        }
    
        public String getData() {
            return buffer.toString();
        }
    
        // other methods
    }
    

    如你所见,StringSubscriber 将当订阅一个发布者时得到的Subscription 存储在其私有的成员变量中。同时,它使用了一个buffer成员来存储它接收的String消息 。你可以通过getData()方法来取回buffer数据。onSubscribe 方法请求了发布者发出单个数据条目。

    onNext 方法的定义如下:

    @Override
    public void onNext(String item) {
        buffer.append(item);
        if (buffer.length() < 5) {
            subscription.request(1);
            return;
        }
        subscription.cancel();
    }
    

    这个方法接收Publisher发布的新消息,并叠加在前一消息后面,当接收到5条消息之后,订阅者便停止了接收。

    并不重要的 onErroronComplete 两个方法的实现如下:

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
    
    @Override
    public void onComplete() {
        System.out.println("Data transfer is complete");
    }
    

    这个Test验证了我们的实现:

    @Test
    public void whenTransferingDataDirectly_thenGettingString() throws Exception {
        StringSubscriber subscriber = new StringSubscriber();
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
        publisher.subscribe(subscriber);
    
        String[] data = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
        Arrays.stream(data).forEach(publisher::submit);
    
        Thread.sleep(100);
        publisher.close();
    
        assertEquals("01234", subscriber.getData());
    }
    

    在上面的test方法中,sleep方法啥都没干,只是等着异步的数据传输完成。

    应用Processor

    我们来添加Processor来让这个简单应用变得复杂一点,这个processor将发布的String转换成Integer,如果转换失败就抛出一个异常。转换之后,processor将这个结果number转发给subscriber。Subscriber的代码实现如下:

    public class NumberSubscriber implements Subscriber<Integer> {
        private Subscription subscription;
        private int sum;
        private int remaining;
    
        @Override
        public void onSubscribe(Subscription subscription) {
            this.subscription = subscription;
            subscription.request(1);
            remaining = 1;
        }
    
        public int getData() {
            return sum;
        }
    
        @Override
    	public void onNext(Integer item) {
      	 sum += item;
       	 if (--remaining == 0) {
         	   subscription.request(3);
        	    remaining = 3;
        	}
    	}	
    }
    

    代码和之前的Subscriber类似,不做解释。Processor 的实现如下:

    public class StringToNumberProcessor extends SubmissionPublisher<Integer> implements Subscriber<String> {
        private Subscription subscription;
    
        @Override
        public void onSubscribe(Subscription subscription) {
            this.subscription = subscription;
            subscription.request(1);
        }
    
        // other methods
    }
    

    本例中,这个processor继承了SubmissionPublisher,因此Subscriber接口的抽象方法。其他的几个方法是:

    @Override
    public void onNext(String item) {
        try {
            submit(Integer.parseInt(item));
        } catch (NumberFormatException e) {
            closeExceptionally(e);
            subscription.cancel();
            return;
        }
        subscription.request(1);
    }
    
    @Override
    public void onError(Throwable throwable) {
        closeExceptionally(throwable);
    }
    
    @Override
    public void onComplete() {
        System.out.println("Data conversion is complete");
        close();
    }
    

    注意,当publisher关闭的时候,processor也需要关闭,向subscriber发布onComplete信号。同样的,当错误发生时——无论是processor还是publisher——processor本身应该通知subscriber这个错误。

    你可以通过调用closecloseExceptionally 方法来实现通知流。

    测试用例如下:

    @Test
    public void whenProcessingDataMidway_thenGettingNumber() throws Exception {
        NumberSubscriber subscriber = new NumberSubscriber();
        StringToNumberProcessor processor = new StringToNumberProcessor();
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
    
        processor.subscribe(subscriber);
        publisher.subscribe(processor);
    
        String[] data = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
        Arrays.stream(data).forEach(publisher::submit);
    
        Thread.sleep(100);
        publisher.close();
    
        assertEquals(45, subscriber.getData());
    }
    

    API使用

    通过上面的应用,你可以更好的理解反应流。但是,它们决不是用于从头开始构建反应式程序的指南。实现一个反应流规范并不容易,因为它要解决的问题一点也不简单。你应该利用有效的库(例如RxJava或者Project Reactor)来编写高效的应用。

    将来,当许多Reactive库支持Java 9时,甚至可以组合来自不同工具的各种实现,以充分利用API。

    总结

    这篇文章涉及了Java9中的两个核心技术——模块系统和反应流。

    模块化系统是新的特性,预计不久就会被广泛使用。然而,整个java世界走向模块化系统是必然的,你应该为此做好准备。

    反应流已经存在了一段时间了,Java 9的推出有助于标准化范例,这可能会加速它的运用。

    原文地址:https://stackify.com/exploring-java-9-module-system-and-reactive-streams/

  • 相关阅读:
    5-python基础—获取某个目录下的文件列表(适用于任何系统)
    Automated, Self-Service Provisioning of VMs Using HyperForm (Part 1) (使用HyperForm自动配置虚拟机(第1部分)
    CloudStack Support in Apache libcloud(Apache libcloud中对CloudStack支持)
    Deploying MicroProfile-Based Java Apps to Bluemix(将基于MicroProfile的Java应用程序部署到Bluemix)
    Adding Persistent Storage to Red Hat CDK Kit 3.0 (在Red Hat CDK Kit 3.0添加永久性存储)
    Carve Your Laptop Into VMs Using Vagrant(使用Vagran把您笔记本电脑刻录成虚拟机)
    使用Python生成一张用于登陆验证的字符图片
    Jupyter notebook的安装方法
    Ubuntu16.04使用Anaconda5搭建TensorFlow使用环境 图文详细教程
    不同时区的换算
  • 原文地址:https://www.cnblogs.com/demingblog/p/9104103.html
Copyright © 2011-2022 走看看