Notice
I wrote this article and was originally published on Qiita on 3 September 2019.
Assumption
In this passage Thymeleaf template engine which used in Spring MVC is discussed. Code shown is extracted from Thymeleaf 3.0.11-RELEASE and Spring MVC 5.1.9-RELEASE. Please notes that behavior of Thymeleaf used in other environment may be different.
How?
First look into following code.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Example 1</title>
<meta charset="utf-8">
</head>
<body>
<div>
<div th:text="a == b"></div>
</div>
</body>
</html>
After process, result is
<!DOCTYPE html>
<html>
<head>
<title>Example 1</title>
<meta charset="utf-8">
</head>
<body>
<div>
<div>false</div>
</div>
</body>
</html>
How thymeleaf processes attribute "th:text" of tag "<div th:text="a == b"></div>"? Attribute is handle by class org.thymeleaf.standard.processor.StandardTextTagProcessor.
public final class StandardTextTagProcessor extends AbstractStandardExpressionAttributeTagProcessor {
// ...
}
Look into its parent class org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor, in function doProcess(ITemplateContext, IProcessableElementTag, AttributeName, String, IElementTagStructureHandler), parameter "attributeValue" is "a == b", which is value of attribute "th:text".
public abstract class AbstractStandardExpressionAttributeTagProcessor extends AbstractAttributeTagProcessor {
@Override
protected final void doProcess(
final ITemplateContext context,
final IProcessableElementTag tag,
final AttributeName attributeName,
final String attributeValue,
final IElementTagStructureHandler structureHandler) {
final Object expressionResult;
if (attributeValue != null) {
final IStandardExpression expression = EngineEventUtils.computeAttributeExpression(context, tag, attributeName, attributeValue);
if (expression != null && expression instanceof FragmentExpression) {
// ...
} else {
/*
* Some attributes will require the execution of the expressions contained in them in RESTRICTED
* mode, so that e.g. access to request parameters is forbidden.
*/
expressionResult = expression.execute(context, this.expressionExecutionContext);
}
} else {
expressionResult = null;
}
// If the result of this expression is NO-OP, there is nothing to execute
if (expressionResult == NoOpToken.VALUE) {
if (this.removeIfNoop) {
structureHandler.removeAttribute(attributeName);
}
return;
}
doProcess(
context, tag,
attributeName, attributeValue,
expressionResult, structureHandler);
}
// ...
}
In function org.thymeleaf.engine.EngineEventUtils.computeAttributeExpression(ITemplateContext, IProcessableElementTag, AttributeName, String), "a == b" will be changed to express object org.thymeleaf.standard.expression.EqualsExpression, with interface org.thymeleaf.standard.expression.IStandardExpression.
Following is class implements org.thymeleaf.standard.expression.IStandardExpression
And then value of expression is obtained from function
expressionResult = expression.execute(context, this.expressionExecutionContext);
java.lang.Boolean object which represent "false" value is assigned to variable expressionResult. And then text representation is obtain from function
doProcess(context, tag, attributeName, attributeValue, expressionResult, structureHandler);
Processing is finished.
Expressions in Thymeleaf
From Thymeleaf document, there is 5 type of expressions.
Name | Symbol | Expression class |
---|---|---|
Variable Expressions | ${...} | org.thymeleaf.standard.expression.VariableExpression |
Selection Variable Expressions | *{...} | org.thymeleaf.standard.expression.SelectionVariableExpression |
Message Expressions | #{...} | org.thymeleaf.standard.expression.MessageExpression |
Link URL Expressions | @{...} | org.thymeleaf.standard.expression.LinkExpression |
Fragment Expressions | ~{...} | org.thymeleaf.standard.expression.FragmentExpression |
I will discuss message expressions and variable expressions. For remaining may lookup source code of class states above.
Message expressions
First look into following code.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{applicationName}"></title>
<meta charset="utf-8">
</head>
<body></body>
</html>
Values defined in resources\messages.properties.
applicationName=Example 2
After process, result is
<!DOCTYPE html>
<html>
<head>
<title>Example 2</title>
<meta charset="utf-8">
</head>
<body></body>
</html>
Property value "#{applicationName}" is represent by expression object org.thymeleaf.standard.expression.MessageExpression. And value is evaluated by static function executeMessageExpression(IExpressionContext, MessageExpression, StandardExpressionExecutionContext).
public final class MessageExpression extends SimpleExpression {
static Object executeMessageExpression(
final IExpressionContext context,
final MessageExpression expression, final StandardExpressionExecutionContext expContext) {
// ...
final ITemplateContext templateContext = (ITemplateContext)context;
final IStandardExpression baseExpression = expression.getBase();
Object messageKey = baseExpression.execute(templateContext, expContext);
messageKey = LiteralValue.unwrap(messageKey);
if (messageKey != null && !(messageKey instanceof String)) {
messageKey = messageKey.toString();
}
if (StringUtils.isEmptyOrWhitespace((String) messageKey)) {
throw new TemplateProcessingException(
"Message key for message resolution must be a non-null and non-empty String");
}
final Object[] messageParameters;
if (expression.hasParameters()) {
// ...
} else {
messageParameters = NO_PARAMETERS;
}
return templateContext.getMessage(null, (String)messageKey, messageParameters, true);
}
// ...
}
In some case key for lookup message is from other expression such as variable expression, so code below is for evaluate the expression inside #{...} bracket.
Object messageKey = baseExpression.execute(templateContext, expContext);
In this example, "applicationName" is return which is the same as input. Now using this key to lookup the value by following function.
return templateContext.getMessage(null, (String)messageKey, messageParameters, true);
templateContext is instance of class org.thymeleaf.context.WebEngineContext. Function getMessage(Class<?>, String, Object[], boolean) is defined in its parent class org.thymeleaf.context.AbstractEngineContext.
public abstract class AbstractEngineContext implements IEngineContext {
public final String getMessage(
final Class<?> origin, final String key, final Object[] messageParameters, final boolean useAbsentMessageRepresentation) {
// ...
// only have one messageResolvers: org.thymeleaf.spring5.messageresolver.SpringMessageResolver
final Set<IMessageResolver> messageResolvers = this.configuration.getMessageResolvers();
// Try to resolve the message
for (final IMessageResolver messageResolver : messageResolvers) {
final String resolvedMessage =
messageResolver.resolveMessage(this, origin, key, messageParameters);
if (resolvedMessage != null) {
return resolvedMessage;
}
}
if (useAbsentMessageRepresentation) {
// ...
}
return null;
}
// ...
}
In class org.thymeleaf.spring5.messageresolver.SpringMessageResolver, resolveMessage(ITemplateContext, Class<?>, String, Object[]) actually call org.springframework.context.support.AbstractApplicationContext.getMessage(String, Object[], Locale), facility provided by Spring's application context.
Variable expressions
First look into following code.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Example 3</title>
<meta charset="utf-8">
</head>
<body>
<div th:text="${message}"></div>
<div th:text="${#locale.language}"></div>
</body>
</html>
And model attributes are added in controller
public String index(Model model) {
model.addAttribute("message", "Model attribute");
return "index";
}
After process, result is
<!DOCTYPE html>
<html>
<head>
<title>Example 3</title>
<meta charset="utf-8">
</head>
<body>
<div>Model attribute</div>
<div>en</div>
</body>
</html>
Property value "${message}" and "${#locale.language}" are represent by expression object org.thymeleaf.standard.expression.VariableExpression. And value of expression is evaluated by static function executeVariableExpression(IExpressionContext, VariableExpression, IStandardVariableExpressionEvaluator, StandardExpressionExecutionContext).
public final class VariableExpression extends SimpleExpression implements IStandardVariableExpression {
static Object executeVariableExpression(
final IExpressionContext context,
final VariableExpression expression, final IStandardVariableExpressionEvaluator expressionEvaluator,
final StandardExpressionExecutionContext expContext) {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] Evaluating variable expression: \"{}\"", TemplateEngine.threadIndex(), expression.getStringRepresentation());
}
final StandardExpressionExecutionContext evalExpContext =
(expression.getConvertToString()? expContext.withTypeConversion() : expContext.withoutTypeConversion());
final Object result = expressionEvaluator.evaluate(context, expression, evalExpContext);
if (!expContext.getForbidUnsafeExpressionResults()) {
return result;
}
// ...
}
// ...
}
Actual value of expression is evaluated by
final Object result = expressionEvaluator.evaluate(context, expression, evalExpContext);
Variable "expressionEvaluator" is instance of class org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator. Variable "context" is instance of class org.thymeleaf.context.WebEngineContext. Variable "expression" is instance of class org.thymeleaf.standard.expression.VariableExpression which represent "message" and "#locale.language". Variable "evalExpContext" is instance of org.thymeleaf.standard.expression.StandardExpressionExecutionContext. On function org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(IExpressionContext, IStandardVariableExpression, StandardExpressionExecutionContext)
public class SPELVariableExpressionEvaluator
implements IStandardVariableExpressionEvaluator {
public final Object evaluate(
final IExpressionContext context,
final IStandardVariableExpression expression,
final StandardExpressionExecutionContext expContext) {
if (logger.isTraceEnabled()) {
logger.trace("[THYMELEAF][{}] SpringEL expression: evaluating expression \"{}\" on target", TemplateEngine.threadIndex(), expression.getExpression());
}
try {
final String spelExpression = expression.getExpression();
final boolean useSelectionAsRoot = expression.getUseSelectionAsRoot();
if (spelExpression == null) {
throw new TemplateProcessingException("Expression content is null, which is not allowed");
}
/*
* TRY TO DELEGATE EVALUATION TO SPRING IF EXPRESSION IS ON A BOUND OBJECT
*/
if (expContext.getPerformTypeConversion()) {
// ...
}
final IEngineConfiguration configuration = context.getConfiguration();
/*
* OBTAIN THE EXPRESSION (SpelExpression OBJECT) FROM THE CACHE, OR PARSE IT
*/
final ComputedSpelExpression exp = obtainComputedSpelExpression(configuration, expression, spelExpression);
/*
* COMPUTE EXPRESSION OBJECTS AND ADDITIONAL CONTEXT VARIABLES MAP
* The IExpressionObjects implementation returned by processing contexts that include the Standard
* Dialects will be lazy in the creation of expression objects (i.e. they won't be created until really
* needed).
*/
final IExpressionObjects expressionObjects =
(exp.mightNeedExpressionObjects? context.getExpressionObjects() : null);
/*
* CREATE/OBTAIN THE SPEL EVALUATION CONTEXT OBJECT
*/
EvaluationContext evaluationContext =
(EvaluationContext) context.
getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
if (evaluationContext == null) {
// ...
} else if (!(evaluationContext instanceof IThymeleafEvaluationContext)) {
// ...
}
/*
* AT THIS POINT, WE ARE SURE IT IS AN IThymeleafEvaluationContext
*
* This is needed in order to be sure we can modify the 'requestParametersRestricted' flag and also the
* expression objects.
*/
final IThymeleafEvaluationContext thymeleafEvaluationContext = (IThymeleafEvaluationContext) evaluationContext;
/*
* CONFIGURE THE IThymeleafEvaluationContext INSTANCE: expression objects and restrictions
*
* NOTE this is possible even if the evaluation context object is shared for the whole template execution
* because evaluation contexts are not thread-safe and are only used in a single template execution
*/
thymeleafEvaluationContext.setExpressionObjects(expressionObjects);
thymeleafEvaluationContext.setVariableAccessRestricted(expContext.getRestrictVariableAccess());
/*
* RESOLVE THE EVALUATION ROOT
*/
final ITemplateContext templateContext = (context instanceof ITemplateContext ? (ITemplateContext) context : null);
final Object evaluationRoot =
(useSelectionAsRoot && templateContext != null && templateContext.hasSelectionTarget()?
templateContext.getSelectionTarget() : new SPELContextMapWrapper(context, thymeleafEvaluationContext));
/*
* If no conversion is to be made, JUST RETURN
*/
if (!expContext.getPerformTypeConversion()) {
return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot);
}
// ...
} catch (final TemplateProcessingException e) {
throw e;
} catch(final Exception e) {
throw new TemplateProcessingException(
"Exception evaluating SpringEL expression: \"" + expression.getExpression() + "\"", e);
}
}
// ...
}
"message", "#locale.language" read from VariableExpression and SpelExpression instance is built (or get from cache if available) on following
final ComputedSpelExpression exp = obtainComputedSpelExpression(configuration, expression, spelExpression);
EvaluationContext which used by SpelExpression is first saved into model variable with key "thymeleaf::EvaluationContext" (defined in org.thymeleaf.spring5.expression.ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME) in function org.thymeleaf.spring5.view.ThymeleafView.renderFragment(Set, Map, HttpServletRequest, HttpServletResponse). And extracted on following
EvaluationContext evaluationContext =
(EvaluationContext) context.
getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
SpelExpression is finally evaluated on following
return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot);
SpEL variable can be obtained by function
org.thymeleaf.spring5.expression.ThymeleafEvaluationContext.lookupVariable(String). Model variable can be obtained from evaluationRoot variable, which is instance of org.thymeleaf.spring5.expression.SPELContextMapWrapper. SPELContextMapWrapper is a wrapper of org.thymeleaf.context.WebEngineContext. Model variable can be obtained from WebEngineContext by getVariable(String) function.
SpEL variable which can be used in SpEL when writing variable expression
In SpEL, variable can be referenced in the expression using the syntax #variableName. following pre-defined variable can be used on writing expression in Thymeleaf template.
org.thymeleaf.spring5.expression.SpringStandardExpressionObjectFactory
Name | Class of instance |
---|---|
#fields | org.thymeleaf.spring5.expression.Mvc |
#themes | org.thymeleaf.spring5.expression.Themes |
#mvc | org.thymeleaf.spring5.expression.Fields |
#requestdatavalues | org.thymeleaf.spring5.expression.RequestDataValues |
org.thymeleaf.standard.expression.StandardExpressionObjectFactory
Name | Class of instance |
---|---|
#ctx | class implements org.thymeleaf.context.IExpressionContext |
#root | class implements org.thymeleaf.context.IExpressionContext |
#vars | class implements org.thymeleaf.context.IExpressionContext |
#object | java.lang.Object or class implements org.thymeleaf.context.IExpressionContext |
#locale | java.util.Locale |
#request | class implements javax.servlet.http.HttpServletRequest |
#response | class implements javax.servlet.http.HttpServletResponse |
#session | class implements javax.servlet.http.HttpSession |
#servletContext | class implements javax.servlet.ServletContext |
#conversions | org.thymeleaf.expression.Conversions |
#uris | org.thymeleaf.expression.Uris |
#calendars | org.thymeleaf.expression.Calendars |
#dates | org.thymeleaf.expression.Dates |
#bools | org.thymeleaf.expression.Bools |
#numbers | org.thymeleaf.expression.Numbers |
#objects | org.thymeleaf.expression.Objects |
#strings | org.thymeleaf.expression.Strings |
#arrays | org.thymeleaf.expression.Arrays |
#lists | org.thymeleaf.expression.Lists |
#sets | org.thymeleaf.expression.Sets |
#maps | org.thymeleaf.expression.Maps |
#aggregates | org.thymeleaf.expression.Aggregates |
#messages | org.thymeleaf.expression.Messages |
#ids | org.thymeleaf.expression.Ids |
#execInfo | org.thymeleaf.expression.ExecutionInfo |
#httpServletRequest (Deprecated) | class implements javax.servlet.http.HttpServletRequest |
#httpSession (Deprecated) | class implements javax.servlet.http.HttpSession |
org.thymeleaf.extras.java8time.dialect.Java8TimeExpressionFactory
Name | Class of instance |
---|---|
#temporals | org.thymeleaf.extras.java8time.expression.Temporals |
org.thymeleaf.extras.springsecurity5.dialect.expression.SpringSecurityExpressionObjectFactory
Name | Class of instance |
---|---|
#authentication | class implements org.springframework.security.core.Authentication |
#authorization | org.thymeleaf.extras.springsecurity5.auth.Authorization |
Model variable which can be used in SpEL when writing variable expression
Name | Class of instance |
---|---|
session | org.thymeleaf.context.WebEngineContext$SessionAttributesMap |
param | org.thymeleaf.context.WebEngineContext$RequestParametersMap |
application | org.thymeleaf.context.WebEngineContext$ServletContextAttributesMap |
thymeleaf::EvaluationContext | org.thymeleaf.spring5.expression.ThymeleafEvaluationContext |
thymeleafRequestContext | org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext |
springMacroRequestContext | org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext |
springRequestContext | org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext |
Top comments (0)