Skip to content

Private final fields in BigDecimal inspected in Java 17 #3980

@phillipuniverse

Description

@phillipuniverse

I am upgrading from spring-data-mongodb 3.2.3 to 3.3.1, and also to Java 17. Here is a test that worked before:

EDIT: this seems to be a Java 17 problem, the same issue exists in Spring Data Mongo 3.2.9 with Java 17 and Kotlin 1.6. Although this did work ok with Java 11.

package com.shipwell.opus.utils.numerics

import org.assertj.core.api.Assertions.assertThat
import org.bson.types.ObjectId
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.MongoRepository
import java.math.BigDecimal

@DataMongoTest
class RateDecimalMongoTest(@Autowired private val repository: RateDecimalMongoTestDocumentRepository) {

    @Test
    fun `test rate decimal saved and loaded properly`() {
        val rateDecimal = RateDecimal("203.0008")
        val objectId = repository.save(RateDecimalMongoTestDocument(someObj = rateDecimal)).id!!

        val savedObject = repository.findById(objectId).get()
        assertThat(savedObject.someObj).isInstanceOf(RateDecimal::class.java)
        assertThat(savedObject.someObj).isEqualTo(BigDecimal("203.0008"))
    }
}

@Document(collection = "RateDecimalMongoTestDocumentRepository")
class RateDecimalMongoTestDocument(
    var someObj: Any,
    @JvmField @Id var id: ObjectId? = null
)

interface RateDecimalMongoTestDocumentRepository : MongoRepository<RateDecimalMongoTestDocument, ObjectId>

This test was fine in 3.2.3 was fine with Java 11, but now fails with:

Unable to make field private final java.math.BigInteger java.math.BigDecimal.intVal accessible: module java.base does not "opens java.math" to unnamed module @7dc36524
java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.math.BigInteger java.math.BigDecimal.intVal accessible: module java.base does not "opens java.math" to unnamed module @7dc36524
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at org.springframework.util.ReflectionUtils.makeAccessible(ReflectionUtils.java:787)
	at org.springframework.data.mapping.context.AbstractMappingContext$PersistentPropertyCreator.doWith(AbstractMappingContext.java:550)
	at org.springframework.util.ReflectionUtils.doWithFields(ReflectionUtils.java:710)
	at org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:424)
	at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:381)
	at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:266)
	at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:92)
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:341)
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter$ConversionContext.convert(MappingMongoConverter.java:2077)

Where the RateDecimalConfig class looks like this:

public class RateDecimal extends AbstractFixedDecimal implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final int scale = 4;

    /**
     * Necessary constructor that MongoDB will use to create the instance
     * @param val String whose value is used to construct the instance
     */
    @PersistenceConstructor
    public RateDecimal(@Value("#root.amount") String val) {
        super(scaleToString(new BigDecimal(val), scale));
    }

    public RateDecimal(double val) {
        super(scaleToString(new BigDecimal(val), scale));
    }

    public RateDecimal(int val) {
        super(scaleToString(new BigDecimal((val)), scale));
    }

    public RateDecimal(long val) {
        super(scaleToString(new BigDecimal(val), scale));
    }

}

And then AbstractFixedDecimal:

public abstract class AbstractFixedDecimal extends BigDecimal {

    AbstractFixedDecimal(String val) {
        super(val);
    }

    static String scaleToString(BigDecimal unscaledVal, int scale) {
        return unscaledVal.setScale(scale, RoundingMode.HALF_UP).toString();
    }
}

To deal with the serialization of a RateDecimal to a document, I have this converter registered as a mongoCustomConversions bean:

    @Bean
    fun mongoCustomConversions(): MongoCustomConversions {
        return MongoCustomConversions(mutableListOf(
           RateDecimalConverter()
        ))
    }

    private final static class FixedDecimalToDocumentConverter<T extends AbstractFixedDecimal> implements Converter<T, Document> {
        @Override
        public Document convert(@NotNull T source) {
            Document doc = new Document();
            doc.append(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, source.getClass().getName());

            // Mongo by default has BigDecimal converters to both Decimal128 and String, so there is no obvious
            // correct format to use when storing the value. Choosing String because of spring data Mongo documentation.
            // See https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mapping-conversion
            doc.append("amount", source.toString());
            return doc;
        }
    }

My question is, what changed in 3.3 that caused the process to look at these private fields when before it didn't? It kind of makes me think that my PersistentConstructor is being ignored.

This is the same behavior in 3.2, but has to do with using Java 17.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: invalidAn issue that we don't feel is valid

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions