This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch FREEMARKER-183
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 5a1272a0fb12d9fec04e7b2818edff0c2149ed6c
Author: ddekany <[email protected]>
AuthorDate: Sat Jan 13 18:12:11 2024 +0100

    Added support for marking obj.prop and obj.prop() to be the same in 
templates. Made Java zero argument methods to be such properties by default, if 
incompatibleImprovements is at least 2.3.33. Added 
ZeroArgumentNonVoidMethodPolicy, and 
BeansWrapperConfiguration.nonRecordZeroArgumentNonVoidMethodPolicy, and 
recordZeroArgumentNonVoidMethodPolicy to implement these. Also,added 
GenericObjectModel which implements MethodCallAwareTemplateHashModel, and 
create that in BeansWrapper instead of [...]
---
 build.gradle.kts                                   |   2 +-
 .../main/java/freemarker/core/Configurable.java    |   2 +-
 .../src/main/java/freemarker/core/Dot.java         |  21 +-
 .../java/freemarker/core/DotBeforeMethodCall.java  |  59 +++++
 .../src/main/java/freemarker/core/MethodCall.java  |   3 +
 .../main/java/freemarker/ext/beans/APIModel.java   |  12 +-
 .../main/java/freemarker/ext/beans/BeanModel.java  | 101 +++++++-
 .../java/freemarker/ext/beans/BeansWrapper.java    | 140 ++++++++--
 .../ext/beans/BeansWrapperConfiguration.java       |  36 +++
 .../freemarker/ext/beans/ClassIntrospector.java    |  88 ++++++-
 .../ext/beans/ClassIntrospectorBuilder.java        |  48 +++-
 .../ext/beans/FastPropertyDescriptor.java          |  18 +-
 .../freemarker/ext/beans/GenericObjectModel.java   |  71 ++++++
 .../ext/beans/MethodAppearanceFineTuner.java       |  20 +-
 .../beans/MethodCallAwareTemplateHashModel.java    | 129 ++++++++++
 .../java/freemarker/ext/beans/StringModel.java     |  14 +-
 .../ext/beans/ZeroArgumentNonVoidMethodPolicy.java |  65 +++++
 .../main/java/freemarker/ext/beans/_BeansAPI.java  |   4 +-
 .../src/main/javacc/freemarker/core/FTL.jj         |   3 +
 .../freemarker/template/ConfigurationTest.java     |   4 +-
 .../beans/TestZeroArgumentNonVoidMethodPolicy.java | 283 +++++++++++++++++++++
 .../template/DefaultObjectWrapperTest.java         |   4 +-
 freemarker-manual/src/main/docgen/en_US/book.xml   | 100 ++++++++
 23 files changed, 1159 insertions(+), 68 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index ac45a48c..ce56fa89 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -61,7 +61,7 @@ freemarkerRoot {
     configureSourceSet("jython20")
     configureSourceSet("jython22")
     configureSourceSet("jython25") { enableTests() }
-    configureSourceSet("core16", "16")
+    configureSourceSet("core16", "16") { enableTests() }
 }
 
 val compileJavacc = 
tasks.register<freemarker.build.CompileJavaccTask>("compileJavacc") {
diff --git a/freemarker-core/src/main/java/freemarker/core/Configurable.java 
b/freemarker-core/src/main/java/freemarker/core/Configurable.java
index fc98db58..6eaf8eda 100644
--- a/freemarker-core/src/main/java/freemarker/core/Configurable.java
+++ b/freemarker-core/src/main/java/freemarker/core/Configurable.java
@@ -2553,7 +2553,7 @@ public class Configurable {
      *      <p>If you have no constructor arguments and property setters, and 
the <code><i>className</i></code> class has
      *      a public static {@code INSTANCE} field, the value of that filed 
will be the value of the expression, and
      *      the constructor won't be called. Note that if you use the backward 
compatible
-     *      syntax, where these's no parenthesis after the class name, then it 
will not look for {@code INSTANCE}.
+     *      syntax, where there's no parenthesis after the class name, then it 
will not look for {@code INSTANCE}.
      *   </li>
      *   <li>
      *      <p>If there exists a class named 
<code><i>className</i>Builder</code>, then that class will be instantiated
diff --git a/freemarker-core/src/main/java/freemarker/core/Dot.java 
b/freemarker-core/src/main/java/freemarker/core/Dot.java
index 54bae57b..f360a955 100644
--- a/freemarker-core/src/main/java/freemarker/core/Dot.java
+++ b/freemarker-core/src/main/java/freemarker/core/Dot.java
@@ -27,7 +27,7 @@ import freemarker.template.TemplateModel;
  * The dot operator. Used to reference items inside a
  * <code>TemplateHashModel</code>.
  */
-final class Dot extends Expression {
+class Dot extends Expression {
     private final Expression target;
     private final String key;
 
@@ -36,11 +36,20 @@ final class Dot extends Expression {
         this.key = key;
     }
 
+    /**
+     * Shallow copy constructor
+     */
+    Dot(Dot dot) {
+        this(dot.target, dot.key);
+        this.constantValue = dot.constantValue; // Probably always will be 
null here
+        copyFieldsFrom(dot);
+    }
+
     @Override
     TemplateModel _eval(Environment env) throws TemplateException {
         TemplateModel leftModel = target.eval(env);
         if (leftModel instanceof TemplateHashModel) {
-            return ((TemplateHashModel) leftModel).get(key);
+            return evalOnHash((TemplateHashModel) leftModel);
         }
         if (leftModel == null && env.isClassicCompatible()) {
             return null; // ${noSuchVar.foo} has just printed nothing in FM 1.
@@ -48,6 +57,14 @@ final class Dot extends Expression {
         throw new NonHashException(target, leftModel, env);
     }
 
+    protected TemplateModel evalOnHash(TemplateHashModel leftModel) throws 
TemplateException {
+        return leftModel.get(key);
+    }
+
+    String getKey() {
+        return key;
+    }
+
     @Override
     public String getCanonicalForm() {
         return target.getCanonicalForm() + getNodeTypeSymbol() + 
_CoreStringUtils.toFTLIdentifierReferenceAfterDot(key);
diff --git 
a/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java 
b/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java
new file mode 100644
index 00000000..579cb8c6
--- /dev/null
+++ b/freemarker-core/src/main/java/freemarker/core/DotBeforeMethodCall.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import freemarker.ext.beans.BeansWrapper;
+import freemarker.ext.beans.MethodCallAwareTemplateHashModel;
+import freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateModel;
+
+/**
+ * Like {@link Dot}, but when used before method call (but as of 2.3.33, 
before 0-argument calls only), as in
+ * {@code obj.key()}. The reason it's only used before 0-argument calls (as of 
2.3.33 at least) is that it adds some
+ * overhead, and this {@link Dot} subclass was added to implement
+ * {@link ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}
+ * (via {@link 
BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}).
 We don't
+ * necessarily want to go beyond that hack, as we don't have separate method 
namespace in the template language.
+ */
+class DotBeforeMethodCall extends Dot {
+    public DotBeforeMethodCall(Dot dot) {
+        super(dot);
+    }
+
+    @Override
+    protected TemplateModel evalOnHash(TemplateHashModel leftModel) throws 
TemplateException {
+        if (leftModel instanceof MethodCallAwareTemplateHashModel) {
+            try {
+                return ((MethodCallAwareTemplateHashModel) 
leftModel).getBeforeMethodCall(getKey());
+            } catch 
(MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) {
+                String hint = e.getHint();
+                throw new NonMethodException(
+                        this,
+                        e.getActualValue(),
+                        hint != null ? new String[] { hint } : null,
+                        Environment.getCurrentEnvironment());
+            }
+        } else {
+            return super.evalOnHash(leftModel);
+        }
+    }
+}
diff --git a/freemarker-core/src/main/java/freemarker/core/MethodCall.java 
b/freemarker-core/src/main/java/freemarker/core/MethodCall.java
index 2ece2a0a..1eebbe59 100644
--- a/freemarker-core/src/main/java/freemarker/core/MethodCall.java
+++ b/freemarker-core/src/main/java/freemarker/core/MethodCall.java
@@ -66,6 +66,9 @@ final class MethodCall extends Expression {
         } else {
             throw new NonMethodException(target, targetModel, true, false, 
null, env);
         }
+        // ATTENTION! If you add support for calling any new type, ensure that
+        // freemarker.ext.beans.BeanModel.invokeThroughDescriptor sees that 
type as callable too,
+        // where it deals with the beforeMethodCall logic!
     }
 
     @Override
diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java
index 4580ac8d..5f1274be 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/APIModel.java
@@ -19,6 +19,9 @@
 
 package freemarker.ext.beans;
 
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
 /**
  * Exposes the Java API (and properties) of an object.
  * 
@@ -32,7 +35,7 @@ package freemarker.ext.beans;
  * 
  * @since 2.3.22
  */
-final class APIModel extends BeanModel {
+final class APIModel extends BeanModel implements 
MethodCallAwareTemplateHashModel {
 
     APIModel(Object object, BeansWrapper wrapper) {
         super(object, wrapper, false);
@@ -41,5 +44,10 @@ final class APIModel extends BeanModel {
     protected boolean isMethodsShadowItems() {
         return true;
     }
-    
+
+    @Override
+    public TemplateModel getBeforeMethodCall(String key) throws 
TemplateModelException,
+            ShouldNotBeGetAsMethodException {
+        return super.getBeforeMethodCall(key);
+    }
 }
diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java
index 79110b90..4b5462dd 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeanModel.java
@@ -30,7 +30,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import freemarker.core.BugException;
 import freemarker.core.CollectionAndSequence;
+import freemarker.core.Macro;
 import freemarker.core._DelayedFTLTypeDescription;
 import freemarker.core._DelayedJQuote;
 import freemarker.core._TemplateModelException;
@@ -43,11 +45,14 @@ import freemarker.template.SimpleScalar;
 import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateMethodModelEx;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateModelWithAPISupport;
 import freemarker.template.TemplateScalarModel;
+import freemarker.template.utility.CollectionUtils;
 import freemarker.template.utility.StringUtil;
 
 /**
@@ -134,12 +139,67 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
      * then {@code non-void-return-type get(java.lang.Object)}, or 
      * alternatively (if the wrapped object is a resource bundle) 
      * {@code Object getObject(java.lang.String)}.
+     *
+     * <p>As of 2.3.33, the default implementation of this method delegates to 
{@link #get(String, boolean)}. It's
+     * better to override that, instead of this method. Otherwise, unwanted 
behavior can arise if the model class also
+     * implements {@link MethodCallAwareTemplateHashModel}, as that will 
certainly call {@link #get(String, boolean)}
+     * internally, and not the overridden version of this method.
+     *
      * @throws TemplateModelException if there was no property nor method nor
      * a generic {@code get} method to invoke.
      */
     @Override
-    public TemplateModel get(String key)
-        throws TemplateModelException {
+    public TemplateModel get(String key) throws TemplateModelException {
+        try {
+            return get(key, false);
+        } catch 
(MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) {
+            throw new BugException(e);
+        }
+    }
+
+    /**
+     * Can be overridden to be public, to implement {@link 
MethodCallAwareTemplateHashModel}. We don't implement that
+     * in {@link BeanModel} for backward compatibility, but the functionality 
is present. If you expose this method by
+     * implementing {@link MethodCallAwareTemplateHashModel}, then be sure 
that {@link #get(String)} is
+     * not overridden in custom subclasses; if it is, then those subclasses 
should be modernized to override
+     * {@link #get(String, boolean)} instead.
+     *
+     * @since 2.3.33
+     */
+    protected TemplateModel getBeforeMethodCall(String key)
+            throws TemplateModelException, 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException {
+        TemplateModel result = get(key, true);
+        if (result instanceof  TemplateMethodModelEx) {
+            return (TemplateMethodModelEx) result;
+        }
+        if (result == null) {
+            return null;
+        }
+        throw new 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException(result, null);
+    }
+
+    /**
+     * Override this if you want to customize the behavior of {@link 
#get(String)}.
+     * In standard implementations at least, this is what {@link 
#get(String)}, and
+     * {@link MethodCallAwareTemplateHashModel#getBeforeMethodCall(String)} 
delegates to.
+     *
+     * @param key
+     *      Same as the parameter of {@link #get(String)}.
+     * @param beforeMethodCall
+     *      This is a hint that tells that the returned value will be called 
in the template. This was added to
+     *      implement {@link 
BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}.
+     *      This parameter is {@code false} when {@link #get(String)} is 
called, and
+     *      {@code true} when {@link 
MethodCallAwareTemplateHashModel#getBeforeMethodCall(String)} is called.
+     *      If this is {@code true}, this method should return a {@link 
TemplateMethodModelEx}, or {@code null},
+     *      or fail with {@link 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException}.
+     *
+     * @since 2.3.33
+     */
+    // Before calling this from FreeMarker classes, consider that some users 
may have overridden {@link #get(String)}
+    // instead, as this class didn't exist before 2.3.33. So with 
incompatibleImprovements before that, that should be
+    // the only place where this gets called, or else the behavior of the 
model will be inconsistent.
+    protected TemplateModel get(String key, boolean beforeMethodCall)
+        throws TemplateModelException, 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException {
         Class<?> clazz = object.getClass();
         Map<Object, Object> classInfo = 
wrapper.getClassIntrospector().get(clazz);
         TemplateModel retval = null;
@@ -148,7 +208,7 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
             if (wrapper.isMethodsShadowItems()) {
                 Object fd = classInfo.get(key);
                 if (fd != null) {
-                    retval = invokeThroughDescriptor(fd, classInfo);
+                    retval = invokeThroughDescriptor(fd, classInfo, 
beforeMethodCall);
                 } else {
                     retval = invokeGenericGet(classInfo, clazz, key);
                 }
@@ -160,7 +220,7 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
                 }
                 Object fd = classInfo.get(key);
                 if (fd != null) {
-                    retval = invokeThroughDescriptor(fd, classInfo);
+                    retval = invokeThroughDescriptor(fd, classInfo, 
beforeMethodCall);
                     if (retval == UNKNOWN && model == nullModel) {
                         // This is the (somewhat subtle) case where the 
generic get() returns null
                         // and we have no bean info, so we respect the fact 
that
@@ -178,9 +238,12 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
                 retval = wrapper.wrap(null);
             }
             return retval;
-        } catch (TemplateModelException e) {
+        } catch (TemplateModelException | 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException e) {
             throw e;
         } catch (Exception e) {
+            if (beforeMethodCall && e instanceof 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException) {
+                throw 
(MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException) e;
+            }
             throw new _TemplateModelException(e,
                     "An error has occurred when reading existing sub-variable 
", new _DelayedJQuote(key),
                     "; see cause exception! The type of the containing value 
was: ",
@@ -203,8 +266,9 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
         return 
wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY)
 != null;
     }
     
-    private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, 
Object> classInfo)
-            throws IllegalAccessException, InvocationTargetException, 
TemplateModelException {
+    private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, 
Object> classInfo, boolean beforeMethodCall)
+            throws IllegalAccessException, InvocationTargetException, 
TemplateModelException,
+            MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException {
         // See if this particular instance has a cached implementation for the 
requested feature descriptor
         TemplateModel cachedModel;
         synchronized (this) {
@@ -229,8 +293,27 @@ implements TemplateHashModelEx, AdapterTemplateModel, 
WrapperTemplateModel, Temp
                                 ClassIntrospector.getArgTypes(classInfo, 
indexedReadMethod), wrapper);
                 }
             } else {
-                resultModel = wrapper.invokeMethod(object, pd.getReadMethod(), 
null);
-                // cachedModel remains null, as we don't cache these
+                if (!beforeMethodCall) {
+                    resultModel = wrapper.invokeMethod(object, 
pd.getReadMethod(), null);
+                    // cachedModel remains null, as we don't cache these
+                } else {
+                    if (pd.isMethodInsteadOfPropertyValueBeforeCall()) {
+                        resultModel = cachedModel = new SimpleMethodModel(
+                                object, pd.getReadMethod(), 
CollectionUtils.EMPTY_CLASS_ARRAY, wrapper);
+                    } else {
+                        resultModel = wrapper.invokeMethod(object, 
pd.getReadMethod(), null);
+                        // cachedModel remains null, as we don't cache these
+
+                        // Checks if freemarker.core.MethodCall would accept 
this result:
+                        if (!(resultModel instanceof TemplateMethodModel || 
resultModel instanceof Macro)) {
+                            throw new 
MethodCallAwareTemplateHashModel.ShouldNotBeGetAsMethodException(
+                                    resultModel,
+                                    "This member of the parent object is seen 
by templates as a property of it "
+                                            + "(with other words, an 
attribute, or a field), not a method of it. "
+                                            + "Thus, to get its value, it must 
not be called as a method.");
+                        }
+                    }
+                }
             }
         } else if (desc instanceof Field) {
             resultModel = wrapper.readField(object, (Field) desc);
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java
index be2455eb..0fbf3561 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -19,6 +19,7 @@
 
 package freemarker.ext.beans;
 
+import java.beans.IntrospectionException;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.AccessibleObject;
@@ -153,7 +154,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      * performance. In theory that's not needed, but apps might fail to keep 
the rules.
      */
     private ClassIntrospector classIntrospector;
-    
+
     /**
      * {@link String} class name to {@link StaticModel} cache.
      * This object only belongs to a single {@link BeansWrapper}.
@@ -193,9 +194,10 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
     private boolean simpleMapWrapper;  // initialized from the 
BeansWrapperConfiguration
     private boolean strict;  // initialized from the BeansWrapperConfiguration
     private boolean preferIndexedReadMethod; // initialized from the 
BeansWrapperConfiguration
-    
+
     private final Version incompatibleImprovements;
-    
+
+
     /**
      * Creates a new instance with the incompatible-improvements-version 
specified in
      * {@link Configuration#DEFAULT_INCOMPATIBLE_IMPROVEMENTS}.
@@ -262,6 +264,16 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      *       The default of the {@link #setPreferIndexedReadMethod(boolean) 
preferIndexedReadMethod} setting changes
      *       from {@code true} to {@code false}.
      *     </li>  
+     *     <li>
+     *       <p>2.3.33 (or higher):
+     *       The default of {@link 
BeansWrapper#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}
+     *       has changes to {@link 
ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}, from
+     *       {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}. This means 
that Java records public methods with
+     *       0-arguments and non-void return type are now exposed both as 
properties, and as methods, while earlier they
+     *       were only exposed as methods. That is, if in a record you have 
{@code public String name()}, now in
+     *       templates the value can be accessed both as {@code obj.name} 
(like a property), and as {@code obj.name()}
+     *       (for better backward compatibility only - it's bad style).
+     *     </li>
      *   </ul>
      *   
      *   <p>Note that the version will be normalized to the lowest version 
where the same incompatible
@@ -289,7 +301,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
     }
     
     /**
-     * Initializes the instance based on the the {@link 
BeansWrapperConfiguration} specified.
+     * Initializes the instance based on the {@link BeansWrapperConfiguration} 
specified.
      * 
      * @param writeProtected Makes the instance's configuration settings 
read-only via
      *     {@link WriteProtectable#writeProtect()}; this way it can use the 
shared class introspection cache.
@@ -320,7 +332,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
                 }
             } catch (Throwable e) {
                 // The security manager sometimes doesn't allow this
-                LOG.info("Failed to check if finetuneMethodAppearance is 
overidden in " + thisClass.getName()
+                LOG.info("Failed to check if finetuneMethodAppearance is 
overridden in " + thisClass.getName()
                         + "; acting like if it was, but this way it won't 
utilize the shared class introspection "
                         + "cache.",
                         e);
@@ -353,7 +365,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         defaultDateType = bwConf.getDefaultDateType();
         outerIdentity = bwConf.getOuterIdentity() != null ? 
bwConf.getOuterIdentity() : this;
         strict = bwConf.isStrict();
-        
+
         if (!writeProtected) {
             // As this is not a read-only BeansWrapper, the classIntrospector 
will be possibly replaced for a few times,
             // but we need to use the same sharedInrospectionLock forever, 
because that's what the model factories
@@ -367,7 +379,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
             classIntrospector = 
_BeansAPI.getClassIntrospectorBuilder(bwConf).build();
             sharedIntrospectionLock = classIntrospector.getSharedLock(); 
         }
-        
+
         falseModel = new BooleanModel(Boolean.FALSE, this);
         trueModel = new BooleanModel(Boolean.TRUE, this);
         
@@ -633,7 +645,45 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
             replaceClassIntrospector(builder);
         }
     }
-    
+
+    /**
+     * Sets the {@link ZeroArgumentNonVoidMethodPolicy} for classes that are 
not Java records;
+     * defaults to {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}.
+     *
+     * <p>Note that methods in this class are inherited by {@link 
DefaultObjectWrapper}, which is what you normally use.
+     *
+     * @since 2.3.33
+     */
+    public void 
setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
nonRecordZeroArgumentNonVoidMethodPolicy) {
+        checkModifiable();
+
+        if (classIntrospector.getNonRecordZeroArgumentNonVoidMethodPolicy() != 
nonRecordZeroArgumentNonVoidMethodPolicy) {
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            
builder.setNonRecordZeroArgumentNonVoidMethodPolicy(nonRecordZeroArgumentNonVoidMethodPolicy);
+            replaceClassIntrospector(builder);
+        }
+    }
+
+    /**
+     * Sets the {@link ZeroArgumentNonVoidMethodPolicy} for classes that are 
Java records; if the
+     * {@code BeansWrapper#BeansWrapper(Version) incompatibleImprovements} of 
the object wrapper is at least 2.3.33,
+     * then it defaults to {@link 
ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}, otherwise it 
defaults to
+     * {@link ZeroArgumentNonVoidMethodPolicy#METHOD_ONLY}.
+     *
+     * <p>Note that methods in this class are inherited by {@link 
DefaultObjectWrapper}, which is what you normally use.
+     *
+     * @since 2.3.33
+     */
+    public void 
setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
recordZeroArgumentNonVoidMethodPolicy) {
+        checkModifiable();
+
+        if (classIntrospector.getRecordZeroArgumentNonVoidMethodPolicy() != 
recordZeroArgumentNonVoidMethodPolicy) {
+            ClassIntrospectorBuilder builder = 
classIntrospector.createBuilder();
+            
builder.setRecordZeroArgumentNonVoidMethodPolicy(recordZeroArgumentNonVoidMethodPolicy);
+            replaceClassIntrospector(builder);
+        }
+    }
+
     /**
      * Returns whether exposure of public instance fields of classes is 
      * enabled. See {@link #setExposeFields(boolean)} for details.
@@ -651,7 +701,25 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
     public boolean getTreatDefaultMethodsAsBeanMembers() {
         return classIntrospector.getTreatDefaultMethodsAsBeanMembers();
     }
-    
+
+    /**
+     * See {@link 
#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}.
+     *
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getNonRecordZeroArgumentNonVoidMethodPolicy() {
+        return classIntrospector.getNonRecordZeroArgumentNonVoidMethodPolicy();
+    }
+
+    /**
+     * See {@link 
#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}.
+     *
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getRecordZeroArgumentNonVoidMethodPolicy() {
+        return classIntrospector.getRecordZeroArgumentNonVoidMethodPolicy();
+    }
+
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return classIntrospector.getMethodAppearanceFineTuner();
     }
@@ -865,7 +933,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
     /**
      * Returns the version given with {@link #BeansWrapper(Version)}, 
normalized to the lowest version where a change
      * has occurred. Thus, this is not necessarily the same version than that 
was given to the constructor.
-     * 
+     *
      * @since 2.3.21
      */
     public Version getIncompatibleImprovements() {
@@ -894,7 +962,8 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      */
     protected static Version normalizeIncompatibleImprovementsVersion(Version 
incompatibleImprovements) {
         _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
-        return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_27 ? 
Configuration.VERSION_2_3_27
+        return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? 
Configuration.VERSION_2_3_33
+                : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_27 
? Configuration.VERSION_2_3_27
                 : incompatibleImprovements.intValue() == _VersionInts.V_2_3_26 
? Configuration.VERSION_2_3_26
                 : is2324Bugfixed(incompatibleImprovements) ? 
Configuration.VERSION_2_3_24
                 : is2321Bugfixed(incompatibleImprovements) ? 
Configuration.VERSION_2_3_21
@@ -937,7 +1006,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      * <li>if the object is an Iterator, returns a {@link IteratorModel} for it
      * <li>if the object is an Enumeration, returns a {@link EnumerationModel} 
for it
      * <li>if the object is a String, returns a {@link StringModel} for it
-     * <li>otherwise, returns a generic {@link StringModel} for it.
+     * <li>otherwise, returns a {@link GenericObjectModel} for it.
      * </ul>
      */
     @Override
@@ -1033,7 +1102,7 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
         if (clazz.isArray()) {
             return ArrayModel.FACTORY;
         }
-        return StringModel.FACTORY;
+        return GenericObjectModel.FACTORY;
     }
 
     /**
@@ -1855,15 +1924,33 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
      */
     static public final class MethodAppearanceDecision {
         private PropertyDescriptor exposeAsProperty;
+        private boolean methodInsteadOfPropertyValueBeforeCall;
         private boolean replaceExistingProperty;
         private String exposeMethodAs;
         private boolean methodShadowsProperty;
-        
-        void setDefaults(Method m) {
-            exposeAsProperty = null;
-            replaceExistingProperty = false;
-            exposeMethodAs = m.getName();
+
+        /**
+         * @param appliedZeroArgumentNonVoidMethodPolicy
+         *      {@code null} if this is not a zero argument method with 
non-void return type.
+         */
+        void setDefaults(Method m, ZeroArgumentNonVoidMethodPolicy 
appliedZeroArgumentNonVoidMethodPolicy) {
+            if (appliedZeroArgumentNonVoidMethodPolicy != null
+                    && appliedZeroArgumentNonVoidMethodPolicy != 
ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY) {
+                try {
+                    exposeAsProperty = new PropertyDescriptor(m.getName(), m, 
null);
+                } catch (IntrospectionException e) {
+                    throw new BugException("Failed to create 
PropertyDescriptor for " + m, e);
+                }
+                methodInsteadOfPropertyValueBeforeCall = 
appliedZeroArgumentNonVoidMethodPolicy ==
+                        
ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD;
+            } else {
+                exposeAsProperty = null;
+                methodInsteadOfPropertyValueBeforeCall = false;
+            }
+            exposeMethodAs = appliedZeroArgumentNonVoidMethodPolicy != 
ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY
+                    ? m.getName() : null;
             methodShadowsProperty = true;
+            replaceExistingProperty = false;
         }
         
         /**
@@ -1935,6 +2022,23 @@ public class BeansWrapper implements RichObjectWrapper, 
WriteProtectable {
             this.methodShadowsProperty = shadowEarlierProperty;
         }
 
+        /**
+         * See in the documentation of {@link 
MethodAppearanceFineTuner#process}.
+         *
+         * @since 2.3.33
+         */
+        public boolean isMethodInsteadOfPropertyValueBeforeCall() {
+            return methodInsteadOfPropertyValueBeforeCall;
+        }
+
+        /**
+         * See in the documentation of {@link 
MethodAppearanceFineTuner#process}.
+         *
+         * @since 2.3.33
+         */
+        public void setMethodInsteadOfPropertyValueBeforeCall(boolean 
methodInsteadOfPropertyValueBeforeCall) {
+            this.methodInsteadOfPropertyValueBeforeCall = 
methodInsteadOfPropertyValueBeforeCall;
+        }
     }
     
     /**
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
index 5daaa909..a49fabbc 100644
--- 
a/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
@@ -251,6 +251,42 @@ public abstract class BeansWrapperConfiguration implements 
Cloneable {
         
classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
     }
 
+    /**
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getNonRecordZeroArgumentNonVoidMethodPolicy() {
+        return 
classIntrospectorBuilder.getNonRecordZeroArgumentNonVoidMethodPolicy();
+    }
+
+    /**
+     * See {@link 
BeansWrapper#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}.
+     *
+     * <p>Note that methods in this class are inherited by {@link 
DefaultObjectWrapperBuilder}, which is what you normally use.
+     *
+     * @since 2.3.33
+     */
+    public void 
setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
nonRecordZeroArgumentNonVoidMethodPolicy) {
+        
classIntrospectorBuilder.setNonRecordZeroArgumentNonVoidMethodPolicy(nonRecordZeroArgumentNonVoidMethodPolicy);
+    }
+
+    /**
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getRecordZeroArgumentNonVoidMethodPolicy() {
+        return 
classIntrospectorBuilder.getRecordZeroArgumentNonVoidMethodPolicy();
+    }
+
+    /**
+     * See {@link 
BeansWrapper#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}
+     *
+     * <p>Note that methods in this class are inherited by {@link 
DefaultObjectWrapperBuilder}, which is what you normally use.
+     *
+     * @since 2.3.33
+     */
+    public void 
setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
recordZeroArgumentNonVoidMethodPolicy) {
+        
classIntrospectorBuilder.setRecordZeroArgumentNonVoidMethodPolicy(recordZeroArgumentNonVoidMethodPolicy);
+    }
+
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return classIntrospectorBuilder.getMethodAppearanceFineTuner();
     }
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index bf867814..81f4ffe0 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -48,11 +48,13 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import freemarker.core.BugException;
+import freemarker.core._JavaVersions;
 import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision;
 import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput;
 import freemarker.ext.util.ModelCache;
 import freemarker.log.Logger;
 import freemarker.template.Version;
+import freemarker.template.utility.CollectionUtils;
 import freemarker.template.utility.NullArgumentException;
 import freemarker.template.utility.SecurityUtilities;
 
@@ -82,7 +84,7 @@ class ClassIntrospector {
     private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE =
             new ExecutableMemberSignature("get", new Class[] { Object.class });
     private static final ExecutableMemberSignature TO_STRING_SIGNATURE =
-            new ExecutableMemberSignature("toString", new Class[0]);
+            new ExecutableMemberSignature("toString", 
CollectionUtils.EMPTY_CLASS_ARRAY);
 
     /**
      * When this property is true, some things are stricter. This is mostly to 
catch suspicious things in development
@@ -151,6 +153,9 @@ class ClassIntrospector {
     final MethodAppearanceFineTuner methodAppearanceFineTuner;
     final MethodSorter methodSorter;
     final boolean treatDefaultMethodsAsBeanMembers;
+    final ZeroArgumentNonVoidMethodPolicy 
nonRecordZeroArgumentNonVoidMethodPolicy;
+    final ZeroArgumentNonVoidMethodPolicy 
recordZeroArgumentNonVoidMethodPolicy;
+    final private boolean recordAware;
     final Version incompatibleImprovements;
 
     /** See {@link #getHasSharedInstanceRestrictions()} */
@@ -192,6 +197,14 @@ class ClassIntrospector {
         this.methodAppearanceFineTuner = 
builder.getMethodAppearanceFineTuner();
         this.methodSorter = builder.getMethodSorter();
         this.treatDefaultMethodsAsBeanMembers = 
builder.getTreatDefaultMethodsAsBeanMembers();
+        this.nonRecordZeroArgumentNonVoidMethodPolicy = 
builder.getNonRecordZeroArgumentNonVoidMethodPolicy();
+        this.recordZeroArgumentNonVoidMethodPolicy = 
builder.getRecordZeroArgumentNonVoidMethodPolicy();
+        this.recordAware = nonRecordZeroArgumentNonVoidMethodPolicy != 
recordZeroArgumentNonVoidMethodPolicy;
+        if (recordAware && _JavaVersions.JAVA_16 == null) {
+            throw new IllegalArgumentException(
+                    "nonRecordZeroArgumentNonVoidMethodPolicy != 
recordZeroArgumentNonVoidMethodPolicy, " +
+                    "but Java 16 support is not available.");
+        }
         this.incompatibleImprovements = builder.getIncompatibleImprovements();
 
         this.sharedLock = sharedLock;
@@ -329,13 +342,26 @@ class ClassIntrospector {
             Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
             ClassMemberAccessPolicy effClassMemberAccessPolicy) throws 
IntrospectionException {
         BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
+
+        boolean treatClassAsRecord = recordAware && 
_JavaVersions.JAVA_16.isRecord(clazz);
+        ZeroArgumentNonVoidMethodPolicy zeroArgumentNonVoidMethodPolicy = 
treatClassAsRecord
+                ? recordZeroArgumentNonVoidMethodPolicy
+                : nonRecordZeroArgumentNonVoidMethodPolicy;
+
+        // For real Java Beans properties only, used to exclude them from 
creating fake properties based on ZeroArgumentNonVoidMethod.
+        Set<String> beanPropertyReadMethodNameCollector = 
zeroArgumentNonVoidMethodPolicy != ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY
+                ? new HashSet<String>()
+                : null;
+
         List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, 
clazz);
         int pdasLength = pdas.size();
         // Reverse order shouldn't mater, but we keep it to not risk backward 
incompatibility.
         for (int i = pdasLength - 1; i >= 0; --i) {
             addPropertyDescriptorToClassIntrospectionData(
-                    introspData, pdas.get(i),
-                    accessibleMethods, effClassMemberAccessPolicy);
+                    introspData, pdas.get(i), false,
+                    accessibleMethods,
+                    beanPropertyReadMethodNameCollector,
+                    effClassMemberAccessPolicy);
         }
 
         if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
@@ -348,7 +374,11 @@ class ClassIntrospector {
             for (int i = mdsSize - 1; i >= 0; --i) {
                 final Method method = 
getMatchingAccessibleMethod(mds.get(i).getMethod(), accessibleMethods);
                 if (method != null && 
effClassMemberAccessPolicy.isMethodExposed(method)) {
-                    decision.setDefaults(method);
+                    ZeroArgumentNonVoidMethodPolicy 
appliedZeroArgumentNonVoidMethodPolicy =
+                            getAppliedZeroArgumentNonVoidMethodPolicy(
+                                    method, 
beanPropertyReadMethodNameCollector, zeroArgumentNonVoidMethodPolicy);
+
+                    decision.setDefaults(method, 
appliedZeroArgumentNonVoidMethodPolicy);
                     if (methodAppearanceFineTuner != null) {
                         if (decisionInput == null) {
                             decisionInput = new 
MethodAppearanceDecisionInput();
@@ -359,24 +389,31 @@ class ClassIntrospector {
                         methodAppearanceFineTuner.process(decisionInput, 
decision);
                     }
 
+                    String exposedMethodName = decision.getExposeMethodAs();
+
                     PropertyDescriptor propDesc = 
decision.getExposeAsProperty();
                     if (propDesc != null &&
                             (decision.getReplaceExistingProperty()
                                     || !(introspData.get(propDesc.getName()) 
instanceof FastPropertyDescriptor))) {
+                        boolean methodInsteadOfPropertyValueBeforeCall = 
decision.isMethodInsteadOfPropertyValueBeforeCall();
                         addPropertyDescriptorToClassIntrospectionData(
-                                introspData, propDesc, accessibleMethods, 
effClassMemberAccessPolicy);
+                                introspData, propDesc, 
methodInsteadOfPropertyValueBeforeCall,
+                                accessibleMethods, null, 
effClassMemberAccessPolicy);
+                        if (methodInsteadOfPropertyValueBeforeCall
+                                && exposedMethodName != null && 
exposedMethodName.equals(propDesc.getName())) {
+                            exposedMethodName = null; // We have already 
exposed this as property with the method name
+                        }
                     }
 
-                    String methodKey = decision.getExposeMethodAs();
-                    if (methodKey != null) {
-                        Object previous = introspData.get(methodKey);
+                    if (exposedMethodName != null) {
+                        Object previous = introspData.get(exposedMethodName);
                         if (previous instanceof Method) {
                             // Overloaded method - replace Method with a 
OverloadedMethods
                             OverloadedMethods overloadedMethods =
                                     new OverloadedMethods(is2321Bugfixed());
                             overloadedMethods.addMethod((Method) previous);
                             overloadedMethods.addMethod(method);
-                            introspData.put(methodKey, overloadedMethods);
+                            introspData.put(exposedMethodName, 
overloadedMethods);
                             // Remove parameter type information (unless an 
indexed property reader needs it):
                             if (argTypesUsedByIndexerPropReaders == null
                                     || 
!argTypesUsedByIndexerPropReaders.containsKey(previous)) {
@@ -388,7 +425,7 @@ class ClassIntrospector {
                         } else if (decision.getMethodShadowsProperty()
                                 || !(previous instanceof 
FastPropertyDescriptor)) {
                             // Simple method (so far)
-                            introspData.put(methodKey, method);
+                            introspData.put(exposedMethodName, method);
                             Class<?>[] replaced = 
getArgTypesByMethod(introspData).put(method,
                                     method.getParameterTypes());
                             if (replaced != null) {
@@ -404,6 +441,18 @@ class ClassIntrospector {
         } // end if (exposureLevel < EXPOSE_PROPERTIES_ONLY)
     }
 
+    private static ZeroArgumentNonVoidMethodPolicy 
getAppliedZeroArgumentNonVoidMethodPolicy(Method method, Set<String> 
beanPropertyReadMethodNameCollector, ZeroArgumentNonVoidMethodPolicy 
zeroArgumentNonVoidMethodPolicy) {
+        if (method.getParameterCount() == 0 && method.getReturnType() != 
void.class) {
+            if (beanPropertyReadMethodNameCollector != null && 
beanPropertyReadMethodNameCollector.contains(method.getName())) {
+                return ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY;
+            } else {
+                return zeroArgumentNonVoidMethodPolicy;
+            }
+        } else {
+            return null;
+        }
+    }
+
     /**
      * Very similar to {@link BeanInfo#getPropertyDescriptors()}, but can deal 
with Java 8 default methods too.
      */
@@ -673,8 +722,9 @@ class ClassIntrospector {
     }
 
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, 
Object> introspData,
-            PropertyDescriptor pd,
+            PropertyDescriptor pd, boolean 
methodInsteadOfPropertyValueBeforeCall,
             Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            Set<String> beanPropertyReadMethodNameCollector,
             ClassMemberAccessPolicy effClassMemberAccessPolicy) {
         Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), 
accessibleMethods);
         if (readMethod != null && 
!effClassMemberAccessPolicy.isMethodExposed(readMethod)) {
@@ -697,7 +747,13 @@ class ClassIntrospector {
         }
         
         if (readMethod != null || indexedReadMethod != null) {
-            introspData.put(pd.getName(), new 
FastPropertyDescriptor(readMethod, indexedReadMethod));
+            introspData.put(pd.getName(), new FastPropertyDescriptor(
+                    readMethod, indexedReadMethod,
+                    methodInsteadOfPropertyValueBeforeCall));
+        }
+
+        if (readMethod != null && beanPropertyReadMethodNameCollector != null) 
{
+            beanPropertyReadMethodNameCollector.add(readMethod.getName());
         }
     }
 
@@ -1076,6 +1132,14 @@ class ClassIntrospector {
         return treatDefaultMethodsAsBeanMembers;
     }
 
+    ZeroArgumentNonVoidMethodPolicy 
getNonRecordZeroArgumentNonVoidMethodPolicy() {
+        return nonRecordZeroArgumentNonVoidMethodPolicy;
+    }
+
+    ZeroArgumentNonVoidMethodPolicy getRecordZeroArgumentNonVoidMethodPolicy() 
{
+        return recordZeroArgumentNonVoidMethodPolicy;
+    }
+
     MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return methodAppearanceFineTuner;
     }
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 76e42318..24ad2735 100644
--- 
a/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -26,6 +26,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
+import freemarker.core._JavaVersions;
 import freemarker.template.Configuration;
 import freemarker.template.Version;
 import freemarker.template._TemplateAPI;
@@ -46,6 +47,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
     private boolean exposeFields;
     private MemberAccessPolicy memberAccessPolicy;
     private boolean treatDefaultMethodsAsBeanMembers;
+    private ZeroArgumentNonVoidMethodPolicy 
nonRecordZeroArgumentNonVoidMethodPolicy;
+    private ZeroArgumentNonVoidMethodPolicy 
recordZeroArgumentNonVoidMethodPolicy;
     private MethodAppearanceFineTuner methodAppearanceFineTuner;
     private MethodSorter methodSorter;
     // Attention:
@@ -60,6 +63,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
         exposeFields = ci.exposeFields;
         memberAccessPolicy = ci.memberAccessPolicy;
         treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers;
+        nonRecordZeroArgumentNonVoidMethodPolicy = 
ci.nonRecordZeroArgumentNonVoidMethodPolicy;
+        recordZeroArgumentNonVoidMethodPolicy = 
ci.recordZeroArgumentNonVoidMethodPolicy;
         methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
         methodSorter = ci.methodSorter;
     }
@@ -69,15 +74,18 @@ final class ClassIntrospectorBuilder implements Cloneable {
         // change in the BeansWrapper.normalizeIncompatibleImprovements 
results. That is, this class may don't react
         // to some version changes that affects BeansWrapper, but not the 
other way around.
         this.incompatibleImprovements = 
normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
-        treatDefaultMethodsAsBeanMembers
-                = incompatibleImprovements.intValue() >= _VersionInts.V_2_3_26;
+        treatDefaultMethodsAsBeanMembers = incompatibleImprovements.intValue() 
>= _VersionInts.V_2_3_26;
+        nonRecordZeroArgumentNonVoidMethodPolicy = 
ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY;
+        recordZeroArgumentNonVoidMethodPolicy = 
incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 && 
_JavaVersions.JAVA_16 != null
+                ? ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD : 
ZeroArgumentNonVoidMethodPolicy.METHOD_ONLY;
         memberAccessPolicy = 
DefaultMemberAccessPolicy.getInstance(this.incompatibleImprovements);
     }
 
     private static Version normalizeIncompatibleImprovementsVersion(Version 
incompatibleImprovements) {
         _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
         // All breakpoints here must occur in 
BeansWrapper.normalizeIncompatibleImprovements!
-        return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_30 ? 
Configuration.VERSION_2_3_30
+        return incompatibleImprovements.intValue() >= _VersionInts.V_2_3_33 ? 
Configuration.VERSION_2_3_33
+                : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_30 
? Configuration.VERSION_2_3_30
                 : incompatibleImprovements.intValue() >= _VersionInts.V_2_3_21 
? Configuration.VERSION_2_3_21
                 : Configuration.VERSION_2_3_0;
     }
@@ -98,6 +106,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
         result = prime * result + incompatibleImprovements.hashCode();
         result = prime * result + (exposeFields ? 1231 : 1237);
         result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 
1237);
+        result = prime * result + 
nonRecordZeroArgumentNonVoidMethodPolicy.hashCode();
+        result = prime * result + 
recordZeroArgumentNonVoidMethodPolicy.hashCode();
         result = prime * result + exposureLevel;
         result = prime * result + memberAccessPolicy.hashCode();
         result = prime * result + 
System.identityHashCode(methodAppearanceFineTuner);
@@ -115,6 +125,8 @@ final class ClassIntrospectorBuilder implements Cloneable {
         if (!incompatibleImprovements.equals(other.incompatibleImprovements)) 
return false;
         if (exposeFields != other.exposeFields) return false;
         if (treatDefaultMethodsAsBeanMembers != 
other.treatDefaultMethodsAsBeanMembers) return false;
+        if (nonRecordZeroArgumentNonVoidMethodPolicy != 
other.nonRecordZeroArgumentNonVoidMethodPolicy) return false;
+        if (recordZeroArgumentNonVoidMethodPolicy != 
other.recordZeroArgumentNonVoidMethodPolicy) return false;
         if (exposureLevel != other.exposureLevel) return false;
         if (!memberAccessPolicy.equals(other.memberAccessPolicy)) return false;
         if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) 
return false;
@@ -153,6 +165,36 @@ final class ClassIntrospectorBuilder implements Cloneable {
         this.treatDefaultMethodsAsBeanMembers = 
treatDefaultMethodsAsBeanMembers;
     }
 
+    /**
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getNonRecordZeroArgumentNonVoidMethodPolicy() {
+        return nonRecordZeroArgumentNonVoidMethodPolicy;
+    }
+
+    /**
+     * @since 2.3.33
+     */
+    public void 
setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
nonRecordZeroArgumentNonVoidMethodPolicy) {
+        NullArgumentException.check(nonRecordZeroArgumentNonVoidMethodPolicy);
+        this.nonRecordZeroArgumentNonVoidMethodPolicy = 
nonRecordZeroArgumentNonVoidMethodPolicy;
+    }
+
+    /**
+     * @since 2.3.33
+     */
+    public ZeroArgumentNonVoidMethodPolicy 
getRecordZeroArgumentNonVoidMethodPolicy() {
+        return recordZeroArgumentNonVoidMethodPolicy;
+    }
+
+    /**
+     * @since 2.3.33
+     */
+    public void 
setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy 
recordZeroArgumentNonVoidMethodPolicy) {
+        NullArgumentException.check(recordZeroArgumentNonVoidMethodPolicy);
+        this.recordZeroArgumentNonVoidMethodPolicy = 
recordZeroArgumentNonVoidMethodPolicy;
+    }
+
     public MemberAccessPolicy getMemberAccessPolicy() {
         return memberAccessPolicy;
     }
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
index 12d43de1..7ae8b674 100644
--- 
a/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/FastPropertyDescriptor.java
@@ -29,10 +29,13 @@ import java.lang.reflect.Method;
 final class FastPropertyDescriptor {
     private final Method readMethod;
     private final Method indexedReadMethod;
-    
-    public FastPropertyDescriptor(Method readMethod, Method indexedReadMethod) 
{
+    private final boolean methodInsteadOfPropertyValueBeforeCall;
+
+    public FastPropertyDescriptor(
+            Method readMethod, Method indexedReadMethod, boolean 
methodInsteadOfPropertyValueBeforeCall) {
         this.readMethod = readMethod;
         this.indexedReadMethod = indexedReadMethod;
+        this.methodInsteadOfPropertyValueBeforeCall = 
methodInsteadOfPropertyValueBeforeCall;
     }
 
     public Method getReadMethod() {
@@ -42,5 +45,14 @@ final class FastPropertyDescriptor {
     public Method getIndexedReadMethod() {
         return indexedReadMethod;
     }
-    
+
+    /**
+     * If this is true, and the property value is referred directly before 
it's called in a template, then
+     * the instead of the property value, it the value should be the read 
method (which therefore will be called).
+     *
+     * @since 2.3.33
+     */
+    public boolean isMethodInsteadOfPropertyValueBeforeCall() {
+        return methodInsteadOfPropertyValueBeforeCall;
+    }
 }
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java
new file mode 100644
index 00000000..dd6fd165
--- /dev/null
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/GenericObjectModel.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.util.Collection;
+import java.util.Map;
+
+import freemarker.ext.util.ModelFactory;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+/**
+ * This is used for wrapping objects that has no special treatment ({@link 
Map}-s, {@link Collection}-s,
+ * {@link Number}-s, {@link Boolean}-s, and some more commonly have), hence 
they are just "generic" Java
+ * objects. Users usually just want to call the public Java methods on such 
objects.
+ * These objects can also be used as string values in templates, and that 
value is provided by
+ * the {@link Object#toString()} method of the wrapped object.
+ *
+ * <p>This extends {@link StringModel} for backward compatibility, as now 
{@link BeansWrapper} returns instances of
+ * {@link GenericObjectModel} instead of {@link StringModel}-s, but user code 
may have {@code insteanceof StringModel},
+ * or casing to {@link StringModel}. {@link StringModel} served the same 
purpose as this class, but didn't implement
+ * {@link MethodCallAwareTemplateHashModel}.
+ *
+ * @since 2.3.33
+ */
+public class GenericObjectModel extends StringModel implements 
MethodCallAwareTemplateHashModel {
+    static final ModelFactory FACTORY = (object, wrapper) -> new 
GenericObjectModel(object, (BeansWrapper) wrapper);
+
+    /**
+     * Creates a new model that wraps the specified object with BeanModel + 
scalar functionality.
+     *
+     * @param object
+     *         the object to wrap into a model.
+     * @param wrapper
+     *         the {@link BeansWrapper} associated with this model. Every 
model has to have an associated
+     *         {@link BeansWrapper} instance. The model gains many attributes 
from its wrapper, including the caching
+     *         behavior, method exposure level, method-over-item shadowing 
policy etc.
+     */
+    public GenericObjectModel(Object object, BeansWrapper wrapper) {
+        super(object, wrapper);
+    }
+
+    // Made this final, to ensure that users override get(key, boolean) 
instead.
+    @Override
+    public final TemplateModel get(String key) throws TemplateModelException {
+        return super.get(key);
+    }
+
+    @Override
+    public TemplateModel getBeforeMethodCall(String key) throws 
TemplateModelException,
+            ShouldNotBeGetAsMethodException {
+        return super.getBeforeMethodCall(key);
+    }
+}
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java
index 98c6416c..bfa6bc9f 100644
--- 
a/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java
@@ -53,7 +53,10 @@ public interface MethodAppearanceFineTuner {
      *   <li>Show the method with a different name in the data-model than its
      *     real name by calling
      *     {@link MethodAppearanceDecision#setExposeMethodAs(String)}
-     *     with non-{@code null} parameter.
+     *     with non-{@code null} parameter. Also, if set to {@code null}, the 
method won't be exposed.
+     *     The default is the name of the method. Note that if {@code 
methodInsteadOfPropertyValueBeforeCall} is
+     *     {@code true}, the method is not exposed if the method name set here 
is the same as the name of the property
+     *     set for this method with {@link 
MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}.
      *   <li>Create a fake JavaBean property for this method by calling
      *     {@link 
MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}.
      *     For example, if you have {@code int size()} in a class, but you
@@ -76,6 +79,21 @@ public interface MethodAppearanceFineTuner {
      *     of the same name was already assigned earlier, it won't be
      *     replaced by the new one by default, however this can be changed with
      *     {@link 
MethodAppearanceDecision#setReplaceExistingProperty(boolean)}.
+     *   <li>If something is exposed as property via
+     *     {@link 
MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)} (and not only 
because it's a real
+     *     JavaBeans property), and you also want the property value to be 
accessible in templates as the return value
+     *     of a 0-argument method of the same name, then call
+     *     {@link 
MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)} 
with {@code true}.
+     *     Here's an example to explain that. Let's say, you have a class that 
contains "public String name()", and you
+     *     exposed that as a property via {@link 
MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}. So
+     *     far, you can access the property value from templates as {@code 
user.name}, but {@code user.name()} will
+     *     fail, saying that you try to call a {@code String} (because you 
apply the {@code ()} operator on the result
+     *     of {@code user.name}). But with
+     *     {@link 
MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)} 
{@code true},
+     *     both {@code user.name}, and {@code user.name()} will do the same.
+     *     The default of this is influenced by
+     *     {@link 
BeansWrapperConfiguration#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)},
+     *     {@link 
BeansWrapperConfiguration#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)}.
      *   <li>Prevent the method to hide a JavaBeans property (fake or real) of
      *     the same name by calling
      *     {@link MethodAppearanceDecision#setMethodShadowsProperty(boolean)}
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/MethodCallAwareTemplateHashModel.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/MethodCallAwareTemplateHashModel.java
new file mode 100644
index 00000000..337a8828
--- /dev/null
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/MethodCallAwareTemplateHashModel.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.util.Collection;
+import java.util.Map;
+
+import freemarker.core.Macro;
+import freemarker.core.NonMethodException;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Adds a getter method that can return different result than {@link 
#get(String)}, knowing that the result of it will
+ * be called as a method. At least as of 2.3.33, this is only utilized by the 
template language for 0-argument method
+ * calls directly after the dot operator and the key. For example, if in the 
template you have
+ * {@code someRecord.someComponent()}, and there {@code someRecord} was 
wrapped by the {@link ObjectWrapper} into
+ * a {@link TemplateHashModel} that also implements this interface, then the 
dot operator will call
+ * {@link #getBeforeMethodCall(String) getBeforeMethodCall("someComponent")}, 
rather than
+ * {@link #get(String) get("someComponent")}. This is needed to implement the
+ * {@link 
BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}
 feature,
+ * which is needed to implement {@link 
ZeroArgumentNonVoidMethodPolicy#BOTH_PROPERTY_AND_METHOD}.
+ *
+ * <p>While technically we could do the same for method calls with more the 0 
arguments, as of 2.3.33 at least we
+ * don't want to generalize this to that case. The FreeMarker 2.x template 
language doesn't have separated namespace for
+ * methods, so this is already a hack as is, but we had to address the issue 
with Java records (again, see that
+ * at {@link 
BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)}).
+ *
+ * <p>Objects wrapped with {@link BeansWrapper}, and {@link 
DefaultObjectWrapper} will implement this interface, when
+ * they are "generic" object (that is, when they are not classes with special 
wrapper, like {@link Map}-s,
+ * {@link Collection}-s, {@link Number}-s, etc.).
+ *
+ * @since 2.3.33
+ */
+public interface MethodCallAwareTemplateHashModel extends TemplateHashModel {
+
+    /**
+     * This is called instead of {@link #get(String)}, if we know that the 
return value should be callable like a
+     * method. The advantage of this is that we can coerce the value to a 
method when desirable, and otherwise can give
+     * a more specific error message in the resulting exception than the 
standard {@link NonMethodException} would.
+     *
+     * @param key
+     *      Same as for {@link #get(String)}
+     *
+     * @return
+     *      Same as for just like {@link #get(String)}, except it should 
return a
+     *      {@link TemplateMethodModelEx}, or a {@link TemplateMethodModel}, 
or in very rare case a {@link Macro}
+     *      that was created with the {@code function} directive. Or, {@code 
null} in the same case as
+     *      {@link #get(String)}. The method should never return something 
that's not callable in the template language
+     *      as a method or function.
+     *
+     * @throws ShouldNotBeGetAsMethodException
+     *      If the value for the given key exists, but it shouldn't be coerced 
something callable as a method. This will
+     *      be converted to {@link NonMethodException} by the engine, but in 
this exception you can optionally give a
+     *      more specific explanation, and that will be added to the resulting 
{@link NonMethodException} as a hint to
+     *      the user.
+     */
+    TemplateModel getBeforeMethodCall(String key)
+            throws TemplateModelException, ShouldNotBeGetAsMethodException;
+
+    /**
+     * Thrown by {@link #getBeforeMethodCall(String)}; see there.
+     */
+    final class ShouldNotBeGetAsMethodException extends Exception {
+        private final TemplateModel actualValue;
+        private final String hint;
+
+        /**
+         * Same as {@link ShouldNotBeGetAsMethodException(TemplateModel, 
String, Throwable)}, with {@code null}
+         * cause exception argument.
+         */
+        public ShouldNotBeGetAsMethodException(TemplateModel actualValue, 
String hint) {
+            this(actualValue, hint, null);
+        }
+
+        /**
+         * @param actualValue
+         *      The actual value we got instead of a method; can't be {@code 
null}!
+         * @param hint
+         *      Hint for the user, that's added to the error message; {@code 
null} if you just want the plain
+         *      {@link NonMethodException} error message.
+         * @param cause
+         *      Can be {@code null}.
+         */
+        public ShouldNotBeGetAsMethodException(TemplateModel actualValue, 
String hint, Throwable cause) {
+            super(null, cause, true, false);
+            NullArgumentException.check(actualValue);
+            this.actualValue = actualValue;
+            this.hint = hint;
+        }
+
+        /**
+         * The actual value we got instead of a method; not {@code null}.
+         */
+        public TemplateModel getActualValue() {
+            return actualValue;
+        }
+
+        /**
+         * Additional hint for the user; maybe {@code null}.
+         */
+        public String getHint() {
+            return hint;
+        }
+    }
+}
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java
index b53872d0..c1052a01 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/StringModel.java
@@ -20,25 +20,19 @@
 package freemarker.ext.beans;
 
 import freemarker.ext.util.ModelFactory;
-import freemarker.template.ObjectWrapper;
-import freemarker.template.TemplateModel;
 import freemarker.template.TemplateScalarModel;
 
 /**
  * Subclass of {@link BeanModel} that exposes the return value of the {@link
  * java.lang.Object#toString()} method through the {@link TemplateScalarModel}
  * interface.
+ *
+ * @deprecated Use {@link GenericObjectModel} instead, which implements {@link 
MethodCallAwareTemplateHashModel}.
  */
+@Deprecated
 public class StringModel extends BeanModel
 implements TemplateScalarModel {
-    static final ModelFactory FACTORY =
-        new ModelFactory()
-        {
-            @Override
-            public TemplateModel create(Object object, ObjectWrapper wrapper) {
-                return new StringModel(object, (BeansWrapper) wrapper);
-            }
-        };
+    static final ModelFactory FACTORY = (object, wrapper) -> new 
StringModel(object, (BeansWrapper) wrapper);
 
     // Package visible for testing
     static final String TO_STRING_NOT_EXPOSED = "[toString not exposed]";
diff --git 
a/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java
 
b/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java
new file mode 100644
index 00000000..85f2390a
--- /dev/null
+++ 
b/freemarker-core/src/main/java/freemarker/ext/beans/ZeroArgumentNonVoidMethodPolicy.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import freemarker.template.DefaultObjectWrapper;
+
+/**
+ * How to show 0 argument non-void public methods to templates, which are not 
standard Java Beans read methods.
+ * Used in {@link BeansWrapper}, and therefore in {@link DefaultObjectWrapper}.
+ * This policy doesn't apply to methods that Java Beans introspector discovers 
as a property read method (which
+ * typically look like {@code getSomething()}/{@code getSomething()}). It's 
only applicable to methods like
+ * {@code something()}, including the component read methods of Java records.
+ *
+ * @see 
BeansWrapperConfiguration#setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)
+ * @see 
BeansWrapperConfiguration#setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy)
+ * @see 
BeansWrapper.MethodAppearanceDecision#setMethodInsteadOfPropertyValueBeforeCall(boolean)
+ *
+ * @since 2.3.33
+ */
+public enum ZeroArgumentNonVoidMethodPolicy {
+
+    /**
+     * Both {@code obj.m}, and {@code obj.m()} gives back the value that the 
{@code m} Java method returns, and it's
+     * not possible to get the method itself.
+     *
+     * <p>This is a parse-time trick that only works when the result of the 
dot operator is called immediately in a
+     * template (and therefore the dot operator knows that you will call the 
result of it). The practical reason for
+     * this feature is that the convention of having {@code SomeType 
something()} instead of
+     * {@code SomeType getSomething()} spreads in the Java ecosystem (and is a 
standard in some other JVM languages),
+     * and thus we can't tell anymore if {@code SomeType something()} just 
reads a value, and hence should be accessed
+     * like {@code obj.something}, or it's more like an operation that has 
side effect, and therefore should be
+     * accessed like {@code obj.something()}. So with be allowing both, the 
template author is free to decide which is
+     * the more fitting. Also, for accessing Java records components, the 
proper way is {@code obj.something}, but
+     * before FreeMarker was aware of records (and hence that those methods 
are like property read methods), the
+     * only way that worked was {@code obj.something()}, so to be more 
backward compatible, we have to support both.
+     */
+    BOTH_PROPERTY_AND_METHOD,
+
+    /**
+     * Only {@code obj.m()} gives back the value, {@code obj.m} just gives the 
method itself.
+     */
+    METHOD_ONLY,
+
+    /**
+     * {@code obj.m} in gives back the value, and the method itself can't be 
get.
+     */
+    PROPERTY_ONLY
+}
diff --git a/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java 
b/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java
index 64d9797e..a8a6c775 100644
--- a/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java
+++ b/freemarker-core/src/main/java/freemarker/ext/beans/_BeansAPI.java
@@ -116,7 +116,7 @@ public class _BeansAPI {
             
             packedArgs = new Object[fixedArgCnt + 1]; 
             for (int i = 0; i < fixedArgCnt; i++) {
-                packedArgs[i] = args[i];
+packedArgs[i] = args[i];
             }
             
             final Class<?> compType = 
paramTypes[fixedArgCnt].getComponentType();
@@ -226,5 +226,5 @@ public class _BeansAPI {
     public static ClassIntrospectorBuilder 
getClassIntrospectorBuilder(BeansWrapperConfiguration bwc) {
         return bwc.getClassIntrospectorBuilder();
     }
-    
+
 }
diff --git a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj 
b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
index f4390862..06495a36 100644
--- a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
+++ b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
@@ -2467,6 +2467,9 @@ MethodCall MethodArgs(Expression exp) :
         end = <CLOSE_PAREN>
         {
             args.trimToSize();
+            if (args.isEmpty() && exp instanceof Dot) {
+                exp = new DotBeforeMethodCall((Dot) exp);
+            }
             MethodCall result = new MethodCall(exp, args);
             result.setLocation(template, exp, end);
             return result;
diff --git 
a/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java 
b/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java
index 6a08ea91..4f4e6f94 100644
--- a/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/freemarker-core/src/test/java/freemarker/template/ConfigurationTest.java
@@ -90,10 +90,10 @@ import freemarker.core.XMLOutputFormat;
 import freemarker.core.XSCFormat;
 import freemarker.core._CoreStringUtils;
 import freemarker.ext.beans.BeansWrapperBuilder;
+import freemarker.ext.beans.GenericObjectModel;
 import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
 import freemarker.ext.beans.MemberAccessPolicy;
 import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy;
-import freemarker.ext.beans.StringModel;
 import freemarker.ext.beans.WhitelistMemberAccessPolicy;
 import freemarker.template.utility.DateUtil;
 import freemarker.template.utility.NullArgumentException;
@@ -1316,7 +1316,7 @@ public class ConfigurationTest extends TestCase {
         {
             TemplateScalarModel aVal = (TemplateScalarModel) 
cfg.getSharedVariable("a");
             assertEquals("aa", aVal.getAsString());
-            assertEquals(StringModel.class, aVal.getClass());
+            assertEquals(GenericObjectModel.class, aVal.getClass());
             
             TemplateScalarModel bVal = (TemplateScalarModel) 
cfg.getSharedVariable("b");
             assertEquals("bbLegacy", bVal.getAsString());
diff --git 
a/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java
 
b/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java
new file mode 100644
index 00000000..968850ec
--- /dev/null
+++ 
b/freemarker-core16/src/test/java/freemarker/ext/beans/TestZeroArgumentNonVoidMethodPolicy.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static freemarker.template.Configuration.*;
+
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class TestZeroArgumentNonVoidMethodPolicy extends TemplateTest {
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        // Don't use default, as then the object wrapper is a shared static 
mutable object:
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
+        return cfg;
+    }
+
+    @Before
+    public void setup() throws Exception {
+        addToDataModel("rec", new TestRecord(1, "S"));
+        addToDataModel("nrc", new TestNonRecord());
+    }
+
+    @Test
+    public void testDefaultWithHighIncompatibleImprovements() throws 
TemplateException, IOException {
+        getConfiguration().setObjectWrapper(new 
DefaultObjectWrapper(VERSION_2_3_33));
+        assertRecIsBothPropertyAndMethod();
+        assertNrcIsMethodOnly();
+    }
+
+    @Test
+    public void testDefaultWithLowIncompatibleImprovements() throws 
TemplateException, IOException {
+        getConfiguration().setObjectWrapper(new 
DefaultObjectWrapper(VERSION_2_3_32));
+        assertRecIsMethodOnly();
+        assertNrcIsMethodOnly();
+    }
+
+    @Test
+    public void testDefaultWithLowIncompatibleImprovements2() throws 
TemplateException, IOException {
+        DefaultObjectWrapper beansWrapper = new 
DefaultObjectWrapper(VERSION_2_3_32);
+        getConfiguration().setObjectWrapper(beansWrapper);
+        
beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD);
+        assertRecIsBothPropertyAndMethod();
+        assertNrcIsMethodOnly();
+    }
+
+    @Test
+    public void testDefaultWithRecordsPropertyOnly() throws TemplateException, 
IOException {
+        DefaultObjectWrapper beansWrapper = new 
DefaultObjectWrapper(VERSION_2_3_32);
+        getConfiguration().setObjectWrapper(beansWrapper);
+        
beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY);
+        assertRecIsPropertyOnly();
+        assertNrcIsMethodOnly();
+    }
+
+    @Test
+    public void testDefaultWithRecordsPropertyOnly2() throws 
TemplateException, IOException {
+        DefaultObjectWrapper beansWrapper = new 
DefaultObjectWrapper(VERSION_2_3_33);
+        getConfiguration().setObjectWrapper(beansWrapper);
+        
beansWrapper.setRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY);
+        assertRecIsPropertyOnly();
+        assertNrcIsMethodOnly();
+    }
+
+    @Test
+    public void testDefaultWithNonRecordsPropertyOnly() throws 
TemplateException, IOException {
+        DefaultObjectWrapper beansWrapper = new 
DefaultObjectWrapper(VERSION_2_3_32);
+        getConfiguration().setObjectWrapper(beansWrapper);
+        
beansWrapper.setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.PROPERTY_ONLY);
+        assertRecIsMethodOnly();
+        assertNrcIsPropertyOnly();
+    }
+
+    @Test
+    public void testDefaultWithBothPropertyAndMethod() throws 
TemplateException, IOException {
+        DefaultObjectWrapper beansWrapper = new 
DefaultObjectWrapper(VERSION_2_3_33);
+        getConfiguration().setObjectWrapper(beansWrapper);
+        
beansWrapper.setNonRecordZeroArgumentNonVoidMethodPolicy(ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD);
+        assertRecIsBothPropertyAndMethod();
+        assertNrcIsBothPropertyAndMethod();
+    }
+
+    @Test
+    public void testSettings() throws TemplateException, IOException {
+        getConfiguration().setSetting(
+                "objectWrapper",
+                "DefaultObjectWrapper(2.3.33, 
nonRecordZeroArgumentNonVoidMethodPolicy=freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD)");
+        assertRecIsBothPropertyAndMethod();
+        assertNrcIsBothPropertyAndMethod();
+    }
+
+    private void assertRecIsBothPropertyAndMethod() throws IOException, 
TemplateException {
+        assertOutput("${rec.x}", "1");
+        assertOutput("${rec.x()}", "1");
+        assertOutput("${rec.s}", "S");
+        assertOutput("${rec.s()}", "S");
+        assertOutput("${rec.y}", "2");
+        assertOutput("${rec.y()}", "2");
+        assertOutput("${rec.tenX}", "10");
+        assertOutput("${rec.tenX()}", "10");
+
+        assertRecPolicyIndependentMembers();
+    }
+
+    private void assertRecIsMethodOnly() throws IOException, TemplateException 
{
+        assertErrorContains("${rec.x}", "SimpleMethodModel");
+        assertOutput("${rec.x()}", "1");
+        assertErrorContains("${rec.s}", "SimpleMethodModel");
+        assertOutput("${rec.s()}", "S");
+        assertErrorContains("${rec.y}", "SimpleMethodModel");
+        assertOutput("${rec.y()}", "2");
+        assertErrorContains("${rec.tenX}", "SimpleMethodModel");
+        assertOutput("${rec.tenX()}", "10");
+
+        assertRecPolicyIndependentMembers();
+    }
+
+    private void assertRecIsPropertyOnly() throws IOException, 
TemplateException {
+        assertOutput("${rec.x}", "1");
+        assertErrorContains("${rec.x()}", "SimpleNumber", "must not be called 
as a method");
+        assertOutput("${rec.s}", "S");
+        assertErrorContains("${rec.s()}", "SimpleScalar");
+        assertOutput("${rec.y}", "2");
+        assertErrorContains("${rec.y()}", "SimpleNumber");
+        assertOutput("${rec.tenX}", "10");
+        assertErrorContains("${rec.tenX()}", "SimpleNumber");
+
+        assertRecPolicyIndependentMembers();
+    }
+
+    private void assertRecPolicyIndependentMembers() throws IOException, 
TemplateException {
+        assertOutput("${rec.z}", "3");
+        assertErrorContains("${rec.z()}", "SimpleNumber");
+        assertOutput("${rec.getZ()}", "3");
+        assertOutput("${rec.xTimes(5)}", "5");
+        assertErrorContains("${rec.xTimes}", "SimpleMethodModel");
+        assertOutput("${rec.voidMethod()}", "");
+        assertErrorContains("${rec.voidMethod}", "SimpleMethodModel");
+    }
+
+    private void assertNrcIsMethodOnly() throws IOException, TemplateException 
{
+        assertErrorContains("${nrc.x}", "SimpleMethodModel");
+        assertOutput("${nrc.x()}", "1");
+        assertErrorContains("${nrc.y}", "SimpleMethodModel");
+        assertOutput("${nrc.y()}", "2");
+        assertErrorContains("${nrc.tenX}", "SimpleMethodModel");
+        assertOutput("${nrc.tenX()}", "10");
+
+        assertNrcPolicyIndependentMembers();
+    }
+
+    private void assertNrcIsBothPropertyAndMethod() throws IOException, 
TemplateException {
+        assertOutput("${nrc.x}", "1");
+        assertOutput("${nrc.x()}", "1");
+        assertOutput("${nrc.y}", "2");
+        assertOutput("${nrc.y()}", "2");
+        assertOutput("${nrc.tenX}", "10");
+        assertOutput("${nrc.tenX()}", "10");
+
+        assertNrcPolicyIndependentMembers();
+    }
+
+    private void assertNrcIsPropertyOnly() throws IOException, 
TemplateException {
+        assertOutput("${nrc.x}", "1");
+        assertErrorContains("${nrc.x()}", "SimpleNumber", "must not be called 
as a method");
+        assertOutput("${nrc.y}", "2");
+        assertErrorContains("${nrc.y()}", "SimpleNumber");
+        assertOutput("${nrc.tenX}", "10");
+        assertErrorContains("${nrc.tenX()}", "SimpleNumber");
+
+        assertNrcPolicyIndependentMembers();
+    }
+
+    private void assertNrcPolicyIndependentMembers() throws IOException, 
TemplateException {
+        assertOutput("${nrc.z}", "3");
+        assertErrorContains("${nrc.z()}", "SimpleNumber");
+        assertOutput("${nrc.getZ()}", "3");
+        assertOutput("${nrc.xTimes(5)}", "5");
+        assertErrorContains("${nrc.xTimes}", "SimpleMethodModel");
+        assertOutput("${nrc.voidMethod()}", "");
+        assertErrorContains("${nrc.voidMethod}", "SimpleMethodModel");
+    }
+
+    public interface TestInterface {
+        int y();
+
+        /**
+         * Defines a real JavaBeans property, "z", so the {@link 
ZeroArgumentNonVoidMethodPolicy} shouldn't affect this
+         */
+        int getZ();
+    }
+
+    /**
+     * Defines record component readers for "x" and "s", and some other 
non-record-component methods that are still
+     * potentially exposed as if there were properties.
+     */
+    public record TestRecord(int x, String s) implements TestInterface {
+        @Override
+        public int y() {
+            return 2;
+        }
+
+        @Override
+        public int getZ() {
+            return 3;
+        }
+
+        public int tenX() {
+            return x * 10;
+        }
+
+        /**
+         * Has an argument, so this never should be exposed as property.
+         */
+        public int xTimes(int m) {
+            return x * m;
+        }
+
+        /**
+         * Has a void return type, so this never should be exposed as property.
+         */
+        public void voidMethod() {
+            // do nothing
+        }
+    }
+
+    public static class TestNonRecord implements TestInterface {
+        public int x() {
+            return 1;
+        }
+
+        @Override
+        public int y() {
+            return 2;
+        }
+
+        @Override
+        public int getZ() {
+            return 3;
+        }
+
+        public int tenX() {
+            return x() * 10;
+        }
+
+        public int xTimes(int m) {
+            return x() * m;
+        }
+
+        /**
+         * Has a void return type, so this never should be exposed as property.
+         */
+        public void voidMethod() {
+            // do nothing
+        }
+    }
+
+}
diff --git 
a/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
 
b/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index d67d1950..51152998 100644
--- 
a/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ 
b/freemarker-jython25/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -104,7 +104,7 @@ public class DefaultObjectWrapperTest {
         expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 
2.3.30
         expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 
2.3.31
         expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 
2.3.32
-        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 
2.3.33
+        expected.add(Configuration.VERSION_2_3_33);
 
         List<Version> actual = new ArrayList<>();
         for (int i = _VersionInts.V_2_3_0; i <= 
Configuration.getVersion().intValue(); i++) {
@@ -383,7 +383,7 @@ public class DefaultObjectWrapperTest {
             assertTrue(ow.getUseAdaptersForContainers());
             assertTrue(ow.getForceLegacyNonListCollections());
         }
-        
+
         try {
             new DefaultObjectWrapper(new Version(99, 9, 9));
             fail();
diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml 
b/freemarker-manual/src/main/docgen/en_US/book.xml
index dfeadb1e..bc0df5bb 100644
--- a/freemarker-manual/src/main/docgen/en_US/book.xml
+++ b/freemarker-manual/src/main/docgen/en_US/book.xml
@@ -30213,6 +30213,106 @@ TemplateModel x = env.getVariable("x");  // get 
variable x</programlisting>
           <title>Changes on the Java side</title>
 
           <itemizedlist>
+            <listitem>
+              <para><link
+              
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-183";>FREEMARKER-183</link>:
+              Better support for Java records, if you set the <link
+              
linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
+              setting</link> to 2.3.33 or higher (or if you create your own
+              <literal>ObjectWrapper</literal>, then set its
+              <literal>incompatible_improvements</literal>, or just its
+              <literal>recordZeroArgumentNonVoidMethodPolicy</literal>
+              property to <literal>BOTH_PROPERTY_AND_METHOD</literal>). If in
+              a Java record you have something like <literal>int
+              price()</literal>, earlier you could only read the value in
+              templates as <literal>obj.price()</literal>. With this
+              improvement <literal>obj.price</literal> will do the same. This
+              has always worked for JavaBeans properties, like <literal>int
+              getPrice()</literal> could always be used in templates as
+              <literal>obj.price</literal>, in additionally to as
+              <literal>obj.getPrice()</literal>. Now this also works for Java
+              records, and there we simply treat all methods that has 0
+              arguments and non-void return type like a property. Although
+              this is a trickier situation, as the name of the method is the
+              same as the name of our fake property (both are
+              <literal>price</literal>, while with JavaBeans one was
+              <literal>price</literal>, and the other is
+              <literal>getPrice</literal>), so there are some dirty tricks
+              involved, but as far as most uses care, it just works. Some more
+              technical related changes:</para>
+
+              <itemizedlist>
+                <listitem>
+                  <para>Added two new setting to
+                  <literal>BeansWrapper</literal>, and therefore
+                  <literal>DefaultObjectWrapper</literal>:
+                  <literal>recordZeroArgumentNonVoidMethodPolicy</literal>,
+                  and
+                  <literal>nonRecordZeroArgumentNonVoidMethodPolicy</literal>.
+                  Each has enum type
+                  
<literal>freemarker.ext.beans.ZeroArgumentNonVoidMethodPolicy</literal>,
+                  the can be <literal>METHOD_ONLY</literal>,
+                  <literal>PROPERTY_ONLY</literal>, or
+                  <literal>BOTH_PROPERTY_AND_METHOD</literal>.
+                  Therefore:</para>
+
+                  <itemizedlist>
+                    <listitem>
+                      <para>Note that with
+                      
<literal>nonRecordZeroArgumentNonVoidMethodPolicy</literal>
+                      you can set similar behavior to non-records. That is,
+                      you can call 0 argument non-void methods without
+                      <literal>()</literal>, if you want. It's only meant to
+                      be used for methods that are mere value readers, and has
+                      no side effect.</para>
+                    </listitem>
+
+                    <listitem>
+                      <para>For records, you can enforce proper style with
+                      setting
+                      <literal>recordZeroArgumentNonVoidMethodPolicy</literal>
+                      to <literal>PROPERTY_ONLY</literal>. The default is more
+                      lenient (as there using <literal>()</literal> is
+                      allowed) for backward compatibility (and because people
+                      often just use the Java syntax).</para>
+                    </listitem>
+                  </itemizedlist>
+                </listitem>
+
+                <listitem>
+                  <para>Added new interface,
+                  <literal>MethodCallAwareTemplateHashModel</literal>, which
+                  adds <literal>getBeforeMethodCall(String key)</literal>. If
+                  you have something like <literal>obj.price()</literal>, the
+                  that's called instead of
+                  <literal>TemplateHashModel.get("price")</literal>, in a
+                  template. This is needed if for
+                  
<literal>ZeroArgumentNonVoidMethodPolicy.BOTH_PROPERTY_AND_METHOD</literal>
+                  to work as expected.</para>
+                </listitem>
+
+                <listitem>
+                  <para>Added <literal>GenericObjectModel</literal>, which
+                  extends <literal>StringModel</literal> with implementing
+                  <literal>MethodCallAwareTemplateHashModel</literal>, and has
+                  a more telling name. <literal>BeansWrapper</literal>, and
+                  therefore <literal>DefaultObjectWrapper</literal> now
+                  creates <literal>GenericObjectModel</literal>-s instead of
+                  <literal>StringModel</literal>-s. This is like so regardless
+                  of any setting, like regardless of
+                  <literal>incompatible_improvements</literal>.</para>
+                </listitem>
+
+                <listitem>
+                  <para>You shouldn't override
+                  <literal>BeanModel.get(String)</literal> anymore, but
+                  <literal>BeanModel.get(String, boolean)</literal>. If you
+                  have overridden <literal>get</literal>, then see in the
+                  Javadoc for more.</para>
+                </listitem>
+              </itemizedlist>
+            </listitem>
+
             <listitem>
               <para><link
               xlink:href="https://github.com/apache/freemarker/pull/88";>GitHub


Reply via email to