I have a java project with gradle dependency from org.javamoney:moneta:1.3
.
Also I have two Kubernetes clusters. I deploy my java application using docker-container.
When I deploy my app in the first Kubernetes cluster everything is fine. But when I deploy my app (the same docker-container) in the second Kubernetes cluster following error appears:
javax.money.MonetaryException: No MonetaryAmountsSingletonSpi loaded.
at javax.money.Monetary.lambda$getDefaultAmountFactory$13(Monetary.java:291)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at javax.money.Monetary.getDefaultAmountFactory(Monetary.java:291)
It appears in the following code:
MonetaryAmount amount = javax.money.Monetary.getDefaultAmountFactory()
.setCurrency("USD")
.setNumber(1L)
.create();
1.3
.6.0.1
.openjdk:11.0.7-jdk-slim
.2.2.7.RELEASE
.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 -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)
.I found this question and it gave me an idea try to declare gradle-dependency in some different way. I have tried:
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'
Unfortunately, it did not give any positive results.
As mentioned in this comment I've tried to copy service loader configuration from Moneta to following project directory: src/main/resources/META-INF/services
.
Unfortunately, it didn't help.
I've tried to do it just in the Main-class, but it didn't solve the problem.
The problem was in concurrent moneta SPI initialization within Java 11.
The problem can be solved by extracting MonetaryAmountFactory
to spring-bean and injecting it where needed:
@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);
}
}
instead of using this factory directly:
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);
}
}
I discovered that there were diferrent resource limit configuration on above-mentioned Kubernetes-clusters.
Cluster with exception:
Limits:
cpu: 6
memory: 20G
Requests:
cpu: 3
memory: 20G
Cluster without exception:
Limits:
cpu: 2
memory: 2G
Requests:
cpu: 2
memory: 128Mi
Seems that cluster with more resources gives more opportunity to concurrent moneta initialization happened.
The minimal reproducible example can be found in this github-repository.
It is worth mentioned that the bug is not reproduced on Java 8.
as a workaround you can create a service provider like
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;
}
}
}
and before using any money library class call
Bootstrap.init(new CbplMonetaServiceProvider());
this will fix the Currency error too.
the only changed line on the provider we added compared to the PriorityAwareServiceProvider is this line
for(T service:ServiceLoader.load(serviceType, Monetary.class.getClassLoader())){
we just specified the class loader so instead of Thread.getCurrentThread().getClassLoader() it is using the class loader we provide.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With