Java的模块化系统(JPMS):implied reads与exports的访问控制规则

Java 模块化系统 (JPMS): Implied Reads 与 Exports 的访问控制规则

大家好!今天我们来深入探讨 Java 模块化系统 (JPMS) 中两个非常重要的概念:implied readsexports,以及它们如何共同影响模块间的访问控制。JPMS 的核心目标之一就是增强代码的封装性和可维护性,而理解这两个概念对于编写良好定义的模块化 Java 应用至关重要。

模块化的基础:模块声明 (module-info.java)

在深入 implied readsexports 之前,我们先回顾一下模块化的基础。每个模块都通过一个 module-info.java 文件来声明其名称、依赖关系以及对外暴露的内容。

一个简单的 module-info.java 文件可能如下所示:

module com.example.mymodule {
    requires java.base;  // 显式声明对 java.base 模块的依赖
    exports com.example.mymodule.api; // 导出 com.example.mymodule.api 包
}

这个例子中,requires 关键字声明了模块对其他模块的依赖,而 exports 关键字声明了模块对外暴露的包。

Exports: 精确控制对外暴露的 API

exports 指令用于声明模块对外暴露的包。这意味着其他模块可以访问被 exports 的包中的 publicprotected 类型(以及它们的成员)。

语法:

exports <package-name>;
exports <package-name> to <module-name> [, <module-name> ...]; // 限定导出

示例:

  • exports com.example.mymodule.api; – 将 com.example.mymodule.api 包中的所有 publicprotected 类型对外暴露给所有其他模块。
  • exports com.example.mymodule.internal to com.example.anothermodule; – 将 com.example.mymodule.internal 包只对外暴露给 com.example.anothermodule 模块。 这被称为限定导出 (Qualified Exports),它允许更细粒度的访问控制,隐藏内部实现细节。

重要规则:

  • 只有 publicprotected 类型(以及它们的成员)才会被导出。package-private 类型即使在导出的包中也是不可见的。
  • 如果没有明确声明 exports,模块中的任何包都不会被其他模块访问,即使它们是 public 的。
  • exports 必须在 module-info.java 文件中声明。

代码示例:

假设我们有两个模块:com.example.greetercom.example.app

com.example.greeter/module-info.java:

module com.example.greeter {
    exports com.example.greeter.api;
}

com.example.greeter/com/example/greeter/api/Greeter.java:

package com.example.greeter.api;

public interface Greeter {
    String greet(String name);
}

com.example.greeter/com/example/greeter/internal/SimpleGreeter.java:

package com.example.greeter.internal;

import com.example.greeter.api.Greeter;

class SimpleGreeter implements Greeter {
    @Override
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

com.example.app/module-info.java:

module com.example.app {
    requires com.example.greeter;
}

com.example.app/com/example/app/Main.java:

package com.example.app;

import com.example.greeter.api.Greeter;

public class Main {
    public static void main(String[] args) {
        //  无法直接实例化 SimpleGreeter,因为它不是 public 且包未导出。
        //  Greeter greeter = new com.example.greeter.internal.SimpleGreeter(); // 编译错误

        //  只能通过 Greeter 接口访问
        Greeter greeter = new Greeter() {
            @Override
            public String greet(String name) {
                return "Hello, " + name + "!";
            }
        };

        System.out.println(greeter.greet("World"));
    }
}

在这个例子中,com.example.greeter 模块导出了 com.example.greeter.api 包,所以 com.example.app 模块可以访问 Greeter 接口。但是,由于 com.example.greeter.internal 包没有被导出,com.example.app 模块无法直接访问 SimpleGreeter 类。

限定导出的示例:

假设我们想让只有 com.example.admin 模块才能访问 com.example.greeter 的一个特殊的管理 API。

com.example.greeter/module-info.java:

module com.example.greeter {
    exports com.example.greeter.api;
    exports com.example.greeter.admin to com.example.admin;
}

这样,只有 com.example.admin 模块才能访问 com.example.greeter.admin 包中的类型。

Implied Reads: 隐藏的依赖关系

implied reads 是一种机制,允许模块隐式地读取其他模块,而无需在 requires 指令中显式声明。当一个模块 A 导出包 P,而包 P 中使用的类型来自另一个模块 B 时,模块 A 就会 隐含地读取 模块 B。

规则:

  • 如果模块 A exports 一个包,并且该包中的类型依赖于模块 B 中的类型,那么模块 A 隐含地读取 模块 B。
  • implied reads 是自动发生的,不需要在 module-info.java 文件中显式声明。

重要性:

  • implied reads 简化了模块的声明,避免了冗余的 requires 语句。
  • 它提高了代码的灵活性,允许模块在不修改 module-info.java 文件的情况下,使用来自其他模块的类型。

示例:

假设我们有三个模块:com.example.data, com.example.service, 和 com.example.app

com.example.data/module-info.java:

module com.example.data {
    exports com.example.data.model;
}

com.example.data/com/example/data/model/User.java:

package com.example.data.model;

import java.time.LocalDate;

public class User {
    private String name;
    private LocalDate birthday; // 使用 java.time.LocalDate

    public User(String name, LocalDate birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

    public LocalDate getBirthday() {
        return birthday;
    }
}

com.example.service/module-info.java:

module com.example.service {
    requires com.example.data;
    exports com.example.service.api;
}

com.example.service/com/example/service/api/UserService.java:

package com.example.service.api;

import com.example.data.model.User;

public interface UserService {
    User getUser(String username);
}

com.example.app/module-info.java:

module com.example.app {
    requires com.example.service;
}

在这个例子中,com.example.data 模块导出了 com.example.data.model 包,其中包含 User 类,该类使用了 java.time.LocalDate 类。 com.example.service 模块导出了 com.example.service.api 包,该包中的 UserService 接口使用了 User 类。

虽然 com.example.service 模块没有显式地 requires java.base,但由于它导出的 com.example.service.api 包中的 UserService 接口使用了 com.example.data.model.User 类,而 User 类使用了 java.time.LocalDate(来自 java.base 模块),所以 com.example.service 模块 隐含地读取java.base 模块。

com.example.app 模块 requires com.example.service,这意味着它也 间接地 访问了 java.base 模块,即使它没有直接使用 java.time.LocalDate

更详细的例子:

假设我们有一个 com.example.geometry 模块,它依赖于 Apache Commons Math 库。

com.example.geometry/module-info.java:

module com.example.geometry {
    requires org.apache.commons.math3; // 显式依赖 Commons Math
    exports com.example.geometry.shapes;
}

com.example.geometry/com/example/geometry/shapes/Circle.java:

package com.example.geometry.shapes;

import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;

public class Circle {
    private Vector2D center;
    private double radius;

    public Circle(Vector2D center, double radius) {
        this.center = center;
        this.radius = radius;
    }

    public Vector2D getCenter() {
        return center;
    }

    public double getRadius() {
        return radius;
    }
}

org.apache.commons.math3/module-info.java: (假设 Commons Math 是一个模块)

module org.apache.commons.math3 {
    exports org.apache.commons.math3.geometry.euclidean.twod;
    // ... 其他 exports
}

现在,假设我们有一个 com.example.drawing 模块,它使用了 com.example.geometry 模块。

com.example.drawing/module-info.java:

module com.example.drawing {
    requires com.example.geometry;
    //  不需要显式 requires org.apache.commons.math3,因为 com.example.geometry 已经依赖它
}

com.example.drawing/com/example/drawing/DrawingApp.java:

package com.example.drawing;

import com.example.geometry.shapes.Circle;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;

public class DrawingApp {
    public static void main(String[] args) {
        Vector2D center = new Vector2D(1.0, 2.0);
        Circle circle = new Circle(center, 5.0);
        System.out.println("Circle center: " + circle.getCenter());
    }
}

在这个例子中,com.example.geometry 模块导出了 com.example.geometry.shapes 包,该包中的 Circle 类使用了 org.apache.commons.math3.geometry.euclidean.twod.Vector2D 类。 com.example.drawing 模块 requires com.example.geometry

由于 Circle 类使用了 Vector2D 类,com.example.geometry 模块 隐含地读取org.apache.commons.math3 模块。因此,com.example.drawing 模块可以访问 Vector2D 类,即使它没有显式地 requires org.apache.commons.math3

Exports 和 Implied Reads 的交互

exportsimplied reads 共同定义了模块间的依赖关系和访问控制。 exports 决定了哪些包可以被其他模块访问,而 implied reads 确保了模块可以访问其导出的包所需的依赖项。

总结:

特性 Exports Implied Reads
目的 声明模块对外暴露的包 允许模块隐式读取其导出的包所需的依赖模块
声明方式 module-info.java 文件中使用 exports 关键字 自动发生,无需显式声明
控制 精确控制哪些包可以被其他模块访问 自动推断,基于导出的包中使用的类型
影响 影响模块间的可见性 影响模块间的依赖关系

常见的错误和注意事项

  • 忘记 exports: 如果你忘记在 module-info.java 文件中声明 exports,即使你的类是 public 的,其他模块也无法访问它们。
  • 循环依赖: JPMS 禁止模块间的循环依赖。 确保你的模块依赖关系是单向的。
  • 过度导出: 只导出必要的 API。 避免导出内部实现细节,以提高代码的封装性。
  • 不理解 implied reads: 如果你不理解 implied reads,可能会导致意外的依赖关系或编译错误。 仔细检查你的模块依赖关系,确保所有必需的模块都被正确读取。
  • 运行时错误: 即使代码可以编译,如果缺少必要的模块依赖,可能会在运行时出现 NoClassDefFoundErrorClassNotFoundException 错误。
  • 反射: JPMS 默认情况下限制反射访问。 如果你的代码使用了反射,你可能需要使用 opens 指令来允许其他模块访问你的内部类型。
  • 服务加载器: 如果你的模块提供了服务,你需要使用 providesuses 指令来声明你的服务提供者和消费者。

代码示例:解决常见的模块化问题

假设我们遇到了以下问题:

