JEP 396 Strongly Encapsulate JDK Internals by Default

Contexte

Retour en septembre 2017, le JDK 9 vient de sortie avec la mise en place du système de modules provenant du Projet Jigsaw. Cela a aussi été l’occassion d’introduire la fonctionnalité JEP 260: Encapsulate Most Internal APIs pour l’encapsulation des APIs internes.

La communauté s’inquiète vis à vis de l’accès de ces API, soit vis à vis de la mise en oeuvre d’alternative ou l’absence d’alternative. Cela avait fait couler beaucoup d’encre à l’époque.

avertissement Cela a été aussi un frein à l’adoption du JDK 9 en pensant que beaucoup de librairies allaient être cassées. Dans cette logique, beaucoup de projets sont restés au JDK 8.

Pourtant, suite aux retours de la communauté, la JEP introduit une nouvelle option afin de configurer l’accès ou non à ces API. La valeur par défaut est de les autoriser comme en JDK 8. Ce qui va faciliter la transition.

astuce De même aujourd’hui, beaucoup de développeur Java pense qu’il faut faire des modules dès que nous travaillons avec un JDK 9+. Cela n’est absolument pas obligatoire.

Description

L’objectif de ce renforcement est d’améliorer le sécurité et la maintenance du JDK. En effet, si les développeurs n’ont pas accès aux classes internes du JDK (même via la réflexion), cela permettrait de faciliter le travail sur les prochaines évolutions du JDK.

Le but est de cette JEP est de passer une étape supplémentaire, en encourageant les développeurs à migrer des API internes du JDK aux API stantard. Le but n’est pas de supprimer, ni de modifier les API internes critiques qui n’ont pas de remplacement standard au niveau de l’API.

astuce Cela implique que la fameux classe sun.misc.Unsafe reste disponible

Cela concerne quoi ?

Les classes, méthodes, attributs concernées sont :

  • Les classes non publiques, les méthodes non publiques et les attributs non public des packages java.*

  • Tous les classes, les méthodes et les attributs des packages sun., com.sun. et jdk.*.

avertissement Dés le début, Sun alertait qu’il ne fallait pas utiliser ces classes FAQ de Sun

Illustration

Pour illustrer le sujet, nous allons faire un programme qui veut accèder à un attribut protégé du JDK.

var ks = java.security.KeyStore.getInstance("jceks");
var f = ks.getClass().getDeclaredField("keyStoreSpi");
f.setAccessible(true);

System.out.println(f.get(ks)); // l'attribut est maintenant accessible

Le programme fonctionne très bien en JDK 8.

Le programme fonctionne aussi très bien avec un JDK 9+. Cependant, un message d’avertissement apparait lors du premier accès illégal aux packages.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by fr.lbenoit.billets.codes_sources.ReflexionProgramme (file:/media/lbenoit/DATA/opt/sources/lbenoit/blog/billet-codes-sources/2021/02/2021-02-StrongEncapsulation/target/classes/) to field java.security.KeyStore.keyStoreSpi
WARNING: Please consider reporting this to the maintainers of fr.lbenoit.billets.codes_sources.ReflexionProgramme
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
com.sun.crypto.provider.JceKeyStore@439f5b3d

En revanche, avec le renforcement de l’encapsulation, le programme ne fonctionne plus en JDK 16.

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.security.KeyStoreSpi java.security.KeyStore.keyStoreSpi accessible: module java.base does not "opens java.security" to unnamed module @60e89085
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:177)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:171)
	at fr.lbenoit.billets.codes_sources.ReflexionProgramme.main(ReflexionProgramme.java:10)

Désactiver ce renforcement

Comme évoqué plus haut, dès le JDK 9, l’équipe avait prévu une option qui permet de controller l’accès ou non à ces API.

  • --illegal-access=permit autorise les accès illégaux en affichant un avertissement lors du premier accès illégal du package.

  • --illegal-access=warn identique à permit mais l’avertissement apparait à chaque appel.

  • --illegal-access=debug identique à warn mais l’avertissement est accompagné d’une pile d’appel.

  • --illegal-access=deny interdit tous les accès illégaux.

