MonetaryException:没有加载 MonetaryAmountsSingletonSpi
MonetaryException: No MonetaryAmountsSingletonSpi loaded
问题描述
我有一个 java 项目,其中 gradle 依赖于 org.javamoney:moneta:1.3
。
我还有两个 Kubernetes 集群。我使用 docker-container.
部署我的 java 应用程序
当我在 first Kubernetes 集群中部署我的应用程序时,一切都很好。但是当我在 second Kubernetes 集群中部署我的应用程序(相同的 docker-容器)时,出现以下错误:
javax.money.MonetaryException: No MonetaryAmountsSingletonSpi loaded.
at javax.money.Monetary.lambda$getDefaultAmountFactory(Monetary.java:291)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at javax.money.Monetary.getDefaultAmountFactory(Monetary.java:291)
它出现在下面的代码中:
MonetaryAmount amount = javax.money.Monetary.getDefaultAmountFactory()
.setCurrency("USD")
.setNumber(1L)
.create();
软件版本
- Moneta:
1.3
.
- Gradle:
6.0.1
.
- 基础docker-图像:
openjdk:11.0.7-jdk-slim
.
- Spring 开机:
2.2.7.RELEASE
.
- Kubernetes(两个集群上的版本相同):
Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.3", GitCommit:"2d3c76f9091b6bec110a5e63777c332469e0cba2", GitTreeState:"clean", BuildDate:"2019-08-19T11:05:50Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}
.
- Java:
java -version
openjdk version "11.0.7" 2020-04-14
OpenJDK Runtime Environment 18.9 (build 11.0.7+10)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.7+10, mixed mode)
.
我试过的
以不同方式声明 gradle-依赖关系
我找到了 ,它给了我一个尝试以不同方式声明 gradle-依赖性的想法。我试过:
implementation 'org.javamoney:moneta:1.3'
compile group: 'org.javamoney', name: 'moneta', version: '1.3', ext: 'pom'
compile 'org.javamoney:moneta:1.3'
runtimeOnly 'org.javamoney:moneta:1.3'
不幸的是,它没有给出任何积极的结果。
复制粘贴 Moneta 的服务加载器配置
如 I've tried to copy service loader configuration from Moneta中所述到以下项目目录:src/main/resources/META-INF/services
.
不幸的是,它没有帮助。
在没有 spring
的情况下初始化自定义货币
我试过在 Main-class 中这样做,但没有解决问题。
问题
- 这个问题的根本原因是什么?
- 这个问题的正确解决方案是什么?
TL;DR
问题出在 Java 11 内的并发 moneta SPI 初始化中。
问题解决
问题可以通过将MonetaryAmountFactory
提取到spring-bean并在需要的地方注入来解决:
@Bean
public MonetaryAmountFactory<?> money() {
return Monetary.getDefaultAmountFactory();
}
@Component
@RequiredArgsConstructor
public static class Runner implements CommandLineRunner {
private final MonetaryAmountFactory<?> amountFactory;
@Override
public void run(String... args) {
var monetaryAmount = this.amountFactory
.setCurrency("EUR")
.setNumber(1)
.create();
System.out.println("monetaryAmount = " + monetaryAmount);
}
}
而不是直接使用这个工厂:
public static class Runner implements CommandLineRunner {
@Override
public void run(String... args) {
var monetaryAmount = Monetary.getDefaultAmountFactory()
.setCurrency("EUR")
.setNumber(1)
.create();
System.out.println("monetaryAmount = " + monetaryAmount);
}
}
为什么 Kubernetes 集群会出现问题?
我发现在上述 Kubernetes 集群上有不同的resource limit configuration。
有异常的集群:
Limits:
cpu: 6
memory: 20G
Requests:
cpu: 3
memory: 20G
集群无一例外:
Limits:
cpu: 2
memory: 2G
Requests:
cpu: 2
memory: 128Mi
似乎具有更多资源的集群为并发 moneta 初始化提供了更多机会。
最小的可重现示例
可以在 this github-repository 中找到最小的可重现示例。
值得一提的是,这个bug在Java8.
上没有重现
作为解决方法,您可以创建一个服务提供商,例如
public class MyServiceLoader implements ServiceProvider {
/**
* List of services loaded, per class.
*/
private final ConcurrentHashMap<Class<?>, List<Object>> servicesLoaded = new ConcurrentHashMap<>();
private static final int PRIORITY = 10;
/**
* Returns a priority value of 10.
*
* @return 10, overriding the default provider.
*/
@Override
public int getPriority() {
return PRIORITY;
}
/**
* Loads and registers services.
*
* @param serviceType The service type.
* @param <T> the concrete type.
* @return the items found, never {@code null}.
*/
@Override
public <T> List<T> getServices(final Class<T> serviceType) {
@SuppressWarnings("unchecked")
List<T> found = (List<T>) servicesLoaded.get(serviceType);
if (found != null) {
return found;
}
return loadServices(serviceType);
}
public static int compareServices(Object o1, Object o2) {
int prio1 = 0;
int prio2 = 0;
Priority prio1Annot = o1.getClass().getAnnotation(Priority.class);
if (prio1Annot != null) {
prio1 = prio1Annot.value();
}
Priority prio2Annot = o2.getClass().getAnnotation(Priority.class);
if (prio2Annot != null) {
prio2 = prio2Annot.value();
}
if (prio1 < prio2) {
return 1;
}
if (prio2 < prio1) {
return -1;
}
return o2.getClass().getSimpleName().compareTo(o1.getClass().getSimpleName());
}
/**
* Loads and registers services.
*
* @param serviceType The service type.
* @param <T> the concrete type.
* @return the items found, never {@code null}.
*/
private <T> List<T> loadServices(final Class<T> serviceType) {
List<T> services = new ArrayList<>();
try {
for (T t : ServiceLoader.load(serviceType, Monetary.class.getClassLoader())) {
services.add(t);
}
services.sort(CbplMonetaServiceProvider::compareServices);
@SuppressWarnings("unchecked") final List<T> previousServices = (List<T>) servicesLoaded.putIfAbsent(serviceType, (List<Object>) services);
return Collections.unmodifiableList(previousServices != null ? previousServices : services);
} catch (Exception e) {
Logger.getLogger(CbplMonetaServiceProvider.class.getName()).log(Level.WARNING,
"Error loading services of type " + serviceType, e);
services.sort(CbplMonetaServiceProvider::compareServices);
return services;
}
}
}
并且在使用任何货币库之前class调用
Bootstrap.init(new CbplMonetaServiceProvider());
这也将修复货币错误。
与 PriorityAwareServiceProvider 相比,我们添加的提供商的唯一更改行是这一行
for(T service:ServiceLoader.load(serviceType, Monetary.class.getClassLoader())){
我们刚刚指定了 class 加载器,因此它使用的是我们提供的 class 加载器,而不是 Thread.getCurrentThread().getClassLoader()。
问题描述
我有一个 java 项目,其中 gradle 依赖于 org.javamoney:moneta:1.3
。
我还有两个 Kubernetes 集群。我使用 docker-container.
部署我的 java 应用程序当我在 first Kubernetes 集群中部署我的应用程序时,一切都很好。但是当我在 second Kubernetes 集群中部署我的应用程序(相同的 docker-容器)时,出现以下错误:
javax.money.MonetaryException: No MonetaryAmountsSingletonSpi loaded.
at javax.money.Monetary.lambda$getDefaultAmountFactory(Monetary.java:291)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at javax.money.Monetary.getDefaultAmountFactory(Monetary.java:291)
它出现在下面的代码中:
MonetaryAmount amount = javax.money.Monetary.getDefaultAmountFactory()
.setCurrency("USD")
.setNumber(1L)
.create();
软件版本
- Moneta:
1.3
. - Gradle:
6.0.1
. - 基础docker-图像:
openjdk:11.0.7-jdk-slim
. - Spring 开机:
2.2.7.RELEASE
. - Kubernetes(两个集群上的版本相同):
Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.3", GitCommit:"2d3c76f9091b6bec110a5e63777c332469e0cba2", GitTreeState:"clean", BuildDate:"2019-08-19T11:05:50Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}
. - Java:
java -version openjdk version "11.0.7" 2020-04-14 OpenJDK Runtime Environment 18.9 (build 11.0.7+10) OpenJDK 64-Bit Server VM 18.9 (build 11.0.7+10, mixed mode)
.
我试过的
以不同方式声明 gradle-依赖关系
我找到了
implementation 'org.javamoney:moneta:1.3'
compile group: 'org.javamoney', name: 'moneta', version: '1.3', ext: 'pom'
compile 'org.javamoney:moneta:1.3'
runtimeOnly 'org.javamoney:moneta:1.3'
不幸的是,它没有给出任何积极的结果。
复制粘贴 Moneta 的服务加载器配置
如src/main/resources/META-INF/services
.
不幸的是,它没有帮助。
在没有 spring
的情况下初始化自定义货币我试过在 Main-class 中这样做,但没有解决问题。
问题
- 这个问题的根本原因是什么?
- 这个问题的正确解决方案是什么?
TL;DR
问题出在 Java 11 内的并发 moneta SPI 初始化中。
问题解决
问题可以通过将MonetaryAmountFactory
提取到spring-bean并在需要的地方注入来解决:
@Bean
public MonetaryAmountFactory<?> money() {
return Monetary.getDefaultAmountFactory();
}
@Component
@RequiredArgsConstructor
public static class Runner implements CommandLineRunner {
private final MonetaryAmountFactory<?> amountFactory;
@Override
public void run(String... args) {
var monetaryAmount = this.amountFactory
.setCurrency("EUR")
.setNumber(1)
.create();
System.out.println("monetaryAmount = " + monetaryAmount);
}
}
而不是直接使用这个工厂:
public static class Runner implements CommandLineRunner {
@Override
public void run(String... args) {
var monetaryAmount = Monetary.getDefaultAmountFactory()
.setCurrency("EUR")
.setNumber(1)
.create();
System.out.println("monetaryAmount = " + monetaryAmount);
}
}
为什么 Kubernetes 集群会出现问题?
我发现在上述 Kubernetes 集群上有不同的resource limit configuration。
有异常的集群:
Limits:
cpu: 6
memory: 20G
Requests:
cpu: 3
memory: 20G
集群无一例外:
Limits:
cpu: 2
memory: 2G
Requests:
cpu: 2
memory: 128Mi
似乎具有更多资源的集群为并发 moneta 初始化提供了更多机会。
最小的可重现示例
可以在 this github-repository 中找到最小的可重现示例。
值得一提的是,这个bug在Java8.
上没有重现作为解决方法,您可以创建一个服务提供商,例如
public class MyServiceLoader implements ServiceProvider {
/**
* List of services loaded, per class.
*/
private final ConcurrentHashMap<Class<?>, List<Object>> servicesLoaded = new ConcurrentHashMap<>();
private static final int PRIORITY = 10;
/**
* Returns a priority value of 10.
*
* @return 10, overriding the default provider.
*/
@Override
public int getPriority() {
return PRIORITY;
}
/**
* Loads and registers services.
*
* @param serviceType The service type.
* @param <T> the concrete type.
* @return the items found, never {@code null}.
*/
@Override
public <T> List<T> getServices(final Class<T> serviceType) {
@SuppressWarnings("unchecked")
List<T> found = (List<T>) servicesLoaded.get(serviceType);
if (found != null) {
return found;
}
return loadServices(serviceType);
}
public static int compareServices(Object o1, Object o2) {
int prio1 = 0;
int prio2 = 0;
Priority prio1Annot = o1.getClass().getAnnotation(Priority.class);
if (prio1Annot != null) {
prio1 = prio1Annot.value();
}
Priority prio2Annot = o2.getClass().getAnnotation(Priority.class);
if (prio2Annot != null) {
prio2 = prio2Annot.value();
}
if (prio1 < prio2) {
return 1;
}
if (prio2 < prio1) {
return -1;
}
return o2.getClass().getSimpleName().compareTo(o1.getClass().getSimpleName());
}
/**
* Loads and registers services.
*
* @param serviceType The service type.
* @param <T> the concrete type.
* @return the items found, never {@code null}.
*/
private <T> List<T> loadServices(final Class<T> serviceType) {
List<T> services = new ArrayList<>();
try {
for (T t : ServiceLoader.load(serviceType, Monetary.class.getClassLoader())) {
services.add(t);
}
services.sort(CbplMonetaServiceProvider::compareServices);
@SuppressWarnings("unchecked") final List<T> previousServices = (List<T>) servicesLoaded.putIfAbsent(serviceType, (List<Object>) services);
return Collections.unmodifiableList(previousServices != null ? previousServices : services);
} catch (Exception e) {
Logger.getLogger(CbplMonetaServiceProvider.class.getName()).log(Level.WARNING,
"Error loading services of type " + serviceType, e);
services.sort(CbplMonetaServiceProvider::compareServices);
return services;
}
}
}
并且在使用任何货币库之前class调用
Bootstrap.init(new CbplMonetaServiceProvider());
这也将修复货币错误。
与 PriorityAwareServiceProvider 相比,我们添加的提供商的唯一更改行是这一行
for(T service:ServiceLoader.load(serviceType, Monetary.class.getClassLoader())){
我们刚刚指定了 class 加载器,因此它使用的是我们提供的 class 加载器,而不是 Thread.getCurrentThread().getClassLoader()。