除非你过去几个月和几年都躲在石头下面,否则你很可能已经听说过将模块系统添加到Java平台上的努力,该项目代号为"Project Jigsaw"。
定义一个模块系统并将像JDK这样的大型系统模块化绝不是一项简单任务,因此Jigsaw的推出被多次推迟并不令人惊讶。但我想,现在可以相当肯定地预计Jigsaw最终会作为JDK 9的一部分发布(确切发布日期尚未确定),尤其是在它成为早期访问构建的一部分已经有一段时间了。
这意味着,如果你是库或框架的作者,你应该获取最新的JDK 预览构建,并确保你的库可以在Java 9上使用,并在使用Jigsaw模块化的应用程序中使用。
下面我们将更详细地讨论这一点,以Bean Validation及其参考实现Hibernate Validator为例。我们将看看将它们转换为Jigsaw模块并在模块化环境中使用它们需要什么。
现在,有人可能会问,为什么拥有一个模块系统,并基于这样的系统提供库作为模块,是一件好事?这个问题有很多方面,但我想一个很好的答案是,模块化是构建软件系统的强大工具,可以从封装的、松耦合和可重用的组件中构建,这些组件具有清晰定义的接口。这使得API设计成为一个非常明智的决定,另一方面,它给库作者提供了在不损害与客户端兼容性的情况下更改其模块的内部实现方面的自由。
入门
为了跟随我们的“将Bean Validation转换为Jigsaw”的小实验,您应该使用兼容Bash的shell,并且能够运行如wget之类的命令。在默认不支持Bash的系统上,可以使用Cygwin。您还需要git来从GitHub下载一些源代码。
让我们开始下载并安装最新的JDK 9早期访问构建(本文撰写时使用了构建122)。然后运行java -version
以确认JDK 9已启用。您应该看到如下输出
java version "9-ea"
Java(TM) SE Runtime Environment (build 9-ea+122)
Java HotSpot(TM) 64-Bit Server VM (build 9-ea+122, mixed mode)
之后,为我们的实验创建一个基本目录
mkdir beanvalidation-with-jigsaw
进入该目录并创建一些子目录来存储所需的模块和第三方库
cd beanvalidation-with-jigsaw
mkdir sources
mkdir modules
mkdir automatic-modules
mkdir tools
由于Java 9 / Jigsaw的工具体支持仍然相当有限,我们将使用普通的javac
和java
命令来编译和测试代码。虽然听起来可能不太好,但实际上这是一个很好的练习,可以了解现有的和新编译器选项,我承认我期待着已知的构建工具(如Maven)将完全支持Jigsaw,并允许编译和测试模块化源代码。但到目前为止,普通的CLI工具将解决这个问题:)
从GitHub下载Bean Validation和Hibernate Validator的源代码
git clone https://github.com/beanvalidation/beanvalidation-api.git sources/beanvalidation-api
git clone https://github.com/hibernate/hibernate-validator.git sources/hibernate-validator
由于我们无法利用Maven的依赖管理,我们通过wget获取Hibernate Validator所需的依赖项,分别存储在automatic-modules(依赖项)和tools(生成日志实现所需的JBoss Logging注解处理器)目录中
wget http://repo1.maven.org/maven2/joda-time/joda-time/2.9/joda-time-2.9.jar -P automatic-modules
wget http://repo1.maven.org/maven2/javax/el/javax.el-api/2.2.4/javax.el-api-2.2.4.jar -P automatic-modules
wget http://repo1.maven.org/maven2/org/jsoup/jsoup/1.8.3/jsoup-1.8.3.jar -P automatic-modules
wget http://repo1.maven.org/maven2/org/jboss/logging/jboss-logging-annotations/2.0.1.Final/jboss-logging-annotations-2.0.1.Final.jar -P automatic-modules
wget http://repo1.maven.org/maven2/org/jboss/logging/jboss-logging/3.3.0.Final/jboss-logging-3.3.0.Final.jar -P automatic-modules
wget http://repo1.maven.org/maven2/com/fasterxml/classmate/1.1.0/classmate-1.1.0.jar -P automatic-modules
wget http://repo1.maven.org/maven2/com/thoughtworks/paranamer/paranamer/2.5.5/paranamer-2.5.5.jar -P automatic-modules
wget http://repo1.maven.org/maven2/org/hibernate/javax/persistence/hibernate-jpa-2.1-api/1.0.0.Final/hibernate-jpa-2.1-api-1.0.0.Final.jar -P automatic-modules
wget http://repo1.maven.org/maven2/org/jboss/logging/jboss-logging-processor/2.0.1.Final/jboss-logging-processor-2.0.1.Final.jar -P tools
wget http://repo1.maven.org/maven2/org/jboss/jdeparser/jdeparser/2.0.0.Final/jdeparser-2.0.0.Final.jar -P tools
wget http://repo1.maven.org/maven2/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar -P tools
其模块名称是从JAR文件名称派生的,应用了一些规则来拆分工件名称和版本,并用点替换连字符。例如,jboss-logging-annotations-2.0.1.Final.jar将具有自动模块名称jboss.logging.annotations。
为Bean Validation API和Hibernate Validator创建模块
目前,Bean Validation API和Hibernate Validator的状态是可以使用Java 9直接编译的。但仍然缺失的是所需的模块描述符,它描述了一个模块的名称、其公共API、其与其他模块的依赖关系以及一些其他内容。
模块描述符是名为module-info.java
的Java文件,并位于给定模块的根目录中。使用以下内容创建Bean Validation API的描述符
module javax.validation {(1)
exports javax.validation;(2)
exports javax.validation.bootstrap;
exports javax.validation.constraints;
exports javax.validation.constraintvalidation;
exports javax.validation.executable;
exports javax.validation.groups;
exports javax.validation.metadata;
exports javax.validation.spi;
uses javax.validation.spi.ValidationProvider;(3)
}
1 | 模块名称 |
2 | 模块导出的所有包(作为API模块,所有包含的包都导出) |
3 | 使用ValidationProvider 服务 |
服务在Java中已经存在很长时间了。最初作为JDK的一个内部组件添加,从Java 6开始,服务加载器机制成为平台的一个官方部分。
从那时起,它被广泛采用于构建由松散耦合组件组成的可扩展应用程序。有了它的帮助,服务消费者可以仅针对一个明确定义的服务契约进行实现,而无需事先了解特定的服务提供者和其实施。 Jigsaw 融入了现有的服务概念,并使服务成为模块化世界的第一公民。
幸运的是,Bean Validation从一开始就使用服务机制来定位提供者(如Hibernate Validator),因此与Jigsaw配合得很好。正如我们马上就会看到的,Hibernate Validator提供了ValidationProvider
服务的实现,使用户可以在不依赖于这种特定实现的情况下启动它。
但现在让我们编译Bean Validation模块
export BASE=`pwd`
cd sources/beanvalidation-api
javac -d $BASE/modules/javax.validation $(find src/main/java -name "*.java")
cd $BASE
编译完成后,构建的模块可以在modules/javax.validation下找到。请注意,模块通常会被打包并重新分发为JAR文件,但为了保持简单,我们在这里只使用类目录结构。
当涉及到Hibernate Validator时,事情变得更有趣。其模块描述符应该如下所示
module org.hibernate.validator.engine {(1)
exports org.hibernate.validator;(2)
exports org.hibernate.validator.cfg;
exports org.hibernate.validator.cfg.context;
exports org.hibernate.validator.cfg.defs;
exports org.hibernate.validator.constraints;
exports org.hibernate.validator.constraints.br;
exports org.hibernate.validator.constraintvalidation;
exports org.hibernate.validator.constraintvalidators;
exports org.hibernate.validator.engine;
exports org.hibernate.validator.group;
exports org.hibernate.validator.messageinterpolation;
exports org.hibernate.validator.parameternameprovider;
exports org.hibernate.validator.path;
exports org.hibernate.validator.resourceloading;
exports org.hibernate.validator.spi.cfg;
exports org.hibernate.validator.spi.group;
exports org.hibernate.validator.spi.resourceloading;
exports org.hibernate.validator.spi.time;
exports org.hibernate.validator.spi.valuehandling;
exports org.hibernate.validator.valuehandling;
exports org.hibernate.validator.internal.util.logging to jboss.logging;(3)
exports org.hibernate.validator.internal.xml to java.xml.bind;
requires javax.validation;(4)
requires joda.time;
requires javax.el.api;
requires jsoup;
requires jboss.logging.annotations;
requires jboss.logging;
requires classmate;
requires paranamer;
requires hibernate.jpa;
requires java.xml.bind;
requires java.xml;
requires java.scripting;
requires javafx.base;
provides javax.validation.spi.ValidationProvider with
org.hibernate.validator.HibernateValidator;(5)
uses javax.validation.ConstraintValidator;(6)
}
1 | 模块名称 |
2 | 模块导出的所有包;Hibernate Validator始终有一个非常明确定义的公共API,所有不打算公开使用的代码部分都位于一个internal 包中。自然地,只有非内部部分被导出。在将没有这样明确定义公共API的现有组件模块化时,事情将会更复杂。你可能会首先需要移动一些类,解开公共API和内部实现部分。 |
3 | 两个值得注意的例外是o.h.v.internal.util.logging 和o.h.v.internal.xml ,它们通过所谓的“合格导出”导出。这意味着只有jboss.logging模块可以访问日志包,只有java.xml.bind可以访问XML包。这是必需的,因为这些模块需要分别对日志和XML类进行反射访问。使用合格导出,这种内部类的暴露可以限制到最小的程度。 |
4 | 此模块需要的所有模块。这些是我们刚刚构建的javax.validation 模块,我们之前下载的所有自动模块以及随JDK本身提供的某些模块(如java.xml.bind 、javafx.base 等)。其中一些依赖关系可能在运行时被认为是可选的,例如,Joda Time只有在实际使用 @Past 或@Future 验证Joda Time类型时才需要。遗憾的是——与OSGi或JBoss Modules不同——Jigsaw不支持可选模块需求的概念,这意味着所有模块需求都必须在编译时以及运行时得到满足。这真遗憾,因为它阻止了库的常见模式,这些库公开的功能取决于运行时或不可用的依赖项/类。使用Jigsaw的“正确答案”是将这些可选功能提取到它们自己的模块中(例如,hibernate.validator.joda.time、hibernate.validator.jsoup等),但这会使事情对用户变得更复杂,然后他们需要处理所有这些模块。 |
5 | 该模块提供了一个ValidationProvider 服务的实现 |
6 | 该模块使用ConstraintValidator 服务,见下文 |
模块描述符就位后,我们可以编译Jigsaw启用的Hibernate Validator模块
cd sources/hibernate-validator/engine
mkdir -p target/generated-sources/jaxb
xjc -enableIntrospection -p org.hibernate.validator.internal.xml \(1)
-extension \
-target 2.1 \
-d target/generated-sources/jaxb \
src/main/xsd/validation-configuration-1.1.xsd src/main/xsd/validation-mapping-1.1.xsd \
-b src/main/xjb/binding-customization.xjb
javac -addmods java.xml.bind,java.annotations.common \(2)
-g \
-modulepath $BASE/modules:$BASE/automatic-modules \
-processorpath $BASE/tools/jboss-logging-processor-2.0.1.Final.jar:$BASE/tools/jdeparser-2.0.0.Final.jar:$BASE/tools/jsr250-api-1.0.jar:$BASE/automatic-modules/jboss-logging-annotations-2.0.1.Final.jar::$BASE/automatic-modules/jboss-logging-3.3.0.Final.jar \
-d $BASE/modules/org.hibernate.validator.engine \
$(find src/main/java -name "*.java") $(find target/generated-sources/jaxb -name "*.java")
cp -r src/main/resources/* $BASE/modules/org.hibernate.validator.engine;(3)
cp -r src/main/xsd/* $BASE/modules/org.hibernate.validator.engine/META-INF;(4)
cd $BASE
1 | 使用xjc 实用工具从XML约束描述符模式创建一些JAXB类型 |
2 | 通过javac 编译源代码 |
3 | 将错误信息资源包复制到模块目录 |
4 | 将XML模式文件复制到模块目录 |
注意编译时使用的模块路径指的是modules目录(包含javax.validation模块)和automatic-modules目录(包含所有依赖项,如Joda Time等)。
生成的模块位于modules/org.hibernate.validator.engine下。
试驾
将Bean Validation API和Hibernate Validator转换成合适的Jigsaw模块后,现在是时候对这些模块进行测试了。为此创建一个新的编译单元。
mkdir -p sources/com.example.acme/src/main/java/com/example/acme
在这个目录结构中,创建一个非常简单的域类和一个带有main方法用于验证它的类。
package com.example.acme;
import java.util.List;
import javax.validation.constraints.Min;
public class Car {
@Min(1)
public int seatCount;
public List<String> passengers;
public Car(int seatCount, List<String> passengers) {
this.seatCount = seatCount;
this.passengers = passengers;
}
}
package com.example.acme;
import java.util.Collections;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
public class ValidationTest {
public static void main(String... args) {
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<Car>> violations = validator.validate( new Car( 0, Collections.emptyList() ) );
System.out.println( "Validation error: " + violations.iterator().next().getMessage() );
}
}
这通过Validation#buildDefaultValidatorFactory()
获取一个Validator
对象(它内部使用上面描述的服务机制),并对一个Car
对象进行简单的验证。
当然我们还需要一个module-info.java文件。
module com.example.acme {
exports com.example.acme;
requires javax.validation;
}
现在应该很熟悉了:我们只导出了一个包(因此Hibernate Validator可以访问Car
对象的状态),并依赖于Bean Validation API模块。
关于这个模块的编译并没有什么新鲜事。
cd sources/com.example.acme
javac \
-g \
-modulepath $BASE/modules:$BASE/automatic-modules \
-d $BASE/modules/com.example.acme $(find src/main/java -name "*.java")
cd $BASE
有了这些,我们最终可以在Jigsaw下运行Bean Validation的第一个测试。
java \
-modulepath modules:automatic-modules \
-m com.example.acme/com.example.acme.ValidationTest
类似于javac,java命令也有一个新的modulepath选项,指向一个或多个包含Jigsaw模块的目录。使用-m开关指定要运行的主类,通过给出其模块名和完全限定类名。
嗯,这并不成功。
HV000149: An exception occurred during message interpolation
...
Caused by: java.lang.UnsupportedOperationException: ResourceBundle.Control not supported in named modules
at java.util.ResourceBundle.checkNamedModule(java.base@9-ea/ResourceBundle.java:1551)
at java.util.ResourceBundle.getBundle(java.base@9-ea/ResourceBundle.java:1533)
at org.hibernate.validator.resourceloading.PlatformResourceBundleLocator.loadBundle(org.hibernate.validator.engine/PlatformResourceBundleLocator.java:135)
这是怎么回事?Hibernate Validator正在使用Control类来合并具有相同名称的几个资源包的内容(错误消息)。在模块化环境中不再支持这一点,因此引发了上面的异常。最终,Hibernate Validator应该会自动处理这种情况(这被跟踪在HV-1073)。
现在我们先临时解决这个问题,禁用Hibernate Validator的AbstractMessageInterpolator
中的麻烦的包聚合。为此,将第165行构造函数调用中的true
改为false
。
...
new PlatformResourceBundleLocator(
CONTRIBUTOR_VALIDATION_MESSAGES,
null,
false
);
...
重新编译Hibernate Validator模块。再次运行测试后,你现在应该在控制台上看到以下输出。
Validation error: must be greater than or equal to 1
太好了,Jigsaw环境中的第一个成功的Bean Validation :)。
让我快速回顾一下到目前为止发生的事情。
-
我们向Bean Validation API添加了一个模块描述符,使其成为一个合适的Jigsaw模块。
-
我们向Hibernate Validator添加了一个模块描述符;这个Bean Validation提供者将通过服务机制被API模块发现。
-
我们创建了一个带有main方法的测试模块,它使用Bean Validation API来执行简单的对象验证。
(不)越界
现在让我们恶意地看看模块系统是否真的像预期的那样工作。为此,将Hibernate Validator的模块需求添加到模块描述符中(这样它才会被考虑进行编译),并在ValidationTest
中将验证器强制转换为内部实现类型。
module com.example.acme {
exports com.example.acme;
requires javax.validation;
requires org.hibernate.validator.engine;
}
package com.example.acme;
import javax.validation.Validation;
import org.hibernate.validator.internal.engine.ValidatorImpl;
public class ValidationTest {
public static void main(String... args) throws Exception{
ValidatorImpl validator = (ValidatorImpl) Validation.buildDefaultValidatorFactory()
.getValidator();
}
}
再次运行javac,你现在应该得到一个编译错误,抱怨找不到类型。所以Jigsaw阻止访问未导出的类型。如果你愿意,尝试从Hibernate Validator导出的包中引用任何内容,这将正常工作。
这与传统的平坦类路径有很大的不同,在平坦类路径中,你可能已经将你的代码库组织成公共和内部部分,但你必须希望你的库的用户不会跨越界限,并且无意或故意地访问内部类。
自定义约束
模块基本工作正常后,是时候更加深入一些,创建一个自定义Bean验证约束了。这个约束应该确保汽车上的乘客数量不超过座位数。
为此,我们需要一个注解类型
package com.example.acme;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Constraint(validatedBy = { PassengersDontExceedSeatCountValidator.class })
@Target({ TYPE })
@Retention(RUNTIME)
public @interface PassengersDontExceedSeatCount {
String message() default "{com.example.acme.PassengersDontExceedSeatCount.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
还需要一个约束验证器实现
package com.example.acme;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.example.acme.PassengersDontExceedSeatCount;
public class PassengersDontExceedSeatCountValidator implements
ConstraintValidator<PassengersDontExceedSeatCount, Car> {
@Override
public void initialize(PassengersDontExceedSeatCount constraintAnnotation) {}
@Override
public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
if ( car == null ) {
return true;
}
return car.passengers == null || car.passengers.size() <= car.seatCount;
}
}
还需要一个资源包,用于约束的错误信息
com.example.acme.PassengersDontExceedSeatCount.message=Passenger count must not exceed seat count
现在我们可以将新的约束类型添加到Car
类中,并最终进行验证
@PassengersDontExceedSeatCount
public class Car {
...
}
package com.example.acme;
import java.util.Arrays;
import java.util.Set;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ConstraintViolation;
public class ValidationTest {
public static void main(String... args) throws Exception{
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<Car>> violations = validator.validate(
new Car( 2, Arrays.asList( "Anna", "Bob", "Alice" ) )
);
System.out.println( "Validation error: " + violations.iterator().next().getMessage() );
}
}
再次编译示例模块;别忘了将资源包复制到模块目录
cd sources/com.example.acme
javac \
-g \
-modulepath $BASE/modules:$BASE/automatic-modules \
-d $BASE/modules/com.example.acme $(find src/main/java -name "*.java")
cp -r src/main/resources/* $BASE/modules/com.example.acme
cd $BASE
像之前一样运行它,你应该会得到一个错误消息。但是这是怎么回事呢?
Validation error: {com.example.acme.PassengersDontExceedSeatCount.message}
看起来错误消息没有被正确解析,所以返回了注解定义中未处理的原始插值消息键。这是为什么?
Bean验证错误消息是通过java.util.ResourceBundle
加载的,由于模块化环境的强封装性,Hibernate Validator模块无法“看到”示例模块中提供的资源包。
ResourceBundle
的更新JavaDocs清楚地表明,只有位于调用ResourceBundle#getBundle()
的调用者相同模块的包中的包才能访问。为了从其他模块访问资源包,需要按照Java 9使用服务加载机制;为该目的在JDK中添加了一个新的SPI接口,ResourceBundleProvider。
最终,Bean验证应该利用这个机制,但我们应该如何让它现在工作呢?Hibernate Validator有一个自定义资源包检索的扩展点,ResourceBundleLocator。
这现在非常有用:我们只需要在示例模块中创建该SPI的一个实现
package com.example.acme.internal;
import java.util.Locale;
import java.util.ResourceBundle;
import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator;
public class MyResourceBundleLocator implements ResourceBundleLocator {
@Override
public ResourceBundle getResourceBundle(Locale locale) {
return ResourceBundle.getBundle( "ValidationMessages", locale );
}
}
当启动验证器工厂时,使用该包定位器配置一个消息插值器,如下所示
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import com.example.acme.internal.MyResourceBundleLocator;
...
Validator validator = Validation.byDefaultProvider()
.configure()
.messageInterpolator( new ResourceBundleMessageInterpolator( new MyResourceBundleLocator() ) )
.buildValidatorFactory()
.getValidator();
由于现在调用ResourceBundle#getBundle()
的调用者与声明ValidationMessages
包的模块相同,因此可以找到包,并且错误消息将被正确插值。成功了!
保护您的隐私
在放置了自定义约束后,让我们更多地考虑封装。如果约束验证器实现不在导出的包中,而是在internal
下,那岂不是更好?毕竟,该类是实现细节,不应该直接被@PassengersDontExceedSeatCount
约束的用户引用。
Hibernate Validator的另一个特性在这里很有帮助:基于服务加载器的约束验证器发现。
这允许我们从约束注解中删除对其验证器的引用(只需添加一个空的@Constraint({})
注解)并将验证器实现重新定位到internal
包中
mv sources/com.example.acme/src/main/java/com/example/acme/PassengersDontExceedSeatCountValidator.java \
sources/com.example.acme/src/main/java/com/example/acme/internal
相应地修改源文件中的包声明并添加对Car
类型的导入。然后我们需要在模块描述符中将约束验证器声明为服务提供者
module com.example.acme {
...
provides javax.validation.ConstraintValidator
with com.example.acme.internal.PassengersDontExceedSeatCountValidator;
}
再次编译并运行示例模块。你应该会得到一个类似这样的错误
java.lang.IllegalAccessException: class org.hibernate.validator.internal.util.privilegedactions.NewInstance (in module org.hibernate.validator.engine) cannot access class com.example.acme.internal.PassengersDontExceedSeatCountValidator (in module com.example.acme) because module com.example.acme does not export com.example.acme.internal to module org.hibernate.validator.engine
这源于Hibernate Validator仅使用服务加载机制来检测验证器类型,并为每个特定约束使用实例化它们的事实。由于internal
包尚未导出,这种实例化注定会失败。现在你有两种选择
-
在module-info.java中使用有资格的导出,将此包暴露给Hibernate Validator模块
-
使用
java
命令的新-XaddExports
选项在运行模块时动态添加此导出
采用后一种方法,java
调用将如下所示
java \
-modulepath modules:automatic-modules \
-XaddExports:com.example.acme/com.example.acme.internal=org.hibernate.validator.engine \
-m com.example.acme/com.example.acme.ValidationTest
虽然这种方法可行,但在涉及到需要执行非导出类型反射操作的其它库时,可能会变得有点繁琐。Hibernate ORM和依赖注入框架等JPA提供者是其中的两个例子。
幸运的是,OpenJDK团队已经注意到了这个问题,并在Java模块系统需求列表中为其添加了条目:ReflectiveAccessToNonExportedTypes。我真诚地希望这个问题在Java 9最终确定之前得到解决。
XML配置
作为Bean Validation“Jigsaw-ify”之旅的最后一部分,让我们看看基于XML的约束配置。如果你不能通过注解或例如想要外部覆盖现有基于注解的约束,这是一个有用的替代方案。
Bean Validation规范定义了一个验证映射文件,该文件可以指向一个或多个约束映射XML文件。为了覆盖Car
类的@Min
约束,请按以下顺序创建以下文件:
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
xmlns="https://jboss.com.cn/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jboss.com.cn/xml/ns/javax/validation/configuration">
<constraint-mapping>META-INF/constraints-car.xml</constraint-mapping>
</validation-config>
<?xml version="1.0" encoding="UTF-8"?>
<constraint-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jboss.com.cn/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
xmlns="https://jboss.com.cn/xml/ns/javax/validation/mapping" version="1.1">
<bean class="com.example.acme.Car" ignore-annotations="true">
<field name="seatCount">
<constraint annotation="javax.validation.constraints.Min">
<element name="value">2</element>
</constraint>
</field>
</bean>
</constraint-mappings>
传统上,Bean Validation将在类路径上查找META-INF/validation.xml,并将任何链接的约束映射文件相对于该文件解析。如果你已经阅读了这篇文章,你可能不会对这一点感到惊讶,因为在Jigsaw中这不会工作。不再有“平坦类路径”的概念,因此验证提供者无法看到其他模块中的XML文件,类似于上面讨论的错误消息包的情况。
更具体地说,Hibernate Validator用来打开映射文件的ClassLoader#getResourceAsStream()
方法从JDK 9开始将不会对命名模块工作。这个变化将在许多项目迁移到Java 9时成为一个难题,因为它使得现有模块化环境(如OSGi)中已知的一些资源加载策略失效。例如,Hibernate Validator允许传递一个类加载器以加载用户提供的资源。在OSGi中,这可以用来传递所谓的包加载器,允许Hibernate Validator访问用户提供的约束映射文件和其他内容。不幸的是,这种模式在Jigsaw中无法使用,因为getResourceAsStream()
“不会在命名模块中找到资源”。
但是,Bean Validation也有解决这个问题的方法,它允许通过启动代码打开的InputStream
传递约束映射。由于Class#getResourceAsStream()
对于同一模块的资源仍然有效,因此当以这种方式启动验证器工厂时,一切都会按预期工作(不要忘记之后关闭流)
InputStream constraintMapping = ValidationTest.class.getResourceAsStream( "/META-INF/constraints-car.xml" );
Validator validator = Validation.byDefaultProvider()
.configure()
.addMapping( constraintMapping )
.buildValidatorFactory()
.getValidator();
这样,约束映射就是由同一模块中的代码打开的,因此可以被访问并传递给Bean Validation提供者。
总结
因此,我们完成了Bean Validation在Jigsaw模块系统中的准备工作实验。总体来说,一切进展顺利,而且不需要太多努力,Bean Validation和Hibernate Validator就可以在完全模块化的世界中成为一等公民。
实验中的一些观察
-
在我看来,缺少可选模块的要求导致了可用性问题,因为它阻止了库根据特定环境中运行时的类和模块来暴露额外的功能。这意味着库的用户要么需要提供他们实际上不需要的其他模块,要么如果库已经被分割成多个模块,每个模块对应一个可选依赖,那么他们现在需要添加几个模块,而之前只需要一个。
-
需要明确暴露内部包,以便像Hibernate Validator这样的库以及JPA提供者或DI容器可以通过反射方式访问,这可能变得很繁琐。我希望会有一种更全局的方式来启用此类访问,例如,通过将“可信模块”如上述库列入白名单。
-
关于加载由库用户提供的资源(如配置文件或资源包)的更改行为可能会在迁移到Java 9时影响许多应用程序。接受外部类加载器来加载用户资源的既定模式将不再适用,因此库需要通过提供专门的扩展点(类似于Bean Validation中的
addMapping(InputStream)
)或迁移到Jigsaw制作者所设想的基于服务的方法来适应。 -
如Maven(包括Surefire等插件用于运行测试)或Gradle等工具,以及IDE仍需要跟上Jigsaw的步伐。使用普通的
javac
和java
可能在一开始很有趣,但你很快就会希望拥有更强大的工具:) -
由于幸运地从一开始就非常注意适当的API/实现分离,将Hibernate Validator转换为Jigsaw模块相对容易。在没有这种明确区分的情况下对现有库或应用程序进行模块化将是一项更加艰巨的练习,因为它可能需要移动大量的类型,并分解不想要的(包)依赖。有一些工具可以帮助完成这项工作,但这可能需要一篇单独的博客文章来介绍。
有一点是可以肯定的:有趣的时光即将到来!虽然迁移可能会有些痛苦,但我认为Java迫切需要一个合适的模块系统,我非常期待看到它成为平台的一个集成部分。
如果您遵循了上述步骤或自己进行了Jigsaw的实验,有何反馈?让我们一起从不同的经验和见解中学习,请在下面分享任何关于这个话题的想法。
非常感谢Sander Mak、Sanne Grinovero和Guillaume Smet对这篇文章的审阅!