To be fair, this is an OGNL issue, not a Thymeleaf issue.
If you are not familiar with OGNL, it’s the core expression language on which Thymeleaf is built.
Here is a simple Thymeleaf template expression:
|
|
We expect this to display <div>true</div>
or <div>false</div>
, depending on what we pass to the template:
|
|
It actually throws an exception, with the following stack trace reason:
Caused by: java.lang.NumberFormatException: For input string: “A”
Wait, what? Why did that happen?
OGNL attempts to compare "A"
with 'Z'
. In Java terms, this is a String
compared to a char
. OGNL categorizes char
(and Character
) as numeric types.
The OGNL code therefore attempts to cast the string "A"
to a number, to facilitate the comparison with the char 'Z'
- leading to the exception.
(If you’re really interested, you can see this happening in the OgnlOps.java
source code.)
But why does OGNL consider char
to be numeric? Because it is! char
is one of Java’s integral types:
The values of the integral types are integers in the following ranges:
For
byte
, from -128 to 127, inclusive
Forshort
, from -32768 to 32767, inclusive
Forint
, from -2147483648 to 2147483647, inclusive
Forlong
, from -9223372036854775808 to 9223372036854775807, inclusive
Forchar
, from ‘\u0000’ to ‘\uffff’ inclusive, that is, from 0 to 65535
Here, the word “integral” is used in the mathematical sense, meaning “of or denoted by an integer”.
Java’s char
has something of a split personality. We generally understand what a “character” is, in its simplest meaning (it’s a letter of the alphabet!). But Java has a different understanding, given the reason char
exists in Java:
The
char
data type (and therefore the value that aCharacter
object encapsulates) are based on the original Unicode specification, which defined characters as fixed-width 16-bit entities.
OGNL (and Thymeleaf) allows you to place a string inside single- or double-quotes. For Thymeleaf, that is very convenient, because you can use th:text="${word == 'foo'}"
. In that case, the literal 'foo'
will be treated as a string by OGNL - and it will be compared to the string we provide in our model. No number format exception will be thrown.
But if the expression only contains a single character (th:text="${letter == 'Z'}"
), then OGNL treats it as a char
- and we get the exception.
Is this a bug? I don’t think so. I think it’s more a consequence of my code not anticipating the problematic comparison I am asking it to do.
Is it unfortunate? Absolutely, yes, because it’s an unpleasant surprise; not necessarily obvious what went wrong; and an easy mistake to make.
OPTION 1: Try to remember to never use single-character literals in your templates - especially when performing comparisons using those literals. (Easier said than done.)
OPTION 2: If you need a string containing only a single character, you can use "
instead of a double quote (
"
):
|
|
Now, the Z
is handled as a String by OGNL and Thymeleaf. But… yuck.
OPTION 3: Don’t use OGNL. For example, you can configure Thymeleaf to use SpEL (Spring’s expression language). SpEL does not have the same behavior. It will treat 'Z'
as comparable with "A"
, without throwing an exception.
You don’t need to use the entire Spring framework to do this. See Spring 6 and Thymeleaf Without Spring.
In modern Java, the char
primitive is problematic for more fundamental reasons that the issue discussed here. There’s a great article about that by Cay Horstmann: