I. Contexte▲
En suivant l'actualité du projet OpenJDK, j'ai remarqué qu'une nouvelle JEP (JDK Enhancement Proposals) a été acceptée pour être incluse dans la version 14 du JDK.
Targeted to JDK 14 : JEP 358: Helpful NullPointerExceptions : openjdk.java.net/jeps/358 #jdk14 #openjdk #java
— OpenJDK
sur Twitter (2 octobre 2019)
Une proposition de plus, mais son petit nom ne me laisse pas indiffé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 développeur Java n'a pas été confronté à un NullPointerException sur son poste de développement ou en production ?
Initialement, pour illustrer cet article, j'étais parti sur une trace quelconque d'un NullPointerException. Mais, j'ai pu rencontrer un cas concret qui m'a permis de voir un vrai cas d'école. Je le présente ci-dessous :
2.
3.
4.
5.
6.
7.
8.
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 pour toute 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).
— getMessage
Extrait de la Javadoc
Mais comme d'habitude, 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 :
35.
36.
37.
38.
39.
40.
41.
42.
public
final
String obtainCookieValue
(
final
Cookie cookie, final
HttpServletRequest request) {
final
String cookieValue =
cipherExecutor.decode
(
cookie.getValue
(
), new
Object[]{}
).toString
(
);
LOGGER.debug
(
"Decoded cookie value is [{}]"
, cookieValue);
if
(
StringUtils.isBlank
(
cookieValue)) {
LOGGER.debug
(
"Retrieved decoded cookie value is blank. Failed to decode cookie [{}]"
, cookie.getName
(
));
return
null
;
}
return
obtainValueFromCompoundCookie
(
cookieValue, request);
}
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.
II. Mise en pratique▲
II-A. Premier test▲
Passons à la mise en pratique, pour cela, nous allons commencer par un code simple
2.
3.
4.
5.
6.
7.
8.
9.
10.
@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 de valeur nulle. Mais voyons ensemble le résultat obtenu.
On commence par faire tourner avec un JDK classique. En l'occurrence, je prends un JDK 11. Voici le message obtenu (classique et malheureusement bien connu)
2.
3.
4.
5.
6.
7.
8.
9.
-------------------------------------------------------
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
-------------------------------------------------------
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.
II-B. 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 :
2.
3.
4.
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é :
2.
3.
4.
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)
II-C. Lecture du message▲
En réalité, le message est composé de deux parties :
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
Can not invoke method 'int java.lang.String.length()'
— nous ne pouvons pas invoquer la méthode length().
- La première partie est la raison du NullPointerException.
- La seconde est la conséquence de l'exception.
II-D. 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
II-E. Exemple avec les tableaux▲
Prenons le cas de manipulation de tableau, en ligne 17, nous avons l’instruction suivante :
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'appels, nous obtenons la trace suivante :
2.
3.
4.
5.
6.
-------------------------------------------------------
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)
II-F. Activation▲
Dans le 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.
II-G. Approfondissement▲
En étudiant la proposition d'évolution, nous pouvons voir que l'exception peut provoquer en réalité plusieurs conséquences :
- ne peut pas charger un élément du tableau (« Cannot load from <element type> array ») ;
- ne peut pas lire la longueur du tableau (« Cannot read the array length ») ;
- ne peut pas enregistrer l'élément du tableau (« Cannot store to <element type> array ») ;
- ne peut pas lever une exception (« Cannot throw exception ») ;
- ne peut pas lire un attribut (« Cannot read field '<field name>' ») ;
- ne peut pas appeler une méthode (« Cannot invoke '<method>' ») ;
- ne peut pas entrer dans un bloc synchronisé (« Cannot enter synchronized block ») ;
- ne peut pas sortir d'un bloc synchronisé (« Cannot exit synchronized block ») ;
- ne peut pas affecter l'attribut (« Cannot assign field '<field name>' »).
II-H. Comment tester ?▲
Le projet Java contenant les cas de test présentés dans ce billet est disponible sur GitHub.
III. Remerciements et références▲
Cet article a été publié avec l'aimable autorisation de Lilian Benoit. L'article original (JEP 358 Helpful NullPointerExceptions) peut être vu sur le blog https://www.lilian-benoit.fr/.
Nous tenons à remercier escartefigue pour sa relecture attentive de cet article et Winjerome pour la mise au gabarit.
- Annonce Twitter
- JEP 358
- Fiche associée à la JEP 358
- Branche JEP-8220715-NPE_messages
- Résultat des tests
III-A. Liens supplémentaires▲
- Annonce de la proposition sur la liste de diffusion jdk-dev
- Proposition acceptée sur la liste de diffusion jdk-dev