3

jdk.internal.misc.SharedSecrets describes itself as:

A repository of "shared secrets", which are a mechanism for calling implementation-private methods in another package without using reflection. A package-private class implements a public interface and provides the ability to call package-private methods within that package; the object implementing that interface is provided through a third package to which access is restricted. This framework avoids the primary disadvantage of using reflection for this purpose, namely the loss of compile-time checking.

Can someone please provide an example that demonstrates how this mechanism enables classes in one package to access package-private methods in a different package?

Holger
  • 285,553
  • 42
  • 434
  • 765
Gili
  • 86,244
  • 97
  • 390
  • 689
  • 1
    Since this technique works since Java 1.1 and Sun’s `SharedSecrets` is not much younger, there is no relevance to `[java-9]`. This is illustrated by the fact that the article you’ve cited in the answer is almost ten years old. – Holger Oct 13 '17 at 07:08
  • ....and the only relevant change in JDK9 about `SharedSecrets` I could find [was this](https://stackoverflow.com/questions/46612648/javalangaccess-and-sharedsecrets-in-java-9) – Naman Oct 13 '17 at 07:45

1 Answers1

8

Quoting Andrew John Hughes:

When looking through OpenJDK for the VM project, I noticed that they have a rather interesting solution to this. This is encapsulated in sun.misc.SharedSecrets. This class provides access to instances of a number of public interfaces, such as sun.misc.JavaLangAccess. The actual implementations are provided as inner classes in the appropriate package e.g. java.lang, where it has access to the private and package-private variables and methods within.

Say that you have API classes scattered across multiple packages. You want them to be able to access each other's internals, without exposing them to end-users. What do you do?

Option 1: Without Java Modules

  1. Create an "internal" package that will be omitted from the public Javadoc (e.g. com.example.internal)
  2. Declare one or more interfaces in the internal package, referencing the private functionality you are trying to access.
  3. Declare a public class (e.g. SharedSecrets) in the internal package to hold implementations of these interfaces.
  4. Use static initializers in your API classes to get/set implementations of these interfaces from/to SharedSecrets.
  5. Now, API classes can access each other's internals by piggybacking through a trusted intermediary (SharedSecrets).

Option 2: With Java Modules

  1. Say you have two modules: main and test, and you want test to access private or package-private methods and fields inside main.
  2. Declare a public class SharedSecrets inside the main module, in a non-exported package. For example: main.internal.SharedSecrets.
  3. In main's module-info.java, add exports main.internal to test.
  4. Meaning, the package main.internal will only be accessible to module test.
  5. Because SharedSecrets is public, anyone in main (even from different packages) can push bridge functions or fields into it. It actually works the other way as well (test can push bridge functions into main) but I've never needed to do this to date.
  6. Now, anytime test wishes to access the internals of main, it simply piggybacks its calls through SharedSecrets.

This solution is especially nice because the resulting Javadoc and IDE auto-complete will look a lot cleaner.

Concrete Example

External Users
├── external
│   └── EndUser.java
└── module-info.java

Library
├── library
│   ├── character
│   │   └── Character.java
│   ├── story
│   │   └── Story.java
│   └── internal
│       ├── SharedSecrets.java
│       └── SecretCharacter.java
└── module-info.java
  • We want to expose Character's internals to Story without EndUser gaining access.

End-user code


external/EndUser.java:

package external;

import library.character.Character;
import library.story.Story;

public class EndUser
{
    public static void main(String[] args)
    {
        Story story = new Story();
        story.introduce(Character.HARRY_POTTER);
        story.introduce(Character.RON_WEASLEY);
        story.introduce(Character.HERMIONE_GRANGER);
    }
}

module-info.java:

module external
{
    requires library;
}

Library code


library/story/Story.java

package library.story;

import library.character.Character;
import library.internal.SecretCharacter;
import library.internal.SharedSecrets;

public final class Story
{
    private static final SharedSecrets sharedSecrets =
      SharedSecrets.INSTANCE;

    public void introduce(Character character)
    {
        System.out.println(character.name() + " enters the room and says: " + 
          sharedSecrets.secretCharacter.getPhrase(character));
    }
}

library/character/Character.java:

package library.character;

import library.internal.SecretCharacter;
import library.internal.SharedSecrets;

public enum Character
{
    HARRY_POTTER
    {
        @Override
        String getPhrase()
        {
            return "Your bird, there was nothing I could do. He just caught fire.";
        }
    },
    RON_WEASLEY
    {
        @Override
        String getPhrase()
        {
            return "Who are you and what have you done with Hermione Granger?";
        }
    },
    HERMIONE_GRANGER
    {
        @Override
        String getPhrase()
        {
            return "I'm not an owl!";
        }
    };

    static
    {
        SharedSecrets.INSTANCE.secretCharacter = new SecretCharacter()
        {
            @Override
            public String getPhrase(Character character)
            {
                return character.getPhrase();
            }
        };
    }

    abstract String getPhrase();
}

library/internal/SharedSecrets.java:

package library.internal;

public enum SharedSecrets
{
    INSTANCE;
    public SecretCharacter secretCharacter;
}

library/internal/SecretCharacter.java:

package library.internal;

import library.character.Character;

public interface SecretCharacter
{
    String getPhrase(Character character);
}

module-info.java:

module library
{
    exports library.character;
    exports library.story;
}

Output

HARRY_POTTER enters the room and says: Your bird, there was nothing I could do. He just caught fire.

RON_WEASLEY enters the room and says: Who are you and what have you done with Hermione Granger?

HERMIONE_GRANGER enters the room and says: I'm not an owl!

Notice

  • Character.getPhrase() is package-protected.
  • Story is located in a different package.
  • Normally Story wouldn't be able to invoke Character.getPhrase(); however, SharedSecrets allows Character to share access with classes that it trusts.
  • Story invokes SharedSecrets.INSTANCE.secretCharacter which uses an anonymous nested class to access Character's internals.
  • Story can access SharedSecrets because the two are located in the same module, but external users cannot access it because module-info.java does not export that package.
Gili
  • 86,244
  • 97
  • 390
  • 689
  • As the code is written it will raise a NullPointerException as the Story class will be initialized first by EndUser main, however, SharedSecrets.INSTANCE.secretCharacter is null when Story.secretCharacter is set. Better to not have static secretCharacter variable in Story. – Ceki Oct 01 '22 at 18:57
  • 1
    @Ceki You are right. I updated the example code to match what I actually do in my libraries. This should fix the problem. – Gili Oct 01 '22 at 20:04
  • It's a small world. – Ceki Oct 01 '22 at 20:24