Descripción del problema

Tengo un proyecto java con dependencia de gradle de org.javamoney:moneta:1.3.

También tengo dos grupos de Kubernetes. Implemento mi aplicación java usando docker-container.

Cuando implemento mi aplicación en el primer clúster de Kubernetes, todo está bien. Pero cuando implemento mi aplicación (el mismo contenedor acoplable) en el segundo clúster de Kubernetes, aparece el siguiente error:

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)

Aparece en el siguiente código:

MonetaryAmount amount = javax.money.Monetary.getDefaultAmountFactory()
    .setCurrency("USD")
    .setNumber(1L)
    .create();

Versiones de software

  • Moneta: 1.3.
  • Gradle: 6.0.1.
  • Imagen base de acoplador: openjdk:11.0.7-jdk-slim.
  • Bota de primavera: 2.2.7.RELEASE.
  • Kubernetes (la misma versión en ambos grupos): 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 versión de openjdk "11.0.7" 2020-04-14 OpenJDK Runtime Environment 18.9 (compilación 11.0.7 + 10) OpenJDK 64-Bit Server VM 18.9 (compilación 11.0.7 + 10, modo mixto) .

Lo que he intentado

Declarar la dependencia de Gradle de manera diferente

Encontré esta pregunta y me dio la idea de intentar declarar la dependencia de Gradle de alguna manera diferente. Yo he tratado:

  • 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'

Desafortunadamente, no dio ningún resultado positivo.

Copiar y pegar configuraciones del cargador de servicios para Moneta

Como se menciona en este comentario he intentado copiar la configuración del cargador de servicios de Moneta al siguiente directorio del proyecto: src/main/resources/META-INF/services.

Lamentablemente, no ayudó.

Moneda personalizada inicial sin resorte

Intenté hacerlo solo en la clase Main, pero no resolvió el problema.

Preguntas

  1. ¿Cuál es la causa raíz de este problema?
  2. ¿Cuál es la solución adecuada a este problema?
4
Maksim Iakunin 27 abr. 2020 a las 21:27

2 respuestas

La mejor respuesta

TL; DR

El problema estaba en la inicialización moneta SPI concurrente dentro de Java 11.

Solución del problema

El problema se puede resolver extrayendo MonetaryAmountFactory a spring-bean e inyectándolo donde sea necesario:

@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);
    }
}

En lugar de usar esta fábrica directamente:

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);
    }
}

¿Por qué se produce un problema en los grupos de Kubernetes?

Descubrí que había configuración de límite de recursos diferente en la mencionada anteriormente. Kubernetes-racimos.

Clúster con excepción:

Limits:
  cpu:     6
  memory:  20G
Requests:
  cpu:      3
  memory:   20G

Clúster sin excepción:

Limits:
  cpu:     2
  memory:  2G
Requests:
  cpu:      2
  memory:   128Mi

Parece que el clúster con más recursos da más oportunidades para que ocurra la inicialización monetaria concurrente.

Ejemplo reproducible mínimo

El ejemplo mínimo reproducible se puede encontrar en este repositorio de github.

Vale la pena mencionar que el error no se reproduce en Java 8.

1
Maksim Iakunin 10 may. 2020 a las 22:20

Como solución alternativa, puede crear un proveedor de servicios como

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;
    }
}
}

Y antes de usar cualquier llamada de clase de biblioteca de dinero

Bootstrap.init(new CbplMonetaServiceProvider());

Esto también solucionará el error de moneda.

La única línea cambiada en el proveedor que agregamos en comparación con PriorityAwareServiceProvider es esta línea

for(T service:ServiceLoader.load(serviceType, Monetary.class.getClassLoader())){

Acabamos de especificar el cargador de clases, por lo que en lugar de Thread.getCurrentThread (). getClassLoader () está utilizando el cargador de clases que proporcionamos.

1
Utkan Ozyurek 4 jun. 2020 a las 17:54