JDK 16+ access exceptions

Problem symptoms

On JDK 16 and above you may encounter InaccessibleObjectException or IllegalAccessException in the following known cases:

  1. Some usages of mockkStatic on Java standard library classes, e.g.
    mockkStatic(Instant::class)
    every { Instant.now() } returns Instant.parse("2022-09-15T09:31:26.919Z")
    
    java.time.format.DateTimeParseException: Text '2022-09-15T09:31:26.919Z' could not be parsed:
      Unable to make private static java.time.Instant java.time.Instant.create(long,int) accessible: 
      module java.base does not "opens java.time" to unnamed module @4501b7af
    
  2. Spying on Java standard library classes, e.g.
    val socket = Socket()
    val spy = spyk<Socket>(socket)
    

    Note: this includes creating @SpyKBean on spring data repository instances, because they are instances of java.lang.reflect.Proxy in fact.

    Error creating bean with name 'featureRepository': Post-processing of FactoryBean's singleton object failed;
     nested exception is java.lang.IllegalAccessException: class io.mockk.impl.InternalPlatform 
     cannot access a member of class java.lang.reflect.Proxy (in module java.base) with modifiers "protected"
    

Problem cause

JDK 16 enforces strong encapsulation of standard modules. In practice this mean overriding accessibility modifiers in JDK classes (for example, privateMember.setAccessible(true)) is forbidden.

https://blogs.oracle.com/javamagazine/post/a-peek-into-java-17-continuing-the-drive-to-encapsulate-the-java-runtime-internals

Solution / workaround

Add JVM argument --add-opens java.base/full.package.name=ALL-UNNAMED for each package you need to mock. Package name should be fully qualified.
Description: https://docs.oracle.com/en/java/javase/16/migrate/migrating-jdk-8-later-jdk-releases.html

Example for Gradle users:

tasks.test {
    jvmArgs(
        "--add-opens", "java.base/java.time=ALL-UNNAMED",
        "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED"
    )
}

Example for Gradle users within an Android module:

android {
    testOptions {
        unitTests.all {
            jvmArgs(
                "--add-opens", "java.base/java.time=ALL-UNNAMED",
                "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED"
            )
        }
    }
}

Example for Maven users:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            --add-opens java.base/java.time=ALL-UNNAMED
            --add-opens java.base/java.lang.reflect=ALL-UNNAMED
        </argLine>
    </configuration>
</plugin>

Linked issues