第九章 Java平台模块系统
Java平台模块系统是Java 9中引入的一个新特性,它可以更好地管理Java应用程序的依赖关系,提高应用程序的可扩展性和安全性。模块系统可以将Java应用程序的代码组织为独立的模块,每个模块都有自己的接口和实现。模块可以声明自己的依赖关系,并且只会导出需要公开的API,从而减少了依赖项的耦合性和可能出现的命名冲突。同时,模块系统还提供了更好的可见性和访问控制,允许开发人员更好地控制代码的复杂性和安全性。
9.1 模块的概念
在Java 9的模块系统中,一个模块是一个可以进行依赖管理和版本控制的独立单元。每个模块都有自己的名称、版本号和路径。
模块可以包含以下元素:
- 模块描述文件module-info.java:这是每个模块必须包含的声明模块名称、版本号和依赖项的文件,同时也指定了该模块将要导出给其他模块的包。
- 包:模块包含的代码实现。
- 类:模块包含的代码实现的基本单元。
- 接口:模块包含的代码实现的基本单元,其他模块可以通过导入它来使用。
- 枚举:模块包含的一种特殊类型的数据类型。
- 注解:模块包含的一种特殊类型的元数据。
模块可以和其他模块建立依赖关系,声明自己的依赖关系,并且只导出需要公开的API,避免了依赖项的耦合性和命名冲突。模块系统提供了更好的可见性和访问控制,使得开发者可以更好地控制代码的复杂性和安全性。
9.2 对模块命名
Java平台模块系统中,对模块命名需要遵循以下规则:
-
模块名称必须是合法的Java标识符,例如com.example.module。
-
模块名称应该是唯一的,以避免与其他模块名称相同。
-
模块名称应该具有语义化,以便开发人员轻松理解该模块的用途和功能。
-
模块名称应该遵循命名规范,例如使用小写字母、使用单词之间的下划线进行分隔等。
例如,我们可以对一个名为“用户管理”的模块进行命名,它的名称可以是“com.example.usermanagement”或者“com.example.user_management”。这样的命名可以很好的体现该模块的用途和功能。
下面是一个示例代码:
module com.example.mymodule {
requires java.base;
requires org.slf4j;
exports com.example.mymodule.package1;
exports com.example.mymodule.package2 to com.example.othermodule;
}
其中,module com.example.mymodule
指定了该模块的命名为 com.example.mymodule
。
requires
关键字指定了该模块所依赖的模块,这里依赖了 Java 标准库的基础模块 java.base
和第三方日志框架 org.slf4j
。
exports
关键字指定了该模块可导出的包,使得其他模块可以访问该模块中的公共类和接口。其中,com.example.mymodule.package1
包被导出给所有模块,而 com.example.mymodule.package2
包只被导出给名为 com.example.othermodule
的模块。
9.3 模块化的 “Hello World”程序
下面是一个模块化的 “Hello World” 程序的示例,它使用了 Java 9 及以上版本的模块化功能:
模块文件 module-info.java:
module hello.world {
requires java.base;
}
Java 代码文件 HelloWorld.java:
package hello.world;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
在该示例中,module-info.java
文件定义了 hello.world
模块,它需要 java.base
模块。
Java 代码文件 HelloWorld.java
中定义了一个 hello.world
包中的 HelloWorld
类,它包含一个 main
方法,输出 “Hello, World!”。在模块化的 Java 应用程序中,要运行该程序,需要使用 java
命令行工具,并提供模块路径和模块名:
java --module-path <path/to/modules> -m hello.world/hello.world.HelloWorld
其中,<path/to/modules>
是包含 hello.world
模块的顶层目录的路径。该命令行将运行 HelloWorld
类中的 main
方法,并打印 “Hello, World!”。
9.4 对模块的需求
Java 模块系统中,模块的需求指的是一个模块依赖于其他模块的能力,也就是它需要这些模块才能正常运行。Java 模块系统中,一个模块可以通过 requires
关键字声明其对其他模块的需求。
模块的需求有以下几个方面:
- 模块需要哪些模块。使用
requires
关键字声明对其他模块的需求。 - 模块需要哪些版本的模块。使用
requires
关键字的版本声明来指定对其他模块版本的需求。 - 模块需要其他模块的哪些导出包。使用
requires
关键字的指令形式来声明对其他模块导出包的需求。
需要注意的是,模块的需求是在编译时就确定的,也就是说在程序启动时,Java 虚拟机会检查模块的需求并加载所需的模块。如果在程序运行时缺少了必要的模块,则会抛出 java.lang.module.FindException
异常。
以下是一个模块的需求的示例(位于 module-info.java 文件中):
module com.example.mymodule {
requires java.base; // 表示该模块需要 java.base 模块
requires kotlin.stdlib; // 表示该模块需要 kotlin.stdlib 模块
requires transitive com.example.util; // 表示该模块需要 com.example.util 模块,并且该模块的所有依赖方都需要 com.example.util 模块
}
在该示例中,com.example.mymodule
模块需要 java.base
和 kotlin.stdlib
模块,并且需要 com.example.util
模块及其所有的导出包。
9.5 导出包
Java模块化系统中,使用exports关键字来声明一个包可以被其他模块访问。导出包的语法为:
module <模块名> {
exports <包名>;
}
其中,<模块名>为当前模块的名称,<包名>为要导出的包的名称。可以在一个模块中声明多个导出的包,例如:
module mymodule {
exports com.example.package1;
exports com.example.package2;
}
在上述示例中,mymodule模块导出了两个包:com.example.package1和com.example.package2。
值得注意的是,导出的包中的所有public类、接口和枚举都可以被其他模块访问。如果需要限制其他模块访问某些包中的类、接口或枚举,可以使用exports指令的modifiers属性来指定可访问的类,例如:
module mymodule {
exports com.example.package1 to othermodule;
exports com.example.package2 to othermodule {
opens com.example.package2.internal to othermodule;
}
}
在上述示例中,mymodule模块只导出了com.example.package1包给othermodule模块访问,而com.example.package2包只导出了它的public类、接口、枚举以及com.example.package2.internal包内部的所有类、接口和枚举给othermodule模块访问。
假设我们有一个名为"mymodule"的模块,其包含一个名为"com.example"的包,其中定义了一个名为"Greeting"的类,我们希望将该包导出给其他模块使用。
首先,我们需要在模块描述文件module-info.java中声明导出该包的指令,示例代码如下:
module mymodule {
exports com.example;
}
在上述示例中,我们使用exports关键字声明了com.example包可以被其他模块访问。
接下来,我们可以创建一个名为"othermodule"的模块,它依赖于mymodule模块,并需要访问com.example包中的Greeting类。示例代码如下:
module othermodule {
requires mymodule;
}
在上述示例中,我们使用requires关键字声明了othermodule模块依赖于mymodule模块。
现在,我们可以在othermodule模块中使用com.example包中的Greeting类了。示例代码如下:
package com.example;
public class Greeting {
public static void sayHello() {
System.out.println("Hello, world!");
}
}
package otherpackage;
import com.example.Greeting;
public class MyClass {
public static void main(String[] args) {
Greeting.sayHello(); // 输出 "Hello, world!"
}
}
在上述示例中,我们在otherpackage包中的MyClass类中使用了com.example包中的Greeting类的静态方法sayHello(),输出了"Hello, world!"。
这就是导出包的实际操作过程,通过使用exports指令,我们可以控制哪些包可以被其他模块访问,从而实现更加灵活和安全的模块化开发。
9.6 模块化的JAR
模块化的JAR是指使用Java 9中引入的模块化系统,在构建JAR文件时按照特定的模块化结构组织代码,并在模块描述文件中声明模块之间的依赖关系。这样可以更好地管理Java应用程序的依赖关系和版本冲突。模块化的JAR不再使用传统的classpath来加载类和资源文件,而是通过模块路径来加载模块。这种方式可以使代码更加模块化,减少不必要的类加载和提高应用程序的性能。
以一个简单的模块化的JAR为例,假设我们有两个模块:一个是com.example.moduleA
和一个是com.example.moduleB
。其中,moduleB
依赖于moduleA
,并且在moduleB
中使用了moduleA
的代码。
- 创建模块化的JAR
首先,我们需要在每个模块的根目录下创建一个module-info.java
文件来声明模块及其依赖关系。下面是moduleA
的module-info.java
文件:
module com.example.moduleA {
exports com.example.moduleA;
}
在上面的代码中,我们声明了一个名为com.example.moduleA
的模块,并且通过exports
关键字导出了com.example.moduleA
包,以供其他模块使用。
接下来,我们创建一个名为moduleA.jar
的JAR文件,并将com.example.moduleA
模块的所有代码和依赖项打包到JAR中。
同样地,我们创建moduleB
的module-info.java
文件:
module com.example.moduleB {
requires com.example.moduleA;
}
在上面的代码中,我们声明了一个名为com.example.moduleB
的模块,并且通过requires
关键字声明了它依赖于com.example.moduleA
模块。
然后,我们创建一个名为moduleB.jar
的JAR文件,并将com.example.moduleB
模块的所有代码和依赖项打包到JAR中。
- 运行模块化的JAR
一旦我们创建了模块化的JAR文件,我们就可以使用java
命令来运行它们。假设我们想在moduleB
中运行com.example.moduleB.Main
类,可以使用以下命令:
java --module-path moduleA.jar:moduleB.jar --module com.example.moduleB/com.example.moduleB.Main
在上面的命令中,我们将moduleA.jar
和moduleB.jar
添加到模块路径中,并指定要运行的主类为com.example.moduleB.Main
。
通过模块化的JAR,我们可以更好地管理Java应用程序的依赖关系,减少不必要的类加载和提高应用程序的性能。
9.7 模块和反射式访问
在Java中,模块是指一组相关的类和接口,可以归档在一起形成一个独立的单元,以便于管理和部署。Java 9引入了模块化系统,使得Java应用程序可以更加模块化和安全。
反射式访问是指在运行时通过Java反射API动态地访问Java类和对象的方法、字段和构造方法。反射式访问可用于检查和修改类的行为,以及实现动态代码生成和方法调用等功能。
模块化系统和反射式访问都是Java语言中的重要特性,可以提高Java程序的可维护性和灵活性。但是,使用反射式访问需要小心,因为它可能会降低代码的可读性和性能,并且容易引入安全漏洞。
下面是一个简单的Java模块和反射式访问的示例代码:
// module-info.java
module mymodule {
exports com.example;
}
// Main.java
package com.example;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 反射式调用MyClass类的sayHello方法
Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(obj);
}
}
// MyClass.java
package com.example;
public class MyClass {
public void sayHello() {
System.out.println("Hello from MyClass");
}
}
在这个示例中,mymodule
模块导出了com.example
包,其中包含了Main
和MyClass
两个类。
在Main
类中,我们使用Java反射API动态地加载MyClass
类,并调用其中的sayHello
方法,输出一条简单的问候语。
这个示例演示了如何在Java 9中创建模块并导出包,以及如何使用反射式访问动态地访问Java类和对象。
9.8 自动模块
自动模块是指没有module-info.java文件的JAR文件,在Java 9之前,这种类型的JAR文件没有任何限制,可以被其他JAR文件使用,但在Java 9中,为了支持模块化,自动模块需要遵循一些规则。
一个自动模块的名称是由其JAR文件的名称来自动生成的,例如,如果一个JAR文件的名称为my-library.jar
,那么它的自动模块名称就是my.library
。
自动模块可以使用其他模块中导出的包,但不能导出自己的包。这意味着自动模块不能被其他模块所依赖。
以下是一个使用Java模块化的简单示例代码:
模块 module-info.java 文件:
module mymodule {
exports com.example.mymodule;
}
Java类 MyClass.java 文件:
package com.example.mymodule;
public class MyClass {
public void sayHello() {
System.out.println("Hello from mymodule!");
}
}
Main.java 文件:
import com.example.mymodule.MyClass;
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.sayHello();
}
}
在这个示例中,我们定义了一个模块 mymodule,它导出了 com.example.mymodule 包。我们也定义了一个 MyClass 类,它位于 com.example.mymodule 包中。在 Main 类中,我们导入了 MyClass,并实例化它并调用 sayHello() 方法。
请注意,为了使 MyClass 可以从 Main 类中访问,我们必须在模块描述文件 module-info.java 中导出 com.example.mymodule 包。否则,编译器会抛出编译错误。
9.9 不具名模块
在Java 9中引入了模块化系统,使得开发人员可以将代码组织成更小、更简洁和更可维护的单元。每个模块都需要在其module-info.java文件中声明其名称、依赖关系和公开的接口。但是,有时候我们可能需要创建一个不具有名称的模块,这种模块也称为匿名模块或不具名模块。
不具名模块是一种没有模块名称或module-info.java声明的模块。它们可以包含在类路径中的Jar文件中,并且可以通过传递-Jclasspath或–class-path选项来添加到模块路径中。不具名模块中的所有类都属于未命名模块,并且不能使用模块路径上其他模块的公共API。
不具名模块适用于以下情况:
- 在不想发布模块化应用程序或库的情况下,可以使用不具名模块来管理应用程序或库的类路径。
- 当需要使用类路径上的库或第三方库时,使用不具名模块可以避免这些库与模块路径上的其他模块发生冲突。
请注意,使用不具名模块可能会导致许多与模块化相关的好处丧失,例如可靠的配置、强制限制、更好的可维护性和更少的类加载错误。因此,除非需要使用类路径上的库或第三方库,否则应始终建议使用命名模块。
以下是一个简单的不具名模块示例代码:
- 创建一个没有module-info.java文件的Java项目,例如:
src/
└── com
└── example
└── MyClass.java
- 编写MyClass.java文件:
package com.example;
public class MyClass {
public void sayHello() {
System.out.println("Hello from MyClass!");
}
}
- 编译项目,并将生成的class文件打包成jar文件:
javac -d out/ src/com/example/MyClass.java
jar cvf mylibrary.jar -C out/ .
- 创建一个包含以下内容的Java文件:
import com.example.MyClass;
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.sayHello();
}
}
- 可以将mylibrary.jar文件添加到类路径中,并使用以下命令运行Main类:
java -cp mylibrary.jar:. Main
输出:
Hello from MyClass!
在这个例子中,我们使用了不具名模块来管理mylibrary.jar文件中的代码,而不是使用Java 9模块系统。如果要使用模块化方法,我们需要在MyClass中创建一个模块声明。
9.10 用于迁移的命令行标识
Java模块化中常用的命令行标识包括:
-
–module-path:指定模块路径,即模块所在的目录或JAR文件的路径。
-
–module:指定要运行的模块名称。
-
–add-modules:指定要自动添加到模块路径中的模块。
-
–patch-module:指定要修改的模块及其类文件路径。
-
–upgrade-module-path:指定模块升级所需的新模块路径。
-
–list-modules:列出当前模块路径上的所有模块。
-
–show-module-resolution:显示模块解析的详细信息。
这些命令行标识可帮助开发者管理Java模块化应用程序的组件和依赖,并实现应用程序的迁移。
下面是一些Java模块化中常用的命令行标识的实例:
- 指定模块路径和要运行的模块:
java --module-path mods -m com.example.app/com.example.app.Main
这里,--module-path
指定了模块路径为mods
目录,--module
指定要运行的模块为com.example.app
,并执行其中的Main
类。
- 添加模块依赖:
java --module-path mods -m com.example.app/com.example.app.Main --add-modules com.example.utils
这里,--add-modules
指定将com.example.utils
模块自动添加到模块路径中。
- 修改模块:
java --module-path mods -m com.example.app --patch-module com.example.app=patches
这里,--patch-module
指定将com.example.app
模块中的类改为patches/com.example.app
目录中的类。
- 列出模块:
java --list-modules --module-path mods
这里,--list-modules
指定列出当前模块路径上的所有模块,并指定模块路径为mods
目录。
- 显示模块解析的详细信息:
java --show-module-resolution --module-path mods -m com.example.app/com.example.app.Main
这里,--show-module-resolution
指定显示模块解析的详细信息,以便进行调试和故障排除。
9.11 传递的需求和静态的需求
在Java模块化中,有传递的需求和静态的需求。
传递的需求是指模块间依赖关系的传递性,即如果模块A依赖模块B,而模块B又依赖模块C,那么模块A就间接依赖于模块C。这种传递性的依赖关系是在运行时进行的,因为在运行时,模块间的依赖关系才会被确定。
静态的需求是指在编译时就可以确定的依赖关系。在Java模块化中,使用模块描述文件(module-info.java)来声明模块的依赖关系,这种依赖关系是静态的,因为它们在编译时就会被确定。模块描述文件可以声明依赖的模块、访问的包以及导出的包等信息。
通过使用Java模块化,可以更好地管理模块间的依赖关系,提高应用程序的可维护性和可扩展性。
以下是一个Java模块化实例代码,展示了传递的需求和静态的需求:
模块A:
module A {
// 导出com.example.a包
exports com.example.a;
// 依赖模块B
requires B;
}
模块B:
module B {
// 导出com.example.b包
exports com.example.b;
// 依赖模块C
requires C;
}
模块C:
module C {
// 导出com.example.c包
exports com.example.c;
}
在这个例子中,模块A依赖模块B,而模块B又依赖模块C。因此,在运行时,模块A就间接依赖了模块C,这是传递的需求。
同时,模块描述文件中声明了每个模块所依赖的模块,这是静态的需求。在编译时,编译器就会检查每个模块所依赖的模块是否存在,以及是否有权访问导出的包等信息。
9.12 限定导出和开放
Java模块化系统(Java Module System)是自Java 9版本开始引入的一个重要功能。该功能旨在使Java平台更加模块化和安全。在Java模块化系统中,模块是一个包含代码和资源的封装单元,具有明确的依赖关系和导出限制。
在Java模块化系统中,可以限制哪些模块可以访问另一个模块中的内容。这是通过使用exports关键字来实现的。exports关键字用于指定模块导出的包或模块的名称。只有导出的包或模块才可以被其他模块访问。
此外,Java模块化系统还引入了可读性和可写性的概念。模块可以声明它们可以读取哪些模块,以及哪些模块可以读取它们。同样地,模块也可以声明它们可以写入哪些模块,以及哪些模块可以写入它们。
通过使用Java模块化系统,开发人员可以更清晰地定义模块之间的依赖关系,并限制其他模块对其代码和资源的访问。这有助于提高Java平台的安全性和可维护性。
总的来说,限定导出和开放是Java模块化系统的核心特性之一,可以帮助开发人员更好地组织和管理代码库。
下面是一个具有限定导出和开放特性的Java模块化系统的示例:
模块A:
module com.example.a {
exports com.example.a.package1;
opens com.example.a.package2;
}
在这个示例中,模块A导出了com.example.a.package1包,因此其他模块可以访问它的公共API。另一方面,模块A开放了com.example.a.package2包,这意味着在运行时,其他模块可以访问包中的私有成员,例如方法和属性。
模块B:
module com.example.b {
requires com.example.a;
}
在这个示例中,模块B声明了对模块A的依赖关系(requires)。由于模块A只导出了com.example.a.package1包,因此在模块B中只能访问该包中的公共API。
这种限定导出和开放的机制可以提高模块化系统的安全性和可维护性。例如,一个模块可以仅导出它的公共API,而保持其私有实现细节的保密性。同时,开放一个包中的私有成员可以帮助其他模块更有效地使用该包中的代码和资源。
9.13 服务加载
Java模块化系统(Java Module System,JMS)是Java 9引入的一项重要的功能。JMS提供了一种新的方式来组织和管理Java应用程序中的代码,它允许将一个大型的应用程序拆分成许多小模块。这些小模块可以被组合成更大的模块,从而形成一个完整的应用程序。
在JMS中,一个模块可以提供服务,也可以使用其他模块提供的服务。服务是指一组类和接口,它们提供了某种功能或服务。通过服务,模块之间可以实现解耦和灵活的交互。
JMS提供了一种灵活的方式来加载服务。模块中的服务可以通过服务注册表(Service Registry)来加载,这个注册表允许模块声明它们提供的服务,以及它们所依赖的服务。服务注册表会自动加载这些服务,并将它们组合起来,以便供应用程序使用。
下面是一个使用服务注册表加载服务的示例:
// 定义服务接口
public interface MyService {
void doSomething();
}
// 在模块中注册服务
module mymodule {
provides MyService with MyServiceImpl;
}
// 定义服务实现类
public class MyServiceImpl implements MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}
// 在另一个模块中使用服务
module myclientmodule {
requires mymodule;
public void run() {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
loader.forEach(MyService::doSomething);
}
}
在这个示例中,模块“mymodule”注册了一个MyService接口的实现类MyServiceImpl。另一个模块“myclientmodule”通过require语句依赖于“mymodule”,并在其中使用ServiceLoader来加载MyService服务。当“myclientmodule”模块被加载时,服务注册表会自动加载“mymodule”模块中注册的MyService服务,并将它们注入到ServiceLoader中。然后,ServiceLoader就可以迭代这些服务,并调用它们的doSomething方法。
总之,JMS提供了一种灵活的方式来加载和使用服务。通过使用服务注册表,模块之间可以实现解耦和灵活的交互,从而更好地组织和管理Java应用程序中的代码。
9.14 操作模块的工具
Java模块化系统中,操作模块的工具有以下几种:
- JAR命令:可以使用JAR命令创建和管理JAR文件。在Java 9中,JAR命令已经被更新以支持模块路径,并可以使用–module-path参数指定模块路径。例如,以下命令将创建一个包含模块org.example.demo的JAR文件:
jar --create --file demo.jar --module-version 1.0 --main-class org.example.demo.Main -C bin/org.example.demo .
- jlink命令:可以使用jlink命令构建一个包含所需依赖项的自定义运行时映像。该命令可以减小应用程序的大小并提高启动速度。例如,以下命令将创建一个只包含Java SE模块和org.example.demo模块的运行时映像:
jlink --module-path demo.jar:$JAVA_HOME/jmods --add-modules java.se,org.example.demo --output demo-runtime
- jdeps命令:可以使用jdeps命令查找模块之间的依赖关系,并生成模块描述符文件。例如,以下命令将查找org.example.demo模块的依赖项并生成模块描述符文件module-info.java:
jdeps --generate-module-info . demo.jar
- jmod命令:可以使用jmod命令创建和管理JMOD文件,它是一种新的Java归档格式,用于打包模块。例如,以下命令将创建一个包含org.example.demo模块的JMOD文件:
jmod create --module-version 1.0 --class-path bin/org.example.demo --main-class org.example.demo.Main demo.jmod
这些工具为Java模块化系统提供了灵活性和强大的功能,使得开发人员可以更轻松地管理和部署应用程序。
第十章 安全
Java安全是指保障Java应用程序、Java运行环境和Java开发人员安全的技术和工具。以下是Java安全涉及的一些方面:
1.代码的安全性:保证代码不容易被攻击者利用,如避免常见的漏洞(如SQL注入、XSS攻击等)。
2.数据的安全性:保护数据不被盗取或篡改,包括数据在传输过程中的安全和数据存储的安全。
3.身份验证和授权:确保用户能够被安全地认证和授权,在Java中可以使用Java Authentication and Authorization Service(JAAS)。
4.网络安全:定义安全网络协议,如SSL/TLS协议,来保证数据在网络传输中的安全性。
5.应用程序的完整性:保证应用程序不容易被非法修改,如数字签名和哈希校验等。
6.安全管理:管理和监控Java应用程序的安全性,包括日志记录、审计和安全事件响应等。
Java提供了许多安全性特性,如类加载器、安全管理器、安全通信API等,同时还可以使用第三方的安全性框架和工具来提高Java应用程序的安全性。
10.1 类加载器
Java 类加载器(Java Class Loader)是 Java 虚拟机(JVM)中的一个重要组成部分,它的作用是从指定的位置加载 Java 类文件到 JVM 中,并将其转化为 Class 类型的对象,以供 JVM 运行时使用。Java 类加载器通常会按照一定的顺序进行加载,将类加载到内存中的各个区域,并形成一个类加载器层次结构。
Java 类加载器通常包括以下几种类型:
-
启动类加载器(Bootstrap Class Loader):通过JVM内部的机制加载JDK核心类库,如 rt.jar、resources.jar 和 sun.boot.class.path 系统属性所指定的路径中的类库。
-
扩展类加载器(Extension Class Loader):负责将 Java 平台扩展目录中的类库(如 jre/lib/ext 目录下的 jar 包)加载到 JVM 中。
-
应用程序类加载器(Application Class Loader):负责加载应用程序类路径(即 classpath 路径)下的类库。
-
自定义类加载器(Custom Class Loader):开发者可以通过继承 ClassLoader 类,实现自定义的类加载器,用于加载指定位置的类库。
Java 类加载器具有以下特点:
-
双亲委派模型:Java 类加载器采用双亲委派模型,即在进行类加载时,会先将该任务传递给其父类加载器,直到最终传递到启动类加载器,如果父类加载器无法加载,则由自己来进行加载。
-
懒加载:Java 类加载器采用懒加载的方式,在需要使用类时才会进行加载,以节省内存和提高性能。
-
缓存机制:Java 类加载器会将加载过的类缓存在内存中,以便下次使用,并通过垃圾回收机制对无用的类进行卸载。
总之,Java 类加载器是 Java 虚拟机中非常重要的组成部分,它提供了灵活的类加载机制,支持动态加载和运行时动态扩展,使 Java 应用程序具有更好的可扩展性和灵活性。
10.1.1 类加载过程
Java 类加载过程包括以下步骤:
-
加载阶段:将类的二进制数据读入内存,并在方法区中生成一个对应的 Class 对象。
-
验证阶段:验证类的字节码是否符合 Java 虚拟机规范,包括文件格式验证、元数据验证、符号引用验证和字节码验证等。
-
准备阶段:为类的静态变量分配内存,并设置默认初始值。
-
解析阶段:将类中的符号引用解析为直接引用。
-
初始化阶段:执行类的初始化代码,包括静态变量赋值和静态代码块的执行。
-
使用阶段:当类已经被加载、验证、准备和初始化后,就可以开始使用该类了。
-
卸载阶段:当一个类不再被需要,且它的 Class 对象没有任何引用时,Java 虚拟机会将其从内存中卸载。
以上是 Java 类加载过程的基本步骤,具体实现可能会因不同的虚拟机实现而有所不同。
10.1.2 类加载器的层次结构
类加载器的层次结构是一个树形结构,其中根节点为引导类加载器(Bootstrap ClassLoader),它是由本地代码实现的,用于加载JDK核心类库,如java.lang包、java.util包等。它是由Java虚拟机实现的一部分,无法直接在Java程序中使用。
第二层是扩展类加载器(Extension ClassLoader),它是由sun.misc.Launcher E x t C l a s s L o a d e r 实现的,用于加载 J a v a 的扩展库。扩展类加载器可以通过 j a v a . e x t . d i r s 系统属性指定扩展库所在的目录,如果没有设置则默认为 ExtClassLoader实现的,用于加载Java的扩展库。扩展类加载器可以通过java.ext.dirs系统属性指定扩展库所在的目录,如果没有设置则默认为 ExtClassLoader实现的,用于加载Java的扩展库。扩展类加载器可以通过java.ext.dirs系统属性指定扩展库所在的目录,如果没有设置则默认为JAVA_HOME/lib/ext目录。
第三层是应用程序类加载器(Application ClassLoader),它也称作系统类加载器,由sun.misc.Launcher$AppClassLoader实现的,用于加载用户自己编写的Java类。应用程序类加载器可以通过java.class.path系统属性指定类所在的路径,如果没有设置则默认为当前目录。
除了这三个类加载器之外,还有一些其他的类加载器,如安全类加载器(Security ClassLoader)、平台类加载器(Platform ClassLoader)等。这些类加载器都是特定用途的,一般情况下我们不需要关注。
以下是一个简单示例,演示了类加载器的层次结构:
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
while (cl != null) {
System.out.println(cl.getClass().getName());
cl = cl.getParent();
}
// 输出结果:
// sun.misc.Launcher$AppClassLoader
// sun.misc.Launcher$ExtClassLoader
// null
}
}
在上述示例中,我们获取当前线程的类加载器,并通过循环依次输出它的父类加载器。由于根类加载器是由本地代码实现的,因此输出结果为null。
10.1.3 将类加载器用作命名空间
Java将类加载器用作命名空间的概念,意味着不同的类加载器可以加载具有相同名称的类,但这些类是相互独立的,它们在Java虚拟机中有着不同的身份。
当某个类加载器需要加载一个类时,它首先在自己的类路径下查找该类,如果找到了就加载,否则它会向上委托父类加载器来加载该类。如果所有的父类加载器都无法找到该类,那么该类加载器就会试图自己去加载该类。
在类加载器的命名空间中,两个类的身份并不仅仅由它们的全限定名所确定,还和它们的类加载器相关。如果两个类的全限定名相同,但它们的类加载器不同,那么这两个类就被认为是不同的类。
这种将类加载器用作命名空间的机制在Java中有着广泛的应用,例如,Java servlet容器就会为每个Web应用程序创建一个独立的类加载器来加载该应用程序中使用的类,这样就可以保证不同的Web应用程序中使用的相同类的版本不会相互干扰。
下面是一个Java示例,演示了如何在Java中使用类加载器的命名空间机制:
我们可以编写两个相同全限定名称的Java类,但是由不同的类加载器加载,从而实现在同一程序中使用不同版本的同一个类。
// 定义一个简单的类,我们将会编写两个相同全限定名称的类
public class SimpleClass {
public void print() {
System.out.println("Hello from SimpleClass!");
}
}
// 编写自定义的类加载器,将会加载一个指定的类
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
byte[] data = getClassData(className);
return defineClass(className, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(className);
}
}
private byte[] getClassData(String className) throws IOException {
// 将类名转换为文件路径
String path = className.replace(".", "/");
InputStream is = getClass().getClassLoader().getResourceAsStream(path + ".class");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int length;
while ((length = is.read(buffer)) > 0) {
bos.write(buffer, 0, length);
}
return bos.toByteArray();
}
}
// 将会使用两个不同的类加载器加载SimpleClass类
public class NamespaceDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 使用默认的AppClassLoader加载SimpleClass
SimpleClass simpleClass1 = new SimpleClass();
simpleClass1.print();
System.out.println("SimpleClass1类的类加载器:" + simpleClass1.getClass().getClassLoader());
// 使用自定义的类加载器加载SimpleClass
CustomClassLoader customClassLoader = new CustomClassLoader("/");
Class<?> clazz = customClassLoader.loadClass("SimpleClass");
SimpleClass simpleClass2 = (SimpleClass) clazz.newInstance();
simpleClass2.print();
System.out.println("SimpleClass2类的类加载器:" + simpleClass2.getClass().getClassLoader());
// 比较两个SimpleClass实例的类加载器
System.out.println("SimpleClass1类的类加载器和SimpleClass2类的类加载器是否相同:" + (simpleClass1.getClass().getClassLoader() == simpleClass2.getClass().getClassLoader()));
}
}
在上面的示例中,我们定义了一个SimpleClass类和一个CustomClassLoader类。然后我们使用默认的AppClassLoader加载SimpleClass类,并创建一个SimpleClass实例,接着我们使用自定义的类加载器CustomClassLoader加载SimpleClass类,并创建一个SimpleClass实例。
注意到这里的两个SimpleClass实例虽然有着完全相同的类名,但它们的类加载器并不相同,因此它们在Java虚拟机中的身份是不同的,它们也可以调用各自的print()方法,这个机制实现了类加载器的命名空间机制。
10.1.4 编写你自己的类加载器
编写自己的类加载器需要遵循以下步骤:
-
定义类加载器的类,并继承 java.lang.ClassLoader 类。通常情况下,我们可以继承 URLClassLoader 类,因为它已经实现了一些类加载器的基本功能。
-
重写 findClass() 方法。这个方法是自定义类加载器的核心,它负责查找并加载指定的类。在方法中,我们需要先通过自定义的方式获取类的二进制字节码,然后调用 defineClass() 方法将其转换为一个 Class 对象。
-
实现自己的类加载器逻辑。在 findClass() 方法中,首先尝试使用父类加载器来加载类,在加载失败时,再使用自己的方式来查找和加载类。通常情况下,我们可以从指定的路径或者网络地址中获取类的二进制字节码,然后调用 defineClass() 方法进行类的加载。
-
加载类时需要注意安全问题。在实现自己的类加载器时,需要考虑到安全问题,不允许加载系统类库中的类或者敏感的类。
-
注册自定义类加载器。如果我们需要使用自定义类加载器来加载类,需要将其注册到 Java 虚拟机中。通常情况下,我们可以使用 ClassLoader.registerAsParallelCapable() 方法或者 Thread.setContextClassLoader() 方法来实现类加载器的注册。
以下是一个简单的例子:
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String name) {
String fileName = path + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
InputStream is = new FileInputStream(fileName);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
is.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return bos.toByteArray();
}
}
10.1.5 字节码校验
字节码校验是指在Java程序运行之前,Java虚拟机对字节码文件进行验证的过程。主要包括以下三个方面的校验:
-
文件格式校验:验证字节码文件是否符合Java字节码文件格式规范。
-
元数据校验:验证类、方法、字段等的声明是否符合Java语言规范。
-
字节码语义校验:验证字节码是否能被正确解析和执行,例如代码中的类型转换是否合法、方法调用是否正确等。
如果字节码文件校验失败,Java虚拟机将抛出java.lang.VerifyError异常,表示字节码文件不合法,无法被执行。通过字节码校验,可以确保Java程序的正确性和安全性。
以下是一个演示字节码校验失败的示例:
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
public int add(int a, int b) {
String str = "abc";
int c = Integer.parseInt(str);
return a + b + c;
}
}
在该示例中,add
方法中使用了Integer.parseInt
方法将字符串"abc"
转换为整数。由于"abc"
不是一个合法的整数表示,因此Integer.parseInt
方法会抛出NumberFormatException
异常。但是,在编译这个类时,编译器并不会对add
方法体中的代码进行编译时检查,而是只做基本的语法检查。因此,这个类的字节码文件会被生成,但其中的字节码并不合法,无法被Java虚拟机正确解析和执行。
当我们尝试运行这个类时,Java虚拟机会在字节码文件进行校验,发现add
方法体中的字节码非法,因此会抛出java.lang.VerifyError
异常。
解决这个问题的方法是,在add
方法中对字符串进行合法性校验,例如使用正则表达式判断字符串是否是一个数字:
public int add(int a, int b) {
String str = "abc";
if (!str.matches("^\\d+$")) {
throw new NumberFormatException("Invalid number: " + str);
}
int c = Integer.parseInt(str);
return a + b + c;
}
在该示例中,如果字符串str
不是一个合法的数字,将抛出NumberFormatException
异常,避免了字节码非法的情况。
10.2 安全管理器与访问权限
Java中的安全管理器和访问权限机制可以帮助程序员控制程序的运行时权限,以保证应用程序的安全性。下面分别介绍安全管理器和访问权限机制。
安全管理器
Java的安全管理器可以控制Java虚拟机中的各种资源的访问权限,比如网络、文件系统、系统属性、运行时反射等。安全管理器定义了一系列访问控制策略,来限制代码对这些资源的访问权限。
若程序没有设置安全管理器,则默认所有的访问操作都是被允许的。但若使用了安全管理器,安全管理器会根据安全策略文件中的设置来判断是否允许访问所请求的资源。安全策略文件中可以为某些代码区域和资源设置不同的权限,如读、写、创建等。若某个代码区域没有被授予访问某个资源的权限,则该区域在尝试访问该资源时会抛出SecurityException异常。
可以使用java.security.Policy
类来设置和管理安全策略文件,示例代码如下:
// 指定安全策略文件
System.setProperty("java.security.policy", "file:/path/to/policy/file");
// 启用安全管理器
System.setSecurityManager(new SecurityManager());
访问权限
Java中的访问权限控制规定了类、接口、成员变量、方法等不同成员的可见性和访问权限。Java中的访问权限有4种,分别为private、protected、default、public。
private
:表示该成员只能在当前类中访问,其他类无法访问。protected
:表示该成员在当前类和其子类中都可以访问,但在其他类中无法访问。default
:表示该成员只能在同一个包中的类中访问,其他包中的类无法访问。public
:表示该成员可以在任何类中访问。
Java中,访问权限控制是通过在成员前面添加关键字来实现的。例如:
public class MyClass {
private int num1;
protected int num2;
int num3;
public int num4;
}
在上面的例子中,num1
是私有成员变量,只能在MyClass
类中访问。num2
是受保护的成员变量,可以在MyClass
类及其子类中访问。num3
是默认的成员变量,只能在同一包中的类中访问。num4
是公共的成员变量,可以在任何类中访问。
总之,Java中的访问权限机制和安全管理器机制可以有效地保护Java应用程序的安全性。
10.2.1 权限检查
在Java中进行权限检查通常有两种方法:
- 使用访问控制修饰符
Java中有4个访问控制修饰符:public、protected、default(即没有修饰符)和private。这些修饰符可以加在类、方法和属性上,用于控制不同访问范围的可见性。
public:表示该类、方法或属性可以被任何代码访问;
protected:表示该类、方法或属性可以被同一包中的其他类访问,以及不同包中的子类访问;
default:表示该类、方法或属性可以被同一包中的其他类访问,但是不同包中的类无法访问;
private:表示该类、方法或属性只能被该类本身访问,其他类无法访问。
通过使用适当的访问控制修饰符,可以在Java中实现对某些类、方法或属性的访问进行限制。
- 使用权限管理工具
除了使用访问控制修饰符外,也可以使用Java提供的权限管理工具进行权限检查。Java的权限管理工具包括SecurityManager和AccessController两个类,可以在Java应用程序中设置安全策略和进行权限控制。
SecurityManager类用于设置Java虚拟机的安全策略,可以对Java中执行的所有代码进行访问权限控制。AccessController类用于检查执行请求的线程是否具有执行该操作所需的权限。这些权限可以是Java虚拟机中预定义的权限,也可以是开发者自己定义的权限。
使用Java的权限管理工具可以更加细粒度地控制Java应用程序中的访问权限,提高应用程序的安全性。
以下是一个Java程序中使用权限管理工具来进行权限检查的示例代码:
import java.io.File;
import java.net.SocketPermission;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Policy;
import java.awt.AWTPermission;
public class PermissionCheckExample {
public static void main(String[] args) {
// 设置一个安全策略
System.setProperty("java.security.policy", "path/to/policy/file");
// 重新加载安全策略
Policy.getPolicy().refresh();
// 检查创建一个新的类的权限
try {
Permission perm = new java.lang.RuntimePermission("defineClassInPackage.*");
PermissionCollection perms = perm.newPermissionCollection();
perms.add(perm);
AccessController.checkPermission(perm);
System.out.println("有创建类的权限!");
} catch (AccessControlException e) {
System.out.println("没有创建类的权限!");
}
// 检查退出虚拟机的权限
try {
Permission perm = new java.lang.RuntimePermission("exitVM");
AccessController.checkPermission(perm);
System.out.println("有退出虚拟机的权限!");
} catch (AccessControlException e) {
System.out.println("没有退出虚拟机的权限!");
}
// 检查使用反射访问另一个类的成员的权限
try {
Permission perm = new java.lang.reflect.ReflectPermission("suppressAccessChecks");
AccessController.checkPermission(perm);
System.out.println("有使用反射访问成员的权限!");
} catch (AccessControlException e) {
System.out.println("没有使用反射访问成员的权限!");
}
// 检查访问本地文件的权限
File file = new File("path/to/file");
try {
Permission perm = new java.io.FilePermission(file.getCanonicalPath(), "read");
PermissionCollection perms = perm.newPermissionCollection();
perms.add(perm);
AccessController.checkPermission(perm);
System.out.println("有访问本地文件的权限!");
} catch (AccessControlException e) {
System.out.println("没有访问本地文件的权限!");
}
// 检查打开socket的权限
try {
Permission perm = new SocketPermission("localhost:1234", "connect");
AccessController.checkPermission(perm);
System.out.println("有打开socket的权限!");
} catch (AccessControlException e) {
System.out.println("没有打开socket的权限!");
}
// 检查启动打印作业的权限
try {
Permission perm = new java.awt.print.PrintJobPermission("queuePrintJob");
AccessController.checkPermission(perm);
System.out.println("有打印作业的权限!");
} catch (AccessControlException e) {
System.out.println("没有打印作业的权限!");
}
// 检查访问系统剪贴板的权限
try {
Permission perm = new AWTPermission("accessClipboard");
AccessController.checkPermission(perm);
System.out.println("有访问剪贴板的权限!");
} catch (AccessControlException e) {
System.out.println("没有访问剪贴板的权限!");
}
// 检查访问AWT事件队列的权限
try {
Permission perm = new AWTPermission("accessEventQueue");
AccessController.checkPermission(perm);
System.out.println("有访问事件队列的权限!");
} catch (AccessControlException e) {
System.out.println("没有访问事件队列的权限!");
}
// 检查打开一个顶层窗口的权限
try {
Permission perm = new AWTPermission("showWindowWithoutWarningBanner");
AccessController.checkPermission(perm);
System.out.println("有打开顶层窗口的权限!");
} catch (AccessControlException e) {
System.out.println("没有打开顶层窗口的权限!");
}
}
}
在上述示例代码中,通过分别检查不同类型权限的方式,来验证程序是否拥有相应的权限。如果有权限,则输出相应的提示信息;如果没有权限,则捕获AccessControlException异常并输出相应的提示信息。需要根据实际应用场景,选择相关的权限检查方法。
10.2.2 Java平台安全性
Java平台的安全性主要体现在以下几个方面:
-
程序包含了安全机制:Java平台采用了多层安全机制,包括类加载器、字节码验证、安全管理器,这些机制都在运行时对程序进行检查和控制,以保证程序的安全性。
-
垃圾收集器:Java垃圾收集器自动回收不需要的对象,从而减少了内存泄漏和安全漏洞的可能性。
-
安全管理器:Java平台提供了SecurityManager,它是一个安全管理器,可以用于控制对系统资源(例如文件、网络、类库、进程等)的访问。安全管理器可以被Java程序动态配置和启用来授权或拒绝访问特定系统资源。
-
常见的安全问题解决方案:Java平台提供了一些解决安全问题的方案,例如使用数字签名、加密和解密、安全传输协议(SSL/TLS)、安全套接字层(SSL)等。
总体来说,Java平台非常注重安全性,并提供了多层安全机制来控制和保护程序的安全性。同时,开发人员也应该注意编写安全的Java程序,避免出现安全漏洞。
可以将Java权限类的层次结构按照以下表格列出:
权限类 | 父类 | 描述 |
---|---|---|
java.security.Permission | 无 | 所有权限类的基类。它包含了权限相关信息,如权限名和描述。 |
java.security.BasicPermission | java.security.Permission | 包含了一个单一权限的基类。 |
java.security.UnresolvedPermission | java.security.Permission | 用于延迟解析权限。 |
java.security.PermissionCollection | 无 | 持有一组权限。 |
java.security.CodeSigner | 无 | 代码签名者。 |
java.security.CodeSource | 无 | 代码来源。 |
java.security.ProtectionDomain | 无 | 包含一组权限和代码源。 |
java.security.AccessControlContext | 无 | 用于检查权限的访问控制上下文。 |
java.security.AccessController | 无 | 提供了一种访问控制机制,它使用AccessControlContext来检查权限。 |
以上表格列出了Java权限类的层次结构,其中最基本的权限类是java.security.Permission,而最高级别的类是java.security.AccessController,它提供了一种访问控制机制,并使用其他权限类来检查权限。
10.2.3 安全策略文件
Java安全策略文件是一种用于定义Java安全策略的文本文件,它允许您控制Java应用程序可以访问哪些资源,以及以何种方式访问这些资源。安全策略文件通常用于限制Java应用程序所能执行的操作,从而防止恶意代码对系统进行攻击和破坏。
安全策略文件通常包含以下内容:
- 权限:定义哪些权限可以被Java应用程序请求。
- 代码源:定义哪些代码来源可以被信任。
- 签名:定义哪些代码签名可以被信任。
- 提供者:定义哪些加密和消息摘要提供者可以被信任。
- 默认策略:定义应用程序可以执行哪些操作。
以下是一个简单的Java安全策略文件示例:
grant {
permission java.net.SocketPermission "localhost:1024-", "listen";
permission java.util.PropertyPermission "java.home", "read";
};
上面的安全策略文件授予应用程序以下权限:
- 监听本地主机上从1024到65535的所有端口。
- 读取Java系统属性“java.home”。
您可以将安全策略文件作为运行Java应用程序的命令行参数传递给Java虚拟机,如下所示:
java -Djava.security.manager -Djava.security.policy=security.policy MyApp
以上命令将启用Java安全管理器,并将安全策略文件“security.policy”指定为Java应用程序的策略文件。
10.2.4 定制权限
Java允许您通过定义自己的权限实现自定义权限。以下是定制Java权限的步骤:
-
创建一个类来定义自己的权限。您的类必须扩展
java.security.Permission
类。例如,以下是一个简单的自定义权限类:package com.example.permissions; import java.security.*; public class CustomPermission extends Permission { public CustomPermission(String name) { super(name); } @Override public boolean implies(Permission permission) { return false; // 实现自己的逻辑以确定权限是否包含在内。 } }
-
在您的自定义权限类中实现
implies()
方法。该方法是权限检查的核心逻辑。它接收一个java.security.Permission
实例作为参数,并返回一个布尔值,指示该权限是否包含在您的自定义权限中。在这个方法中,您可以实现自己的逻辑以确定权限是否包含在内。 -
在您的Java应用程序中使用自定义权限。您可以通过以下方式使用自定义权限:
package com.example.app; import com.example.permissions.CustomPermission; public class MyApp { public static void main(String[] args) { SecurityManager securityManager = new SecurityManager(); securityManager.checkPermission(new CustomPermission("my.custom.permission")); } }
上面的例子检查是否存在名为“my.custom.permission”的自定义权限。如果没有,则抛出SecurityException
异常。
通过自定义权限,您可以更好地控制Java应用程序对资源的访问,从而提高应用程序的安全性。
10.2.5 实现权限类
Java中的权限类是用来控制对资源的访问权限的,通过实现Java中的java.security.Permission
类,可以定义自己的权限类。以下是一个简单的实现权限类的示例:
package com.example.permissions;
import java.security.Permission;
public class CustomPermission extends Permission {
private static final long serialVersionUID = 1L;
private String action;
public CustomPermission(String name, String action) {
super(name);
this.action = action;
}
@Override
public boolean implies(Permission permission) {
if (!(permission instanceof CustomPermission))
return false;
CustomPermission cp = (CustomPermission) permission;
if (!this.getName().equals(cp.getName()))
return false;
if (!this.action.equals(cp.action))
return false;
return true;
}
@Override
public String getActions() {
return this.action;
}
}
在上面的示例中,CustomPermission
类继承了java.security.Permission
类,并实现了其中的两个方法:
-
implies(Permission permission)
方法:该方法用于判断当前权限是否包含指定的参数权限。在该方法中,如果传入的权限是CustomPermission
类的实例,则比较其名称和动作是否相等,如果相等则返回true
,否则返回false
。 -
getActions()
方法:该方法用于获取当前权限的动作,即权限所代表的操作。
在定义完自定义权限类后,还需要在Java的安全策略文件中指定相关的权限。例如,如果要将上面的自定义权限添加到Java的安全策略文件中,可以在文件中添加如下语句:
permission com.example.permissions.CustomPermission "my.custom.permission", "read,write";
上面的语句表示定义了一个名为my.custom.permission
的自定义权限,该权限包含读取和写入两个动作。
10.3 用户认证
在Java中,可以使用Java EE中的Servlet API或Spring Framework中的Spring Security来实现用户认证。
使用Servlet API实现用户认证的步骤如下:
-
创建一个Servlet来处理用户登录请求。在该Servlet中,可以使用表单页面(例如JSP页面)来收集用户输入的用户名和密码,并将其传递给身份验证机制。
-
在Servlet中,通过调用request.getRemoteUser()方法来获取用户的身份信息。如果该方法返回null,则说明用户还未登录或登录失败。否则,可以通过该方法获取到已登录用户的用户名。
-
在web.xml文件中配置身份验证机制。例如,可以使用基于表单的身份验证机制,这需要配置包含身份验证信息的登录页面、登录成功的页面、登录失败的页面等。
使用Spring Security实现用户认证的步骤如下:
-
在Spring配置文件中,配置Spring Security的相关组件。这些组件包括AuthenticationProvider、UserDetailsService、PasswordEncoder等。
-
创建一个登录页面,该页面允许用户输入用户名和密码,并将这些信息传递给Spring Security的身份验证机制。
-
创建一个自定义的UserDetailsService接口实现类,该类用于从数据库或其他数据源中获取用户的认证信息。
-
创建一个自定义的AuthenticationProvider接口实现类,该类用于对用户的认证信息进行验证。
-
在Spring配置文件中,将自定义的UserDetailsService接口实现类和AuthenticationProvider接口实现类注册为Spring Security的Bean。
-
配置Spring Security的Web安全过滤器,该过滤器用于拦截请求并进行用户身份验证。
总之,使用Servlet API或Spring Security均可以实现Java用户认证。具体的实现方式取决于应用程序的需求和开发人员的技能水平。
10.3.1 JAAS框架
JAAS(Java Authentication and Authorization Service)是Java平台提供的一个安全框架,用于管理用户的身份验证和授权。JAAS通过将身份验证和授权的逻辑从应用程序中分离出来,使应用程序可以更容易地实现安全性,并且可以使用各种身份验证和授权机制。
JAAS框架提供了一组接口和类,用于将身份验证和授权逻辑集成到应用程序中。通过使用JAAS,开发人员可以将身份验证和授权的实现从应用程序中分离出来,并且可以选择使用多种身份验证和授权机制来保护应用程序。
JAAS框架中的主要组件包括:
- LoginModule:用于执行用户身份验证的模块。
- Subject:代表当前正在执行的用户。
- Principal:代表用户的标识(例如用户名或身份证号码)。
- CallbackHandler:用于与应用程序交互以收集用户凭据。
JAAS框架的工作流程如下:
- 应用程序调用LoginContext.login()方法来启动身份验证过程。
- LoginContext使用配置文件中指定的LoginModule来执行身份验证。
- 如果身份验证成功,LoginContext将创建一个Subject对象来代表当前用户。
- 应用程序可以使用Subject对象来执行授权操作。
- 当应用程序完成时,应用程序调用LoginContext.logout()方法来终止用户会话并清除Subject对象。
JAAS框架可以与各种身份验证和授权机制集成,例如基于密码、数字证书、Kerberos和LDAP的身份验证和授权机制。
下面是一个简单的JAAS框架的应用实例,用于执行基于密码的身份验证:
- 创建一个LoginModule实现,用于执行基于密码的身份验证。该LoginModule类必须实现javax.security.auth.spi.LoginModule接口。
public class SimpleLoginModule implements LoginModule {
private Subject subject;
private CallbackHandler callbackHandler;
private Map<String, ?> options;
private boolean isAuthenticated;
private Principal userPrincipal;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> options,
Map<String, ?> sharedState) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.options = options;
}
@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("Username:");
PasswordCallback passwordCallback = new PasswordCallback("Password:", false);
try {
callbackHandler.handle(new Callback[] {
nameCallback, passwordCallback});
String username = nameCallback.getName();
char[] password = passwordCallback.getPassword();
// Check the user's credentials against a database or other authentication mechanism
if (username.equals("admin") && Arrays.equals(password, "password".toCharArray())) {
isAuthenticated = true;
userPrincipal = new UserPrincipal(username);
return true;
} else {
throw new FailedLoginException("Authentication failed");
}
} catch (IOException | UnsupportedCallbackException e) {
throw new LoginException(e.getMessage());
}
}
@Override
public boolean commit() throws LoginException {
if (isAuthenticated) {
subject.getPrincipals().add(userPrincipal);
return true;
} else {
return false;
}
}
@Override
public boolean abort() throws LoginException {
subject.getPrincipals().remove(userPrincipal);
isAuthenticated = false;
userPrincipal = null;
return true;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().remove(userPrincipal);
isAuthenticated = false;
userPrincipal = null;
return true;
}
}
- 创建一个JAAS配置文件,指定使用上述LoginModule实现进行身份验证。
SimpleLogin {
com.example.SimpleLoginModule required;
};
- 在应用程序中,使用LoginContext类启动身份验证过程。
public class App {
public static void main(String[] args) throws Exception {
String configFile = "/path/to/jaas.config";
System.setProperty("java.security.auth.login.config", configFile);
LoginContext loginContext = new LoginContext("SimpleLogin");
loginContext.login();
Subject subject = loginContext.getSubject();
// Perform authorized actions using the subject
loginContext.logout();
}
}
在此示例中,我们使用了一个简单的LoginModule实现,该实现使用硬编码的用户名和密码进行身份验证。在实际应用中,我们通常会将用户凭据存储在数据库或其他安全存储库中,并使用相应的机制验证用户凭据。此外,我们还可以将LoginModule类的实现替换为其他机制,例如基于数字证书的身份验证。
10.3.2 JAAS登录模块
JAAS(Java Authentication and Authorization Service)是由Java提供的一套API,用于实现用户认证、授权和访问控制等安全功能。JAAS在Java SE平台中包含了一个标准登录模块,可以用于实现用户的认证和授权。
JAAS登录模块主要包括以下几个组件:
-
LoginContext:用于创建和管理登录上下文,负责与登录模块进行交互,并处理用户认证和授权等事项。
-
LoginModule:实现实际的用户认证和授权功能,每个登录模块都对应一个特定的验证源,例如数据库、LDAP等。
-
Subject:表示一个用户或者用户组,包含了一组身份验证信息和授权信息。
-
CallbackHandler:用于与用户进行交互,例如获取用户名和密码。
-
Callback:用于在CallbackHandler和LoginModule之间传递信息,例如用户名和密码。
JAAS登录模块的使用步骤如下:
-
定义回调处理器CallbackHandler,用于与用户进行交互,例如获取用户名和密码。
-
创建LoginContext对象,用于创建登录上下文,并指定登录模块和回调处理器。
-
调用LoginContext对象的login()方法进行用户认证和授权。
-
根据需要从LoginContext对象中获取Subject对象,用于访问身份验证和授权信息。
JAAS登录模块可以帮助我们实现用户认证和授权,可以在企业系统中使用。需要注意的是,JAAS虽然提供了一套标准API,但是具体的实现和配置可能因不同的应用场景而有所差异,需要根据具体的需求进行配置。
下面是一个简单的JAAS登录模块的Java代码示例:
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
public class JAASLoginModule {
public static void main(String[] args) {
String username = "testuser";
String password = "testpassword";
String jaasConfigFile = "/path/to/jaas.config";
System.setProperty("java.security.auth.login.config", jaasConfigFile);
CallbackHandler callbackHandler = new CallbackHandler() {
public void handle(Callback[] callbacks) {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName(username);
} else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword(password.toCharArray());
}
}
}
};
try {
LoginContext loginContext = new LoginContext("myapp", callbackHandler);
loginContext.login();
Subject subject = loginContext.getSubject();
// access authenticated and authorized resources using subject
} catch (LoginException e) {
// handle login failure
}
}
}
在上面的示例代码中,我们首先设置了JAAS配置文件的路径,这个文件可以包含一个或多个登录模块的配置信息。然后,我们定义了一个回调处理器CallbackHandler,用于获取用户名和密码。
接着,我们创建了一个LoginContext对象,并指定登录上下文的名称为"myapp",回调处理器为callbackHandler。然后调用LoginContext对象的login()方法进行用户认证和授权,如果认证和授权成功,我们可以从LoginContext对象中获取Subject对象来访问身份验证和授权信息。
需要注意的是,JAAS登录模块的配置信息可能比较复杂,具体的配置方式需要根据不同的应用场景而定。同时,如果系统中使用了多个登录模块,我们需要在配置文件中指定登录模块的顺序,以确保正确的调用顺序。
10.4 数字签名
数字签名是一种在数字通信中用于保证消息完整性和身份认证的重要机制。在Java中,数字签名可以通过Java Security API(JCA)来实现。
下面是一个简单的Java代码示例,用于生成RSA数字签名和验证签名:
import java.security.*;
import java.util.Base64;
public class DigitalSignature {
public static void main(String[] args) throws Exception {
String message = "Hello, World!";
// 生成RSA密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
// 用私钥对消息进行数字签名
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(keyPair.getPrivate());
signer.update(message.getBytes());
byte[] signature = signer.sign();
// 将数字签名转换为Base64字符串输出
String base64Signature = Base64.getEncoder().encodeToString(signature);
System.out.println("Digital signature: " + base64Signature);
// 用公钥验证数字签名
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(message.getBytes());
boolean isValid = verifier.verify(signature);
System.out.println("Is signature valid? " + isValid);
}
}
在这个例子中,首先生成一个RSA密钥对,然后使用私钥对消息进行数字签名,并将签名转换为Base64字符串输出。接着使用公钥验证数字签名的有效性,最后输出验证结果。
需要注意的是,在实际应用中,密钥对的生成应该由安全的密钥管理系统(如KeyStore)负责,并且数字签名中应该包含消息的摘要值而非原始消息。
10.4.1 消息摘要
消息摘要(Message Digest)是一种将任意长度的数据压缩成固定长度(通常用16进制字符串表示)摘要(也称为哈希值)的算法,其输出的摘要具有唯一性、不可逆性和稳定性。Java提供了多种消息摘要算法的实现,如MD5、SHA-1、SHA-256等。
使用Java实现消息摘要需要进行以下步骤:
-
选择合适的消息摘要算法,如MD5或SHA-256等。
-
创建消息摘要对象,如MessageDigest md = MessageDigest.getInstance(“MD5”);
-
将要计算摘要的数据输入到消息摘要对象中,可以分块多次输入,如md.update(data, 0, len);
-
调用digest方法计算消息摘要,如byte[] digest = md.digest();
-
可以将摘要输出为字符串或16进制字符串的形式,如String digestStr = new String(digest) 或 String hexDigest = DatatypeConverter.printHexBinary(digest)。
需要注意的是,为了保证结果正确性和效率,在计算消息摘要前应该先将数据转换为字节数组,并避免多次拷贝和重复计算。同时,对于需要保密性的应用场景,还应考虑加盐(salt)和加密(encrypt)等措施。
以下是一个使用Java实现消息摘要的示例代码:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MessageDigestExample {
public static void main(String[] args) {
String message = "This is a test message";
String algorithm = "SHA-256"; // 可以使用不同的算法,如MD5、SHA-1、SHA-512等
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(message.getBytes());
System.out.println("Message: " + message);
System.out.println("Algorithm: " + algorithm);
System.out.println("Digest (in hex format): " + bytesToHex(digest));
} catch (NoSuchAlgorithmException e) {
System.out.println("Error: " + e.getMessage());
}
}
// 将字节数组转换成十六进制字符串
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
该程序将输入的消息使用指定的算法进行消息摘要,输出结果为该消息的摘要值(以十六进制字符串形式表示)。可以根据需要修改算法和消息内容。
10.4.2 消息签名
数字签名是一种基于公钥密码学的技术,用于验证消息的完整性和真实性,确保消息是由合法的发送方发送的,并且在传输过程中没有被篡改。数字签名通常包括三个步骤:生成签名、验证签名、提取公钥和私钥。Java提供了一些API来支持数字签名。
Java中的消息签名可以用于验证消息的真实性和完整性,确保消息没有被篡改或伪造。以下是一个实现Java消息签名的示例代码:
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
public class MessageSignExample {
public static void main(String[] args) {
String message = "This is a test message";
String algorithm = "SHA256withRSA"; // 可以使用不同的算法,如MD5withRSA、SHA1withDSA等
try {
// 生成公私钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
KeyPair keyPair = keyGen.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
// 获取签名对象,并设置私钥
Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
// 对消息进行签名
signature.update(message.getBytes());
byte[] sign = signature.sign();
// 获取验证签名的对象,并设置公钥
Signature verification = Signature.getInstance(algorithm);
verification.initVerify(publicKey);
// 验证签名
verification.update(message.getBytes());
boolean valid = verification.verify(sign);
System.out.println("Message: " + message);
System.out.println("Algorithm: " + algorithm);
System.out.println("Public Key: " + publicKey);
System.out.println("Private Key: " + privateKey);
System.out.println("Signature (in hex format): " + bytesToHex(sign));
System.out.println("Valid Signature: " + valid);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
// 将字节数组转换成十六进制字符串
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
该程序生成一对公私钥,使用私钥对消息进行签名,再使用公钥验证签名的真实性和有效性。可以根据需要修改消息内容和算法。
10.4.3 校验签名
校验签名是一种验证数据完整性和真实性的方法,通常用于验证数字签名或消息认证码(MAC)。在校验签名的过程中,接收者使用相同的密钥、散列算法和消息内容来计算出接收到的签名值(即摘要值),然后将该签名值与发送者发送的签名值进行比较。如果两个签名值相等,则说明该数据是真实、完整且未被篡改的。
在进行校验签名时,需要注意以下几点:
-
需要确保接收方和发送方使用相同的密钥和散列算法。
-
接收方需要确保签名值是从发送方接受的,而不是被篡改过的。
-
接收方需要确保签名值与消息数据是匹配的,即签名值是由消息数据计算得出的,而不是其他数据计算得出的。
-
对于数字证书,接收方需要确保证书是由可信任的证书颁发机构颁发的,且证书未过期或被吊销。
校验签名是一种安全保障措施,可以帮助用户确认数据的来源和完整性。在实际应用中,校验签名被广泛应用于电子商务、在线支付、文件传输等领域。
下面是一个简单的Java示例,用于校验签名:
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class VerifySignature {
public static boolean verifySignature(byte[] data, byte[] signature, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
public static void main(String[] args) throws N