  1. com.example.processor 模块需要使用 com.example.api 模块中的接口。
  2. com.example.processor 模块需要在运行时加载 com.example.spi 模块提供的服务实现。
  3. com.example.processor 模块需要反射访问 com.example.internal 模块中的类。

com.example.api/module-info.java:

module com.example.api {
    exports com.example.api.interfaces;
}

com.example.api/com/example/api/interfaces/MyInterface.java:

package com.example.api.interfaces;

public interface MyInterface {
    void doSomething();
}

com.example.spi/module-info.java:

module com.example.spi {
    requires com.example.api;
    exports com.example.spi.impl;
    provides com.example.api.interfaces.MyInterface with com.example.spi.impl.MyInterfaceImpl;
}

com.example.spi/com/example/spi/impl/MyInterfaceImpl.java:

package com.example.spi.impl;

import com.example.api.interfaces.MyInterface;

public class MyInterfaceImpl implements MyInterface {
    @Override
    public void doSomething() {
        System.out.println("Doing something from MyInterfaceImpl");
    }
}

com.example.internal/module-info.java:

module com.example.internal {
    exports com.example.internal.classes;
    opens com.example.internal.classes to com.example.processor; // 允许 com.example.processor 反射访问
}

com.example.internal/com/example/internal/classes/InternalClass.java:

package com.example.internal.classes;

public class InternalClass {
    private String message = "This is an internal message";

    public String getMessage() {
        return message;
    }
}

com.example.processor/module-info.java:

module com.example.processor {
    requires com.example.api;
    requires com.example.spi; // 显式依赖,即使可能可以通过服务加载器间接访问
    requires java.base; //  为了使用 ServiceLoader, 显式声明对 java.base 的依赖
    requires com.example.internal; //  为了反射访问

    uses com.example.api.interfaces.MyInterface;  // 声明使用 MyInterface 服务
}

com.example.processor/com/example/processor/Main.java:

package com.example.processor;

import com.example.api.interfaces.MyInterface;
import com.example.internal.classes.InternalClass;

import java.lang.reflect.Method;
import java.util.ServiceLoader;

public class Main {
    public static void main(String[] args) throws Exception {
        // 使用接口
        MyInterface myInterface = ServiceLoader.load(MyInterface.class).findFirst().orElse(null);
        if (myInterface != null) {
            myInterface.doSomething();
        } else {
            System.out.println("No MyInterface implementation found.");
        }

        // 反射访问
        InternalClass internalClass = new InternalClass();
        Method getMessageMethod = InternalClass.class.getMethod("getMessage");
        String message = (String) getMessageMethod.invoke(internalClass);
        System.out.println("Message from InternalClass: " + message);
    }
}

在这个例子中,我们使用了 requiresexportsprovidesusesopens 指令来解决不同的模块化问题。

  • requires 用于声明对其他模块的依赖。
  • exports 用于对外暴露包。
  • providesuses 用于声明服务提供者和消费者。
  • opens 用于允许其他模块反射访问内部类型。

JPMS访问规则概要

访问类型 规则 module-info.java 声明
显式读取 (Requires) 模块A要访问模块B的导出包,必须在模块A的module-info.java中声明requires B requires B;
导出 (Exports) 模块A要允许其他模块访问其包,必须在模块A的module-info.java中声明exports package.name exports package.name;
限定导出 (Qualified Exports) 模块A要允许特定的模块访问其包,必须在模块A的module-info.java中声明exports package.name to module.name exports package.name to module.name;
隐含读取 (Implied Reads) 如果模块A导出的包依赖于模块B的类型,则模块A隐含读取模块B,无需显式声明。 无需声明
开放 (Opens) 模块A要允许其他模块反射访问其包,必须在模块A的module-info.java中声明opens package.name opens package.name;
限定开放 (Qualified Opens) 模块A要允许特定的模块反射访问其包,必须在模块A的module-info.java中声明opens package.name to module.name opens package.name to module.name;
使用服务 (Uses) 模块A要使用接口的服务提供者,必须在模块A的module-info.java中声明uses interface.name uses interface.name;
提供服务 (Provides) 模块A要提供接口的服务实现,必须在模块A的module-info.java中声明provides interface.name with implementation.class provides interface.name with implementation.class;

总结归纳

我们深入探讨了 JPMS 中 exportsimplied reads 的概念。exports 指令用于精确控制模块对外暴露的 API,而 implied reads 允许模块隐式读取其导出的包所需的依赖项。理解这两个概念对于编写良好定义的模块化 Java 应用至关重要。通过合理使用 exports 和注意 implied reads,可以提高代码的封装性、可维护性和灵活性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注