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
OpenJDK
sur Twitter (2 octobre 2019)

.

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).

— getMessage
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 :