Du JDK 9 à 15, le mode par défaut est permit. Maintenant, depuis le JDK 16, le mode par défaut est deny.

L’option est toujours disponible. Nous pouvons autoriser l’accès illégaux aux APIs internes avec le mode permit.

avertissement Si nous sommes passés par là, il est préférable de corriger le problème en passant par la valeur debug pour avoir des informations afin de corriger le problème. Sinon, c’est reculer pour mieux se prendre le mur.

En effet, la prochaine étape sera la suppression de cette option. Dans ce cas, il faudra utiliser l’option --add-open qui est beaucoup moins simple à utiliser.

La solution

En réalité, les alternatives sont arrivées tout le long des versions du JDK. Voici quelques exemples :

  • Utiliser la librairie Xerces externe et non la copie interne du JDK

  • Utiliser la librairie ASM externe et non la copie interne du JDK

  • sun.misc.Service par java.util.ServiceLoader depuis le JDK 6

  • sun.util.calendar.ZoneInfo par java.util.TimeZone depuis le JDK 8

Un gros travail avait été fait dans le JDK 9. Cette page du wiki du projet OpenJDK est très bien pour avoir les correspondances entre les API internes à ne plus utiliser et les API standard.

Le travail continue pour les méthodes sans alternatives. Par exemple, je vous parlais dans les nouveautés du JDK 15 des classes cachées. Cela est un exemple de remplacement d’une API interne par une API standard.

avertissement Pour vous aider dans ce travail, l’outil jdeps est disponible depuis le JDK 9.

Qualité et communauté

Depuis septembre 2015, Oracle a mis en place un programme qualité avec les projets open-source qui souhaitent y participer. L’objectif est que les projets puissent travailler avec les pré-versions du JDK afin de remonter des régressions ou des problèmes. Ainsi, cela peut être pris en compte par le projet OpenJDK qui y apporte des correctifs avant la disponibilité de la version.

Lors du dernier bulletin de Décembre 2020, parmi les 65 projets ayant répondu à la dernière campagne, 26 projets avaient déjà testé avec succès le passage au JDK 16. Nous avons par exemple Hibernate, Apache Lucene, Apache Tomcat

avertissement Le programme aborde tous les sujets de OpenJDK et pas seulement l’encapsulation renforcé des API internes. Cela permet d’avoir une bonne image de la communauté à ce niveau là.

Ce billet est aussi l’occassion de mettre en lumière ce travail.

Ci-dessous un extrait du rapport. Neuf projets sur les 154 participants ont rempli 11 fiches de bogue en trois mois (de septembre 2020 à décembre 2020) sur le volet JDK 16 (early access)

quality outreach decembre2020

avertissement Le rapport est produit tous les 3 mois. Le prochain sera en mars 2020.

Conclusion

A mon avis, le JDK 16 est une bonne cible pour recevoir cette évolution pour les raisons suivantes :

  • Choisir le JDK 17, soit la prochaine version LTS, cela aurait pu être un frein pour l’adoption de cette version.

  • Choisir un JDK après le JDK 17 aurait repoussé de 3 ans ce renforcement et l’atteinte à cet objectif.

  • Choisir le JDK 16 permet aux librairies et frameworks qui ne sont à jour de le faire avant la sortie du JDK 17.

De plus, la mise en place du programme Qualité avec un ensemble de projet open-source permet de s’assurer que beaucoup de librairies et/ou framework fonctionne d’ores et déjà avec le JDK 16

Enfin, il est encore possible de désactiver ce renforcement comme nous l’avons évoqué plus haut. Mais cela permet de mettre la pression sur les dernières librairies récalcitrantes.