JEP 358 Helpful NullPointerExceptions
Contexte
En suivant l’actualité du projet OpenJDK, j’ai remarqué qu’une nouvelle JEP (JDK Enhancement Proposals) a été accepté pour être inclus dans la version 14 du JDK.
Targeted to JDK 14: JEP 358: Helpful NullPointerExceptions : openjdk.java.net/jeps/358 #jdk14 #openjdk #java
.
Une proposition de plus, mais son petit nom ne me laisse pas indiférent. C’est la Helpful NullPointerExceptions, cela pourrait se traduire par NullPointerException utile. Qu’est ce qui se cache dernière ce nom.
En effet, quel developpeur Java n’a pas été confronté à un NullPointerException sur son poste de dévellopement ou en production ?
Initialement, pour illustrer cet article, j’étais parti sur une trace quelconque d’un NullPointerException. Mais, j’ai pu rencontré un cas concret qui m’a permis de voir un vrai cas d’école. Je le présente ci-dessous :
DEBUG [org.apereo.cas.web.support.CookieRetrievingCookieGenerator] - <null>
java.lang.NullPointerException: null
at org.apereo.cas.web.support.EncryptedCookieValueManager.obtainCookieValue(EncryptedCookieValueManager.java:35) ~[cas-server-core-cookie-api-5.3.6.jar:5.3.6]
at org.apereo.cas.web.support.CookieRetrievingCookieGenerator.retrieveCookieValue(CookieRetrievingCookieGenerator.java:148) ~[cas-server-core-cookie-api-5.3.6.jar:5.3.6]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_40]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_40]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_40]
at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_40]
De plus, comme tout exception, il est normalement possible d’avoir un message.
Returns the detail message string of this throwable.
Returns: the detail message string of this Throwable instance (which may be null).
Extrait de la Javadoc
.
Mais comme d’habiture, nous obtenons le message suivant :
java.lang.NullPointerException: null
.
La seule information que nous avons pour l’instant est que cela se passe sur la ligne 35. En regardant le code, nous avons :
34: public final String obtainCookieValue(final Cookie cookie, final HttpServletRequest request) {
35: final String cookieValue = cipherExecutor.decode(cookie.getValue(), new Object[]{}).toString();
36: LOGGER.debug("Decoded cookie value is [{}]", cookieValue);
37: if (StringUtils.isBlank(cookieValue)) {
38: LOGGER.debug("Retrieved decoded cookie value is blank. Failed to decode cookie [{}]", cookie.getName());
39: return null;
40: }
41:
42: return obtainValueFromCompoundCookie(cookieValue, request);
43: }
Sans plus d’information, nous avons plusieurs possibilités sur cette ligne :
-
Soit l’attribut cipherExecurtor est null,
-
Soit le paramètre cookie est null,
-
Soit la méthode decode(…) renvoie null.
.
Mise en pratique
Premier test
Passons à la mise en pratique, pour cela, nous allons commencer par un code simple
@Test (expected=NullPointerException.class)
public void testSimple() {
try {
String str = null;
str.length();
} catch (NullPointerException e) {
e.printStackTrace();
throw e;
}
}
Je vous l’accorde, c’est facile même l’IDE me dit : la variable str est null. Mais voyons ensemble le résultat obtenu.
On commence par faire tourner avec un JDK classique. En l’occurence, je prends un JDK 11. Voici le message obtenu (classique et malheureusement bien connu)
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest
java.lang.NullPointerException
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest.testSimple(NullPointerExceptionHelpulTest.java:11)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
En utilisant un JDK intégrant cette fonctionnalité, nous obtenons le message suivant:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest
java.lang.NullPointerException: 'str' is null. Can not invoke method 'int java.lang.String.length()'.
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTest.testSimple(NullPointerExceptionHelpulTest.java:11)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
Le message est déjà plus clair.
.
Exemple plus concret
Prenons la méthode suivante :
public boolean isVide(StringHolder holder) {
return (holder.getChaine().length() > 0);
}
Si je lance le test avec un JDK 11, nous obtenons le message suivant :
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest
java.lang.NullPointerException
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.isVide(NullPointerExceptionHelpulHolderTest.java:19)
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.testHolder(NullPointerExceptionHelpulHolderTest.java:11)
Nous sommes déjà sur un cas plus délicat. Nous avons deux possibilités :
-
soit le paramètre holder est null
-
soit la méthode holder.getChaine() renvoie null
En revanche, on obtient un message très clair avec la nouvelle fonctionnalité:
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest
java.lang.NullPointerException: The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null. Can not invoke method 'int java.lang.String.length()'.
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.isVide(NullPointerExceptionHelpulHolderTest.java:19)
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulHolderTest.testHolder(NullPointerExceptionHelpulHolderTest.java:11)
.
Lecture du message
En réalité, le message est décomposé en deux partie:
-
la première partie est la raison du NullPointerException.
The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null
— c'est le retour de la méthode getChaine() qui renvoie null
.
-
La seconde est la conséquence de l’exception.
Can not invoke method 'int java.lang.String.length()'
— nous ne pourrons pas appeler la méthode length()
.
Précision sur le message
Les tests ont été réalisés avec le prototype de mars 2019. Il est disponible sur la branche JEP-8220715-NPE_messages Cependant, dans le cadre de la JEP, les deux parties du message seront inversées. Cela donnera le message suivant :
Can not invoke method 'int java.lang.String.length()' because The return value of 'java.lang.String fr.lbenoit.billets.codes_sources.StringHolder.getChaine()' is null
.
Exemple avec les tableaux
Prenons le cas de manipulation de tableau, en ligne 17, nous avons la ligne suivante :
17: tab[i][j][k] = 25;
En lançant le test avec un JDK 11, nous obtenons le message suivant :
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest
java.lang.NullPointerException
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest.testTableau(NullPointerExceptionHelpulTableauTest.java:17)
En regardant le code, nous nous posons la question sur l’origine :
-
soit c’est tab qui est null
-
soit c’est tab[i] qui est null
-
soit c’est tab[i][j] qui est null
Là encore, la réponse est très claire avec un JDK incluant cette fonctionnalité
'tab[i][j]' is null. Can not store to null int array.
En replaçant le message dans la pile d’appel, nous obtenons la trace suivante :
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest
java.lang.NullPointerException: 'tab[i][j]' is null. Can not store to null int array.
at fr.lbenoit.billets.codes_sources.NullPointerExceptionHelpulTableauTest.testTableau(NullPointerExceptionHelpulTableauTest.java:17)
.
Activation
Dans la cas de la réalisation de cette fonctionnalité, il faut noter qu’elle ne sera pas activée par défaut. Deux raisons sont avancées :
-
Fuite d’informations dans les traces
-
Impact sur les parseurs des journaux.
Donc pour l’activer, il faudra ajouter l’option suivante :
XX:{+|-}ShowCodeDetailsInExceptionMessages
Elle sera activée par défaut dans une prochaine version.
.
Approfondissement
En étudiant la proposition d’évolution, nous pouvons voir que l’exception peut provoquer en réalité plusieurs conséquences :
-
Ne peux pas charger un élément du tableau ( "Cannot load from <element type> array" )
-
Ne peux pas lire la longueur du tableau ( "Cannot read the array length" )
-
Ne peux pas enregister l’élément du tableau ( "Cannot store to <element type> array" )
-
Ne peux pas lever une exception ( "Cannot throw exception" )
-
Ne peux pas lire un attribut ( "Cannot read field '<field name>'" )
-
Ne peux pas appeler une méthode ( "Cannot invoke '<method>'" )
-
Ne peux pas entrer dans un bloc synchronisé ( "Cannot enter synchronized block" )
-
Ne peux pas sortir d’un bloc synchronisé ( "Cannot exit synchronized block" )
-
Ne peux pas affecter l’attribut ( "Cannot assign field '<field name>'" )
.
Comment tester ?
Le projet java contenant les cas de tests présentées dans ce billet est disponible sur github.
.
Références
.
Liens supplémentaires :
-
Annonce de la proposition sur la liste de diffusion jdk-dev
-
Proposition acceptée sur la liste de diffusion jdk-dev
Moteur de recherche
"Eduquer, ce n'est pas remplir des vases mais c'est d'allumer des feux." - Michel Montaigne
Billets récents
- Eclipse plante systématiquement sous Debian (et autres distribution Linux)
- JEP 463, Implicitly Declared Classes and Instance Main Methods (Second Preview)
- Debian - Montée de version de Debian 11 (Bullseye) à Debian 12 (Bookworm)
- JEP 451, Prepare to Disallow the Dynamic Loading of Agents
- JEP 444, Virtual Threads