Merge pull request #64 from tocco/fix_extended_decimal_format_parser

FREEMARKER-125: ExtendedDecimalFormatParser now picks up DecimalFormatSymbols provided by DecimalFormatSymbolsProvider SPI.
diff --git a/README.md b/README.md
index dab9870..d8197cb 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@
 included in OpenJDK anymore. It's not needed on Oracle Java 9,
 or if FreeMarker is configured to use Jaxen for XPath.
 
-The minimum required Java version is currently Java SE 5. (The presence
+The minimum required Java version is currently Java SE 7. (The presence
 of a later version may be detected on runtime and utilized by
 FreeMarker.)
 
@@ -201,7 +201,7 @@
    - Press "Finish"
 - Eclipse will indicate many errors at this point; it's expected, read on.
 - Project -> Properties -> Java Compiler
-  - Set "Compiler Compliance Level" to "1.5" (you will have to uncheck
+  - Set "Compiler Compliance Level" to "1.7" (you will have to uncheck
     "Use compliance from execution environment" for that)
   - In Errors/Warnings, check in "Enable project specific settings", then set
     "Forbidden reference (access rules)" from "Error" to "Warning".
@@ -272,8 +272,8 @@
     - Test Resource Folders:  
       src/test/resources
       
-  - Still inside the "Sources" tab, change the "Language level" to "5". (Yes, we use Java 8 SDK with
-    language level 5 in the IDE, due to the tricks FreeMarker uses to support different Java versions.)
+  - Still inside the "Sources" tab, change the "Language level" to "7". (Yes, we use Java 8 SDK with
+    language level 7 in the IDE, due to the tricks FreeMarker uses to support different Java versions.)
     
   - Switch over to the "Dependencies" tab (still inside "Project Structure" / "Modules"), and add
     all the jar-s inside the `ide-dependencies` directory as dependency. (How: Click the "+" icon
diff --git a/build.properties.sample b/build.properties.sample
index 56315b3..d5e31cc 100644
--- a/build.properties.sample
+++ b/build.properties.sample
@@ -17,8 +17,7 @@
 
 # Copy this file to "build.properties" before editing!
 # These propeties should point to the rt.jar-s of the respective J2SE versions:
-boot.classpath.j2se1.5=C:/Program Files (x86)/Java/jdk1.5.0_16/jre/lib/rt.jar
-boot.classpath.j2se1.6=C:/Program Files/Java/jdk1.6.0_24/jre/lib/rt.jar
+boot.classpath.j2se1.7=c:/Program Files/Java/jre7/lib/rt.jar
 boot.classpath.j2se1.8=C:/Program Files/Java/jdk1.8.0_66/jre/lib/rt.jar
 mvnCommand=C:/Program Files (x86)/maven3/bin/mvn.bat
 gpgCommand=C:/Program Files (x86)/GNU/GnuPG/pub/gpg.exe
\ No newline at end of file
diff --git a/build.xml b/build.xml
index 1953b75..5098e11 100644
--- a/build.xml
+++ b/build.xml
@@ -43,11 +43,8 @@
   <property name="server.ivy.repo.root" value="${basedir}/build/dummy-server-ivy-repo" />
   
   <property file="build.properties"/>
-  <condition property="has.explicit.boot.classpath.j2se1.5">
-    <isset property="boot.classpath.j2se1.5"/>
-  </condition>
-  <condition property="has.explicit.boot.classpath.j2se1.6">
-    <isset property="boot.classpath.j2se1.6"/>
+  <condition property="has.explicit.boot.classpath.j2se1.7">
+    <isset property="boot.classpath.j2se1.7"/>
   </condition>
   <condition property="has.explicit.boot.classpath.j2se1.8">
     <isset property="boot.classpath.j2se1.8"/>
@@ -61,18 +58,13 @@
 
   <!-- When boot.classpath.j2se* is missing, these will be the defaults: -->
   <!-- Note: Target "dist" doesn't allow using these. -->
-  <property name="boot.classpath.j2se1.5" value="${sun.boot.class.path}" />
-  <property name="boot.classpath.j2se1.6" value="${sun.boot.class.path}" />
+  <property name="boot.classpath.j2se1.7" value="${sun.boot.class.path}" />
   <property name="boot.classpath.j2se1.8" value="${sun.boot.class.path}" />
   
   <!-- For checking the correctness of the boot.classpath.j2se* -->
-  <available classpath="${boot.classpath.j2se1.5}"
-    classname="java.util.concurrent.ConcurrentHashMap" ignoresystemclasses="true" 
-    property="boot.classpath.j2se1.5.correct"
-  />
-  <available classpath="${boot.classpath.j2se1.6}"
-    classname="java.util.ServiceLoader" ignoresystemclasses="true" 
-    property="boot.classpath.j2se1.6.correct"
+  <available classpath="${boot.classpath.j2se1.7}"
+    classname="java.nio.file.Path" ignoresystemclasses="true" 
+    property="boot.classpath.j2se1.7.correct"
   />
   <available classpath="${boot.classpath.j2se1.8}"
     classname="java.time.Instant" ignoresystemclasses="true" 
@@ -210,14 +202,8 @@
   </target>
    
   <target name="compile" depends="javacc">
-    <fail unless="boot.classpath.j2se1.5.correct"><!--
-      -->The "boot.classpath.j2se1.5" property value (${boot.classpath.j2se1.5}) <!--
-      -->seems to be an incorrect boot classpath. Please fix it in <!--
-      -->the &lt;projectDir>/build.properties file, or wherever you <!--
-      -->set it.<!--
-    --></fail>
-    <fail unless="boot.classpath.j2se1.6.correct"><!--
-      -->The "boot.classpath.j2se1.6" property value (${boot.classpath.j2se1.6}) <!--
+    <fail unless="boot.classpath.j2se1.7.correct"><!--
+      -->The "boot.classpath.j2se1.7" property value (${boot.classpath.j2se1.7}) <!--
       -->seems to be an incorrect boot classpath. Please fix it in <!--
       -->the &lt;projectDir>/build.properties file, or wherever you <!--
       -->set it.<!--
@@ -229,9 +215,8 @@
       -->set it.<!--
     --></fail>
     <echo level="info"><!--
-      -->Using boot classpaths:<!--
-      -->Java 5: ${boot.classpath.j2se1.5}; <!--
-      -->Java 6: ${boot.classpath.j2se1.6}<!--
+      -->Using boot classpaths: <!--
+      -->Java 7: ${boot.classpath.j2se1.7};<!--
       -->Java 8: ${boot.classpath.j2se1.8}<!--
     --></echo>
 
@@ -259,10 +244,10 @@
     <!-- Note: the "build.base" conf doesn't include optional FreeMarker dependencies. -->
     <ivy:cachepath conf="build.base" pathid="ivy.dep" />
     <javac destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       excludes="
         freemarker/core/_Java?*Impl.java,
         freemarker/ext/jsp/**,
@@ -281,15 +266,6 @@
 
     <ivy:cachepath conf="build.base" pathid="ivy.dep" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.6" source="1.6" encoding="utf-8"
-      includeantruntime="false"
-      classpathref="ivy.dep"
-      bootclasspath="${boot.classpath.j2se1.6}"
-      includes="freemarker/core/_Java6Impl.java"
-    />
-
-    <ivy:cachepath conf="build.base" pathid="ivy.dep" />
-    <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
       debug="on" optimize="off" target="1.8" source="1.8" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep"
@@ -305,10 +281,10 @@
 
     <ivy:cachepath conf="build.jsp2.0" pathid="ivy.dep.jsp2.0" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep.jsp2.0"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       includes="
         freemarker/ext/jsp/**,
         freemarker/ext/servlet/**,
@@ -323,10 +299,10 @@
     
     <ivy:cachepath conf="build.jsp2.1" pathid="ivy.dep.jsp2.1" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep.jsp2.1"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       includes="
         freemarker/ext/jsp/_FreeMarkerPageContext21.java,
         freemarker/ext/jsp/FreeMarkerJspFactory21.java,
@@ -335,10 +311,10 @@
 
     <ivy:cachepath conf="build.jython2.0" pathid="ivy.dep.jython2.0" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep.jython2.0"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       includes="
         freemarker/ext/ant/**,
         freemarker/template/utility/JythonRuntime.java,
@@ -350,20 +326,20 @@
     
     <ivy:cachepath conf="build.jython2.2" pathid="ivy.dep.jython2.2" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep.jython2.2"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       includes="
         freemarker/ext/jython/_Jython22VersionAdapter.java"
     />
     
     <ivy:cachepath conf="build.jython2.5" pathid="ivy.dep.jython2.5" />
     <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off" 
-      debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+      debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
       includeantruntime="false"
       classpathref="ivy.dep.jython2.5"
-      bootclasspath="${boot.classpath.j2se1.5}"
+      bootclasspath="${boot.classpath.j2se1.7}"
       includes="
         freemarker/ext/jython/_Jython25VersionAdapter.java"
     />
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index 40d0bb6..b89b472 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -35,6 +35,7 @@
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 /**
  * An operator for the + operator. Note that this is treated
@@ -266,7 +267,7 @@
         throws TemplateModelException {
             if (keys == null) {
                 HashSet keySet = new HashSet();
-                SimpleSequence keySeq = new SimpleSequence(32);
+                SimpleSequence keySeq = new SimpleSequence(32, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 addKeys(keySet, keySeq, (TemplateHashModelEx) this.left);
                 addKeys(keySet, keySeq, (TemplateHashModelEx) this.right);
                 keys = new CollectionAndSequence(keySeq);
@@ -289,7 +290,7 @@
         private void initValues()
         throws TemplateModelException {
             if (values == null) {
-                SimpleSequence seq = new SimpleSequence(size());
+                SimpleSequence seq = new SimpleSequence(size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 // Note: size() invokes initKeys() if needed.
             
                 int ln = keys.size();
diff --git a/src/main/java/freemarker/core/BuiltInsForNodes.java b/src/main/java/freemarker/core/BuiltInsForNodes.java
index efeae9b..acd9fdf 100644
--- a/src/main/java/freemarker/core/BuiltInsForNodes.java
+++ b/src/main/java/freemarker/core/BuiltInsForNodes.java
@@ -30,8 +30,9 @@
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNodeModel;
 import freemarker.template.TemplateNodeModelEx;
+import freemarker.template._TemplateAPI;
 
-/**
+        /**
  * A holder for builtins that operate exclusively on (XML-)node left-hand value.
  */
 class BuiltInsForNodes {
@@ -122,6 +123,7 @@
         private Environment env;
         
         AncestorSequence(Environment env) {
+            super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
             this.env = env;
         }
         
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index 6d76e1d..fcaeebd 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -45,6 +45,7 @@
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.Constants;
 import freemarker.template.utility.StringUtil;
 
@@ -805,7 +806,7 @@
                 }
             }
 
-            // Sort tje List[KVP]:
+            // Sort the List[KVP]:
             try {
                 Collections.sort(res, keyComparator);
             } catch (Exception exc) {
@@ -870,8 +871,10 @@
             if (!lazilyGeneratedResultEnabled) {
                 SimpleSequence seq =
                         coll instanceof TemplateCollectionModelEx
-                                ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
-                                : new SimpleSequence();
+                                ? new SimpleSequence(
+                                        ((TemplateCollectionModelEx) coll).size(),
+                                        _TemplateAPI.SAFE_OBJECT_WRAPPER)
+                                : new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
                 for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
                     seq.add(iter.next());
                 }
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
index 7583984..4afffb8 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
@@ -35,6 +35,7 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.StringUtil;
 
 class BuiltInsForStringsBasic {
@@ -804,7 +805,7 @@
     static class word_listBI extends BuiltInForString {
         @Override
         TemplateModel calculateResult(String s, Environment env) {
-            SimpleSequence result = new SimpleSequence();
+            SimpleSequence result = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
             StringTokenizer st = new StringTokenizer(s);
             while (st.hasMoreTokens()) {
                result.add(st.nextToken());
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
index 584833d..d6d3082 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
@@ -35,6 +35,7 @@
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.StringUtil;
 
 
@@ -138,11 +139,11 @@
         static class MatchWithGroups implements TemplateScalarModel {
             final String matchedInputPart;
             final SimpleSequence groupsSeq;
-            
+
             MatchWithGroups(String input, Matcher matcher) {
                 matchedInputPart = input.substring(matcher.start(), matcher.end());
                 final int grpCount = matcher.groupCount() + 1;
-                groupsSeq = new SimpleSequence(grpCount);
+                groupsSeq = new SimpleSequence(grpCount, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 for (int i = 0; i < grpCount; i++) {
                     groupsSeq.add(matcher.group(i));
                 }
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 89f3445..a0ad656 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -52,6 +52,7 @@
 import freemarker.cache.TemplateLoader;
 import freemarker.ext.beans.BeansWrapper;
 import freemarker.ext.beans.BeansWrapperBuilder;
+import freemarker.ext.beans.MemberAccessPolicy;
 import freemarker.template.AttemptExceptionReporter;
 import freemarker.template.Configuration;
 import freemarker.template.DefaultObjectWrapper;
@@ -1565,6 +1566,10 @@
      * FreeMarker 2.3.x, and {@link TemplateClassResolver#SAFER_RESOLVER}
      * starting from FreeMarker 2.4.0. If you allow users to upload templates,
      * it's important to use a custom restrictive {@link TemplateClassResolver}.
+     *
+     * <p>Note that the {@link MemberAccessPolicy} used by the {@link ObjectWrapper} also influences what constructors
+     * are available. Allowing the resolution of the class here is not enough in itself, as the
+     * {@link MemberAccessPolicy} has to allow exposing the particular constructor you try to call as well.
      * 
      * @since 2.3.17
      */
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 4a72543..e455b94 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -21,7 +21,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 
 import freemarker.template.SimpleScalar;
@@ -291,8 +290,8 @@
                 resultList.add(targetSeq.get(srcIdx));
                 srcIdx += step;
             }
-            // List items are already wrapped, so the wrapper will be null:
-            return new SimpleSequence(resultList, null);
+            // List items are already wrapped:
+            return new SimpleSequence(resultList, _TemplateAPI.SAFE_OBJECT_WRAPPER);
         } else if (targetLazySeq != null) {
             // As a targetLazySeq can only occur if a new built-in like ?filter or ?map was used somewhere in the target
             // expression, in this case we can return lazily generated sequence without breaking backward compatibility.
@@ -384,8 +383,8 @@
                 }
                 resultList.add(targetIter.next());
             }
-            // List items are already wrapped, so the wrapper will be null:
-            return new SimpleSequence(resultList, null);
+            // List items are already wrapped:
+            return new SimpleSequence(resultList, _TemplateAPI.SAFE_OBJECT_WRAPPER);
         }
     }
 
@@ -432,13 +431,13 @@
                     "Range top index " + highIndex + " (0-based) is outside the sliced sequence of length " +
                     srcIdx + ".");
         }
-        return new SimpleSequence(Arrays.asList(resultElements), null);
+        return new SimpleSequence(Arrays.asList(resultElements), _TemplateAPI.SAFE_OBJECT_WRAPPER);
     }
 
     private TemplateModel emptyResult(boolean seq) {
         return seq
                 ? (_TemplateAPI.getTemplateLanguageVersionAsInt(this) < _TemplateAPI.VERSION_INT_2_3_21
-                        ? new SimpleSequence(Collections.EMPTY_LIST, null)
+                        ? new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER)
                         : Constants.EMPTY_SEQUENCE)
                 : TemplateScalarModel.EMPTY_STRING;
     }
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index fcce497..c48954e 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -725,7 +725,7 @@
     void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces)
             throws TemplateException, IOException {
         if (nodeNamespaces == null) {
-            SimpleSequence ss = new SimpleSequence(1);
+            SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
             ss.add(currentNamespace);
             nodeNamespaces = ss;
         }
@@ -1122,14 +1122,15 @@
 
     private static SimpleSequence initPositionalCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) {
         SimpleSequence positionalCatchAllParamValue;
-        positionalCatchAllParamValue = new SimpleSequence((ObjectWrapper) null);
+        positionalCatchAllParamValue = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
         macroCtx.setLocalVar(catchAllParamName, positionalCatchAllParamValue);
         return positionalCatchAllParamValue;
     }
 
     private static SimpleHash initNamedCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) {
         SimpleHash namedCatchAllParamValue;
-        namedCatchAllParamValue = new SimpleHash(new LinkedHashMap<String, Object>(), null, 0);
+        namedCatchAllParamValue = new SimpleHash(
+                new LinkedHashMap<String, Object>(), _TemplateAPI.SAFE_OBJECT_WRAPPER, 0);
         macroCtx.setLocalVar(catchAllParamName, namedCatchAllParamValue);
         return namedCatchAllParamValue;
     }
@@ -3230,10 +3231,12 @@
         private Template template;
 
         Namespace() {
+            super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
             this.template = Environment.this.getTemplate();
         }
 
         Namespace(Template template) {
+            super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
             this.template = template;
         }
 
diff --git a/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
index d8cd611..398f382 100644
--- a/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
+++ b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
@@ -87,10 +87,6 @@
                             + PARAM_VALUE_RND_HALF_EVEN + ", " + PARAM_VALUE_RND_UNNECESSARY);
                 }
 
-                if (_JavaVersions.JAVA_6 == null) {
-                    throw new InvalidParameterValueException("For setting the rounding mode you need Java 6 or later.");
-                }
-
                 parser.roundingMode = parsedValue;
             }
         });
@@ -136,11 +132,7 @@
         m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
             public void handle(ExtendedDecimalFormatParser parser, String value)
                     throws InvalidParameterValueException {
-                if (_JavaVersions.JAVA_6 == null) {
-                    throw new InvalidParameterValueException(
-                            "For setting the exponent separator you need Java 6 or later.");
-                }
-                _JavaVersions.JAVA_6.setExponentSeparator(parser.symbols, value);
+                parser.symbols.setExponentSeparator(value);
             }
         });
         m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
@@ -241,10 +233,7 @@
         }
 
         if (roundingMode != null) {
-            if (_JavaVersions.JAVA_6 == null) {
-                throw new ParseException("Setting rounding mode needs Java 6 or later", 0);
-            }
-            _JavaVersions.JAVA_6.setRoundingMode(decimalFormat, roundingMode);
+            decimalFormat.setRoundingMode(roundingMode);
         }
 
         if (multiplier != null) {
diff --git a/src/main/java/freemarker/core/GetOptionalTemplateMethod.java b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
index 158b08c..be6c822 100644
--- a/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
+++ b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
@@ -37,6 +37,7 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.TemplateModelUtils;
 
 /**
@@ -142,7 +143,7 @@
                         "; see cause exception");
         }
         
-        SimpleHash result = new SimpleHash(env.getObjectWrapper());
+        SimpleHash result = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
         result.put(RESULT_EXISTS, template != null);
         // If the template is missing, result.include and such will be missing too, so that a default can be
         // conveniently provided like in <@optTemp.include!myDefaultMacro />.
diff --git a/src/main/java/freemarker/core/HashLiteral.java b/src/main/java/freemarker/core/HashLiteral.java
index 113b94d..7b4607c 100644
--- a/src/main/java/freemarker/core/HashLiteral.java
+++ b/src/main/java/freemarker/core/HashLiteral.java
@@ -127,8 +127,8 @@
                 // Legacy hash literal, where repeated keys were kept when doing ?values or ?keys, yet overwritten when
                 // doing hash[key].
                 map = new HashMap<String, TemplateModel>();
-                List<String> keyList = new ArrayList<String>(size);
-                List<TemplateModel> valueList = new ArrayList<TemplateModel>(size);
+                SimpleSequence keyList = new SimpleSequence(size, _TemplateAPI.SAFE_OBJECT_WRAPPER);
+                SimpleSequence valueList = new SimpleSequence(size, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 for (int i = 0; i < size; i++) {
                     Expression keyExp = keys.get(i);
                     Expression valExp = values.get(i);
@@ -141,8 +141,8 @@
                     keyList.add(key);
                     valueList.add(value);
                 }
-                keyCollection = new CollectionAndSequence(new SimpleSequence(keyList));
-                valueCollection = new CollectionAndSequence(new SimpleSequence(valueList));
+                keyCollection = new CollectionAndSequence(keyList);
+                valueCollection = new CollectionAndSequence(valueList);
             }
         }
 
@@ -153,7 +153,8 @@
         public TemplateCollectionModel keys() {
             if (keyCollection == null) {
                 // This can only happen when IcI >= 2.3.21, an the map is a LinkedHashMap.
-                keyCollection = new CollectionAndSequence(new SimpleSequence(map.keySet()));
+                keyCollection = new CollectionAndSequence(
+                        new SimpleSequence(map.keySet(), _TemplateAPI.SAFE_OBJECT_WRAPPER));
             }
             return keyCollection;
         }
@@ -161,7 +162,8 @@
         public TemplateCollectionModel values() {
             if (valueCollection == null) {
                 // This can only happen when IcI >= 2.3.21, an the map is a LinkedHashMap.
-                valueCollection = new CollectionAndSequence(new SimpleSequence(map.values()));
+                valueCollection = new CollectionAndSequence(
+                        new SimpleSequence(map.values(), _TemplateAPI.SAFE_OBJECT_WRAPPER));
             }
             return valueCollection;
         }
diff --git a/src/main/java/freemarker/core/ListLiteral.java b/src/main/java/freemarker/core/ListLiteral.java
index 02b42be..e384cd0 100644
--- a/src/main/java/freemarker/core/ListLiteral.java
+++ b/src/main/java/freemarker/core/ListLiteral.java
@@ -22,7 +22,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 
@@ -32,6 +31,7 @@
 import freemarker.template.TemplateMethodModelEx;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 final class ListLiteral extends Expression {
 
@@ -44,11 +44,10 @@
 
     @Override
     TemplateModel _eval(Environment env) throws TemplateException {
-        SimpleSequence list = new SimpleSequence(items.size());
-        for (Iterator it = items.iterator(); it.hasNext(); ) {
-            Expression exp = (Expression) it.next();
+        SimpleSequence list = new SimpleSequence(items.size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
+        for (Expression exp : items) {
             TemplateModel tm = exp.eval(env);
-            if (env == null || !env.isClassicCompatible()) {            
+            if (env == null || !env.isClassicCompatible()) {
                 exp.assertNonNull(tm, env);
             }
             list.add(tm);
@@ -141,7 +140,7 @@
     
     TemplateSequenceModel evaluateStringsToNamespaces(Environment env) throws TemplateException {
         TemplateSequenceModel val = (TemplateSequenceModel) eval(env);
-        SimpleSequence result = new SimpleSequence(val.size());
+        SimpleSequence result = new SimpleSequence(val.size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
         for (int i = 0; i < items.size(); i++) {
             Object itemExpr = items.get(i);
             if (itemExpr instanceof StringLiteral) {
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index 4e564ef..79b0c83 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -37,6 +37,7 @@
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.Constants;
 
 /**
@@ -342,7 +343,8 @@
                         lengthWithCatchAlls += ((TemplateSequenceModel) catchAllArgValue).size();
                     }
 
-                    SimpleSequence argsSpecVarValue = new SimpleSequence(lengthWithCatchAlls);
+                    SimpleSequence argsSpecVarValue = new SimpleSequence(
+                            lengthWithCatchAlls, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                     for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
                         argsSpecVarValue.add(argsSpecVarDraft[paramIndex]);
                     }
@@ -377,7 +379,7 @@
 
                     SimpleHash argsSpecVarValue = new SimpleHash(
                             new LinkedHashMap<String, Object>(lengthWithCatchAlls * 4 / 3, 1.0f),
-                            null, 0);
+                            _TemplateAPI.SAFE_OBJECT_WRAPPER, 0);
                     for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
                         argsSpecVarValue.put(paramNames[paramIndex], argsSpecVarDraft[paramIndex]);
                     }
diff --git a/src/main/java/freemarker/core/RecurseNode.java b/src/main/java/freemarker/core/RecurseNode.java
index 42a8d32..c7a4b09 100644
--- a/src/main/java/freemarker/core/RecurseNode.java
+++ b/src/main/java/freemarker/core/RecurseNode.java
@@ -28,6 +28,7 @@
 import freemarker.template.TemplateNodeModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 
 /**
@@ -57,7 +58,7 @@
         }
         if (nss != null) {
             if (nss instanceof TemplateHashModel) {
-                SimpleSequence ss = new SimpleSequence(1);
+                SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 ss.add(nss);
                 nss = ss;
             } else if (!(nss instanceof TemplateSequenceModel)) {
diff --git a/src/main/java/freemarker/core/TemplateElement.java b/src/main/java/freemarker/core/TemplateElement.java
index 5f90b60..6cb9b54 100644
--- a/src/main/java/freemarker/core/TemplateElement.java
+++ b/src/main/java/freemarker/core/TemplateElement.java
@@ -22,7 +22,6 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Enumeration;
-import java.util.Map;
 
 import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateException;
@@ -53,7 +52,7 @@
      * Contains 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} exactly if there are
      * no nested elements. Normally, the {@link #parent} of these is the {@code this}, however, in some exceptional
      * cases it's not so, to avoid copying the whole descendant tree with a different parent (as in the result of
-     * {@link Macro#Macro(Macro, Map)}.
+     * {@link Macro#Macro(Macro, Macro.WithArgs)}.
      */
     private TemplateElement[] childBuffer;
 
diff --git a/src/main/java/freemarker/core/VisitNode.java b/src/main/java/freemarker/core/VisitNode.java
index be80fe2..daaf0a7 100644
--- a/src/main/java/freemarker/core/VisitNode.java
+++ b/src/main/java/freemarker/core/VisitNode.java
@@ -27,6 +27,7 @@
 import freemarker.template.TemplateNodeModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 
 /**
@@ -56,7 +57,7 @@
         }
         if (nss != null) {
             if (nss instanceof Environment.Namespace) {
-                SimpleSequence ss = new SimpleSequence(1);
+                SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 ss.add(nss);
                 nss = ss;
             } else if (!(nss instanceof TemplateSequenceModel)) {
diff --git a/src/main/java/freemarker/core/_Java6Impl.java b/src/main/java/freemarker/core/_Java6Impl.java
deleted file mode 100644
index 090a58d..0000000
--- a/src/main/java/freemarker/core/_Java6Impl.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-
-/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
- */
-//Compile this against Java 6
-@SuppressWarnings("Since15") // For IntelliJ inspection   
-public final class _Java6Impl implements _Java6 {
-    
-    public static final _Java6 INSTANCE = new _Java6Impl();
-
-    private _Java6Impl() {
-        // Not meant to be instantiated
-    }
-
-    public void setRoundingMode(DecimalFormat df, RoundingMode roundingMode) {
-        df.setRoundingMode(roundingMode);
-    }
-
-    public void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator) {
-        dfs.setExponentSeparator(exponentSeparator);
-    }
-    
-}
diff --git a/src/main/java/freemarker/core/_JavaVersions.java b/src/main/java/freemarker/core/_JavaVersions.java
index 7e6f1eb..3f07673 100644
--- a/src/main/java/freemarker/core/_JavaVersions.java
+++ b/src/main/java/freemarker/core/_JavaVersions.java
@@ -31,49 +31,6 @@
         // Not meant to be instantiated
     }
 
-    private static final boolean IS_AT_LEAST_6;
-    static {
-        boolean result = false;
-        String vStr = SecurityUtilities.getSystemProperty("java.version", null);
-        if (vStr != null) {
-            try {
-                Version v = new Version(vStr);
-                result = v.getMajor() == 1 && v.getMinor() >= 6 || v.getMajor() > 1;
-            } catch (Exception e) {
-                // Ignore
-            }
-        }
-        if (vStr == null) {
-            try {
-                Class.forName("java.util.ServiceLoader");
-                result = true;
-            } catch (Exception e) {
-                // Ignore
-            }
-        }
-        IS_AT_LEAST_6 = result;
-    }
-    
-    static public final _Java6 JAVA_6;
-    static {
-        _Java6 java6;
-        if (IS_AT_LEAST_6) {
-            try {
-                java6 = (_Java6) Class.forName("freemarker.core._Java6Impl").getField("INSTANCE").get(null);
-            } catch (Exception e) {
-                try {
-                    Logger.getLogger("freemarker.runtime").error("Failed to access Java 6 functionality", e);
-                } catch (Exception e2) {
-                    // Suppressed
-                }
-                java6 = null;
-            }
-        } else {
-            java6 = null;
-        }
-        JAVA_6 = java6;
-    }
-    
     private static final boolean IS_AT_LEAST_8;
     static {
         boolean result = false;
diff --git a/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java b/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
index 59fed7b..c8943e5 100644
--- a/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
+++ b/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
@@ -49,6 +49,7 @@
 import freemarker.template.Template;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateNodeModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.ClassUtil;
 import freemarker.template.utility.SecurityUtilities;
 
@@ -603,7 +604,7 @@
     }
 
     private static TemplateModel wrapMap(Map table) {
-        SimpleHash model = new SimpleHash();
+        SimpleHash model = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
         for (Iterator it = table.entrySet().iterator(); it.hasNext(); ) {
             Map.Entry entry = (Map.Entry) it.next();
             model.put(String.valueOf(entry.getKey()), new SimpleScalar(String.valueOf(entry.getValue())));
diff --git a/src/main/java/freemarker/ext/beans/BeanModel.java b/src/main/java/freemarker/ext/beans/BeanModel.java
index 1f885fa..a6c5e4a 100644
--- a/src/main/java/freemarker/ext/beans/BeanModel.java
+++ b/src/main/java/freemarker/ext/beans/BeanModel.java
@@ -116,7 +116,7 @@
      * matching the key name. If a method or property is found, it's wrapped
      * into {@link freemarker.template.TemplateMethodModelEx} (for a method or
      * indexed property), or evaluated on-the-fly and the return value wrapped
-     * into appropriate model (for a non-indexed property) Models for various
+     * into appropriate model (for a non-indexed property). Models for various
      * properties and methods are cached on a per-class basis, so the costly
      * introspection is performed only once per property or method of a class.
      * (Side-note: this also implies that any class whose method has been called
@@ -232,7 +232,7 @@
                 // cachedModel remains null, as we don't cache these
             }
         } else if (desc instanceof Field) {
-            resultModel = wrapper.wrap(((Field) desc).get(object));
+            resultModel = wrapper.readField(object, (Field) desc);
             // cachedModel remains null, as we don't cache these
         } else if (desc instanceof Method) {
             Method method = (Method) desc;
diff --git a/src/main/java/freemarker/ext/beans/BeansModelCache.java b/src/main/java/freemarker/ext/beans/BeansModelCache.java
index 7cf18e9..8d6cae5 100644
--- a/src/main/java/freemarker/ext/beans/BeansModelCache.java
+++ b/src/main/java/freemarker/ext/beans/BeansModelCache.java
@@ -30,8 +30,8 @@
 import freemarker.template.TemplateModel;
 
 public class BeansModelCache extends ModelCache {
-    private final Map classToFactory = new ConcurrentHashMap();
-    private final Set mappedClassNames = new HashSet();
+    private final Map<Class<?>, ModelFactory> classToFactory = new ConcurrentHashMap<Class<?>, ModelFactory>();
+    private final Set<String> mappedClassNames = new HashSet<String>();
 
     private final BeansWrapper wrapper;
     
@@ -49,7 +49,7 @@
     protected TemplateModel create(Object object) {
         Class clazz = object.getClass();
         
-        ModelFactory factory = (ModelFactory) classToFactory.get(clazz);
+        ModelFactory factory = classToFactory.get(clazz);
         
         if (factory == null) {
             // Synchronized so that we won't unnecessarily create the same factory for multiple times in parallel.
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index 586ee12..8190715 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -24,6 +24,7 @@
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.math.BigDecimal;
@@ -96,7 +97,8 @@
     
     /**
      * At this level of exposure, all methods and properties of the
-     * wrapped objects are exposed to the template.
+     * wrapped objects are exposed to the template, and the {@link MemberAccessPolicy}
+     * will be ignored.
      */
     public static final int EXPOSE_ALL = 0;
     
@@ -351,7 +353,7 @@
             // synchronize on, even during the classIntrospector is being replaced.
             sharedIntrospectionLock = new Object();
             classIntrospector = new ClassIntrospector(
-                    _BeansAPI.getClassIntrospectorBuilder(bwConf), sharedIntrospectionLock);
+                    _BeansAPI.getClassIntrospectorBuilder(bwConf), sharedIntrospectionLock, false, false);
         } else {
             // As this is a read-only BeansWrapper, the classIntrospector is never replaced, and since it's shared by
             // other BeansWrapper instances, we use the lock belonging to the shared ClassIntrospector.
@@ -657,6 +659,29 @@
         }
     }
 
+    /**
+     * @since 2.3.30
+     */
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return classIntrospector.getMemberAccessPolicy();
+    }
+
+    /**
+     * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which
+     * is not appropriate if template editors aren't trusted.
+     *
+     * @since 2.3.30
+     */
+    public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+        checkModifiable();
+
+        if (classIntrospector.getMemberAccessPolicy() != memberAccessPolicy) {
+            ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
+            builder.setMemberAccessPolicy(memberAccessPolicy);
+            replaceClassIntrospector(builder);
+        }
+    }
+
     MethodSorter getMethodSorter() {
         return classIntrospector.getMethodSorter();
     }
@@ -682,7 +707,7 @@
      * @since 2.3.21
      */
     public boolean isClassIntrospectionCacheRestricted() {
-        return classIntrospector.getHasSharedInstanceRestrictons();
+        return classIntrospector.getHasSharedInstanceRestrictions();
     }
     
     /** 
@@ -692,7 +717,7 @@
     private void replaceClassIntrospector(ClassIntrospectorBuilder builder) {
         checkModifiable();
         
-        final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock);
+        final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock, false, false);
         final ClassIntrospector oldCI;
         
         // In principle this need not be synchronized, but as apps might publish the configuration improperly, or
@@ -858,9 +883,6 @@
      */
     protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
         _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
-        if (incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_0) {
-            throw new IllegalArgumentException("Version must be at least 2.3.0.");
-        }
         return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27
                 : incompatibleImprovements.intValue() == _TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26
                 : is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24
@@ -1479,12 +1501,23 @@
             }
         }
     }
-    
+
     /**
-     * Invokes the specified method, wrapping the return value. The specialty
-     * of this method is that if the return value is null, and the return type
-     * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
-     * @param object the object to invoke the method on
+     * Invokes the specified method, wrapping the return value. All method invocations done in templates should go
+     * through this (assuming the target object was wrapped with this {@link ObjectWrapper}).
+     *
+     * <p>This method is protected since 2.3.30; before that it was package private. The intended application of
+     * overriding this is monitoring what calls are made from templates. That can be useful to asses what will be needed
+     * in a {@link WhitelistMemberAccessPolicy} for example. Note that {@link Object#toString} calls caused by type
+     * conversion (like when you have <code>${myObject}</code>) will not go through here, as they aren't called by the
+     * template directly (and aren't called via reflection). On the other hand, <code>${myObject[key]}</code>,
+     * if {@code myObject} is not a {@link Map}, will go through here as a {@code get(String|Object)} method call, if
+     * there's a such method.
+     *
+     * <p>If the return value is null, and the return type of the invoked method is void,
+     * {@link TemplateModel#NOTHING} is returned.
+     *
+     * @param object the object to invoke the method on ({@code null} may be null for static methods)
      * @param method the method to invoke 
      * @param args the arguments to the method
      * @return the wrapped return value of the method.
@@ -1495,9 +1528,13 @@
      * (this can happen if the wrapper has an outer identity or is subclassed,
      * and the outer identity or the subclass throws an exception. Plain
      * BeansWrapper never throws TemplateModelException).
+     *
+     * @see #readField(Object, Field)
+     *
+     * @since 2.3.30
      */
-    TemplateModel invokeMethod(Object object, Method method, Object[] args)
-    throws InvocationTargetException,
+    protected TemplateModel invokeMethod(Object object, Method method, Object[] args)
+            throws InvocationTargetException,
         IllegalAccessException,
         TemplateModelException {
         // [2.4]: Java's Method.invoke truncates numbers if the target type has not enough bits to hold the value.
@@ -1509,6 +1546,24 @@
             : getOuterIdentity().wrap(retval); 
     }
 
+    /**
+     * Reads the specified field, returns its value as {@link TemplateModel}.  All field reading done in templates
+     * should go through this (assuming the target object was wrapped with this {@link ObjectWrapper}).
+     *
+     * <p>Just like in the case of {@link #invokeMethod(Object, Method, Object[])}, overriding this can be useful if you
+     * want to monitor what members are accessed by templates. However, it has the caveat that final field values are
+     * possibly cached, so you won't see all reads. Furthermore, at least static models pre-read final fields, so
+     * they will be read even if the templates don't read them.
+     *
+     * @see #invokeMethod(Object, Method, Object[])
+     *
+     * @since 2.3.30
+     */
+    protected TemplateModel readField(Object object, Field field)
+            throws IllegalAccessException, TemplateModelException {
+        return getOuterIdentity().wrap(field.get(object));
+    }
+
    /**
      * Returns a hash model that represents the so-called class static models.
      * Every class static model is itself a hash through which you can call
@@ -1525,8 +1580,7 @@
     public TemplateHashModel getStaticModels() {
         return staticModels;
     }
-    
-    
+
     /**
      * Returns a hash model that represents the so-called class enum models.
      * Every class' enum model is itself a hash through which you can access
@@ -1569,7 +1623,7 @@
             Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
             if (ctors == null) {
                 throw new TemplateModelException("Class " + clazz.getName() + 
-                        " has no public constructors.");
+                        " has no exposed constructors.");
             }
             Constructor<?> ctor = null;
             Object[] objargs;
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java b/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
index 047a9dd..6072a1e 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
@@ -75,9 +75,12 @@
  *     {@code freemarker.jar}-s (typically, in two Web Application's {@code WEB-INF/lib} directories), those won't
  *     share their caches (as they don't share the same FreeMarker classes).
  *     Also, currently there's a separate cache for each permutation of the property values that influence class
- *     introspection: {@link BeansWrapperBuilder#setExposeFields(boolean) expose_fields} and
- *     {@link BeansWrapperBuilder#setExposureLevel(int) exposure_level}. So only {@link BeansWrapper} where those
- *     properties are the same may share class introspection caches among each other.
+ *     introspection:
+ *     {@link BeansWrapperBuilder#setExposeFields(boolean) expose_fields}, and
+ *     {@link BeansWrapperBuilder#setExposureLevel(int) exposure_level}, and
+ *     {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)}  member_access_policy}.
+ *     So only {@link BeansWrapper} where those properties are the same may share class introspection caches among each
+ *     other.
  *   </li>
  *   <li><p>Model caches: These are local to a {@link BeansWrapper}. {@link BeansWrapperBuilder} returns the same
  *     {@link BeansWrapper} instance for equivalent properties (unless the existing instance was garbage collected
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
index 905bde9..323275e 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
@@ -225,10 +225,19 @@
         classIntrospectorBuilder.setExposeFields(exposeFields);
     }
 
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return classIntrospectorBuilder.getMemberAccessPolicy();
+    }
+
+    /** See {@link BeansWrapper#setMemberAccessPolicy(MemberAccessPolicy)}. */
+    public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+        classIntrospectorBuilder.setMemberAccessPolicy(memberAccessPolicy);
+    }
+
     public boolean getTreatDefaultMethodsAsBeanMembers() {
         return classIntrospectorBuilder.getTreatDefaultMethodsAsBeanMembers();
     }
-    
+
     /** See {@link BeansWrapper#setTreatDefaultMethodsAsBeanMembers(boolean)} */
     public void setTreatDefaultMethodsAsBeanMembers(boolean treatDefaultMethodsAsBeanMembers) {
         classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
diff --git a/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java
new file mode 100644
index 0000000..69d0410
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+/**
+ * Blacklist-based member access policy, that is, members that are matched by the listing will not be accessible, all
+ * others will be. Note that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place,
+ * and the {@link MemberAccessPolicy} just removes from that set of members, never adds to it.
+ *
+ * <p>This class is rarely useful in itself, and mostly meant to be used when composing a {@link MemberAccessPolicy}
+ * from other {@link MemberAccessPolicy}-es. If you are serious about security, never use this alone; consider using
+ * {@link WhitelistMemberAccessPolicy} as part of your solution.
+ *
+ * <p>See more about the rules at {@link MemberSelectorListMemberAccessPolicy}. Unlike
+ * {@link WhitelistMemberAccessPolicy}, {@link BlacklistMemberAccessPolicy} doesn't have annotations that can be used
+ * to add members to the member selector list.
+ *
+ * @since 2.3.30
+ */
+public class BlacklistMemberAccessPolicy extends MemberSelectorListMemberAccessPolicy {
+
+    /**
+     * @param memberSelectors
+     *      List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+     *      more.
+     */
+    public BlacklistMemberAccessPolicy(Collection<? extends MemberSelector> memberSelectors) {
+        super(memberSelectors, ListType.BLACKLIST, null);
+    }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java b/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
index cc4a258..fd836d4 100644
--- a/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
+++ b/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
@@ -37,8 +37,8 @@
 abstract class ClassBasedModelFactory implements TemplateHashModel {
     private final BeansWrapper wrapper;
     
-    private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap();
-    private final Set classIntrospectionsInProgress = new HashSet();
+    private final Map<String,TemplateModel> cache = new ConcurrentHashMap<String,TemplateModel>();
+    private final Set<String> classIntrospectionsInProgress = new HashSet<String>();
     
     protected ClassBasedModelFactory(BeansWrapper wrapper) {
         this.wrapper = wrapper;
@@ -59,7 +59,7 @@
 
     private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException {
         {
-            TemplateModel model = (TemplateModel) cache.get(key);
+            TemplateModel model = cache.get(key);
             if (model != null) return model;
         }
 
@@ -67,19 +67,17 @@
         int classIntrospectorClearingCounter;
         final Object sharedLock = wrapper.getSharedIntrospectionLock();
         synchronized (sharedLock) {
-            TemplateModel model = (TemplateModel) cache.get(key);
+            TemplateModel model = cache.get(key);
             if (model != null) return model;
             
-            while (model == null
-                    && classIntrospectionsInProgress.contains(key)) {
+            while (model == null && classIntrospectionsInProgress.contains(key)) {
                 // Another thread is already introspecting this class;
                 // waiting for its result.
                 try {
                     sharedLock.wait();
-                    model = (TemplateModel) cache.get(key);
+                    model = cache.get(key);
                 } catch (InterruptedException e) {
-                    throw new RuntimeException(
-                            "Class inrospection data lookup aborded: " + e);
+                    throw new RuntimeException("Class inrospection data lookup aborted: " + e);
                 }
             }
             if (model != null) return model;
@@ -93,7 +91,7 @@
             classIntrospectorClearingCounter = classIntrospector.getClearingCounter();
         }
         try {
-            final Class clazz = ClassUtil.forName(key);
+            final Class<?> clazz = ClassUtil.forName(key);
             
             // This is called so that we trigger the
             // class-reloading detector. If clazz is a reloaded class,
@@ -129,7 +127,7 @@
         }
     }
     
-    void removeFromCache(Class clazz) {
+    void removeFromCache(Class<?> clazz) {
         synchronized (wrapper.getSharedIntrospectionLock()) {
             cache.remove(clazz.getName());
         }
@@ -139,8 +137,7 @@
         return false;
     }
     
-    protected abstract TemplateModel createModel(Class clazz) 
-    throws TemplateModelException;
+    protected abstract TemplateModel createModel(Class<?> clazz) throws TemplateModelException;
     
     protected BeansWrapper getWrapper() {
         return wrapper;
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index a31d430..630bf95 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -53,6 +53,7 @@
 import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput;
 import freemarker.ext.util.ModelCache;
 import freemarker.log.Logger;
+import freemarker.template.Version;
 import freemarker.template.utility.NullArgumentException;
 import freemarker.template.utility.SecurityUtilities;
 
@@ -77,6 +78,11 @@
     private static final String JREBEL_INTEGRATION_ERROR_MSG
             = "Error initializing JRebel integration. JRebel integration disabled.";
 
+    private static final ExecutableMemberSignature GET_STRING_SIGNATURE =
+            new ExecutableMemberSignature("get", new Class[] { String.class });
+    private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE =
+            new ExecutableMemberSignature("get", new Class[] { Object.class });
+
     /**
      * When this property is true, some things are stricter. This is mostly to catch suspicious things in development
      * that can otherwise be valid situations.
@@ -138,13 +144,14 @@
 
     final int exposureLevel;
     final boolean exposeFields;
+    final MemberAccessPolicy memberAccessPolicy;
     final MethodAppearanceFineTuner methodAppearanceFineTuner;
     final MethodSorter methodSorter;
     final boolean treatDefaultMethodsAsBeanMembers;
-    final boolean bugfixed;
+    final Version incompatibleImprovements;
 
-    /** See {@link #getHasSharedInstanceRestrictons()} */
-    final private boolean hasSharedInstanceRestrictons;
+    /** See {@link #getHasSharedInstanceRestrictions()} */
+    final private boolean hasSharedInstanceRestrictions;
 
     /** See {@link #isShared()} */
     final private boolean shared;
@@ -168,34 +175,25 @@
     // Instantiation:
 
     /**
-     * Creates a new instance, that is hence surely not shared (singleton) instance.
-     * 
-     * @param pa
-     *            Stores what the values of the JavaBean properties of the returned instance will be. Not {@code null}.
-     */
-    ClassIntrospector(ClassIntrospectorBuilder pa, Object sharedLock) {
-        this(pa, sharedLock, false, false);
-    }
-
-    /**
-     * @param hasSharedInstanceRestrictons
+     * @param hasSharedInstanceRestrictions
      *            {@code true} exactly if we are creating a new instance with {@link ClassIntrospectorBuilder}. Then
      *            it's {@code true} even if it won't put the instance into the cache.
      */
     ClassIntrospector(ClassIntrospectorBuilder builder, Object sharedLock,
-            boolean hasSharedInstanceRestrictons, boolean shared) {
+            boolean hasSharedInstanceRestrictions, boolean shared) {
         NullArgumentException.check("sharedLock", sharedLock);
 
         this.exposureLevel = builder.getExposureLevel();
         this.exposeFields = builder.getExposeFields();
+        this.memberAccessPolicy = builder.getMemberAccessPolicy();
         this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
         this.methodSorter = builder.getMethodSorter();
         this.treatDefaultMethodsAsBeanMembers = builder.getTreatDefaultMethodsAsBeanMembers();
-        this.bugfixed = builder.isBugfixed();
+        this.incompatibleImprovements = builder.getIncompatibleImprovements();
 
         this.sharedLock = sharedLock;
 
-        this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons;
+        this.hasSharedInstanceRestrictions = hasSharedInstanceRestrictions;
         this.shared = shared;
 
         if (CLASS_CHANGE_NOTIFIER != null) {
@@ -204,14 +202,15 @@
     }
 
     /**
-     * Returns a {@link ClassIntrospectorBuilder}-s that could be used to create an identical {@link #ClassIntrospector}
-     * . The returned {@link ClassIntrospectorBuilder} can be modified without interfering with anything.
+     * Returns a {@link ClassIntrospectorBuilder} that could be used to create an identical
+     * {@link #ClassIntrospector}. The returned {@link ClassIntrospectorBuilder} can be modified without interfering
+     * with anything.
      */
     ClassIntrospectorBuilder createBuilder() {
         return new ClassIntrospectorBuilder(this);
     }
 
-    // ------------------------------------------------------------------------------------------------------------------
+    // -----------------------------------------------------------------------------------------------------------------
     // Introspection:
 
     /**
@@ -273,25 +272,26 @@
      */
     private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
         final Map<Object, Object> introspData = new HashMap<Object, Object>();
+        ClassMemberAccessPolicy classMemberAccessPolicy = getClassMemberAccessPolicyIfNotIgnored(clazz);
 
         if (exposeFields) {
-            addFieldsToClassIntrospectionData(introspData, clazz);
+            addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
         }
 
-        final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+        final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
 
-        addGenericGetToClassIntrospectionData(introspData, accessibleMethods);
+        addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy);
 
         if (exposureLevel != BeansWrapper.EXPOSE_NOTHING) {
             try {
-                addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods);
+                addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods, classMemberAccessPolicy);
             } catch (IntrospectionException e) {
                 LOG.warn("Couldn't properly perform introspection for class " + clazz, e);
                 introspData.clear(); // FIXME NBC: Don't drop everything here.
             }
         }
 
-        addConstructorsToClassIntrospectionData(introspData, clazz);
+        addConstructorsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
 
         if (introspData.size() > 1) {
             return introspData;
@@ -303,28 +303,31 @@
         }
     }
 
-    private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz)
-            throws SecurityException {
+    private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz,
+            ClassMemberAccessPolicy classMemberAccessPolicy) throws SecurityException {
         Field[] fields = clazz.getFields();
         for (int i = 0; i < fields.length; i++) {
             Field field = fields[i];
             if ((field.getModifiers() & Modifier.STATIC) == 0) {
-                introspData.put(field.getName(), field);
+                if (classMemberAccessPolicy == null || classMemberAccessPolicy.isFieldExposed(field)) {
+                    introspData.put(field.getName(), field);
+                }
             }
         }
     }
 
     private void addBeanInfoToClassIntrospectionData(
-            Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods)
-            throws IntrospectionException {
+            Map<Object, Object> introspData, Class<?> clazz,
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            ClassMemberAccessPolicy classMemberAccessPolicy) throws IntrospectionException {
         BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
         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), clazz,
-                    accessibleMethods);
+                    introspData, pdas.get(i),
+                    accessibleMethods, classMemberAccessPolicy);
         }
 
         if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
@@ -336,7 +339,7 @@
             IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null;
             for (int i = mdsSize - 1; i >= 0; --i) {
                 final Method method = getMatchingAccessibleMethod(mds.get(i).getMethod(), accessibleMethods);
-                if (method != null && isAllowedToExpose(method)) {
+                if (method != null && (isMethodExposed(classMemberAccessPolicy, method))) {
                     decision.setDefaults(method);
                     if (methodAppearanceFineTuner != null) {
                         if (decisionInput == null) {
@@ -353,7 +356,7 @@
                             (decision.getReplaceExistingProperty()
                                     || !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) {
                         addPropertyDescriptorToClassIntrospectionData(
-                                introspData, propDesc, clazz, accessibleMethods);
+                                introspData, propDesc, accessibleMethods, classMemberAccessPolicy);
                     }
 
                     String methodKey = decision.getExposeMethodAs();
@@ -361,7 +364,8 @@
                         Object previous = introspData.get(methodKey);
                         if (previous instanceof Method) {
                             // Overloaded method - replace Method with a OverloadedMethods
-                            OverloadedMethods overloadedMethods = new OverloadedMethods(bugfixed);
+                            OverloadedMethods overloadedMethods =
+                                    new OverloadedMethods(is2321Bugfixed());
                             overloadedMethods.addMethod((Method) previous);
                             overloadedMethods.addMethod(method);
                             introspData.put(methodKey, overloadedMethods);
@@ -661,9 +665,11 @@
     }
 
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
-            PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
+            PropertyDescriptor pd,
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods);
-        if (readMethod != null && !isAllowedToExpose(readMethod)) {
+        if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) {
             readMethod = null;
         }
         
@@ -671,7 +677,7 @@
         if (pd instanceof IndexedPropertyDescriptor) {
             indexedReadMethod = getMatchingAccessibleMethod(
                     ((IndexedPropertyDescriptor) pd).getIndexedReadMethod(), accessibleMethods);
-            if (indexedReadMethod != null && !isAllowedToExpose(indexedReadMethod)) {
+            if (indexedReadMethod != null && !isMethodExposed(classMemberAccessPolicy, indexedReadMethod)) {
                 indexedReadMethod = null;
             }
             if (indexedReadMethod != null) {
@@ -688,31 +694,41 @@
     }
 
     private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
-            Map<MethodSignature, List<Method>> accessibleMethods) {
-        Method genericGet = getFirstAccessibleMethod(
-                MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            ClassMemberAccessPolicy classMemberAccessPolicy) {
+        Method genericGet = getFirstAccessibleMethod(GET_STRING_SIGNATURE, accessibleMethods);
         if (genericGet == null) {
-            genericGet = getFirstAccessibleMethod(
-                    MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+            genericGet = getFirstAccessibleMethod(GET_OBJECT_SIGNATURE, accessibleMethods);
         }
-        if (genericGet != null) {
+        if (genericGet != null && isMethodExposed(classMemberAccessPolicy, genericGet)) {
             introspData.put(GENERIC_GET_KEY, genericGet);
         }
     }
 
     private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData,
-            Class<?> clazz) {
+            Class<?> clazz, ClassMemberAccessPolicy classMemberAccessPolicy) {
         try {
-            Constructor<?>[] ctors = clazz.getConstructors();
-            if (ctors.length == 1) {
-                Constructor<?> ctor = ctors[0];
-                introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes()));
-            } else if (ctors.length > 1) {
-                OverloadedMethods overloadedCtors = new OverloadedMethods(bugfixed);
-                for (int i = 0; i < ctors.length; i++) {
-                    overloadedCtors.addConstructor(ctors[i]);
+            Constructor<?>[] ctorsUnfiltered = clazz.getConstructors();
+            List<Constructor<?>> ctors = new ArrayList<Constructor<?>>(ctorsUnfiltered.length);
+            for (Constructor<?> ctor : ctorsUnfiltered) {
+                if (classMemberAccessPolicy == null || classMemberAccessPolicy.isConstructorExposed(ctor)) {
+                    ctors.add(ctor);
                 }
-                introspData.put(CONSTRUCTORS_KEY, overloadedCtors);
+            }
+
+            if (!ctors.isEmpty()) {
+                final Object ctorsIntrospData;
+                if (ctors.size() == 1) {
+                    Constructor<?> ctor = ctors.get(0);
+                    ctorsIntrospData = new SimpleMethod(ctor, ctor.getParameterTypes());
+                } else {
+                    OverloadedMethods overloadedCtors = new OverloadedMethods(is2321Bugfixed());
+                    for (Constructor<?> ctor : ctors) {
+                        overloadedCtors.addConstructor(ctor);
+                    }
+                    ctorsIntrospData = overloadedCtors;
+                }
+                introspData.put(CONSTRUCTORS_KEY, ctorsIntrospData);
             }
         } catch (SecurityException e) {
             LOG.warn("Can't discover constructors for class " + clazz.getName(), e);
@@ -720,23 +736,24 @@
     }
 
     /**
-     * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the
-     * class is not public, retrieves methods with same signature as its public methods from public superclasses and
-     * interfaces. Basically upcasts every method to the nearest accessible method.
+     * Retrieves mapping of {@link ExecutableMemberSignature}-s to a {@link List} of accessible methods for a class. In
+     * case the class is not public, retrieves methods with same signature as its public methods from public
+     * superclasses and interfaces. Basically upcasts every method to the nearest accessible method.
      */
-    private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
-        Map<MethodSignature, List<Method>> accessibles = new HashMap<MethodSignature, List<Method>>();
+    private static Map<ExecutableMemberSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
+        Map<ExecutableMemberSignature, List<Method>> accessibles = new HashMap<ExecutableMemberSignature, List<Method>>();
         discoverAccessibleMethods(clazz, accessibles);
         return accessibles;
     }
 
-    private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) {
+    private static void discoverAccessibleMethods(
+            Class<?> clazz, Map<ExecutableMemberSignature, List<Method>> accessibles) {
         if (Modifier.isPublic(clazz.getModifiers())) {
             try {
                 Method[] methods = clazz.getMethods();
                 for (int i = 0; i < methods.length; i++) {
                     Method method = methods[i];
-                    MethodSignature sig = new MethodSignature(method);
+                    ExecutableMemberSignature sig = new ExecutableMemberSignature(method);
                     // Contrary to intuition, a class can actually have several
                     // different methods with same signature *but* different
                     // return types. These can't be constructed using Java the
@@ -775,11 +792,11 @@
         }
     }
 
-    private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) {
+    private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) {
         if (m == null) {
             return null;
         }
-        MethodSignature sig = new MethodSignature(m);
+        ExecutableMemberSignature sig = new ExecutableMemberSignature(m);
         List<Method> ams = accessibles.get(sig);
         if (ams == null) {
             return null;
@@ -792,7 +809,8 @@
         return null;
     }
 
-    private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) {
+    private static Method getFirstAccessibleMethod(
+            ExecutableMemberSignature sig, Map<ExecutableMemberSignature, List<Method>> accessibles) {
         List<Method> ams = accessibles.get(sig);
         if (ams == null || ams.isEmpty()) {
             return null;
@@ -809,8 +827,28 @@
         }
     }
 
-    boolean isAllowedToExpose(Method method) {
-        return exposureLevel < BeansWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method);
+    /**
+     * Returns the {@link ClassMemberAccessPolicy}, or {@code null} if it should be ignored because of other settings.
+     * (Ideally, all such rules should be contained in {@link ClassMemberAccessPolicy} alone, but that interface was
+     * added late in history.)
+     *
+     * @see #isMethodExposed(ClassMemberAccessPolicy, Method)
+     */
+    ClassMemberAccessPolicy getClassMemberAccessPolicyIfNotIgnored(Class containingClass) {
+        return exposureLevel < BeansWrapper.EXPOSE_SAFE ? null : memberAccessPolicy.forClass(containingClass);
+    }
+
+    /**
+     * @param classMemberAccessPolicyIfNotIgnored
+     *      The value returned by {@link #getClassMemberAccessPolicyIfNotIgnored(Class)}
+     */
+    static boolean isMethodExposed(ClassMemberAccessPolicy classMemberAccessPolicyIfNotIgnored, Method method) {
+        return classMemberAccessPolicyIfNotIgnored == null
+                || classMemberAccessPolicyIfNotIgnored.isMethodExposed(method);
+    }
+
+    private boolean is2321Bugfixed() {
+        return BeansWrapper.is2321Bugfixed(incompatibleImprovements);
     }
 
     private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) {
@@ -823,39 +861,6 @@
         return argTypes;
     }
 
-    private static final class MethodSignature {
-        private static final MethodSignature GET_STRING_SIGNATURE =
-                new MethodSignature("get", new Class[] { String.class });
-        private static final MethodSignature GET_OBJECT_SIGNATURE =
-                new MethodSignature("get", new Class[] { Object.class });
-
-        private final String name;
-        private final Class<?>[] args;
-
-        private MethodSignature(String name, Class<?>[] args) {
-            this.name = name;
-            this.args = args;
-        }
-
-        MethodSignature(Method method) {
-            this(method.getName(), method.getParameterTypes());
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (o instanceof MethodSignature) {
-                MethodSignature ms = (MethodSignature) o;
-                return ms.name.equals(name) && Arrays.equals(args, ms.args);
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem?
-        }
-    }
-
     // -----------------------------------------------------------------------------------------------------------------
     // Cache management:
 
@@ -865,7 +870,7 @@
      * @since 2.3.20
      */
     void clearCache() {
-        if (getHasSharedInstanceRestrictons()) {
+        if (getHasSharedInstanceRestrictions()) {
             throw new IllegalStateException(
                     "It's not allowed to clear the whole cache in a read-only " + this.getClass().getName() +
                             "instance. Use removeFromClassIntrospectionCache(String prefix) instead.");
@@ -1044,7 +1049,11 @@
     boolean getExposeFields() {
         return exposeFields;
     }
-    
+
+    MemberAccessPolicy getMemberAccessPolicy() {
+        return memberAccessPolicy;
+    }
+
     boolean getTreatDefaultMethodsAsBeanMembers() {
         return treatDefaultMethodsAsBeanMembers;
     }
@@ -1061,14 +1070,14 @@
      * Returns {@code true} if this instance was created with {@link ClassIntrospectorBuilder}, even if it wasn't
      * actually put into the cache (as we reserve the right to do so in later versions).
      */
-    boolean getHasSharedInstanceRestrictons() {
-        return hasSharedInstanceRestrictons;
+    boolean getHasSharedInstanceRestrictions() {
+        return hasSharedInstanceRestrictions;
     }
 
     /**
      * Tells if this instance is (potentially) shared among {@link BeansWrapper} instances.
      * 
-     * @see #getHasSharedInstanceRestrictons()
+     * @see #getHasSharedInstanceRestrictions()
      */
     boolean isShared() {
         return shared;
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 25688e5..e2847ab 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -26,19 +26,24 @@
 import java.util.Iterator;
 import java.util.Map;
 
+import freemarker.template.Configuration;
 import freemarker.template.Version;
 import freemarker.template._TemplateAPI;
+import freemarker.template.utility.NullArgumentException;
 
 final class ClassIntrospectorBuilder implements Cloneable {
-    
-    private final boolean bugfixed;
 
-    private static final Map/*<PropertyAssignments, Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap();
-    private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue(); 
-    
+    private static final Map<ClassIntrospectorBuilder, Reference<ClassIntrospector>> INSTANCE_CACHE
+            = new HashMap<ClassIntrospectorBuilder, Reference<ClassIntrospector>>();
+    private static final ReferenceQueue<ClassIntrospector> INSTANCE_CACHE_REF_QUEUE
+            = new ReferenceQueue<ClassIntrospector>();
+
+    private final Version incompatibleImprovements;
+
     // Properties and their *defaults*:
     private int exposureLevel = BeansWrapper.EXPOSE_SAFE;
     private boolean exposeFields;
+    private MemberAccessPolicy memberAccessPolicy;
     private boolean treatDefaultMethodsAsBeanMembers;
     private MethodAppearanceFineTuner methodAppearanceFineTuner;
     private MethodSorter methodSorter;
@@ -49,23 +54,33 @@
     // - If you add a new field, review all methods in this class, also the ClassIntrospector constructor
     
     ClassIntrospectorBuilder(ClassIntrospector ci) {
-        bugfixed = ci.bugfixed;
+        incompatibleImprovements = ci.incompatibleImprovements;
         exposureLevel = ci.exposureLevel;
         exposeFields = ci.exposeFields;
+        memberAccessPolicy = ci.memberAccessPolicy;
         treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers;
         methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
-        methodSorter = ci.methodSorter; 
+        methodSorter = ci.methodSorter;
     }
     
     ClassIntrospectorBuilder(Version incompatibleImprovements) {
         // Warning: incompatibleImprovements must not affect this object at versions increments where there's no
         // 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. 
-        bugfixed = BeansWrapper.is2321Bugfixed(incompatibleImprovements);
+        // to some version changes that affects BeansWrapper, but not the other way around.
+        this.incompatibleImprovements = normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
         treatDefaultMethodsAsBeanMembers
                 = incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_26;
+        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() >= _TemplateAPI.VERSION_INT_2_3_30 ? Configuration.VERSION_2_3_30
+                : incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_21 ? Configuration.VERSION_2_3_21
+                : Configuration.VERSION_2_3_0;
+    }
+
     @Override
     protected Object clone() {
         try {
@@ -79,10 +94,11 @@
     public int hashCode() {
         final int prime = 31;
         int result = 1;
-        result = prime * result + (bugfixed ? 1231 : 1237);
+        result = prime * result + incompatibleImprovements.hashCode();
         result = prime * result + (exposeFields ? 1231 : 1237);
         result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 1237);
         result = prime * result + exposureLevel;
+        result = prime * result + memberAccessPolicy.hashCode();
         result = prime * result + System.identityHashCode(methodAppearanceFineTuner);
         result = prime * result + System.identityHashCode(methodSorter);
         return result;
@@ -95,10 +111,11 @@
         if (getClass() != obj.getClass()) return false;
         ClassIntrospectorBuilder other = (ClassIntrospectorBuilder) obj;
         
-        if (bugfixed != other.bugfixed) return false;
+        if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
         if (exposeFields != other.exposeFields) return false;
         if (treatDefaultMethodsAsBeanMembers != other.treatDefaultMethodsAsBeanMembers) return false;
         if (exposureLevel != other.exposureLevel) return false;
+        if (!memberAccessPolicy.equals(other.memberAccessPolicy)) return false;
         if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false;
         if (methodSorter != other.methodSorter) return false;
         
@@ -135,6 +152,21 @@
         this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers;
     }
 
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return memberAccessPolicy;
+    }
+
+    /**
+     * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which
+     * is not appropriate if template editors aren't trusted.
+     *
+     * @since 2.3.30
+     */
+    public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+        NullArgumentException.check(memberAccessPolicy);
+        this.memberAccessPolicy = memberAccessPolicy;
+    }
+
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return methodAppearanceFineTuner;
     }
@@ -151,11 +183,19 @@
         this.methodSorter = methodSorter;
     }
 
+    /**
+     * Returns the normalized incompatible improvements.
+     */
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
     private static void removeClearedReferencesFromInstanceCache() {
-        Reference clearedRef;
+        Reference<? extends ClassIntrospector> clearedRef;
         while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
             synchronized (INSTANCE_CACHE) {
-                findClearedRef: for (Iterator it = INSTANCE_CACHE.values().iterator(); it.hasNext(); ) {
+                findClearedRef: for (Iterator<Reference<ClassIntrospector>> it = INSTANCE_CACHE.values().iterator();
+                         it.hasNext(); ) {
                     if (it.next() == clearedRef) {
                         it.remove();
                         break findClearedRef;
@@ -173,7 +213,7 @@
     }
     
     /** For unit testing only */
-    static Map getInstanceCache() {
+    static Map<ClassIntrospectorBuilder, Reference<ClassIntrospector>> getInstanceCache() {
         return INSTANCE_CACHE;
     }
 
@@ -187,12 +227,12 @@
             // Instance can be cached.
             ClassIntrospector instance;
             synchronized (INSTANCE_CACHE) {
-                Reference instanceRef = (Reference) INSTANCE_CACHE.get(this);
-                instance = instanceRef != null ? (ClassIntrospector) instanceRef.get() : null;
+                Reference<ClassIntrospector> instanceRef = INSTANCE_CACHE.get(this);
+                instance = instanceRef != null ? instanceRef.get() : null;
                 if (instance == null) {
                     ClassIntrospectorBuilder thisClone = (ClassIntrospectorBuilder) clone();  // prevent any aliasing issues
                     instance = new ClassIntrospector(thisClone, new Object(), true, true);
-                    INSTANCE_CACHE.put(thisClone, new WeakReference(instance, INSTANCE_CACHE_REF_QUEUE));
+                    INSTANCE_CACHE.put(thisClone, new WeakReference<ClassIntrospector>(instance, INSTANCE_CACHE_REF_QUEUE));
                 }
             }
             
@@ -207,8 +247,4 @@
         }
     }
 
-    public boolean isBugfixed() {
-        return bugfixed;
-    }
-    
 }
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java
new file mode 100644
index 0000000..3a1e0e6
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java
@@ -0,0 +1,38 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Returned by {@link MemberAccessPolicy#forClass(Class)}. The idea is that {@link MemberAccessPolicy#forClass(Class)}
+ * is called once per class, and then the methods of the resulting {@link ClassMemberAccessPolicy} object will be
+ * called for each member of the class. This can speed up the process as the class-specific lookups will be done only
+ * once per class, not once per member.
+ *
+ * @since 2.3.30
+ */
+public interface ClassMemberAccessPolicy {
+    boolean isMethodExposed(Method method);
+    boolean isConstructorExposed(Constructor<?> constructor);
+    boolean isFieldExposed(Field field);
+}
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
similarity index 62%
copy from src/main/java/freemarker/core/_Java6.java
copy to src/main/java/freemarker/ext/beans/ConstructorMatcher.java
index cd03fb2..0f5548c 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
@@ -16,20 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package freemarker.core;
 
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
 
 /**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
+ * {@link MemberMatcher} for constructors.
+ *
+ * @since 2.3.30
  */
-public interface _Java6 {
+final class ConstructorMatcher extends MemberMatcher<Constructor<?>, ExecutableMemberSignature> {
+    @Override
+    protected ExecutableMemberSignature toMemberSignature(Constructor<?> member) {
+        return new ExecutableMemberSignature(member);
+    }
 
-    void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
-    void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-    
+    @Override
+    protected boolean matchInUpperBoundTypeSubtypes() {
+        return false;
+    }
 }
diff --git a/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
new file mode 100644
index 0000000..c27f662
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
@@ -0,0 +1,187 @@
+/*
+ * 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.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy.MemberSelector;
+import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
+
+/**
+ * Member access policy, used  to implement default behavior that's mostly compatible with pre-2.3.30 versions, but is
+ * somewhat safer; it still can't provide safety in practice, if you allow untrusted users to edit templates! Use
+ * {@link WhitelistMemberAccessPolicy} if you need stricter control.
+ *
+ * @since 2.3.30
+ */
+public final class DefaultMemberAccessPolicy implements MemberAccessPolicy {
+
+    private static final DefaultMemberAccessPolicy INSTANCE = new DefaultMemberAccessPolicy();
+
+    private final Set<Class<?>> whitelistRuleFinalClasses;
+    private final Set<Class<?>> whitelistRuleNonFinalClasses;
+    private final WhitelistMemberAccessPolicy whitelistMemberAccessPolicy;
+    private final BlacklistMemberAccessPolicy blacklistMemberAccessPolicy;
+
+    /**
+     * Returns the singleton that's compatible with the given incompatible improvements version.
+     */
+    public static DefaultMemberAccessPolicy getInstance(Version incompatibleImprovements) {
+        _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+        // All breakpoints here must occur in ClassIntrospectorBuilder.normalizeIncompatibleImprovementsVersion!
+        // Though currently we don't have any.
+        return INSTANCE;
+    }
+
+    private DefaultMemberAccessPolicy() {
+        try {
+            ClassLoader classLoader = DefaultMemberAccessPolicy.class.getClassLoader();
+
+            whitelistRuleFinalClasses = new HashSet<Class<?>>();
+            whitelistRuleNonFinalClasses = new HashSet<Class<?>>();
+            Set<Class<?>> typesWithBlacklistUnlistedRule = new HashSet<Class<?>>();
+            List<MemberSelector> whitelistMemberSelectors = new ArrayList<MemberSelector>();
+            for (String line : loadMemberSelectorFileLines()) {
+                line = line.trim();
+                if (!MemberSelector.isIgnoredLine(line)) {
+                    if (line.startsWith("@")) {
+                        String[] lineParts = line.split("\\s+");
+                        if (lineParts.length != 2) {
+                            throw new IllegalStateException("Malformed @ line: " + line);
+                        }
+                        String typeName = lineParts[1];
+                        Class<?> upperBoundType;
+                        try {
+                            upperBoundType = classLoader.loadClass(typeName);
+                        } catch (ClassNotFoundException e) {
+                            upperBoundType = null;
+                        }
+                        String rule = lineParts[0].substring(1);
+                        if (rule.equals("whitelistPolicyIfAssignable")) {
+                            if (upperBoundType != null) {
+                                Set<Class<?>> targetSet =
+                                        (upperBoundType.getModifiers() & Modifier.FINAL) != 0
+                                                ? whitelistRuleFinalClasses
+                                                : whitelistRuleNonFinalClasses;
+                                targetSet.add(upperBoundType);
+                            }
+                        } else if (rule.equals("blacklistUnlistedMembers")) {
+                            if (upperBoundType != null) {
+                                typesWithBlacklistUnlistedRule.add(upperBoundType);
+                            }
+                        } else {
+                            throw new IllegalStateException("Unhandled rule: " + rule);
+                        }
+                    } else {
+                        MemberSelector memberSelector =
+                                MemberSelector.parse(line, classLoader);
+                        Class<?> upperBoundType = memberSelector.getUpperBoundType();
+                        if (upperBoundType != null) {
+                            if (!whitelistRuleFinalClasses.contains(upperBoundType)
+                                    && !whitelistRuleNonFinalClasses.contains(upperBoundType)
+                                    && !typesWithBlacklistUnlistedRule.contains(upperBoundType)) {
+                                throw new IllegalStateException("Type without rule: " + upperBoundType.getName());
+                            }
+                            // We always do the same, as "blacklistUnlistedMembers" is also defined via a whitelist:
+                            whitelistMemberSelectors.add(memberSelector);
+                        }
+                    }
+                }
+            }
+
+            whitelistMemberAccessPolicy = new WhitelistMemberAccessPolicy(whitelistMemberSelectors);
+
+            // Generate blacklists based on the whitelist and the members of "blacklistUnlistedMembers" types:
+            List<MemberSelector> blacklistMemberSelectors = new ArrayList<MemberSelector>();
+            for (Class<?> blacklistUnlistedRuleType : typesWithBlacklistUnlistedRule) {
+                ClassMemberAccessPolicy classPolicy = whitelistMemberAccessPolicy.forClass(blacklistUnlistedRuleType);
+                for (Method method : blacklistUnlistedRuleType.getMethods()) {
+                    if (!classPolicy.isMethodExposed(method)) {
+                        blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, method));
+                    }
+                }
+                for (Constructor<?> constructor : blacklistUnlistedRuleType.getConstructors()) {
+                    if (!classPolicy.isConstructorExposed(constructor)) {
+                        blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, constructor));
+                    }
+                }
+                for (Field field : blacklistUnlistedRuleType.getFields()) {
+                    if (!classPolicy.isFieldExposed(field)) {
+                        blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, field));
+                    }
+                }
+            }
+            blacklistMemberAccessPolicy = new BlacklistMemberAccessPolicy(blacklistMemberSelectors);
+        } catch (Exception e) {
+            throw new IllegalStateException("Couldn't init " + this.getClass().getName() + " instance", e);
+        }
+    }
+
+    private static List<String> loadMemberSelectorFileLines() throws IOException {
+        List<String> whitelist = new ArrayList<String>();
+        BufferedReader reader =
+                new BufferedReader(
+                        new InputStreamReader(
+                                DefaultMemberAccessPolicy.class.getResourceAsStream("DefaultMemberAccessPolicy-rules"),
+                                "UTF-8"));
+        try {
+            String line;
+            while ((line = reader.readLine()) != null){
+                whitelist.add(line);
+            }
+        } finally {
+            reader.close();
+        }
+
+        return whitelist;
+    }
+
+    public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+        if (isTypeWithWhitelistRule(contextClass)) {
+            return whitelistMemberAccessPolicy.forClass(contextClass);
+        } else {
+            return blacklistMemberAccessPolicy.forClass(contextClass);
+        }
+    }
+
+    private boolean isTypeWithWhitelistRule(Class<?> contextClass) {
+        if (whitelistRuleFinalClasses.contains(contextClass)) {
+            return true;
+        }
+        for (Class<?> nonFinalClass : whitelistRuleNonFinalClasses) {
+            if (nonFinalClass.isAssignableFrom(contextClass)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
new file mode 100644
index 0000000..dfba692
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
@@ -0,0 +1,69 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Used as a key in a {@link Map} or {@link Set} of methods or constructors.
+ *
+ * @since 2.3.30
+ */
+final class ExecutableMemberSignature {
+    private final String name;
+    private final Class<?>[] args;
+
+    ExecutableMemberSignature(String name, Class<?>[] args) {
+        this.name = name;
+        this.args = args;
+    }
+
+    /**
+     * Uses the method name, and the parameter types.
+     */
+    ExecutableMemberSignature(Method method) {
+        this(method.getName(), method.getParameterTypes());
+    }
+
+    /**
+     * Doesn't use the constructor name, only the parameter types.
+     */
+    ExecutableMemberSignature(Constructor<?> constructor) {
+        this("<init>", constructor.getParameterTypes());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof ExecutableMemberSignature) {
+            ExecutableMemberSignature ms = (ExecutableMemberSignature) o;
+            return ms.name.equals(name) && Arrays.equals(args, ms.args);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return name.hashCode() + args.length * 31;
+    }
+}
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/main/java/freemarker/ext/beans/FieldMatcher.java
similarity index 64%
rename from src/main/java/freemarker/core/_Java6.java
rename to src/main/java/freemarker/ext/beans/FieldMatcher.java
index cd03fb2..179ea26 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/main/java/freemarker/ext/beans/FieldMatcher.java
@@ -16,20 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package freemarker.core;
 
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+package freemarker.ext.beans;
+
+import java.lang.reflect.Field;
 
 /**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
+ * {@link MemberMatcher} for fields.
+ *
+ * @since 2.3.30
  */
-public interface _Java6 {
+final class FieldMatcher extends MemberMatcher<Field, String> {
+    @Override
+    protected String toMemberSignature(Field member) {
+        return member.getName();
+    }
 
-    void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
-    void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-    
+    @Override
+    protected boolean matchInUpperBoundTypeSubtypes() {
+        return true;
+    }
 }
diff --git a/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java
new file mode 100644
index 0000000..13b6c41
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java
@@ -0,0 +1,112 @@
+/*
+ * 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.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import freemarker.template.utility.ClassUtil;
+
+/**
+ * Legacy blacklist based member access policy, used only to keep old behavior, as it can't provide meaningful safety.
+ * Do not use it if you allow untrusted users to edit templates! Use {@link WhitelistMemberAccessPolicy} then.
+ *
+ * @since 2.3.30
+ */
+public final class LegacyDefaultMemberAccessPolicy implements MemberAccessPolicy {
+
+    public static final LegacyDefaultMemberAccessPolicy INSTANCE = new LegacyDefaultMemberAccessPolicy();
+
+    private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties";
+    private static final Set<Method> UNSAFE_METHODS = createUnsafeMethodsSet();
+
+    private static Set<Method> createUnsafeMethodsSet() {
+        try {
+            Properties props = ClassUtil.loadProperties(BeansWrapper.class, UNSAFE_METHODS_PROPERTIES);
+            Set<Method> set = new HashSet<Method>(props.size() * 4 / 3, 1f);
+            for (Object key : props.keySet()) {
+                try {
+                    set.add(parseMethodSpec((String) key));
+                } catch (ClassNotFoundException e) {
+                    if (ClassIntrospector.DEVELOPMENT_MODE) {
+                        throw e;
+                    }
+                } catch (NoSuchMethodException e) {
+                    if (ClassIntrospector.DEVELOPMENT_MODE) {
+                        throw e;
+                    }
+                }
+            }
+            return set;
+        } catch (Exception e) {
+            throw new RuntimeException("Could not load unsafe method set", e);
+        }
+    }
+
+    private static Method parseMethodSpec(String methodSpec)
+    throws ClassNotFoundException,
+        NoSuchMethodException {
+        int brace = methodSpec.indexOf('(');
+        int dot = methodSpec.lastIndexOf('.', brace);
+        Class<?> clazz = ClassUtil.forName(methodSpec.substring(0, dot));
+        String methodName = methodSpec.substring(dot + 1, brace);
+        String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
+        StringTokenizer tok = new StringTokenizer(argSpec, ",");
+        int argcount = tok.countTokens();
+        Class<?>[] argTypes = new Class[argcount];
+        for (int i = 0; i < argcount; i++) {
+            String argClassName = tok.nextToken();
+            argTypes[i] = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
+            if (argTypes[i] == null) {
+                argTypes[i] = ClassUtil.forName(argClassName);
+            }
+        }
+        return clazz.getMethod(methodName, argTypes);
+    }
+
+    private LegacyDefaultMemberAccessPolicy() {
+    }
+
+    public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+        return CLASS_MEMBER_ACCESS_POLICY_INSTANCE;
+    }
+
+    private static final BlacklistClassMemberAccessPolicy CLASS_MEMBER_ACCESS_POLICY_INSTANCE
+            = new BlacklistClassMemberAccessPolicy();
+    private static class BlacklistClassMemberAccessPolicy implements ClassMemberAccessPolicy {
+
+        public boolean isMethodExposed(Method method) {
+            return !UNSAFE_METHODS.contains(method);
+        }
+
+        public boolean isConstructorExposed(Constructor<?> constructor) {
+            return true;
+        }
+
+        public boolean isFieldExposed(Field field) {
+            return true;
+        }
+    }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
new file mode 100644
index 0000000..400f1ce
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
@@ -0,0 +1,73 @@
+/*
+ * 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.core.Environment;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateModel;
+
+/**
+ * Implement this to restrict what class members (methods, fields, constructors) are accessible from templates.
+ * Note, however, that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place, and the
+ * {@link MemberAccessPolicy} just removes from that set of members, never adds to it. Practically speaking, it's the
+ * last filter in the chain.
+ *
+ * <p>{@link MemberAccessPolicy}-s meant to be used inside {@link ObjectWrapper}-s, and their existence is transparent
+ * for the rest of the system. The instance is usually set via
+ * {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} (or if you use {@link DefaultObjectWrapper},
+ * with {@link DefaultObjectWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)}).
+ *
+ * <p>As {@link BeansWrapper}, and its subclasses like {@link DefaultObjectWrapper}, only discover public
+ * members, it's pointless to whitelist non-public members. (Also, while public members declared in non-public classes
+ * are discovered by {@link BeansWrapper}, Java reflection will not allow accessing those normally, so generally it's
+ * not useful to whitelist those either.)
+ *
+ * <p>Note that if you add {@link TemplateModel}-s directly to the data-model, those are not wrapped by the
+ * {@link ObjectWrapper} (from {@link Environment#getObjectWrapper()}), and so the {@link MemberAccessPolicy} won't
+ * affect those.
+ *
+ * <p>The {@link MemberAccessPolicy} is only used during the class introspection phase (which discovers the members of a
+ * type, and decides if, and how will they be exposed to templates), and the result of that is cached. So, the speed of
+ * an {@link MemberAccessPolicy} implementation is usually not too important, as it won't play a role during template
+ * execution.
+ *
+ * <p>Implementations must be thread-safe, and instances generally should be singletons on JVM level. FreeMarker
+ * caches its class metadata in a global (static, JVM-scope) cache for shared use, and the {@link MemberAccessPolicy}
+ * used is part of the cache key. Thus {@link MemberAccessPolicy} instances used at different places in the JVM
+ * should be equal according to {@link Object#equals(Object)}, as far as they implement exactly the same policy. It's
+ * not recommended to override {@link Object#equals(Object)}; use singletons and the default
+ * {@link Object#equals(Object)} implementation if possible.
+ *
+ * @since 2.3.30
+ */
+public interface MemberAccessPolicy {
+    /**
+     * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class.
+     * {@link ClassMemberAccessPolicy} implementations need not be thread-safe. Because class introspection results are
+     * cached, and so this method is usually only called once for a given class, the {@link ClassMemberAccessPolicy}
+     * instances shouldn't be cached by the implementation of this method.
+     *
+     * @param contextClass
+     *      The exact class of object from which members will be get in the templates.
+     */
+    ClassMemberAccessPolicy forClass(Class<?> contextClass);
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberMatcher.java b/src/main/java/freemarker/ext/beans/MemberMatcher.java
new file mode 100644
index 0000000..34ccc9c
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberMatcher.java
@@ -0,0 +1,123 @@
+/*
+ * 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.lang.reflect.Member;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * For implementing a whitelist or blacklist of class members in {@link MemberAccessPolicy} implementations.
+ * A {@link MemberMatcher} filters by name and/or signature, but not by by visibility, as
+ * the visibility condition is orthogonal to the whitelist or blacklist content.
+ *
+ * @since 2.3.30
+ */
+abstract class MemberMatcher<M extends Member, S> {
+    private final Map<S, Types> signaturesToUpperBoundTypes = new HashMap<S, Types>();
+
+    private static class Types {
+        private final Set<Class<?>> set = new HashSet<Class<?>>();
+        private boolean containsInterfaces;
+    }
+
+    /**
+     * Returns the {@link Map} lookup key used to match the member.
+     */
+    protected abstract S toMemberSignature(M member);
+
+    protected abstract boolean matchInUpperBoundTypeSubtypes();
+
+    /**
+     * Adds a member that this {@link MemberMatcher} will match.
+     *
+     * @param upperBoundType
+     *          The type of the actual object that contains the member must {@code instanceof} this.
+     * @param member
+     *          The member that should match (when the upper bound class condition is also fulfilled). Only the name
+     *          and/or signature of the member will be used for the condition, not the actual member object.
+     */
+    void addMatching(Class<?> upperBoundType, M member) {
+        Class<?> declaringClass = member.getDeclaringClass();
+        if (!declaringClass.isAssignableFrom(upperBoundType)) {
+            throw new IllegalArgumentException("Upper bound class " + upperBoundType.getName() + " is not the same "
+                    + "type or a subtype of the declaring type of member " + member + ".");
+        }
+
+        S memberSignature = toMemberSignature(member);
+        Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature);
+        if (upperBoundTypes == null) {
+            upperBoundTypes = new Types();
+            signaturesToUpperBoundTypes.put(memberSignature, upperBoundTypes);
+        }
+        upperBoundTypes.set.add(upperBoundType);
+        if (upperBoundType.isInterface()) {
+            upperBoundTypes.containsInterfaces = true;
+        }
+    }
+
+    /**
+     * Returns if the given member, if it's referred through the given class, is matched by this {@link MemberMatcher}.
+     *
+     * @param contextClass The actual class through which we access the member
+     * @param member The member that we intend to access
+     *
+     * @return If there was match in this {@link MemberMatcher}.
+     */
+    boolean matches(Class<?> contextClass, M member) {
+        S memberSignature = toMemberSignature(member);
+        Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature);
+
+        return upperBoundTypes != null
+                && (matchInUpperBoundTypeSubtypes()
+                        ? containsTypeOrSuperType(upperBoundTypes, contextClass)
+                        : containsExactType(upperBoundTypes, contextClass));
+    }
+
+    private static boolean containsExactType(Types types, Class<?> c) {
+        if (c == null) {
+            return false;
+        }
+        return types.set.contains(c);
+    }
+
+    private static boolean containsTypeOrSuperType(Types types, Class<?> c) {
+        if (c == null) {
+            return false;
+        }
+
+        if (types.set.contains(c)) {
+            return true;
+        }
+        if (containsTypeOrSuperType(types, c.getSuperclass())) {
+            return true;
+        }
+        if (types.containsInterfaces) {
+            for (Class<?> anInterface : c.getInterfaces()) {
+                if (containsTypeOrSuperType(types, anInterface)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java
new file mode 100644
index 0000000..e0af091
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java
@@ -0,0 +1,464 @@
+/*
+ * 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.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import freemarker.log.Logger;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Superclass for member-selector-list-based member access policies, like {@link WhitelistMemberAccessPolicy}.
+ *
+ * <p>There are two ways you can add members to the member selector list:
+ * <ul>
+ *     <li>Via a list of member selectors passed to the constructor
+ *     <li>Via annotation (concrete type depends on subclass)
+ * </ul>
+ *
+ * <p>Members are identified with the following data (with the example of
+ * {@code com.example.MyClass.myMethod(int, int)}):
+ * <ul>
+ *    <li>Upper bound class ({@code com.example.MyClass} in the example)
+ *    <li>Member name ({@code myMethod} in the example), except for constructors where it's unused
+ *    <li>Parameter types ({@code int, int} in the example), except for fields where it's unused
+ * </ul>
+ *
+ * <p>If a method or field is matched in the upper bound type, it will be automatically matched in all subtypes of that.
+ * It's called "upper bound" type, because the member will only be matched in classes that are {@code instanceof}
+ * the upper bound class. That restriction stands even if the member was inherited from another type (class or
+ * interface), and it wasn't even overridden in the upper bound type; the member won't be matched in the
+ * type where it was inherited from, if that type is more generic than the upper bound type.
+ *
+ * <p>The above inheritance rule doesn't apply to constructors. That's consistent with the fact constructors aren't
+ * inherited in Java (or pretty much any other language). So for example, if you add {@code com.example.A.A()} to
+ * the member selector list, and {@code B extends A}, then {@code com.example.B.B()} is still not matched by that list.
+ * If you want it to be matched, you have to add {@code com.example.B.B()} to list explicitly.
+ *
+ * <p>Note that the return type of methods aren't used in any way. If {@code myMethod(int, int)} has multiple variants
+ * with different return types (which is possible on the bytecode level) but the same parameter types, then all
+ * variants of it is matched, or none is. Similarly, the type of fields isn't used either, only the name of the field
+ * matters.
+ *
+ * @since 2.3.30
+ */
+public abstract class MemberSelectorListMemberAccessPolicy implements MemberAccessPolicy {
+    private static final Logger LOG = Logger.getLogger("freemarker.beans");
+
+    enum ListType {
+        /** Only matched members will be exposed. */
+        WHITELIST,
+        /** Matched members will not be exposed. */
+        BLACKLIST
+    }
+
+    private final ListType listType;
+    private final MethodMatcher methodMatcher;
+    private final ConstructorMatcher constructorMatcher;
+    private final FieldMatcher fieldMatcher;
+    private final Class<? extends Annotation> matchAnnotation;
+
+    /**
+     * A condition that matches some type members. See {@link MemberSelectorListMemberAccessPolicy} documentation for more.
+     * Exactly one of these will be non-{@code null}:
+     * {@link #getMethod()}, {@link #getConstructor()}, {@link #getField()}, {@link #getException()}.
+     *
+     * @since 2.3.30
+     */
+    public final static class MemberSelector {
+        private final Class<?> upperBoundType;
+        private final Method method;
+        private final Constructor<?> constructor;
+        private final Field field;
+        private final Exception exception;
+        private final String exceptionMemberSelectorString;
+
+        /**
+         * Use if you want to match methods similar to the specified one, in types that are {@code instanceof} of
+         * the specified upper bound type. When methods are matched, only the name and the parameter types matter.
+         */
+        public MemberSelector(Class<?> upperBoundType, Method method) {
+            NullArgumentException.check("upperBoundType", upperBoundType);
+            NullArgumentException.check("method", method);
+            this.upperBoundType = upperBoundType;
+            this.method = method;
+            this.constructor = null;
+            this.field = null;
+            this.exception = null;
+            this.exceptionMemberSelectorString = null;
+        }
+
+        /**
+         * Use if you want to match constructors similar to the specified one, in types that are {@code instanceof} of
+         * the specified upper bound type. When constructors are matched, only the parameter types matter.
+         */
+        public MemberSelector(Class<?> upperBoundType, Constructor<?> constructor) {
+            NullArgumentException.check("upperBoundType", upperBoundType);
+            NullArgumentException.check("constructor", constructor);
+            this.upperBoundType = upperBoundType;
+            this.method = null;
+            this.constructor = constructor;
+            this.field = null;
+            this.exception = null;
+            this.exceptionMemberSelectorString = null;
+        }
+
+        /**
+         * Use if you want to match fields similar to the specified one, in types that are {@code instanceof} of
+         * the specified upper bound type. When fields are matched, only the name matters.
+         */
+        public MemberSelector(Class<?> upperBoundType, Field field) {
+            NullArgumentException.check("upperBoundType", upperBoundType);
+            NullArgumentException.check("field", field);
+            this.upperBoundType = upperBoundType;
+            this.method = null;
+            this.constructor = null;
+            this.field = field;
+            this.exception = null;
+            this.exceptionMemberSelectorString = null;
+        }
+
+        /**
+         * Used to store the result of a parsing that's failed for a reason that we can skip on runtime (typically,
+         * when a missing class or member was referred).
+         *
+         * @param upperBoundType {@code null} if resolving the upper bound type itself failed.
+         * @param exception Not {@code null}
+         * @param exceptionMemberSelectorString Not {@code null}; the selector whose resolution has failed, used in
+         *      the log message.
+         */
+        public MemberSelector(Class<?> upperBoundType, Exception exception, String exceptionMemberSelectorString) {
+            NullArgumentException.check("exception", exception);
+            NullArgumentException.check("exceptionMemberSelectorString", exceptionMemberSelectorString);
+            this.upperBoundType = upperBoundType;
+            this.method = null;
+            this.constructor = null;
+            this.field = null;
+            this.exception = exception;
+            this.exceptionMemberSelectorString = exceptionMemberSelectorString;
+        }
+
+        /**
+         * Maybe {@code null} if {@link #getException()} is non-{@code null}.
+         */
+        public Class<?> getUpperBoundType() {
+            return upperBoundType;
+        }
+
+        /**
+         * Maybe {@code null};
+         * set if the selector matches methods similar to the returned one, and there was no exception.
+         */
+        public Method getMethod() {
+            return method;
+        }
+
+        /**
+         * Maybe {@code null};
+         * set if the selector matches constructors similar to the returned one, and there was no exception.
+         */
+        public Constructor<?> getConstructor() {
+            return constructor;
+        }
+
+        /**
+         * Maybe {@code null};
+         * set if the selector matches fields similar to the returned one, and there was no exception.
+         */
+        public Field getField() {
+            return field;
+        }
+
+        /**
+         * Maybe {@code null}
+         */
+        public Exception getException() {
+            return exception;
+        }
+
+        /**
+         * Maybe {@code null}
+         */
+        public String getExceptionMemberSelectorString() {
+            return exceptionMemberSelectorString;
+        }
+
+        /**
+         * Parses a member selector that was specified with a string.
+         *
+         * @param classLoader
+         *      Used to resolve class names in the member selectors. Generally you want to pick a class that belongs to
+         *      you application (not to a 3rd party library, like FreeMarker), and then call
+         *      {@link Class#getClassLoader()} on that. Note that the resolution of the classes is not lazy, and so the
+         *      {@link ClassLoader} won't be stored after this method returns.
+         * @param memberSelectorString
+         *      Describes the member (method, constructor, field) which you want to whitelist. Starts with the full
+         *      qualified name of the member, like {@code com.example.MyClass.myMember}. Unless it's a field, the
+         *      name is followed by comma separated list of the parameter types inside parentheses, like in
+         *      {@code com.example.MyClass.myMember(java.lang.String, boolean)}. The parameter type names must be
+         *      also full qualified names, except primitive type names. Array types must be indicated with one or
+         *      more {@code []}-s after the type name. Varargs arguments shouldn't be marked with {@code ...}, but with
+         *      {@code []}. In the member name, like {@code com.example.MyClass.myMember}, the class refers to the so
+         *      called "upper bound class". Regarding that and inheritance rules see the class level documentation.
+         *
+         * @return The {@link MemberSelector}, which might has non-{@code null} {@link MemberSelector#exception}.
+         */
+        public static MemberSelector parse(String memberSelectorString, ClassLoader classLoader) {
+            if (memberSelectorString.contains("<") || memberSelectorString.contains(">")
+                    || memberSelectorString.contains("...") || memberSelectorString.contains(";")) {
+                throw new IllegalArgumentException(
+                        "Malformed whitelist entry (shouldn't contain \"<\", \">\", \"...\", or \";\"): "
+                                + memberSelectorString);
+            }
+            String cleanedStr = memberSelectorString.trim().replaceAll("\\s*([\\.,\\(\\)\\[\\]])\\s*", "$1");
+
+            int postMemberNameIdx;
+            boolean hasArgList;
+            {
+                int openParenIdx = cleanedStr.indexOf('(');
+                hasArgList = openParenIdx != -1;
+                postMemberNameIdx = hasArgList ? openParenIdx : cleanedStr.length();
+            }
+
+            final int postClassDotIdx = cleanedStr.lastIndexOf('.', postMemberNameIdx);
+            if (postClassDotIdx == -1) {
+                throw new IllegalArgumentException("Malformed whitelist entry (missing dot): " + memberSelectorString);
+            }
+
+            Class<?> upperBoundClass;
+            String upperBoundClassStr = cleanedStr.substring(0, postClassDotIdx);
+            if (!isWellFormedClassName(upperBoundClassStr)) {
+                throw new IllegalArgumentException("Malformed whitelist entry (malformed upper bound class name): "
+                        + memberSelectorString);
+            }
+            try {
+                upperBoundClass = classLoader.loadClass(upperBoundClassStr);
+            } catch (ClassNotFoundException e) {
+                return new MemberSelector(null, e, cleanedStr);
+            }
+
+            String memberName = cleanedStr.substring(postClassDotIdx + 1, postMemberNameIdx);
+            if (!isWellFormedJavaIdentifier(memberName)) {
+                throw new IllegalArgumentException(
+                        "Malformed whitelist entry (malformed member name): " + memberSelectorString);
+            }
+
+            if (hasArgList) {
+                if (cleanedStr.charAt(cleanedStr.length() - 1) != ')') {
+                    throw new IllegalArgumentException("Malformed whitelist entry (should end with ')'): "
+                            + memberSelectorString);
+                }
+                String argsSpec = cleanedStr.substring(postMemberNameIdx + 1, cleanedStr.length() - 1);
+                StringTokenizer tok = new StringTokenizer(argsSpec, ",");
+                int argCount = tok.countTokens();
+                Class<?>[] argTypes = new Class[argCount];
+                for (int i = 0; i < argCount; i++) {
+                    String argClassName = tok.nextToken();
+                    int arrayDimensions = 0;
+                    while (argClassName.endsWith("[]")) {
+                        arrayDimensions++;
+                        argClassName = argClassName.substring(0, argClassName.length() - 2);
+                    }
+                    Class<?> argClass;
+                    Class<?> primArgClass = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
+                    if (primArgClass != null) {
+                        argClass = primArgClass;
+                    } else {
+                        if (!isWellFormedClassName(argClassName)) {
+                            throw new IllegalArgumentException(
+                                    "Malformed whitelist entry (malformed argument class name): " + memberSelectorString);
+                        }
+                        try {
+                            argClass = classLoader.loadClass(argClassName);
+                        } catch (ClassNotFoundException e) {
+                            return new MemberSelector(upperBoundClass, e, cleanedStr);
+                        } catch (SecurityException e) {
+                            return new MemberSelector(upperBoundClass, e, cleanedStr);
+                        }
+                    }
+                    argTypes[i] = ClassUtil.getArrayClass(argClass, arrayDimensions);
+                }
+                try {
+                    return memberName.equals(upperBoundClass.getSimpleName())
+                            ? new MemberSelector(upperBoundClass, upperBoundClass.getConstructor(argTypes))
+                            : new MemberSelector(upperBoundClass, upperBoundClass.getMethod(memberName, argTypes));
+                } catch (NoSuchMethodException e) {
+                    return new MemberSelector(upperBoundClass, e, cleanedStr);
+                } catch (SecurityException e) {
+                    return new MemberSelector(upperBoundClass, e, cleanedStr);
+                }
+            } else {
+                try {
+                    return new MemberSelector(upperBoundClass, upperBoundClass.getField(memberName));
+                } catch (NoSuchFieldException e) {
+                    return new MemberSelector(upperBoundClass, e, cleanedStr);
+                } catch (SecurityException e) {
+                    return new MemberSelector(upperBoundClass, e, cleanedStr);
+                }
+            }
+        }
+
+        /**
+         * Convenience method to parse all member selectors in the collection (see {@link #parse(String, ClassLoader)}),
+         * while also filtering out blank and comment lines; see {@link #parse(String, ClassLoader)},
+         * and {@link #isIgnoredLine(String)}.
+         */
+        public static List<MemberSelector> parse(Collection<String> memberSelectors,
+                ClassLoader classLoader) {
+            List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size());
+            for (String memberSelector : memberSelectors) {
+                if (!isIgnoredLine(memberSelector)) {
+                    parsedMemberSelectors.add(parse(memberSelector, classLoader));
+                }
+            }
+            return parsedMemberSelectors;
+        }
+
+        /**
+         * A line is ignored if it's blank or a comment. A line is be blank if it doesn't contain non-whitespace
+         * character. A line is a comment if it starts with {@code #}, or {@code //} (ignoring any amount of
+         * preceding whitespace).
+         */
+        public static boolean isIgnoredLine(String line) {
+            String trimmedLine = line.trim();
+            return trimmedLine.length() == 0 || trimmedLine.startsWith("#") || trimmedLine.startsWith("//");
+        }
+    }
+
+    /**
+     * @param memberSelectors
+     *      List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+     *      more.
+     * @param listType
+     *      Decides the "color" of the list
+     * @param matchAnnotation
+     */
+    MemberSelectorListMemberAccessPolicy(
+            Collection<? extends MemberSelector> memberSelectors, ListType listType,
+            Class<? extends Annotation> matchAnnotation) {
+        this.listType = listType;
+        this.matchAnnotation = matchAnnotation;
+
+        methodMatcher = new MethodMatcher();
+        constructorMatcher = new ConstructorMatcher();
+        fieldMatcher = new FieldMatcher();
+        for (MemberSelector memberSelector : memberSelectors) {
+            Class<?> upperBoundClass = memberSelector.upperBoundType;
+            if (memberSelector.exception != null) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Member selector ignored due to error: " + memberSelector.getExceptionMemberSelectorString(),
+                            memberSelector.exception);
+                }
+            } else if (memberSelector.constructor != null) {
+                constructorMatcher.addMatching(upperBoundClass, memberSelector.constructor);
+            } else if (memberSelector.method != null) {
+                methodMatcher.addMatching(upperBoundClass, memberSelector.method);
+            } else if (memberSelector.field != null) {
+                fieldMatcher.addMatching(upperBoundClass, memberSelector.field);
+            } else {
+                throw new AssertionError();
+            }
+        }
+    }
+
+    public final ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
+        return new ClassMemberAccessPolicy() {
+            public boolean isMethodExposed(Method method) {
+                return matchResultToIsExposedResult(
+                        methodMatcher.matches(contextClass, method)
+                        || matchAnnotation != null
+                                && _MethodUtil.getInheritableAnnotation(contextClass, method, matchAnnotation)
+                                        != null);
+            }
+
+            public boolean isConstructorExposed(Constructor<?> constructor) {
+                return matchResultToIsExposedResult(
+                        constructorMatcher.matches(contextClass, constructor)
+                        || matchAnnotation != null
+                                && _MethodUtil.getInheritableAnnotation(contextClass, constructor, matchAnnotation)
+                                        != null);
+            }
+
+            public boolean isFieldExposed(Field field) {
+                return matchResultToIsExposedResult(
+                        fieldMatcher.matches(contextClass, field)
+                        || matchAnnotation != null
+                                && _MethodUtil.getInheritableAnnotation(contextClass, field, matchAnnotation)
+                                        != null);
+            }
+        };
+    }
+
+    private boolean matchResultToIsExposedResult(boolean matches) {
+        if (listType == ListType.WHITELIST) {
+            return matches;
+        }
+        if (listType == ListType.BLACKLIST) {
+            return !matches;
+        }
+        throw new AssertionError();
+    }
+
+    private static boolean isWellFormedClassName(String s) {
+        if (s.length() == 0) {
+            return false;
+        }
+        int identifierStartIdx = 0;
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (i == identifierStartIdx) {
+                if (!Character.isJavaIdentifierStart(c)) {
+                    return false;
+                }
+            } else if (c == '.' && i != s.length() - 1) {
+                identifierStartIdx = i + 1;
+            } else {
+                if (!Character.isJavaIdentifierPart(c)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private static boolean isWellFormedJavaIdentifier(String s) {
+        if (s.length() == 0) {
+            return false;
+        }
+        if (!Character.isJavaIdentifierStart(s.charAt(0))) {
+            return false;
+        }
+        for (int i = 1; i < s.length(); i++) {
+            if (!Character.isJavaIdentifierPart(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/MethodMatcher.java b/src/main/java/freemarker/ext/beans/MethodMatcher.java
new file mode 100644
index 0000000..b7a5b07
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MethodMatcher.java
@@ -0,0 +1,43 @@
+/*
+ * 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.lang.reflect.Method;
+
+/**
+ * {@link MemberMatcher} for methods.
+ *
+ * <p>The return type (and visibility) of the methods will be ignored, only the method name and its parameter types
+ * matter. (The {@link MemberAccessPolicy}, and even {@link BeansWrapper} itself will still filter by visibility, it's
+ * just not the duty of the {@link MemberMatcher}.)
+ *
+ * @since 2.3.30
+ */
+final class MethodMatcher extends MemberMatcher<Method, ExecutableMemberSignature> {
+    @Override
+    protected ExecutableMemberSignature toMemberSignature(Method member) {
+        return new ExecutableMemberSignature(member);
+    }
+
+    @Override
+    protected boolean matchInUpperBoundTypeSubtypes() {
+        return true;
+    }
+}
diff --git a/src/main/java/freemarker/ext/beans/StaticModel.java b/src/main/java/freemarker/ext/beans/StaticModel.java
index 28c84bb..fc7504d 100644
--- a/src/main/java/freemarker/ext/beans/StaticModel.java
+++ b/src/main/java/freemarker/ext/beans/StaticModel.java
@@ -42,11 +42,11 @@
  */
 final class StaticModel implements TemplateHashModelEx {
     private static final Logger LOG = Logger.getLogger("freemarker.beans");
-    private final Class clazz;
+    private final Class<?> clazz;
     private final BeansWrapper wrapper;
-    private final Map map = new HashMap();
+    private final Map<String, Object> map = new HashMap<String, Object>();
 
-    StaticModel(Class clazz, BeansWrapper wrapper) throws TemplateModelException {
+    StaticModel(Class<?> clazz, BeansWrapper wrapper) throws TemplateModelException {
         this.clazz = clazz;
         this.wrapper = wrapper;
         populate();
@@ -65,7 +65,7 @@
         // Non-final field; this must be evaluated on each call.
         if (model instanceof Field) {
             try {
-                return wrapper.getOuterIdentity().wrap(((Field) model).get(null));
+                return wrapper.readField(null, (Field) model);
             } catch (IllegalAccessException e) {
                 throw new TemplateModelException(
                     "Illegal access for field " + key + " of class " + clazz.getName());
@@ -107,31 +107,34 @@
         }
 
         Field[] fields = clazz.getFields();
-        for (int i = 0; i < fields.length; ++i) {
-            Field field = fields[i];
+        for (Field field : fields) {
             int mod = field.getModifiers();
             if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) {
-                if (Modifier.isFinal(mod))
+                if (Modifier.isFinal(mod)) {
                     try {
                         // public static final fields are evaluated once and
                         // stored in the map
-                        map.put(field.getName(), wrapper.getOuterIdentity().wrap(field.get(null)));
+                        map.put(field.getName(), wrapper.readField(null, field));
                     } catch (IllegalAccessException e) {
                         // Intentionally ignored
-                    } else
+                    }
+                } else {
                     // This is a special flagging value: Field in the map means
                     // that this is a non-final field, and it must be evaluated
                     // on each get() call.
                     map.put(field.getName(), field);
+                }
             }
         }
         if (wrapper.getExposureLevel() < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
+            ClassMemberAccessPolicy classMemberAccessPolicy =
+                    wrapper.getClassIntrospector().getClassMemberAccessPolicyIfNotIgnored(clazz);
             Method[] methods = clazz.getMethods();
             for (int i = 0; i < methods.length; ++i) {
                 Method method = methods[i];
                 int mod = method.getModifiers();
                 if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
-                        && wrapper.getClassIntrospector().isAllowedToExpose(method)) {
+                        && ClassIntrospector.isMethodExposed(classMemberAccessPolicy, method)) {
                     String name = method.getName();
                     Object obj = map.get(name);
                     if (obj instanceof Method) {
@@ -154,8 +157,8 @@
                     }
                 }
             }
-            for (Iterator entries = map.entrySet().iterator(); entries.hasNext(); ) {
-                Map.Entry entry = (Map.Entry) entries.next();
+            for (Iterator<Map.Entry<String, Object>> entries = map.entrySet().iterator(); entries.hasNext(); ) {
+                Map.Entry<String, Object> entry = entries.next();
                 Object value = entry.getValue();
                 if (value instanceof Method) {
                     Method method = (Method) value;
diff --git a/src/main/java/freemarker/ext/beans/TemplateAccessible.java b/src/main/java/freemarker/ext/beans/TemplateAccessible.java
new file mode 100644
index 0000000..5e07873
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/TemplateAccessible.java
@@ -0,0 +1,45 @@
+/*
+ * 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.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.ObjectWrapper;
+
+/**
+ * Indicates that the the annotated member can be exposed to templates; if the annotated member will be actually
+ * exposed depends on the {@link ObjectWrapper} in use, and how that was configured. When used with
+ * {@link BeansWrapper} or its subclasses, most notably with {@link DefaultObjectWrapper}, and you also set the
+ * {@link MemberAccessPolicy} to a {@link WhitelistMemberAccessPolicy}, it will acts as if the members annotated with
+ * this are in the whitelist. Note that adding something to the whitelist doesn't necessary make it visible from
+ * templates; see {@link WhitelistMemberAccessPolicy} documentation.
+ *
+ * @since 2.3.30
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+public @interface TemplateAccessible {
+}
diff --git a/src/main/java/freemarker/ext/beans/UnsafeMethods.java b/src/main/java/freemarker/ext/beans/UnsafeMethods.java
deleted file mode 100644
index 249a6c1..0000000
--- a/src/main/java/freemarker/ext/beans/UnsafeMethods.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.StringTokenizer;
-
-import freemarker.template.utility.ClassUtil;
-
-class UnsafeMethods {
-
-    private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties";
-    private static final Set UNSAFE_METHODS = createUnsafeMethodsSet();
-    
-    private UnsafeMethods() { }
-    
-    static boolean isUnsafeMethod(Method method) {
-        return UNSAFE_METHODS.contains(method);        
-    }
-    
-    private static final Set createUnsafeMethodsSet() {
-        try {
-            Properties props = ClassUtil.loadProperties(BeansWrapper.class, UNSAFE_METHODS_PROPERTIES);
-            Set set = new HashSet(props.size() * 4 / 3, 1f);
-            Map primClasses = createPrimitiveClassesMap();
-            for (Object key : props.keySet()) {
-                try {
-                    set.add(parseMethodSpec((String) key, primClasses));
-                } catch (ClassNotFoundException e) {
-                    if (ClassIntrospector.DEVELOPMENT_MODE) {
-                        throw e;
-                    }
-                } catch (NoSuchMethodException e) {
-                    if (ClassIntrospector.DEVELOPMENT_MODE) {
-                        throw e;
-                    }
-                }
-            }
-            return set;
-        } catch (Exception e) {
-            throw new RuntimeException("Could not load unsafe method set", e);
-        }
-    }
-
-    private static Method parseMethodSpec(String methodSpec, Map primClasses)
-    throws ClassNotFoundException,
-        NoSuchMethodException {
-        int brace = methodSpec.indexOf('(');
-        int dot = methodSpec.lastIndexOf('.', brace);
-        Class clazz = ClassUtil.forName(methodSpec.substring(0, dot));
-        String methodName = methodSpec.substring(dot + 1, brace);
-        String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
-        StringTokenizer tok = new StringTokenizer(argSpec, ",");
-        int argcount = tok.countTokens();
-        Class[] argTypes = new Class[argcount];
-        for (int i = 0; i < argcount; i++) {
-            String argClassName = tok.nextToken();
-            argTypes[i] = (Class) primClasses.get(argClassName);
-            if (argTypes[i] == null) {
-                argTypes[i] = ClassUtil.forName(argClassName);
-            }
-        }
-        return clazz.getMethod(methodName, argTypes);
-    }
-
-    private static Map createPrimitiveClassesMap() {
-        Map map = new HashMap();
-        map.put("boolean", Boolean.TYPE);
-        map.put("byte", Byte.TYPE);
-        map.put("char", Character.TYPE);
-        map.put("short", Short.TYPE);
-        map.put("int", Integer.TYPE);
-        map.put("long", Long.TYPE);
-        map.put("float", Float.TYPE);
-        map.put("double", Double.TYPE);
-        return map;
-    }
-
-}
diff --git a/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
new file mode 100644
index 0000000..f024d37
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
@@ -0,0 +1,54 @@
+/*
+ * 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 freemarker.template.ObjectWrapper;
+
+/**
+ * Whitelist-based member access policy, that is, only members that are matched by the listing will be exposed.
+ * Note that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place, and the
+ * {@link MemberAccessPolicy} just removes from that set of members, never adds to it.
+ *
+ * <p>The whitelist content is usually application specific, and can be significant work to put together, but it's the
+ * only way you can achieve any practical safety when you don't fully trust the users who can edit templates.
+ *
+ * <p>See more about the rules at {@link MemberSelectorListMemberAccessPolicy}.
+ * {@link TemplateAccessible} annotation may be used to add members to the whitelist.
+ *
+ * <p>Of course, this only can deal with the {@link ObjectWrapper} aspect of safety; please check the Manual to see what
+ * else is needed. Also, since this is related to security, read the documentation of {@link MemberAccessPolicy}, to
+ * know about the pitfalls and edge cases related to {@link MemberAccessPolicy}-es in general.
+ *
+ * @since 2.3.30
+ */
+public class WhitelistMemberAccessPolicy extends MemberSelectorListMemberAccessPolicy {
+
+    /**
+     * @param memberSelectors
+     *      List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+     *      more.
+     */
+    public WhitelistMemberAccessPolicy(Collection<? extends MemberSelector> memberSelectors) {
+        super(memberSelectors, ListType.WHITELIST, TemplateAccessible.class);
+    }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java b/src/main/java/freemarker/ext/beans/_MethodUtil.java
index 782b944..9f743bc 100644
--- a/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -18,7 +18,9 @@
  */
 package freemarker.ext.beans;
 
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
@@ -317,4 +319,143 @@
                         .toString();
     }
 
+    /**
+     * Similar to {@link Method#getAnnotation(Class)}, but will also search the annotation in the implemented
+     * interfaces and in the ancestor classes.
+     */
+    public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Method method, Class<T> annotationClass) {
+        T result = method.getAnnotation(annotationClass);
+        if (result != null) {
+            return result;
+        }
+        return getInheritableMethodAnnotation(
+                contextClass, method.getName(), method.getParameterTypes(), true, annotationClass);
+    }
+
+    private static <T extends Annotation> T getInheritableMethodAnnotation(
+            Class<?> contextClass, String methodName, Class<?>[] methodParamTypes,
+            boolean skipCheckingDirectMethod,
+            Class<T> annotationClass) {
+        if (!skipCheckingDirectMethod) {
+            Method similarMethod;
+            try {
+                similarMethod = contextClass.getMethod(methodName, methodParamTypes);
+            } catch (NoSuchMethodException e) {
+                similarMethod = null;
+            }
+            if (similarMethod != null) {
+                T result = similarMethod.getAnnotation(annotationClass);
+                if (result != null) {
+                    return result;
+                }
+            }
+        }
+        for (Class<?> anInterface : contextClass.getInterfaces()) {
+            if (!anInterface.getName().startsWith("java.")) {
+                Method similarInterfaceMethod;
+                try {
+                    similarInterfaceMethod = anInterface.getMethod(methodName, methodParamTypes);
+                } catch (NoSuchMethodException e) {
+                    similarInterfaceMethod = null;
+                }
+                if (similarInterfaceMethod != null) {
+                    T result = similarInterfaceMethod.getAnnotation(annotationClass);
+                    if (result != null) {
+                        return result;
+                    }
+                }
+            }
+        }
+        Class<?> superClass = contextClass.getSuperclass();
+        if (superClass == Object.class || superClass == null) {
+            return null;
+        }
+        return getInheritableMethodAnnotation(superClass, methodName, methodParamTypes, false, annotationClass);
+    }
+
+    /**
+     * Similar to {@link Constructor#getAnnotation(Class)}, but will also search the annotation in the implemented
+     * interfaces and in the ancestor classes.
+     */
+    public static <T extends Annotation> T getInheritableAnnotation(
+            Class<?> contextClass, Constructor<?> constructor, Class<T> annotationClass) {
+        T result = constructor.getAnnotation(annotationClass);
+        if (result != null) {
+            return result;
+        }
+
+        Class<?>[] paramTypes = constructor.getParameterTypes();
+        while (true) {
+            contextClass = contextClass.getSuperclass();
+            if (contextClass == Object.class || contextClass == null) {
+                return null;
+            }
+            try {
+                constructor = contextClass.getConstructor(paramTypes);
+            } catch (NoSuchMethodException e) {
+                constructor = null;
+            }
+            if (constructor != null) {
+                result = constructor.getAnnotation(annotationClass);
+                if (result != null) {
+                    return result;
+                }
+            }
+        }
+    }
+
+    /**
+     * Similar to {@link Field#getAnnotation(Class)}, but will also search the annotation in the implemented
+     * interfaces and in the ancestor classes.
+     */
+    public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Field field, Class<T> annotationClass) {
+        T result = field.getAnnotation(annotationClass);
+        if (result != null) {
+            return result;
+        }
+        return getInheritableFieldAnnotation(
+                contextClass, field.getName(), true, annotationClass);
+    }
+
+    private static <T extends Annotation> T getInheritableFieldAnnotation(
+            Class<?> contextClass, String fieldName,
+            boolean skipCheckingDirectField,
+            Class<T> annotationClass) {
+        if (!skipCheckingDirectField) {
+            Field similarField;
+            try {
+                similarField = contextClass.getField(fieldName);
+            } catch (NoSuchFieldException e) {
+                similarField = null;
+            }
+            if (similarField != null) {
+                T result = similarField.getAnnotation(annotationClass);
+                if (result != null) {
+                    return result;
+                }
+            }
+        }
+        for (Class<?> anInterface : contextClass.getInterfaces()) {
+            if (!anInterface.getName().startsWith("java.")) {
+                Field similarInterfaceField;
+                try {
+                    similarInterfaceField = anInterface.getField(fieldName);
+                } catch (NoSuchFieldException e) {
+                    similarInterfaceField = null;
+                }
+                if (similarInterfaceField != null) {
+                    T result = similarInterfaceField.getAnnotation(annotationClass);
+                    if (result != null) {
+                        return result;
+                    }
+                }
+            }
+        }
+        Class<?> superClass = contextClass.getSuperclass();
+        if (superClass == Object.class || superClass == null) {
+            return null;
+        }
+        return getInheritableFieldAnnotation(superClass, fieldName, false, annotationClass);
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/jdom/NodeListModel.java b/src/main/java/freemarker/ext/jdom/NodeListModel.java
index bc39bef..ef44832 100644
--- a/src/main/java/freemarker/ext/jdom/NodeListModel.java
+++ b/src/main/java/freemarker/ext/jdom/NodeListModel.java
@@ -60,6 +60,7 @@
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 /**
  * Provides a template for wrapping JDOM objects. It is capable of storing not only
@@ -1138,7 +1139,7 @@
     throws Exception {
         org.jdom.input.SAXBuilder builder = new org.jdom.input.SAXBuilder();
         Document document = builder.build(System.in);
-        SimpleHash model = new SimpleHash();
+        SimpleHash model = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
         model.put("document", new NodeListModel(document));
         FileReader fr = new FileReader(args[0]);
         Template template = new Template(args[0], fr);
diff --git a/src/main/java/freemarker/ext/jsp/FreemarkerTag.java b/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
index 837ce3f..da54f18 100644
--- a/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
+++ b/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
@@ -28,6 +28,7 @@
 import javax.servlet.jsp.tagext.BodyTag;
 import javax.servlet.jsp.tagext.Tag;
 
+import freemarker.template.Configuration;
 import freemarker.template.SimpleHash;
 import freemarker.template.Template;
 
@@ -35,7 +36,10 @@
  * Simple implementation of JSP tag to allow use of FreeMarker templates in
  * JSP. Inspired by similar class in Velocity template engine developed by
  * <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ *
+ * @deprecated This feature is not supported anymore, also, it uses the deprecated default {@link Configuration}.
  */
+@Deprecated
 public class FreemarkerTag implements BodyTag {
     private Tag parent;
     private BodyContent bodyContent;
diff --git a/src/main/java/freemarker/ext/jsp/JspContextModel.java b/src/main/java/freemarker/ext/jsp/JspContextModel.java
index 3c28468..bd824f0 100644
--- a/src/main/java/freemarker/ext/jsp/JspContextModel.java
+++ b/src/main/java/freemarker/ext/jsp/JspContextModel.java
@@ -26,6 +26,10 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 
+/**
+ * @Deprecated This is used by the deprecated {@link FreemarkerTag}.
+ */
+@Deprecated
 class JspContextModel
 implements
     TemplateHashModel {
diff --git a/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java b/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
index 6428dda..16d9eab 100644
--- a/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
+++ b/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
@@ -30,6 +30,7 @@
 import freemarker.template.SimpleHash;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
+import freemarker.template.utility.NullArgumentException;
 
 /**
  * An extension of SimpleHash that looks up keys in the hash, then in the
@@ -51,13 +52,14 @@
     /**
      * Creates a new instance of AllHttpScopesHashModel for handling a single 
      * HTTP servlet request.
-     * @param wrapper the object wrapper to use
+     * @param objectWrapper the object wrapper to use; not {@code null}.
      * @param context the servlet context of the web application
      * @param request the HTTP servlet request being processed
      */
-    public AllHttpScopesHashModel(ObjectWrapper wrapper, 
+    public AllHttpScopesHashModel(ObjectWrapper objectWrapper,
             ServletContext context, HttpServletRequest request) {
-        setObjectWrapper(wrapper);
+        super(objectWrapper);
+        NullArgumentException.check("wrapper", objectWrapper);
         this.context = context;
         this.request = request;
     }
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index c169e9a..3f0031d 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -468,7 +468,10 @@
 
     /** FreeMarker version 2.3.29 (an {@link #Configuration(Version) incompatible improvements break-point}) */
     public static final Version VERSION_2_3_29 = new Version(2, 3, 29);
-    
+
+    /** FreeMarker version 2.3.30 (an {@link #Configuration(Version) incompatible improvements break-point}) */
+    public static final Version VERSION_2_3_30 = new Version(2, 3, 30);
+
     /** The default of {@link #getIncompatibleImprovements()}, currently {@link #VERSION_2_3_0}. */
     public static final Version DEFAULT_INCOMPATIBLE_IMPROVEMENTS = Configuration.VERSION_2_3_0;
     /** @deprecated Use {@link #DEFAULT_INCOMPATIBLE_IMPROVEMENTS} instead. */
diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java
index 4c5a39d..eac0198 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapper.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java
@@ -32,7 +32,10 @@
 
 import freemarker.ext.beans.BeansWrapper;
 import freemarker.ext.beans.BeansWrapperConfiguration;
+import freemarker.ext.beans.DefaultMemberAccessPolicy;
 import freemarker.ext.beans.EnumerationModel;
+import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
+import freemarker.ext.beans.MemberAccessPolicy;
 import freemarker.ext.dom.NodeModel;
 import freemarker.log.Logger;
 
@@ -252,7 +255,9 @@
      * Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type,
      * or for a W3C DOM node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s
      * (allows DOM tree traversal), Jython objects will be delegated to the {@code JythonWrapper}, others will be
-     * wrapped using {@link BeansWrapper#wrap(Object)}.
+     * wrapped using {@link BeansWrapper#wrap(Object)}. Note that if {@link #getMemberAccessPolicy()} doesn't return
+     * a {@link DefaultMemberAccessPolicy} or {@link LegacyDefaultMemberAccessPolicy}, then Jython wrapper will be
+     * skipped for security reasons.
      * 
      * <p>
      * When you override this method, you should first decide if you want to wrap the object in a custom way (and if so
@@ -263,8 +268,12 @@
         if (obj instanceof Node) {
             return wrapDomNode(obj);
         }
-        if (JYTHON_WRAPPER != null  && JYTHON_OBJ_CLASS.isInstance(obj)) {
-            return JYTHON_WRAPPER.wrap(obj);
+        MemberAccessPolicy memberAccessPolicy = getMemberAccessPolicy();
+        if (memberAccessPolicy instanceof DefaultMemberAccessPolicy
+                || memberAccessPolicy instanceof LegacyDefaultMemberAccessPolicy) {
+            if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
+                return JYTHON_WRAPPER.wrap(obj);
+            }
         }
         return super.wrap(obj); 
     }
diff --git a/src/main/java/freemarker/template/SimpleList.java b/src/main/java/freemarker/template/SimpleList.java
index 414fd11..af9506c 100644
--- a/src/main/java/freemarker/template/SimpleList.java
+++ b/src/main/java/freemarker/template/SimpleList.java
@@ -25,7 +25,7 @@
  *
  * <p>This class is thread-safe.
  *
- * @deprecated Use SimpleSequence instead.
+ * @deprecated Use {@link SimpleSequence} instead.
  * @see SimpleSequence
  */
 
diff --git a/src/main/java/freemarker/template/SimpleSequence.java b/src/main/java/freemarker/template/SimpleSequence.java
index 24b44e1..cf5560f 100644
--- a/src/main/java/freemarker/template/SimpleSequence.java
+++ b/src/main/java/freemarker/template/SimpleSequence.java
@@ -91,7 +91,7 @@
      * the default object wrapper set in 
      * {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)}.
      * 
-     * @deprecated Use {@link #SimpleSequence(Collection, ObjectWrapper)}.
+     * @deprecated Use {@link #SimpleSequence(int, ObjectWrapper)}.
      */
     @Deprecated
     public SimpleSequence(int capacity) {
@@ -264,6 +264,9 @@
     }
 
     private class SynchronizedSequence extends SimpleSequence {
+        private SynchronizedSequence() {
+            super(SimpleSequence.this.getObjectWrapper());
+        }
 
         @Override
         public void add(Object obj) {
@@ -292,6 +295,11 @@
                 return SimpleSequence.this.toList();
             }
         }
+
+        @Override
+        public SimpleSequence synchronizedWrapper() {
+            return this;
+        }
     }
     
 }
\ No newline at end of file
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 30227ca..8d1683f 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -52,7 +52,35 @@
     public static final int VERSION_INT_2_3_27 = Configuration.VERSION_2_3_27.intValue();
     public static final int VERSION_INT_2_3_28 = Configuration.VERSION_2_3_28.intValue();
     public static final int VERSION_INT_2_3_29 = Configuration.VERSION_2_3_29.intValue();
+    public static final int VERSION_INT_2_3_30 = Configuration.VERSION_2_3_30.intValue();
     public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
+
+    /**
+     * Kind of a dummy {@link ObjectWrapper} used at places where the internal code earlier used the
+     * {@link ObjectWrapper#DEFAULT_WRAPPER} singleton, because it wasn't supposed to wrap/unwrap anything with it;
+     * never use this {@link ObjectWrapper}r in situations where values of arbitrary types need to be wrapped!
+     * The typical situation is that we are using {@link SimpleSequence}, or {@link SimpleHash}, which always has an
+     * {@link ObjectWrapper} field, even if we don't care in the given situation, and so we didn't set it explicitly.
+     * The concern with the old way is that the {@link ObjectWrapper} set in the {@link Configuration} is possibly
+     * more restrictive than the default, so if the template author can somehow make FreeMarker wrap something with the
+     * default {@link ObjectWrapper}, then we got a security problem. So we try not to have that around, if possible.
+     * The obvious fix, and the better engineering would be just use a such {@link TemplateSequenceModel} or
+     * {@link TemplateHashModelEx2} implementation at those places, which doesn't have an {@link ObjectWrapper} (and
+     * doesn't have the overhead of said implementations either). But, some user code might casts the values it
+     * receives (as directive argument for example) to {@link SimpleSequence} or {@link SimpleHash}, instead of to
+     * {@link TemplateSequenceModel} or {@link TemplateHashModelEx2}. Such user code is wrong, but still, if it worked
+     * so far fine (especially as sequence/hash literals are implemented by these "Simple" classes), it's better if it
+     * keeps working when they upgrade to 2.3.30. Such user code will be still out of luck if it also tries to add items
+     * which are not handled by {@link SimpleObjectWrapper}, but such abuse is even more unlikely, and this is how far
+     * we could go with this backward compatibility hack.
+     *
+     * @since 2.3.30
+     */
+    public static final SimpleObjectWrapper SAFE_OBJECT_WRAPPER;
+    static {
+        SAFE_OBJECT_WRAPPER = new SimpleObjectWrapper(Configuration.VERSION_2_3_0);
+        SAFE_OBJECT_WRAPPER.writeProtect();
+    }
     
     public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
         NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index ad19750..95fd3b8 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -21,8 +21,11 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.reflect.Array;
 import java.net.URL;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 
@@ -88,7 +91,39 @@
         // Fall back to the defining class loader of the FreeMarker classes 
         return Class.forName(className);
     }
-    
+
+    private static final Map<String, Class<?>> PRIMITIVE_CLASSES_BY_NAME;
+    static {
+        PRIMITIVE_CLASSES_BY_NAME = new HashMap<String, Class<?>>();
+        PRIMITIVE_CLASSES_BY_NAME.put("boolean", boolean.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("byte", byte.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("char", char.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("short", short.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("int", int.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("long", long.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("float", float.class);
+        PRIMITIVE_CLASSES_BY_NAME.put("double", double.class);
+    }
+
+    /**
+     * Returns the {@link Class} for a primitive type name, or {@code null} if it's not the name of a primitive type.
+     *
+     * @since 2.3.30
+     */
+    public static Class<?> resolveIfPrimitiveTypeName(String typeName) {
+        return PRIMITIVE_CLASSES_BY_NAME.get(typeName);
+    }
+
+    /**
+     * Returns the array type that corresponds to the element type and the given number of array dimensions.
+     * If the dimension is 0, it just returns the element type as is.
+     *
+     * @since 2.3.30
+     */
+    public static Class<?> getArrayClass(Class<?> elementType, int dimensions) {
+        return dimensions == 0 ? elementType : Array.newInstance(elementType, new int[dimensions]).getClass();
+    }
+
     /**
      * Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}.
      * 
diff --git a/src/main/java/freemarker/template/utility/DOMNodeModel.java b/src/main/java/freemarker/template/utility/DOMNodeModel.java
index 5bb0b29..404fe45 100644
--- a/src/main/java/freemarker/template/utility/DOMNodeModel.java
+++ b/src/main/java/freemarker/template/utility/DOMNodeModel.java
@@ -38,6 +38,7 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
 
 /**
  * A convenient wrapper class for wrapping a Node in the W3C DOM API.
@@ -72,7 +73,7 @@
             if ("attributes".equals(key)) {
                 NamedNodeMap attributes = node.getAttributes();
                 if (attributes != null) {
-                    SimpleHash hash = new SimpleHash();
+                    SimpleHash hash = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
                     for (int i = 0; i < attributes.getLength(); i++) {
                         Attr att = (Attr) attributes.item(i);
                         hash.put(att.getName(), att.getValue());
diff --git a/src/main/java/freemarker/template/utility/TemplateModelUtils.java b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
index be38312..aaadb33 100644
--- a/src/main/java/freemarker/template/utility/TemplateModelUtils.java
+++ b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
@@ -39,6 +39,7 @@
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
 
 /**
  * Static utility method related to {@link TemplateModel}-s that didn't fit elsewhere.
@@ -248,7 +249,7 @@
         private void initKeys() throws TemplateModelException {
             if (keys == null) {
                 Set<String> keySet = new HashSet<String>();
-                SimpleSequence keySeq = new SimpleSequence((ObjectWrapper) null);
+                SimpleSequence keySeq = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
                 for (TemplateHashModelEx hash : hashes) {
                     addKeys(keySet, keySeq, hash);
                 }
@@ -271,7 +272,7 @@
 
         private void initValues() throws TemplateModelException {
             if (values == null) {
-                SimpleSequence seq = new SimpleSequence(size(), null);
+                SimpleSequence seq = new SimpleSequence(size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
                 // Note: size() invokes initKeys() if needed.
             
                 int ln = keys.size();
diff --git a/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules b/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules
new file mode 100644
index 0000000..48001c2
--- /dev/null
+++ b/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules
@@ -0,0 +1,582 @@
+# 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.
+
+# Used by DefaultMemberAccessPolicy (not by LegacyDefaultMemberAccessPolicy).
+# It does NOT provide enough safety if template authors aren't as trusted as the developers; you need to use a custom
+# whitelist then (see WhitelistMemberAccessPolicy).
+
+# Each member entry must have a upper bound type that already has a rule defined. The rules are associated with the
+# upper bound type in the lines starting with @. The possible rules are:
+# - whitelistPolicyIfAssignable: Members of the type and of its subtypes can only access members that were whitelisted.
+#   Thus, if you extend a such type, and add a new method, it won't be exposed, as it wasn't whitelisted.
+# - blacklistUnlistedMembers: Members of the type that are not listed will be blacklisted. Once a member was blacklisted,
+#   it will be blacklisted in subtypes as well. If you extend a type that has tris rule, and add a new method, it will
+#   be exposed, as it wasn't blacklisted.
+
+@blacklistUnlistedMembers java.lang.Object
+# Disallowed since 2.3.0: java.lang.Object.wait(long)
+# Disallowed since 2.3.0: java.lang.Object.wait(long,int)
+# Disallowed since 2.3.0: java.lang.Object.wait()
+java.lang.Object.equals(java.lang.Object)
+java.lang.Object.toString()
+java.lang.Object.hashCode()
+java.lang.Object.getClass()
+# Disallowed since 2.3.0: java.lang.Object.notify()
+# Disallowed since 2.3.0: java.lang.Object.notifyAll()
+
+@blacklistUnlistedMembers java.lang.Thread
+java.lang.Thread.getName()
+# Disallowed since 2.3.0, since 2.3.30 even when overridden: java.lang.Thread.run()
+java.lang.Thread.isInterrupted()
+# Disallowed since 2.3.30: java.lang.Thread.currentThread()
+# Disallowed since 2.3.30: java.lang.Thread.onSpinWait()
+# Disallowed since 2.3.0: java.lang.Thread.join(long,int)
+# Disallowed since 2.3.0: java.lang.Thread.join(long)
+# Disallowed since 2.3.0: java.lang.Thread.join()
+java.lang.Thread.getThreadGroup()
+# Disallowed since 2.3.0: java.lang.Thread.setContextClassLoader(java.lang.ClassLoader)
+java.lang.Thread.holdsLock(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.Thread.getStackTrace()
+java.lang.Thread.checkAccess()
+# Disallowed since 2.3.30: java.lang.Thread.dumpStack()
+# Disallowed since 2.3.0: java.lang.Thread.setPriority(int)
+# Disallowed since 2.3.0: java.lang.Thread.setDaemon(boolean)
+# Disallowed since 2.3.0: java.lang.Thread.start()
+# Disallowed since 2.3.0: java.lang.Thread.sleep(long)
+# Disallowed since 2.3.0: java.lang.Thread.sleep(long,int)
+java.lang.Thread.isDaemon()
+java.lang.Thread.getPriority()
+# Disallowed since 2.3.0: java.lang.Thread.getContextClassLoader()
+# Disallowed since 2.3.0: java.lang.Thread.resume()
+# Disallowed since 2.3.0: java.lang.Thread.interrupt()
+java.lang.Thread.activeCount()
+# Disallowed since 2.3.30: java.lang.Thread.enumerate(java.lang.Thread[])
+java.lang.Thread.isAlive()
+# Disallowed since 2.3.30: java.lang.Thread.setDefaultUncaughtExceptionHandler(java.lang.Thread$UncaughtExceptionHandler)
+# Disallowed since 2.3.30: java.lang.Thread.getUncaughtExceptionHandler()
+# Disallowed since 2.3.30: java.lang.Thread.yield()
+# Disallowed since 2.3.0: java.lang.Thread.stop()
+java.lang.Thread.interrupted()
+# Disallowed since 2.3.0: java.lang.Thread.suspend()
+# Disallowed since 2.3.0: java.lang.Thread.setName(java.lang.String)
+java.lang.Thread.countStackFrames()
+# Disallowed since 2.3.30: java.lang.Thread.getAllStackTraces()
+java.lang.Thread.getId()
+java.lang.Thread.getState()
+# Disallowed since 2.3.30: java.lang.Thread.getDefaultUncaughtExceptionHandler()
+# Disallowed since 2.3.30: java.lang.Thread.setUncaughtExceptionHandler(java.lang.Thread$UncaughtExceptionHandler)
+
+@whitelistPolicyIfAssignable java.lang.ThreadGroup
+java.lang.ThreadGroup.getName()
+# Disallowed since 2.3.30: java.lang.ThreadGroup.list()
+java.lang.ThreadGroup.getParent()
+java.lang.ThreadGroup.checkAccess()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.setDaemon(boolean)
+java.lang.ThreadGroup.isDaemon()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.resume()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.interrupt()
+java.lang.ThreadGroup.getMaxPriority()
+java.lang.ThreadGroup.activeCount()
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.ThreadGroup[],boolean)
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.ThreadGroup[])
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.Thread[])
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.Thread[],boolean)
+# Disallowed since 2.3.30: java.lang.ThreadGroup.uncaughtException(java.lang.Thread,java.lang.Throwable)
+# Disallowed since 2.3.0: java.lang.ThreadGroup.stop()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.suspend()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.setMaxPriority(int)
+java.lang.ThreadGroup.activeGroupCount()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.destroy()
+java.lang.ThreadGroup.isDestroyed()
+java.lang.ThreadGroup.parentOf(java.lang.ThreadGroup)
+# Disallowed since 2.3.0: java.lang.ThreadGroup.allowThreadSuspension(boolean)
+
+@whitelistPolicyIfAssignable java.lang.Runtime
+# Disallowed since 2.3.30: java.lang.Runtime.getRuntime()
+# Disallowed since 2.3.0: java.lang.Runtime.exit(int)
+# Disallowed since 2.3.30: java.lang.Runtime.runFinalization()
+java.lang.Runtime.version()
+# Disallowed since 2.3.0: java.lang.Runtime.loadLibrary(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Runtime.gc()
+# Disallowed since 2.3.0: java.lang.Runtime.load(java.lang.String)
+java.lang.Runtime.freeMemory()
+java.lang.Runtime.maxMemory()
+java.lang.Runtime.availableProcessors()
+# Disallowed since 2.3.0: java.lang.Runtime.halt(int)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String,java.lang.String[],java.io.File)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[],java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[],java.lang.String[],java.io.File)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String,java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.addShutdownHook(java.lang.Thread)
+# Disallowed since 2.3.0: java.lang.Runtime.removeShutdownHook(java.lang.Thread)
+java.lang.Runtime.totalMemory()
+# Disallowed since 2.3.0: java.lang.Runtime.traceInstructions(boolean)
+# Disallowed since 2.3.0: java.lang.Runtime.traceMethodCalls(boolean)
+
+@whitelistPolicyIfAssignable java.lang.System
+# Disallowed since 2.3.0: java.lang.System.exit(int)
+# Disallowed since 2.3.0: java.lang.System.runFinalization()
+# Disallowed since 2.3.0: java.lang.System.runFinalizersOnExit(boolean)
+java.lang.System.getProperty(java.lang.String)
+java.lang.System.getProperty(java.lang.String,java.lang.String)
+java.lang.System.identityHashCode(java.lang.Object)
+java.lang.System.currentTimeMillis()
+java.lang.System.nanoTime()
+# Disallowed since 2.3.30: java.lang.System.arraycopy(java.lang.Object,int,java.lang.Object,int,int)
+# Disallowed since 2.3.30: java.lang.System.getSecurityManager()
+java.lang.System.mapLibraryName(java.lang.String)
+# Disallowed since 2.3.0: java.lang.System.loadLibrary(java.lang.String)
+# Disallowed since 2.3.30: java.lang.System.console()
+# Disallowed since 2.3.30: java.lang.System.inheritedChannel()
+# Disallowed since 2.3.0: java.lang.System.setSecurityManager(java.lang.SecurityManager)
+java.lang.System.lineSeparator()
+# Disallowed since 2.3.0: java.lang.System.setProperty(java.lang.String,java.lang.String)
+java.lang.System.getenv(java.lang.String)
+java.lang.System.getenv()
+# Disallowed since 2.3.30: java.lang.System.getLogger(java.lang.String,java.util.ResourceBundle)
+# Disallowed since 2.3.30: java.lang.System.getLogger(java.lang.String)
+# Disallowed since 2.3.30: java.lang.System.gc()
+# Disallowed since 2.3.0: java.lang.System.setIn(java.io.InputStream)
+# Disallowed since 2.3.0: java.lang.System.setOut(java.io.PrintStream)
+# Disallowed since 2.3.0: java.lang.System.setErr(java.io.PrintStream)
+java.lang.System.getProperties()
+# Disallowed since 2.3.0: java.lang.System.setProperties(java.util.Properties)
+# Disallowed since 2.3.0: java.lang.System.clearProperty(java.lang.String)
+# Disallowed since 2.3.0: java.lang.System.load(java.lang.String)
+
+@whitelistPolicyIfAssignable java.lang.ClassLoader
+java.lang.ClassLoader.getName()
+# Disallowed since 2.3.30: java.lang.ClassLoader.loadClass(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getPlatformClassLoader()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemClassLoader()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResource(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResource(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getDefinedPackage(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.resources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.isRegisteredAsParallelCapable()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getParent()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getUnnamedModule()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getDefinedPackages()
+# Disallowed since 2.3.30: java.lang.ClassLoader.setDefaultAssertionStatus(boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.setPackageAssertionStatus(java.lang.String,boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.setClassAssertionStatus(java.lang.String,boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.clearAssertionStatus()
+
+@whitelistPolicyIfAssignable java.security.ProtectionDomain
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getClassLoader()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getCodeSource()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.implies(java.security.Permission)
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getPermissions()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getPrincipals()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.staticPermissionsOnly()
+
+@whitelistPolicyIfAssignable java.lang.Class
+java.lang.Class.getName()
+# Disallowed since 2.3.30: java.lang.Class.forName(java.lang.Module,java.lang.String)
+# Disallowed since 2.3.0: java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader)
+# Disallowed since 2.3.0: java.lang.Class.forName(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Class.getModule()
+java.lang.Class.getProtectionDomain()
+java.lang.Class.isAssignableFrom(java.lang.Class)
+java.lang.Class.isInstance(java.lang.Object)
+java.lang.Class.getModifiers()
+java.lang.Class.isInterface()
+java.lang.Class.isArray()
+java.lang.Class.isPrimitive()
+java.lang.Class.getSuperclass()
+java.lang.Class.cast(java.lang.Object)
+java.lang.Class.componentType()
+java.lang.Class.componentType()
+java.lang.Class.describeConstable()
+java.lang.Class.getComponentType()
+java.lang.Class.isAnnotation()
+java.lang.Class.isEnum()
+java.lang.Class.getTypeParameters()
+# Disallowed since 2.3.0: java.lang.Class.getClassLoader()
+# Disallowed since 2.3.0: java.lang.Class.newInstance()
+java.lang.Class.getInterfaces()
+java.lang.Class.getEnclosingClass()
+java.lang.Class.getSimpleName()
+java.lang.Class.getCanonicalName()
+# Disallowed since 2.3.30: java.lang.Class.getResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Class.getResource(java.lang.String)
+java.lang.Class.getPackageName()
+java.lang.Class.desiredAssertionStatus()
+java.lang.Class.getMethod(java.lang.String,java.lang.Class[])
+java.lang.Class.isAnnotationPresent(java.lang.Class)
+java.lang.Class.descriptorString()
+java.lang.Class.arrayType()
+java.lang.Class.toGenericString()
+java.lang.Class.isSynthetic()
+java.lang.Class.getGenericSuperclass()
+java.lang.Class.getPackage()
+java.lang.Class.getGenericInterfaces()
+# Disallowed since 2.3.30: java.lang.Class.getSigners()
+java.lang.Class.getEnclosingMethod()
+java.lang.Class.getEnclosingConstructor()
+java.lang.Class.getDeclaringClass()
+java.lang.Class.getTypeName()
+java.lang.Class.isAnonymousClass()
+java.lang.Class.isLocalClass()
+java.lang.Class.isMemberClass()
+java.lang.Class.getClasses()
+java.lang.Class.getFields()
+java.lang.Class.getMethods()
+java.lang.Class.getConstructors()
+java.lang.Class.getField(java.lang.String)
+java.lang.Class.getConstructor(java.lang.Class[])
+java.lang.Class.getDeclaredClasses()
+java.lang.Class.getDeclaredFields()
+java.lang.Class.getDeclaredMethods()
+java.lang.Class.getDeclaredConstructors()
+java.lang.Class.getDeclaredField(java.lang.String)
+java.lang.Class.getDeclaredMethod(java.lang.String,java.lang.Class[])
+java.lang.Class.getDeclaredConstructor(java.lang.Class[])
+java.lang.Class.getEnumConstants()
+java.lang.Class.asSubclass(java.lang.Class)
+java.lang.Class.getAnnotation(java.lang.Class)
+java.lang.Class.getAnnotationsByType(java.lang.Class)
+java.lang.Class.getAnnotations()
+java.lang.Class.getDeclaredAnnotation(java.lang.Class)
+java.lang.Class.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.Class.getDeclaredAnnotations()
+java.lang.Class.getAnnotatedSuperclass()
+java.lang.Class.getAnnotatedInterfaces()
+java.lang.Class.getNestHost()
+java.lang.Class.isNestmateOf(java.lang.Class)
+java.lang.Class.getNestMembers()
+
+@whitelistPolicyIfAssignable java.lang.Package
+java.lang.Package.getName()
+java.lang.Package.isAnnotationPresent(java.lang.Class)
+java.lang.Package.getPackage(java.lang.String)
+java.lang.Package.getAnnotation(java.lang.Class)
+java.lang.Package.getAnnotationsByType(java.lang.Class)
+java.lang.Package.getAnnotations()
+java.lang.Package.getDeclaredAnnotation(java.lang.Class)
+java.lang.Package.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.Package.getDeclaredAnnotations()
+java.lang.Package.getPackages()
+java.lang.Package.isSealed()
+java.lang.Package.isSealed(java.net.URL)
+java.lang.Package.getSpecificationTitle()
+java.lang.Package.getSpecificationVersion()
+java.lang.Package.getSpecificationVendor()
+java.lang.Package.getImplementationTitle()
+java.lang.Package.getImplementationVersion()
+java.lang.Package.getImplementationVendor()
+java.lang.Package.isCompatibleWith(java.lang.String)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Method
+# Disallowed since 2.3.0: java.lang.reflect.Method.invoke(java.lang.Object,java.lang.Object[])
+java.lang.reflect.Method.getName()
+java.lang.reflect.Method.getModifiers()
+java.lang.reflect.Method.getTypeParameters()
+java.lang.reflect.Method.getReturnType()
+java.lang.reflect.Method.getParameterTypes()
+java.lang.reflect.Method.toGenericString()
+java.lang.reflect.Method.isSynthetic()
+java.lang.reflect.Method.getDeclaringClass()
+java.lang.reflect.Method.getAnnotation(java.lang.Class)
+java.lang.reflect.Method.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Method.setAccessible(boolean)
+java.lang.reflect.Method.isVarArgs()
+java.lang.reflect.Method.getParameterCount()
+java.lang.reflect.Method.getParameterAnnotations()
+java.lang.reflect.Method.getGenericParameterTypes()
+java.lang.reflect.Method.getGenericExceptionTypes()
+java.lang.reflect.Method.isDefault()
+java.lang.reflect.Method.getGenericReturnType()
+java.lang.reflect.Method.getExceptionTypes()
+java.lang.reflect.Method.isBridge()
+java.lang.reflect.Method.getDefaultValue()
+java.lang.reflect.Method.getAnnotatedReturnType()
+java.lang.reflect.Method.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Method.getAnnotatedParameterTypes()
+java.lang.reflect.Method.getParameters()
+java.lang.reflect.Method.getAnnotatedReceiverType()
+java.lang.reflect.Method.getAnnotatedExceptionTypes()
+java.lang.reflect.Method.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Method.getAnnotations()
+java.lang.reflect.Method.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Method.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Method.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Method.trySetAccessible()
+java.lang.reflect.Method.isAccessible()
+java.lang.reflect.Method.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Constructor
+java.lang.reflect.Constructor.getName()
+java.lang.reflect.Constructor.getModifiers()
+java.lang.reflect.Constructor.getTypeParameters()
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.newInstance(java.lang.Object[])
+java.lang.reflect.Constructor.getParameterTypes()
+java.lang.reflect.Constructor.toGenericString()
+java.lang.reflect.Constructor.isSynthetic()
+java.lang.reflect.Constructor.getDeclaringClass()
+java.lang.reflect.Constructor.getAnnotation(java.lang.Class)
+java.lang.reflect.Constructor.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.setAccessible(boolean)
+java.lang.reflect.Constructor.isVarArgs()
+java.lang.reflect.Constructor.getParameterCount()
+java.lang.reflect.Constructor.getParameterAnnotations()
+java.lang.reflect.Constructor.getGenericParameterTypes()
+java.lang.reflect.Constructor.getGenericExceptionTypes()
+java.lang.reflect.Constructor.getExceptionTypes()
+java.lang.reflect.Constructor.getAnnotatedReturnType()
+java.lang.reflect.Constructor.getAnnotatedReceiverType()
+java.lang.reflect.Constructor.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Constructor.getAnnotatedParameterTypes()
+java.lang.reflect.Constructor.getParameters()
+java.lang.reflect.Constructor.getAnnotatedExceptionTypes()
+java.lang.reflect.Constructor.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Constructor.getAnnotations()
+java.lang.reflect.Constructor.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Constructor.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.trySetAccessible()
+java.lang.reflect.Constructor.isAccessible()
+java.lang.reflect.Constructor.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Field
+java.lang.reflect.Field.getName()
+java.lang.reflect.Field.getModifiers()
+# Disallowed since 2.3.30: java.lang.reflect.Field.get(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getBoolean(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getByte(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getShort(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getChar(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getInt(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getLong(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getFloat(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getDouble(java.lang.Object)
+java.lang.reflect.Field.toGenericString()
+java.lang.reflect.Field.isSynthetic()
+java.lang.reflect.Field.getDeclaringClass()
+java.lang.reflect.Field.getAnnotation(java.lang.Class)
+java.lang.reflect.Field.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Field.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Field.set(java.lang.Object,java.lang.Object)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setAccessible(boolean)
+java.lang.reflect.Field.getGenericType()
+java.lang.reflect.Field.getType()
+# Disallowed since 2.3.0: java.lang.reflect.Field.setBoolean(java.lang.Object,boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setByte(java.lang.Object,byte)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setChar(java.lang.Object,char)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setShort(java.lang.Object,short)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setInt(java.lang.Object,int)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setLong(java.lang.Object,long)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setFloat(java.lang.Object,float)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setDouble(java.lang.Object,double)
+java.lang.reflect.Field.isEnumConstant()
+java.lang.reflect.Field.getAnnotatedType()
+java.lang.reflect.Field.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Field.getAnnotations()
+java.lang.reflect.Field.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Field.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Field.trySetAccessible()
+java.lang.reflect.Field.isAccessible()
+java.lang.reflect.Field.canAccess(java.lang.Object)
+
+@blacklistUnlistedMembers java.lang.reflect.AccessibleObject
+java.lang.reflect.AccessibleObject.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotation(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotations()
+java.lang.reflect.AccessibleObject.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.AccessibleObject.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.AccessibleObject.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.AccessibleObject.setAccessible(boolean)
+# Disallowed since 2.3.0: java.lang.reflect.AccessibleObject.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.30: java.lang.reflect.AccessibleObject.trySetAccessible()
+java.lang.reflect.AccessibleObject.isAccessible()
+java.lang.reflect.AccessibleObject.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Member
+java.lang.reflect.Member.getName()
+java.lang.reflect.Member.getModifiers()
+java.lang.reflect.Member.isSynthetic()
+java.lang.reflect.Member.getDeclaringClass()
+
+@whitelistPolicyIfAssignable java.lang.reflect.GenericDeclaration
+java.lang.reflect.GenericDeclaration.getTypeParameters()
+java.lang.reflect.GenericDeclaration.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotation(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotations()
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Executable
+java.lang.reflect.Executable.getName()
+java.lang.reflect.Executable.getModifiers()
+java.lang.reflect.Executable.getTypeParameters()
+java.lang.reflect.Executable.getParameterTypes()
+java.lang.reflect.Executable.toGenericString()
+java.lang.reflect.Executable.isSynthetic()
+java.lang.reflect.Executable.getDeclaringClass()
+java.lang.reflect.Executable.getAnnotation(java.lang.Class)
+java.lang.reflect.Executable.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Executable.getDeclaredAnnotations()
+java.lang.reflect.Executable.isVarArgs()
+java.lang.reflect.Executable.getAnnotatedParameterTypes()
+java.lang.reflect.Executable.getParameterCount()
+java.lang.reflect.Executable.getParameterAnnotations()
+java.lang.reflect.Executable.getGenericParameterTypes()
+java.lang.reflect.Executable.getGenericExceptionTypes()
+java.lang.reflect.Executable.getExceptionTypes()
+java.lang.reflect.Executable.getAnnotatedReturnType()
+java.lang.reflect.Executable.getParameters()
+java.lang.reflect.Executable.getAnnotatedReceiverType()
+java.lang.reflect.Executable.getAnnotatedExceptionTypes()
+java.lang.reflect.Executable.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Executable.getAnnotations()
+java.lang.reflect.Executable.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Executable.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.setAccessible(boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.trySetAccessible()
+java.lang.reflect.Executable.isAccessible()
+java.lang.reflect.Executable.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.TypeVariable
+java.lang.reflect.TypeVariable.getName()
+java.lang.reflect.TypeVariable.getBounds()
+java.lang.reflect.TypeVariable.getGenericDeclaration()
+java.lang.reflect.TypeVariable.getAnnotatedBounds()
+java.lang.reflect.TypeVariable.getTypeName()
+java.lang.reflect.TypeVariable.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotation(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotations()
+java.lang.reflect.TypeVariable.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.TypeVariable.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.TypeVariable.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.AnnotatedType
+java.lang.reflect.AnnotatedType.getType()
+java.lang.reflect.AnnotatedType.getAnnotatedOwnerType()
+java.lang.reflect.AnnotatedType.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotation(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotations()
+java.lang.reflect.AnnotatedType.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.AnnotatedType.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.AnnotatedType.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Type
+java.lang.reflect.Type.getTypeName()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Parameter
+java.lang.reflect.Parameter.getName()
+java.lang.reflect.Parameter.getModifiers()
+java.lang.reflect.Parameter.isSynthetic()
+java.lang.reflect.Parameter.getAnnotation(java.lang.Class)
+java.lang.reflect.Parameter.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Parameter.getAnnotations()
+java.lang.reflect.Parameter.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Parameter.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.Parameter.getDeclaredAnnotations()
+java.lang.reflect.Parameter.getType()
+java.lang.reflect.Parameter.getAnnotatedType()
+java.lang.reflect.Parameter.getParameterizedType()
+java.lang.reflect.Parameter.isVarArgs()
+java.lang.reflect.Parameter.isNamePresent()
+java.lang.reflect.Parameter.getDeclaringExecutable()
+java.lang.reflect.Parameter.isImplicit()
+java.lang.reflect.Parameter.isAnnotationPresent(java.lang.Class)
+
+@whitelistPolicyIfAssignable java.lang.annotation.Annotation
+java.lang.annotation.Annotation.annotationType()
+
+@whitelistPolicyIfAssignable java.lang.constant.ClassDesc
+java.lang.constant.ClassDesc.isArray()
+java.lang.constant.ClassDesc.isPrimitive()
+java.lang.constant.ClassDesc.componentType()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.of(java.lang.String)
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.of(java.lang.String,java.lang.String)
+java.lang.constant.ClassDesc.packageName()
+java.lang.constant.ClassDesc.descriptorString()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.ofDescriptor(java.lang.String)
+java.lang.constant.ClassDesc.arrayType()
+java.lang.constant.ClassDesc.arrayType()
+java.lang.constant.ClassDesc.arrayType(int)
+java.lang.constant.ClassDesc.displayName()
+java.lang.constant.ClassDesc.isClassOrInterface()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.nested(java.lang.String,java.lang.String[])
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.nested(java.lang.String)
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup)
+
+@whitelistPolicyIfAssignable java.net.URL
+# Disallowed since 2.3.30: java.net.URL.openStream()
+java.net.URL.getHost()
+java.net.URL.getPort()
+java.net.URL.getDefaultPort()
+java.net.URL.sameFile(java.net.URL)
+java.net.URL.toExternalForm()
+# Disallowed since 2.3.30: java.net.URL.openConnection()
+# Disallowed since 2.3.30: java.net.URL.openConnection(java.net.Proxy)
+# Disallowed since 2.3.30: java.net.URL.getContent()
+# Disallowed since 2.3.30: java.net.URL.getContent(java.lang.Class[])
+java.net.URL.getProtocol()
+java.net.URL.getAuthority()
+java.net.URL.getFile()
+java.net.URL.getRef()
+java.net.URL.getQuery()
+java.net.URL.getPath()
+java.net.URL.getUserInfo()
+java.net.URL.toURI()
+# Disallowed since 2.3.30: java.net.URL.setURLStreamHandlerFactory(java.net.URLStreamHandlerFactory)
+
+@whitelistPolicyIfAssignable java.lang.constant.ClassDesc
+
+@whitelistPolicyIfAssignable java.net.URI
+java.net.URI.getRawAuthority()
+java.net.URI.compareTo(java.lang.Object)
+java.net.URI.compareTo(java.net.URI)
+java.net.URI.isAbsolute()
+java.net.URI.resolve(java.net.URI)
+java.net.URI.resolve(java.lang.String)
+java.net.URI.normalize()
+java.net.URI.getScheme()
+java.net.URI.isOpaque()
+java.net.URI.getRawFragment()
+java.net.URI.getRawQuery()
+java.net.URI.getRawPath()
+java.net.URI.getHost()
+java.net.URI.getPort()
+java.net.URI.create(java.lang.String)
+java.net.URI.getAuthority()
+java.net.URI.getQuery()
+java.net.URI.getPath()
+java.net.URI.getUserInfo()
+java.net.URI.toURL()
+java.net.URI.relativize(java.net.URI)
+java.net.URI.getRawSchemeSpecificPart()
+java.net.URI.parseServerAuthority()
+java.net.URI.getSchemeSpecificPart()
+java.net.URI.getRawUserInfo()
+java.net.URI.getFragment()
+java.net.URI.toASCIIString()
diff --git a/src/main/resources/freemarker/ext/beans/unsafeMethods.properties b/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
index 05c1981..a8025af 100644
--- a/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
+++ b/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
@@ -15,6 +15,14 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# Used by LegacyDefaultMemberAccessPolicy (not by DefaultMemberAccessPolicy).
+# It does NOT provide enough safety if template authors aren't as trusted as the developers; you need to use a custom
+# whitelist then (see WhitelistMemberAccessPolicy).
+
+# This is a blacklist, that is, methods mentioned here will be not be accessible, but everything else will be.
+# Furthermore, overridden version of the blacklisted methods will be accessible (which is strange, but we kept backward
+# compatibility).
+
 java.lang.Object.wait()
 java.lang.Object.wait(long)
 java.lang.Object.wait(long,int)
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index c8a2aab..cc0bd39 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
 <book conformance="docgen" version="5.0" xml:lang="en"
       xmlns="http://docbook.org/ns/docbook"
       xmlns:xlink="http://www.w3.org/1999/xlink"
-      xmlns:ns5="http://www.w3.org/2000/svg"
-      xmlns:ns4="http://www.w3.org/1998/Math/MathML"
-      xmlns:ns3="http://www.w3.org/1999/xhtml"
-      xmlns:ns="http://docbook.org/ns/docbook">
+>
   <info>
     <title>Apache FreeMarker Manual</title>
 
@@ -11194,9 +11191,8 @@
 
           <para>The <literal>TemplateHashModel</literal> returned from
           <literal>BeansWrapper.getEnumModels()</literal> can be used to
-          create hash models for accessing values of enums on JRE 1.5 or
-          later. (An attempt to invoke this method on an earlier JRE will
-          result in an
+          create hash models for accessing values of enums. (An attempt to
+          invoke this method on an earlier JRE will result in an
           <literal>UnsupportedOperationException</literal>.)</para>
 
           <programlisting role="unspecified">BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
@@ -28887,10 +28883,11 @@
 
           <answer>
             <para>In general you shouldn't allow that, unless those users are
-            system administrators or other trusted personnel. Consider
-            templates as part of the source code just like
-            <literal>*.java</literal> files are. If you still want to allow
-            users to upload templates, here are what to consider:</para>
+            application developers, system administrators, or other highly
+            trusted personnel. Consider templates as part of the source code
+            just like <literal>*.java</literal> files are. If you still want
+            to allow users to upload templates, here's what to
+            consider:</para>
 
             <itemizedlist>
               <listitem>
@@ -28915,11 +28912,27 @@
                 <literal>List</literal>-s, <literal>Array</literal>-s,
                 <literal>String</literal>-s, <literal>Number</literal>-s,
                 <literal>Boolean</literal>-s and <literal>Date</literal>-s.
-                For many application though that's too restrictive, and
-                instead you need to implement your own extremely restrictive
-                <literal>ObjectWrapper</literal>, which, for example, only
-                exposes those members of POJO-s that were explicitly marked to
-                be safe (opt-in approach).</para>
+                But for many application that's too restrictive, and instead
+                you have to create a
+                <literal>WhitelistMemberAccessPolicy</literal>, and create a
+                <literal>DefaultObjectWrapper</literal> (or other
+                <literal>BeansWrapper</literal> subclass that you would use)
+                that uses that. See the Java API documentation of
+                <literal>WhitelistMemberAccessPolicy</literal> for more. (Or,
+                you can roll your own <literal>MemberAccessPolicy</literal>
+                implementation, or even your own restrictive
+                <literal>ObjectWrapper</literal> implementation of
+                course.)</para>
+
+                <para>If you are creating <literal>TemplateModel</literal>-s
+                in custom code (instead of the
+                <literal>ObjectWrapper</literal> creating those), be sure you
+                avoid deprecated container constructors like <literal>new
+                SimpleSequence()</literal>, as those will use the also
+                deprecated default object wrapper instance, which doesn't have
+                the same restrictions than the
+                <literal>ObjectWrapper</literal> you have set on the
+                <literal>Configuration</literal>.</para>
 
                 <para>Also, don't forget about the <link
                 linkend="ref_buitin_api_and_has_api"><literal>?api</literal>
@@ -28935,16 +28948,22 @@
                 the <literal>ObjectWrapper</literal> is still in control, as
                 it decides what objects support <literal>?api</literal>, and
                 what will <literal>?api</literal> expose for them (it usually
-                exposes the same as for a generic POJO).</para>
+                exposes the same as for a generic POJO). Members not allowed
+                by the <literal>MemberAccessPolicy</literal> also won't be
+                visible with <literal>?api</literal> (assuming you are using a
+                well behaving <literal>ObjectWrapper</literal>, like
+                <literal>DefaultObjectWrapper</literal> is, hopefully.)</para>
 
                 <para>Last not least, some maybe aware of that the standard
                 object wrappers filters out some well known
                 <quote>unsafe</quote> methods, like
-                <literal>System.exit</literal>. Do not ever rely on this as
-                your only line of defense, since it only blocks the methods
-                that's in a predefined list. Thus, for example, if a new Java
-                version adds a new problematic method, it won't be filtered
-                out.</para>
+                <literal>System.exit</literal>. Do not ever rely on that,
+                since it only blocks the methods that's in a small predefined
+                list (for some historical reasons). The standard Java API is
+                huge and ever growing, and then there are the 3rd party
+                libraries, and the API-s of your own application. Clearly it's
+                impossible to blacklist all the problematic members in
+                those.</para>
               </listitem>
 
               <listitem>
@@ -28986,7 +29005,13 @@
                 <literal>TemplateClassResolver</literal> that restricts the
                 accessible classes (possibly based on which template asks for
                 them), such as
-                <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.</para>
+                <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.
+                Note that if, and only if your
+                <literal>ObjectWrapper</literal> is a
+                <literal>BeansWrapper</literal> or a subclass of it (typically
+                <literal>DefaultObjectWrapper</literal>), constructors not
+                allowed by the <literal>MemberAccessPolicy</literal> also
+                won't be accessible for <literal>?new</literal>.</para>
               </listitem>
 
               <listitem>
@@ -29186,6 +29211,63 @@
 
           <itemizedlist>
             <listitem>
+              <para><link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-124">FREEMARKER-124</link>:
+              Made the default filtering of class members more restrictive
+              (when you are using <literal>BeansWrapper</literal>, or its
+              subclasses like <literal>DefaultObjectWrapper</literal>). This
+              is not strictly backward compatible, but unlikely to break any
+              real-world applications; see
+              <literal>src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules</literal>
+              to see what was changed. This change was made for security
+              reasons, but the default behavior will never be safe enough if
+              untrusted users will edit templates; see <link
+              linkend="faq_template_uploading_security">in the FAQ</link>. In
+              the unlikely case this change breaks your application, then you
+              can still use the old behavior by setting the
+              <literal>memberAccessPolicy</literal> property of the object
+              wrapper to
+              <literal>LegacyDefaultMemberAccessPolicy.INSTANCE</literal>.</para>
+            </listitem>
+
+            <listitem>
+              <para>Added
+              <literal>freemarker.ext.beans.MemberAccessPolicy</literal>
+              interface, and the <literal>memberAccessPolicy</literal>
+              property to <literal>BeansWrapper</literal>, and subclasses like
+              <literal>DefaultObjectWrapper</literal>. This allows users to
+              implement their own program logic to decide what members of
+              classes will be exposed to the templates.</para>
+            </listitem>
+
+            <listitem>
+              <para>Added
+              <literal>freemarker.ext.beans.WhitelistMemberAccessPolicy</literal>,
+              which is a <literal>MemberAccessPolicy</literal> for use cases
+              where you want to allow editing templates to users who shouldn't
+              have the same rights as the developers (the same rights as the
+              Java application). Earlier, the only out of the box solution for
+              that was <literal>SimpleObjectWrapper</literal>, but that's too
+              restrictive for most applications where FreeMarker is used.
+              <literal>WhitelistMemberAccessPolicy</literal> works with
+              <literal>DefaultObjectWrapper</literal> (or any other
+              <literal>BeansWrapper</literal>), allowing you to use all
+              features of it, but it will only allow accessing members that
+              were explicitly listed by the developers, or was annotated with
+              <literal>@TemplateAccessible</literal>.</para>
+            </listitem>
+
+            <listitem>
+              <para><link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-120">FREEMARKER-120</link>:
+              <literal>BeansWrapper</literal> (and it's subclasses like
+              <literal>DefaultObjectWrapper</literal>) now has two protected
+              methods that can be overridden to monitor the accessing of
+              members: <literal>invokeMethod</literal> and
+              <literal>readField</literal>.</para>
+            </listitem>
+
+            <listitem>
               <para>Added
               <literal>Environment.getDataModelOrSharedVariable(String)</literal>.</para>
             </listitem>
@@ -29200,7 +29282,7 @@
               where the caller can provide the <literal>Map</literal> instance
               used as the backing storage, thus allows controlling the
               ordering, and other technical aspects (like the initial
-              capacity) of the underlying <literal>Map</literal>.</para>
+              capacity) of it.</para>
             </listitem>
           </itemizedlist>
         </section>
@@ -29299,6 +29381,11 @@
 
           <itemizedlist>
             <listitem>
+              <para>The minimum required Java version was increased from Java
+              5 to Java 7.</para>
+            </listitem>
+
+            <listitem>
               <para><link
               xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-109">FREEMARKER-109</link>:
               In JSP TLD-s, line breaks inside function parameter lists have
diff --git a/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java
new file mode 100644
index 0000000..324abd1
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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 org.junit.Assert.*;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.security.ProtectionDomain;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+
+public class DefaultMemberAccessPolicyTest {
+
+    private static final DefaultMemberAccessPolicy POLICY = DefaultMemberAccessPolicy.getInstance(Configuration.VERSION_2_3_30);
+
+    @Test
+    public void testWhitelistRuleWithNoMembers() throws NoSuchMethodException {
+        ClassMemberAccessPolicy classPolicy = POLICY.forClass(ProtectionDomain.class);
+        assertFalse(classPolicy.isMethodExposed(ProtectionDomain.class.getMethod("getClassLoader")));
+    }
+
+    @Test
+    public void testWhitelistRuleWithSomeMembers() throws NoSuchMethodException {
+        ClassMemberAccessPolicy classPolicy = POLICY.forClass(URL.class);
+        assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("openStream")));
+        assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("getContent", Class[].class)));
+        assertTrue(classPolicy.isMethodExposed(URL.class.getMethod("getHost")));
+        assertTrue(classPolicy.isMethodExposed(URL.class.getMethod("sameFile", URL.class)));
+    }
+
+    @Test
+    public void testWhitelistRuleOnSubclass() throws NoSuchMethodException {
+        ClassMemberAccessPolicy classPolicy = POLICY.forClass(CustomClassLoader.class);
+        assertFalse(classPolicy.isMethodExposed(CustomClassLoader.class.getMethod("loadClass", String.class)));
+        assertFalse(classPolicy.isMethodExposed(CustomClassLoader.class.getMethod("m1")));
+    }
+
+    @Test
+    public void testBlacklistUnlistedRule() throws NoSuchMethodException {
+        for (Class<?> testedClass : Arrays.asList(
+                Object.class, Thread.class, ThreadSubclass.class, ProtectionDomain.class, CustomClassLoader.class,
+                UserClass.class)) {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(testedClass);
+            assertFalse(classPolicy.isMethodExposed(testedClass.getMethod("wait")));
+            assertTrue(testedClass.getName(), classPolicy.isMethodExposed(testedClass.getMethod("toString")));
+        }
+
+        ClassMemberAccessPolicy classPolicy = POLICY.forClass(UserClass.class);
+        assertTrue(classPolicy.isMethodExposed(UserClass.class.getMethod("foo")));
+    }
+
+    @Test
+    public void testBlacklistUnlistedRuleOnSubclass() throws NoSuchMethodException {
+        ClassMemberAccessPolicy classPolicy = POLICY.forClass(ThreadSubclass.class);
+        assertFalse(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("run")));
+        assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("getName")));
+        assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("m1")));
+    }
+
+    @Test
+    public void testWellKnownUnsafeMethodsAreBanned() throws NoSuchMethodException {
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(Class.class);
+            assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("forName", String.class)));
+            assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("newInstance")));
+            assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getClassLoader")));
+            assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getResourceAsStream", String.class)));
+            assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getResource", String.class)));
+            assertTrue(classPolicy.isMethodExposed(Class.class.getMethod("getProtectionDomain"))); // Allowed
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(ProtectionDomain.class);
+            assertFalse(classPolicy.isMethodExposed(ProtectionDomain.class.getMethod("getClassLoader")));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(ClassLoader.class);
+            assertFalse(classPolicy.isMethodExposed(ClassLoader.class.getMethod("loadClass", String.class)));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(Method.class);
+            assertFalse(classPolicy.isMethodExposed(Method.class.getMethod("invoke", Object.class, Object[].class)));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(Constructor.class);
+            assertFalse(classPolicy.isMethodExposed(Constructor.class.getMethod("newInstance", Object[].class)));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(Field.class);
+            assertFalse(classPolicy.isMethodExposed(Field.class.getMethod("get", Object.class)));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(Object.class);
+            assertTrue(classPolicy.isMethodExposed(Field.class.getMethod("getClass"))); // Allowed by design
+            assertFalse(classPolicy.isMethodExposed(Field.class.getMethod("wait")));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = POLICY.forClass(URL.class);
+            assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("openConnection")));
+        }
+    }
+
+    public static class ThreadSubclass extends Thread {
+        @Override
+        public void run() {
+            super.run();
+        }
+
+        public void m1() {}
+    }
+
+    public static class UserClass {
+        public void foo() {
+        }
+    }
+
+    public static class CustomClassLoader extends ClassLoader {
+        public void m1() {}
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
new file mode 100644
index 0000000..f9fcf47
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -0,0 +1,522 @@
+/*
+ * 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 org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapperAndUnwrapper;
+import freemarker.template.SimpleNumber;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+
+public class DefaultObjectWrapperMemberAccessPolicyTest {
+
+    private final DefaultObjectWrapper dow
+            = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
+
+    @Test
+    public void testMethodsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        TemplateHashModel objM = (TemplateHashModel) dow.wrap(new C());
+
+        assertNotNull(objM.get("m1"));
+        assertEquals("m2(true)", exec(dow, objM.get("m2"), true));
+        assertEquals("staticM()", exec(dow, objM.get("staticM")));
+
+        assertEquals("x", getHashValue(dow, objM, "x"));
+        assertNotNull(objM.get("getX"));
+        assertNotNull(objM.get("setX"));
+
+        assertNull(objM.get("notPublic"));
+
+        assertNull(objM.get("notify"));
+
+        assertEquals("safe wait(1)", exec(dow, objM.get("wait"), 1L));
+        try {
+            exec(dow, objM.get("wait")); // 0 arg overload is not visible, a it's "unsafe"
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("wait(int)"));
+        }
+    }
+
+    @Test
+    public void testFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        TemplateHashModel objM = (TemplateHashModel) dow.wrap(new C());
+        assertFieldsNotExposed(objM);
+    }
+
+    private void assertFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException {
+        assertNull(objM.get("publicField1"));
+        assertNull(objM.get("publicField2"));
+        assertNonPublicFieldsNotExposed(objM);
+    }
+
+    private void assertNonPublicFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException {
+        assertNull(objM.get("nonPublicField1"));
+        assertNull(objM.get("nonPublicField2"));
+
+        // Strangely, static fields are banned historically, while static methods aren't.
+        assertNull(objM.get("STATIC_FIELD"));
+    }
+
+    @Test
+    public void testGenericGetWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        TemplateHashModel objM = (TemplateHashModel) dow.wrap(new CWithGenericGet());
+
+        assertEquals("get(x)", getHashValue(dow, objM, "x"));
+    }
+
+    @Test
+    public void testBlacklistRuleWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        TemplateHashModel objM = (TemplateHashModel) dow.wrap(new CThread());
+
+        assertNull(getHashValue(dow, objM, "run")); // blacklisted in Thread
+        assertNotNull(getHashValue(dow, objM, "m1")); // As Thread doesn't use whitelisted rule
+        assertNotNull(getHashValue(dow, objM, "toString"));
+    }
+
+    @Test
+    public void testConstructorsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        assertNonPublicConstructorNotExposed(dow);
+
+        assertEquals(CWithConstructor.class, dow.newInstance(CWithConstructor.class, Collections.emptyList())
+                .getClass());
+
+        assertEquals(CWithOverloadedConstructor.class,
+                dow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList())
+                        .getClass());
+
+        assertEquals(CWithOverloadedConstructor.class,
+                dow.newInstance(CWithOverloadedConstructor.class, Collections.singletonList(new SimpleNumber(1)))
+                        .getClass());
+    }
+
+    private void assertNonPublicConstructorNotExposed(DefaultObjectWrapper ow) {
+        try {
+            ow.newInstance(C.class, Collections.emptyList());
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("constructor"));
+        }
+    }
+
+    @Test
+    public void testExposeAllWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setExposureLevel(DefaultObjectWrapper.EXPOSE_ALL);
+        DefaultObjectWrapper ow = owb.build();
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+        // Because the MemberAccessPolicy is ignored:
+        assertNotNull(objM.get("notify"));
+        assertFieldsNotExposed(objM);
+    }
+
+    @Test
+    public void testExposeFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setExposeFields(true);
+        DefaultObjectWrapper ow = owb.build();
+        {
+            TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+            assertNull(objM.get("notify"));
+            assertEquals(1, getHashValue(ow, objM, "publicField1"));
+            assertEquals(2, getHashValue(ow, objM, "publicField2"));
+            assertNonPublicFieldsNotExposed(objM);
+        }
+
+        {
+            TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CExtended());
+            assertNull(objM.get("notify"));
+            assertEquals(1, getHashValue(ow, objM, "publicField1"));
+            assertEquals(2, getHashValue(ow, objM, "publicField2"));
+            assertEquals(3, getHashValue(ow, objM, "publicField3"));
+            assertNonPublicFieldsNotExposed(objM);
+        }
+    }
+
+    @Test
+    public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        String name = method.getName();
+                        Class<?>[] paramTypes = method.getParameterTypes();
+                        return name.equals("m3")
+                                || (name.equals("m2")
+                                        && (paramTypes.length == 0 || paramTypes[0].equals(boolean.class)));
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return true;
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return true;
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+        assertNull(objM.get("m1"));
+        assertEquals("m3()", exec(ow, objM.get("m3")));
+        assertEquals("m2()", exec(ow, objM.get("m2")));
+        assertEquals("m2(true)", exec(ow, objM.get("m2"), true));
+        try {
+            exec(ow, objM.get("m2"), 1);
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("overload"));
+        }
+
+        assertNull(objM.get("notify"));
+   }
+
+    @Test
+    public void testFieldsWithCustomMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setExposeFields(true);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        return true;
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return true;
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return field.getName().equals("publicField1")
+                                || field.getName().equals("nonPublicField1");
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+
+        assertNonPublicFieldsNotExposed(objM);
+        assertEquals(1, getHashValue(ow, objM, "publicField1"));
+        assertNull(getHashValue(ow, objM, "publicField2"));
+    }
+
+    @Test
+    public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        return false;
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return true;
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return true;
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CWithGenericGet());
+        assertNull(getHashValue(ow, objM, "x"));
+    }
+
+    @Test
+    public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        return true;
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return constructor.getDeclaringClass() == CWithOverloadedConstructor.class
+                                && constructor.getParameterTypes().length == 1;
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return true;
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        assertNonPublicConstructorNotExposed(ow);
+
+        try {
+            assertEquals(CWithConstructor.class,
+                    ow.newInstance(CWithConstructor.class, Collections.emptyList()).getClass());
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("constructor"));
+        }
+
+        try {
+            ow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList());
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("constructor"));
+        }
+
+        assertEquals(CWithOverloadedConstructor.class,
+                ow.newInstance(CWithOverloadedConstructor.class,
+                        Collections.singletonList(new SimpleNumber(1))).getClass());
+    }
+
+    @Test
+    public void testMemberAccessPolicyAndApiBI() throws IOException, TemplateException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        return method.getName().equals("size");
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return true;
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return true;
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        Map<String, Object> dataModel = ImmutableMap.<String, Object>of("m", ImmutableMap.of("k", "v"));
+
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+        cfg.setObjectWrapper(ow);
+        cfg.setAPIBuiltinEnabled(true);
+        Template template = new Template(null, "size=${m?api.size()} get=${(m?api.get('k'))!'hidden'}", cfg);
+
+        {
+            StringWriter out = new StringWriter();
+            template.process(dataModel, out);
+            assertEquals("size=1 get=hidden", out.toString());
+        }
+
+        cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build());
+        {
+            StringWriter out = new StringWriter();
+            template.process(dataModel, out);
+            assertEquals("size=1 get=v", out.toString());
+        }
+    }
+
+    @Test
+    public void testMemberAccessPolicyAndNewBI() throws IOException, TemplateException, NoSuchMethodException {
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+        owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+                return new ClassMemberAccessPolicy() {
+                    public boolean isMethodExposed(Method method) {
+                        return true;
+                    }
+
+                    public boolean isConstructorExposed(Constructor<?> constructor) {
+                        return constructor.getDeclaringClass().equals(CustomModel.class);
+                    }
+
+                    public boolean isFieldExposed(Field field) {
+                        return true;
+                    }
+                };
+            }
+        });
+        DefaultObjectWrapper ow = owb.build();
+
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+        cfg.setObjectWrapper(ow);
+        cfg.setAPIBuiltinEnabled(true);
+        Template template = new Template(null,
+                "${'" + CustomModel.class.getName() + "'?new()} "
+                        + "<#attempt>${'" + OtherCustomModel.class.getName() + "'?new()}<#recover>failed</#attempt>",
+                cfg);
+
+        {
+            StringWriter out = new StringWriter();
+            template.process(null, out);
+            assertEquals("1 failed", out.toString());
+        }
+
+        DefaultObjectWrapper dow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
+        MemberAccessPolicy pol = dow.getMemberAccessPolicy();
+        ClassMemberAccessPolicy cpol = pol.forClass(CustomModel.class);
+        assertTrue(cpol.isConstructorExposed(CustomModel.class.getConstructor()));
+        cfg.setObjectWrapper(dow);
+        {
+            StringWriter out = new StringWriter();
+            template.process(null, out);
+            assertEquals("1 2", out.toString());
+        }
+    }
+
+    private static Object getHashValue(ObjectWrapperAndUnwrapper ow, TemplateHashModel objM, String key)
+            throws TemplateModelException {
+        return ow.unwrap(objM.get(key));
+    }
+
+    private static Object exec(ObjectWrapperAndUnwrapper ow, TemplateModel objM, Object... args) throws TemplateModelException {
+        assertThat(objM, instanceOf(TemplateMethodModelEx.class));
+        List<TemplateModel> argModels = new ArrayList<TemplateModel>();
+        for (Object arg : args) {
+            argModels.add(ow.wrap(arg));
+        }
+        Object returnValue = ((TemplateMethodModelEx) objM).exec(argModels);
+        return unwrap(ow, returnValue);
+    }
+
+    private static Object unwrap(ObjectWrapperAndUnwrapper ow, Object returnValue) throws TemplateModelException {
+        return returnValue instanceof TemplateModel ? ow.unwrap((TemplateModel) returnValue) : returnValue;
+    }
+
+    public static class C {
+        public static final int STATIC_FIELD = 1;
+        public int publicField1 = 1;
+        public int publicField2 = 2;
+        protected int nonPublicField1 = 1;
+        private int nonPublicField2 = 2;
+
+        // Non-public
+        C() {
+
+        }
+
+        void notPublic() {
+        }
+
+        public void m1() {
+        }
+
+        public String m2() {
+            return "m2()";
+        }
+
+        public String m2(int otherOverload) {
+            return "m2(" + otherOverload + ")";
+        }
+
+        public String m2(boolean otherOverload) {
+            return "m2(" + otherOverload + ")";
+        }
+
+        public String m3() {
+            return "m3()";
+        }
+
+        public static String staticM() {
+            return "staticM()";
+        }
+
+        public String getX() {
+            return "x";
+        }
+
+        public void setX(String x) {
+        }
+
+        public String wait(int otherOverload) {
+            return "safe wait(" + otherOverload + ")";
+        }
+    }
+
+    public static class CExtended extends C {
+        public int publicField3 = 3;
+    }
+
+    public static class CWithGenericGet {
+        public String get(String key) {
+            return "get(" + key + ")";
+        }
+    }
+
+    public static class CWithConstructor implements TemplateModel {
+        public CWithConstructor() {
+        }
+    }
+
+    public static class CThread extends Thread {
+        @Override
+        public void run() {}
+
+        public void m1() {}
+    }
+
+    public static class CWithOverloadedConstructor implements TemplateModel {
+        public CWithOverloadedConstructor() {
+        }
+
+        public CWithOverloadedConstructor(int x) {
+        }
+    }
+
+    public static class CustomModel implements TemplateNumberModel {
+        public Number getAsNumber() {
+            return 1;
+        }
+    }
+
+    public static class OtherCustomModel implements TemplateNumberModel {
+        public Number getAsNumber() {
+            return 2;
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java
new file mode 100644
index 0000000..e301ddf
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class LegacyDefaultMemberAccessPolicyTest {
+
+    @Test
+    public void testBasic() throws NoSuchMethodException, NoSuchFieldException {
+        ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(UserClass.class);
+        assertFalse(classPolicy.isMethodExposed(UserClass.class.getMethod("wait")));
+        assertTrue(classPolicy.isMethodExposed(UserClass.class.getMethod("foo")));
+        assertTrue(classPolicy.isConstructorExposed(UserClass.class.getConstructor()));
+        assertTrue(classPolicy.isFieldExposed(UserClass.class.getField("f1")));
+    }
+
+    @Test
+    public void testThread() throws NoSuchMethodException, NoSuchFieldException {
+        {
+            ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(Thread.class);
+            assertFalse(classPolicy.isMethodExposed(Thread.class.getMethod("run")));
+        }
+        {
+            ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(ThreadSubclass.class);
+            // Strange glitch that we reproduce for backward compatibility:
+            assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("run")));
+        }
+    }
+
+    @Test
+    public void testClass() throws NoSuchMethodException, NoSuchFieldException {
+        ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(Class.class);
+        assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getClassLoader")));
+        assertTrue(classPolicy.isMethodExposed(Class.class.getMethod("getName")));
+    }
+
+    public static class ThreadSubclass extends Thread {
+        @Override
+        public void run() {
+            super.run();
+        }
+
+        public void m1() {}
+    }
+
+    public static class UserClass {
+        public String f1;
+
+        public void foo() {
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java b/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java
new file mode 100644
index 0000000..b53d033
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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 org.junit.Assert.*;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.test.TemplateTest;
+
+public class MemberAccessMonitoringTest extends TemplateTest {
+
+    private final MonitoredDefaultObjectWrapper ow = new MonitoredDefaultObjectWrapper();
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration configuration = super.createConfiguration();
+        configuration.setObjectWrapper(ow);
+        return configuration;
+    }
+
+    @Test
+    public void test() throws TemplateException, IOException {
+        addToDataModel("C1", ow.getStaticModels().get(C1.class.getName()));
+        addToDataModel("c2", new C2());
+
+        assertOutput(
+                "${C1.m1()} ${C1.F1} ${C1.F2} ${c2.m1()} ${c2.f1} ${c2.f2} ${c2['abc']}",
+                "1 11 111 2 22 222 3");
+        assertEquals(
+                ImmutableSet.of("C1.m1()", "C1.F1", "C1.F2", "C2.m1()", "C2.f1", "C2.f2", "C2.get()"),
+                ow.getAccessedMembers());
+    }
+
+    public static class C1 {
+        public static final int F1 = 11;
+        public static int F2 = 111;
+
+        public static int m1() {
+            return 1;
+        }
+
+        public static int get(String k) {
+            return k.length();
+        }
+    }
+
+    public static class C2 {
+        public final int f1 = 22;
+        public int f2 = 222;
+
+        public int m1() {
+            return 2;
+        }
+
+        public int get(String k) {
+            return k.length();
+        }
+    }
+
+    public static class MonitoredDefaultObjectWrapper extends DefaultObjectWrapper {
+        private final Set<String> accessedMembers;
+
+        public MonitoredDefaultObjectWrapper() {
+            super(getBuilder(), true);
+            this.accessedMembers = Collections.synchronizedSet(new HashSet<String>());
+        }
+
+        private static DefaultObjectWrapperBuilder getBuilder() {
+            DefaultObjectWrapperBuilder builder =
+                    new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+            builder.setExposeFields(true);
+            return builder;
+        }
+
+        @Override
+        protected TemplateModel invokeMethod(Object object, Method method, Object[] args) throws
+                InvocationTargetException, IllegalAccessException, TemplateModelException {
+            accessedMembers.add(method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()");
+            return super.invokeMethod(object, method, args);
+        }
+
+        @Override
+        protected TemplateModel readField(Object object, Field field) throws IllegalAccessException,
+                TemplateModelException {
+            accessedMembers.add(field.getDeclaringClass().getSimpleName() + "." + field.getName());
+            return super.readField(object, field);
+        }
+
+        public Set<String> getAccessedMembers() {
+            return accessedMembers;
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java
new file mode 100644
index 0000000..1250c2a
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java
@@ -0,0 +1,614 @@
+/*
+ * 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 org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class MemberSelectorListAccessPolicyTest {
+
+    @Test
+    public void testEmpty() throws NoSuchMethodException, NoSuchFieldException {
+        WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy();
+        ClassMemberAccessPolicy classPolicy = policy.forClass(C1.class);
+        assertFalse(classPolicy.isConstructorExposed(C1.class.getConstructor()));
+        assertFalse(classPolicy.isMethodExposed(C1.class.getMethod("m1")));
+        assertFalse(classPolicy.isFieldExposed(C1.class.getField("f1")));
+    }
+
+    @Test
+    public void testBasics() throws NoSuchMethodException, NoSuchFieldException {
+        WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                C1.class.getName() + "." + C1.class.getSimpleName() + "()",
+                C1.class.getName() + ".m1()",
+                C1.class.getName() + ".m2(int)",
+                C1.class.getName() + ".f1");
+
+        {
+            ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class);
+            assertTrue(c1Policy.isConstructorExposed(C1.class.getConstructor()));
+            assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m1")));
+            assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class)));
+            assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1")));
+        }
+
+        {
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            assertFalse(d1Policy.isMethodExposed(D1.class.getMethod("m1")));
+            assertFalse(d1Policy.isFieldExposed(D1.class.getField("f1")));
+        }
+    }
+
+    @Test
+    public void testInheritanceAndMoreOverloads() throws NoSuchMethodException, NoSuchFieldException {
+        WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                C1.class.getName() + ".m2(int)",
+                C1.class.getName() + ".f1",
+                C2.class.getName() + "." + C2.class.getSimpleName() + "(int)",
+                C2.class.getName() + ".m1()",
+                C2.class.getName() + ".m2(boolean)",
+                C3.class.getName() + ".f2",
+                C3.class.getName() + "." + C3.class.getSimpleName() + "()",
+                C3.class.getName() + ".m4()",
+                C3.class.getName() + ".f3"
+        );
+        ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class);
+        ClassMemberAccessPolicy c2Policy = policy.forClass(C2.class);
+        ClassMemberAccessPolicy c3Policy = policy.forClass(C3.class);
+
+        assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class)));
+        assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", int.class)));
+        assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", int.class)));
+
+        assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1")));
+        assertTrue(c2Policy.isFieldExposed(C2.class.getField("f1")));
+        assertTrue(c3Policy.isFieldExposed(C3.class.getField("f1")));
+
+        assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor(int.class)));
+        assertTrue(c2Policy.isConstructorExposed(C2.class.getConstructor(int.class)));
+        assertFalse(c3Policy.isConstructorExposed(C3.class.getConstructor(int.class))); // Not inherited
+
+        assertFalse(c1Policy.isMethodExposed(C1.class.getMethod("m1")));
+        assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m1")));
+        assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m1")));
+
+        assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class))); // Doesn't exist in C1
+        assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class)));
+        assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", boolean.class)));
+
+        assertFalse(c1Policy.isFieldExposed(C1.class.getField("f2")));
+        assertFalse(c2Policy.isFieldExposed(C2.class.getField("f2")));
+        assertTrue(c3Policy.isFieldExposed(C3.class.getField("f2")));
+
+        assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor()));
+        assertFalse(c2Policy.isConstructorExposed(C1.class.getConstructor())); // Doesn't exist in C2
+        assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor()));
+
+        assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m4"))); // Doesn't exist in C1
+        assertFalse(c2Policy.isMethodExposed(C2.class.getMethod("m4")));
+        assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m4")));
+
+        assertFalse(c1Policy.isFieldExposed(C2.class.getField("f3"))); // Doesn't exist in C1
+        assertFalse(c2Policy.isFieldExposed(C2.class.getField("f3")));
+        assertTrue(c3Policy.isFieldExposed(C3.class.getField("f3")));
+    }
+
+    @Test
+    public void testInterfaces() throws NoSuchMethodException, NoSuchFieldException {
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    I1.class.getName() + ".m1()",
+                    I1.class.getName() + ".f1"
+            );
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+            ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+            ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+            assertTrue(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    I1Sub.class.getName() + ".m1()",
+                    I1Sub.class.getName() + ".m2()",
+                    I1Sub.class.getName() + ".f1"
+            );
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+            ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+            ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+            assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertFalse(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertTrue(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertTrue(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertFalse(d2Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    I1.class.getName() + ".m1()",
+                    I1.class.getName() + ".f1"
+            );
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+            ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+            ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+            assertTrue(d1Policy.isMethodExposed(I1Sub.class.getMethod("m1")));
+            assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertFalse(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertFalse(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+            assertTrue(d1Policy.isFieldExposed(I1Sub.class.getField("f1")));
+            assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    D2.class.getName() + ".m1()",
+                    D2.class.getName() + ".f1"
+            );
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+            assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    I1Sub.class.getName() + ".m1()",
+                    D2.class.getName() + ".m1()",
+                    I1Sub.class.getName() + ".m2()",
+                    J1.class.getName() + ".m2()",
+                    I1.class.getName() + ".f1",
+                    I1Sub.class.getName() + ".f1"
+            );
+            ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+            ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+            ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+            ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+            ClassMemberAccessPolicy f1Policy = policy.forClass(F1.class);
+            assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+            assertFalse(d1Policy.isMethodExposed(J1.class.getMethod("m2")));
+            assertFalse(d2Policy.isMethodExposed(J1.class.getMethod("m2")));
+            assertTrue(e1Policy.isMethodExposed(J1.class.getMethod("m2")));
+            assertTrue(e2Policy.isMethodExposed(J1.class.getMethod("m2")));
+            assertTrue(f1Policy.isMethodExposed(J1.class.getMethod("m2")));
+            assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+            assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+        }
+    }
+
+    @Test
+    public void testArrayArgs() throws NoSuchMethodException {
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    CArrayArgs.class.getName() + ".m1(java.lang.String)",
+                    CArrayArgs.class.getName() + ".m1(java.lang.String[])",
+                    CArrayArgs.class.getName() + ".m1(java.lang.String[][])",
+                    CArrayArgs.class.getName() + ".m2(" + C1.class.getName() + "[])",
+                    CArrayArgs.class.getName() + ".m2("
+                            + C1.class.getName() + "[], "
+                            + C1.class.getName() + "[], "
+                            + C1.class.getName() + ")"
+            );
+            ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class)));
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class)));
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class)));
+            assertTrue(classPolicy.isMethodExposed(
+                    CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                    CArrayArgs.class.getName() + ".m1(java.lang.String)",
+                    CArrayArgs.class.getName() + ".m1(java.lang.String[][])"
+            );
+            ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+            assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class)));
+            assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class)));
+            assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class)));
+            assertFalse(classPolicy.isMethodExposed(
+                    CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+        }
+    }
+
+    @Test
+    public void testBlacklist1() throws NoSuchMethodException, NoSuchFieldException {
+        BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+                C1.class.getName() + ".m1()",
+                C1.class.getName() + ".f1",
+                C1.class.getName() + "." + C1.class.getSimpleName() + "()"
+        );
+
+        for (Class<?> cl : new Class[] { C1.class, C2.class, C3.class }) {
+            ClassMemberAccessPolicy classPolicy = policy.forClass(cl);
+            assertFalse(classPolicy.isMethodExposed(cl.getMethod("m1")));
+            assertTrue(classPolicy.isMethodExposed(cl.getMethod("m2", int.class)));
+            assertTrue(classPolicy.isMethodExposed(cl.getMethod("m3")));
+            assertFalse(classPolicy.isFieldExposed(cl.getField("f1")));
+            assertTrue(classPolicy.isFieldExposed(cl.getField("f2")));
+            if (cl != C2.class) {
+                assertEquals(cl != C1.class, classPolicy.isConstructorExposed(cl.getConstructor()));
+            }
+            assertTrue(classPolicy.isConstructorExposed(cl.getConstructor(int.class)));
+        }
+    }
+
+    @Test
+    public void testBlacklist2() throws NoSuchMethodException, NoSuchFieldException {
+        BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+                C2.class.getName() + ".m1()",
+                C2.class.getName() + ".f1",
+                C2.class.getName() + "." + C2.class.getSimpleName() + "(int)"
+        );
+
+        {
+            Class<C1> lc = C1.class;
+            ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+            assertTrue(classPolicy.isMethodExposed(lc.getMethod("m1")));
+            assertTrue(classPolicy.isFieldExposed(lc.getField("f1")));
+            assertTrue(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+        }
+
+        {
+            Class<C2> lc = C2.class;
+            ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+            assertFalse(classPolicy.isMethodExposed(lc.getMethod("m1")));
+            assertFalse(classPolicy.isFieldExposed(lc.getField("f1")));
+            assertFalse(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+        }
+
+        {
+            Class<C3> lc = C3.class;
+            ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+            assertFalse(classPolicy.isMethodExposed(lc.getMethod("m1")));
+            assertFalse(classPolicy.isFieldExposed(lc.getField("f1")));
+            assertTrue(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+        }
+    }
+
+    @Test
+    public void testBlacklistIgnoredAnnotation() throws NoSuchMethodException, NoSuchFieldException {
+        BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+                CAnnotationsTest1.class.getName() + ".m5()",
+                CAnnotationsTest1.class.getName() + ".f5",
+                CAnnotationsTest1.class.getName() + "." + CAnnotationsTest1.class.getSimpleName() + "()"
+        );
+
+        ClassMemberAccessPolicy classPolicy = policy.forClass(CAnnotationsTest1.class);
+        assertFalse(classPolicy.isMethodExposed(CAnnotationsTest1.class.getMethod("m5")));
+        assertFalse(classPolicy.isFieldExposed(CAnnotationsTest1.class.getField("f5")));
+        assertFalse(classPolicy.isConstructorExposed(CAnnotationsTest1.class.getConstructor()));
+    }
+
+    @Test
+    public void memberSelectorParserIgnoresWhitespace() throws NoSuchMethodException {
+        WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                (CArrayArgs.class.getName() + ".m1(java.lang.String)").replace(".", "\n\t. "),
+                CArrayArgs.class.getName() + ".m2("
+                        + C1.class.getName() + "  [  ]\t,"
+                        + C1.class.getName() + "[]  ,\n "
+                        + C1.class.getName() + " )"
+        );
+        ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+        assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+        assertTrue(classPolicy.isMethodExposed(
+                CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+    }
+
+    @Test
+    public void memberSelectorParsingErrorsTest() {
+        try {
+            newWhitelistMemberAccessPolicy("foo()");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("missing dot"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("com.example.Foo-bar.m()");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("malformed upper bound class name"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("java.util.Date.m-x()");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("malformed member name"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("java.util.Date.to string()");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("malformed member name"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("java.util.Date.toString(");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("should end with ')'"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("java.util.Date.m(com.x-y)");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("malformed argument class name"));
+        }
+        try {
+            newWhitelistMemberAccessPolicy("java.util.Date.m(int[)");
+            fail();
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("malformed argument class name"));
+        }
+    }
+
+    @Test
+    public void testAnnotation() throws NoSuchFieldException, NoSuchMethodException {
+        WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+                CAnnotationsTest2.class.getName() + ".f2",
+                CAnnotationsTest2.class.getName() + ".f3",
+                CAnnotationsTest2.class.getName() + ".m2()",
+                CAnnotationsTest2.class.getName() + ".m3()",
+                CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int)",
+                CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int, int)"
+        );
+        ClassMemberAccessPolicy classPolicy = policy.forClass(CAnnotationsTest2.class);
+
+        assertFalse(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f1")));
+        assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f2")));
+        assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f3")));
+        assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f4")));
+        assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f5")));
+        assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f6")));
+
+        assertFalse(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m1")));
+        assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m2")));
+        assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m3")));
+        assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m4")));
+        assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m5")));
+        assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m6")));
+
+        assertTrue(classPolicy.isConstructorExposed(
+                CAnnotationsTest2.class.getConstructor()));
+        assertTrue(classPolicy.isConstructorExposed(
+                CAnnotationsTest2.class.getConstructor(int.class)));
+        assertTrue(classPolicy.isConstructorExposed(
+                CAnnotationsTest2.class.getConstructor(int.class, int.class)));
+        assertTrue(classPolicy.isConstructorExposed(
+                CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class)));
+        assertFalse(classPolicy.isConstructorExposed(
+                CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class, int.class)));
+    }
+
+    private static WhitelistMemberAccessPolicy newWhitelistMemberAccessPolicy(String... memberSelectors) {
+        return new WhitelistMemberAccessPolicy(
+                MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+                        Arrays.asList(memberSelectors),
+                        MemberSelectorListAccessPolicyTest.class.getClassLoader()));
+    }
+
+    private static BlacklistMemberAccessPolicy newBlacklistMemberAccessPolicy(String... memberSelectors) {
+        return new BlacklistMemberAccessPolicy(
+                MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+                        Arrays.asList(memberSelectors),
+                        MemberSelectorListAccessPolicyTest.class.getClassLoader()));
+    }
+
+    public static class C1 {
+        public int f1;
+        public int f2;
+
+        public C1() {
+        }
+
+        public C1(int x) {
+        }
+
+        public void m1() {
+        }
+
+        public void m2() {
+        }
+
+        public void m2(int x) {
+        }
+
+        public void m2(double x) {
+        }
+
+        public void m3() {
+        }
+    }
+
+    public static class C2 extends C1 {
+        public int f3;
+
+        public C2(int x) {
+            super(x);
+        }
+
+        @Override
+        public void m2(int x) {
+        }
+
+        public void m2(boolean x) {
+        }
+
+        public void m4() {
+        }
+    }
+
+    public static class C3 extends C2 {
+        public C3() {
+            super(0);
+        }
+
+        public C3(int x) {
+            super(x);
+        }
+    }
+
+    public static class D1 implements I1 {
+        public int f1;
+        public void m1() {
+        }
+    }
+
+    public static class D2 extends D1 {
+    }
+
+    public static class E1 implements I1Sub {
+        public void m1() {
+
+        }
+
+        public void m2() {
+        }
+    }
+
+    public static class E2 extends E1 implements J1 {
+    }
+
+    public static class F1 implements J1 {
+        public void m2() {
+        }
+    }
+
+    interface I1 {
+        int f1 = 1;
+        void m1();
+    }
+
+    interface I1Sub extends Serializable, I1 {
+        void m2();
+    }
+
+    interface J1 {
+        void m2();
+    }
+
+    public class CArrayArgs {
+        public void m1(String arg) {
+        }
+
+        public void m1(String[] arg) {
+        }
+
+        public void m1(String[][] arg) {
+        }
+
+        public void m2(C1[] arg) {
+        }
+
+        public void m2(C1[] arg1, C1[] arg2, C1 arg3) {
+        }
+    }
+
+    public static class CAnnotationsTest1 {
+        @TemplateAccessible
+        public int f5;
+
+        @TemplateAccessible
+        public CAnnotationsTest1() {}
+
+        @TemplateAccessible
+        public void m5() {}
+    }
+
+    public interface IAnnotationTest {
+        @TemplateAccessible
+        int f6 = 0;
+
+        @TemplateAccessible
+        void m6();
+    }
+
+    public static class CAnnotationsTest2 extends CAnnotationsTest1 implements IAnnotationTest {
+        public int f1;
+
+        public int f2;
+
+        @TemplateAccessible
+        public int f3;
+
+        @TemplateAccessible
+        public int f4;
+
+        public int f5;
+
+        public int f6;
+
+        public CAnnotationsTest2() {}
+
+        public CAnnotationsTest2(int x) {}
+
+        @TemplateAccessible
+        public CAnnotationsTest2(int x, int y) {}
+
+        @TemplateAccessible
+        public CAnnotationsTest2(int x, int y, int z) {}
+
+        public CAnnotationsTest2(int x, int y, int z, int a) {}
+
+        public void m1() {}
+
+        public void m2() {}
+
+        @TemplateAccessible
+        public void m3() {}
+
+        @TemplateAccessible
+        public void m4() {}
+
+        public void m5() {}
+
+        public void m6() {}
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodMatcherTest.java b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
new file mode 100644
index 0000000..9ac9ca9
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodMatcherTest {
+
+    @Test
+    public void testReturnTypeOverload() throws NoSuchMethodException {
+        MethodMatcher matcher = new MethodMatcher();
+        Method genericM = TestReturnTypeOverloadGeneric.class.getMethod("m");
+        assertEquals(Object.class, genericM.getReturnType());
+        matcher.addMatching(TestReturnTypeOverloadGeneric.class, genericM);
+
+        Method stringM = TestReturnTypeOverloadString.class.getMethod("m");
+        assertEquals(String.class, stringM.getReturnType());
+
+        assertTrue(matcher.matches(TestReturnTypeOverloadGeneric.class, genericM));
+        assertTrue(matcher.matches(TestReturnTypeOverloadString.class, genericM));
+        assertTrue(matcher.matches(TestReturnTypeOverloadString.class, stringM));
+    }
+
+    public static class TestReturnTypeOverloadGeneric<T> {
+        public T m() {
+            return null;
+        };
+    }
+
+    public static class TestReturnTypeOverloadString extends TestReturnTypeOverloadGeneric<String> {
+        public String m() {
+            return "";
+        };
+    }
+
+    /** Mostly to test upper bound classes. */
+    @Test
+    public void testInheritance() throws NoSuchMethodException {
+        {
+            MethodMatcher matcher = new MethodMatcher();
+            Method m = TestInheritanceC2.class.getMethod("m1");
+            assertEquals(m, TestInheritanceC1.class.getMethod("m1"));
+            matcher.addMatching(TestInheritanceC2.class, m);
+            assertFalse(matcher.matches(TestInheritanceC1.class, m));
+            assertTrue(matcher.matches(TestInheritanceC2.class, m));
+            assertTrue(matcher.matches(TestInheritanceC3.class, m));
+        }
+        {
+            MethodMatcher matcher = new MethodMatcher();
+            Method m = TestInheritanceC2.class.getMethod("m2");
+            assertNotEquals(m, TestInheritanceC1.class.getMethod("m2"));
+            matcher.addMatching(TestInheritanceC2.class, m);
+            assertFalse(matcher.matches(TestInheritanceC1.class, m));
+            assertTrue(matcher.matches(TestInheritanceC2.class, m));
+            assertTrue(matcher.matches(TestInheritanceC3.class, m));
+        }
+        {
+            // m2 again, but with a non-same-instance but "equal" method.
+            MethodMatcher matcher = new MethodMatcher();
+            Method m = TestInheritanceC1.class.getMethod("m2");
+            matcher.addMatching(TestInheritanceC2.class, m);
+            assertFalse(matcher.matches(TestInheritanceC1.class, m));
+            assertTrue(matcher.matches(TestInheritanceC2.class, m));
+            assertTrue(matcher.matches(TestInheritanceC3.class, m));
+        }
+        {
+            MethodMatcher matcher = new MethodMatcher();
+            Method m = TestInheritanceC2.class.getMethod("m3");
+            assertEquals(m, TestInheritanceC1.class.getMethod("m3"));
+            assertNotEquals(m, TestInheritanceC3.class.getMethod("m3"));
+            matcher.addMatching(TestInheritanceC2.class, m);
+            assertFalse(matcher.matches(TestInheritanceC1.class, m));
+            assertTrue(matcher.matches(TestInheritanceC2.class, m));
+            assertTrue(matcher.matches(TestInheritanceC3.class, m));
+        }
+    }
+
+    public static class TestInheritanceC1 {
+        public void m1() {
+        }
+
+        public void m2() {
+        }
+
+        public void m3() {
+        }
+    }
+
+    public static class TestInheritanceC2 extends TestInheritanceC1 {
+        @Override
+        public void m2() {
+        }
+    }
+
+    public static class TestInheritanceC3 extends TestInheritanceC2 {
+        @Override
+        public void m3() {
+        }
+    }
+
+    /** Mostly to test when same method associated to multiple unrelated classes. */
+    @Test
+    public void testInheritance2() throws NoSuchMethodException {
+        MethodMatcher matcher = new MethodMatcher();
+        Method m = Runnable.class.getMethod("run");
+        matcher.addMatching(TestInheritance2SafeRunnable1.class, m);
+        matcher.addMatching(TestInheritance2SafeRunnable2.class, m);
+
+        assertTrue(matcher.matches(
+                TestInheritance2SafeRunnable1.class, TestInheritance2SafeRunnable1.class.getMethod("run")));
+        assertTrue(matcher.matches(
+                TestInheritance2SafeRunnable2.class, TestInheritance2SafeRunnable2.class.getMethod("run")));
+        assertFalse(matcher.matches(
+                TestInheritance2UnsafeRunnable.class, TestInheritance2UnsafeRunnable.class.getMethod("run")));
+    }
+
+    public static class TestInheritance2SafeRunnable1 implements Runnable {
+        public void run() {
+        }
+    }
+
+    public static class TestInheritance2SafeRunnable2 implements Runnable {
+        public void run() {
+        }
+    }
+
+    public static class TestInheritance2UnsafeRunnable implements Runnable {
+        public void run() {
+        }
+    }
+
+    @Test
+    public void testOverloads() throws NoSuchMethodException {
+        Method mInt = TestOverloads.class.getMethod("m", int.class);
+        Method mIntInt = TestOverloads.class.getMethod("m", int.class, int.class);
+        {
+            MethodMatcher matcher = new MethodMatcher();
+            matcher.addMatching(TestOverloads.class, mInt);
+            assertTrue(matcher.matches(TestOverloads.class, mInt));
+            assertFalse(matcher.matches(TestOverloads.class, mIntInt));
+        }
+        {
+            MethodMatcher matcher = new MethodMatcher();
+            matcher.addMatching(TestOverloads.class, mIntInt);
+            assertFalse(matcher.matches(TestOverloads.class, mInt));
+            assertTrue(matcher.matches(TestOverloads.class, mIntInt));
+        }
+    }
+
+    public static class TestOverloads {
+        public void m(int x) {
+        }
+
+        public void m(int x, int y) {
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodUtilTest.java b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
new file mode 100644
index 0000000..24291b3
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodUtilTest {
+
+    @Test
+    public void testMethodBasic() throws NoSuchMethodException, NoSuchFieldException {
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getMethod("m1"), TemplateAccessible.class));
+        assertNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getMethod("m2"), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getConstructor(int.class), TemplateAccessible.class));
+        assertNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getConstructor(int.class, int.class), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getField("f1"), TemplateAccessible.class));
+        assertNull(_MethodUtil.getInheritableAnnotation(
+                C1.class, C1.class.getField("f3"), TemplateAccessible.class));
+    }
+
+    @Test
+    public void testMethodInheritance() throws NoSuchMethodException, NoSuchFieldException {
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m1"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m2"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m3"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m4"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m5"), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getConstructor(int.class), TemplateAccessible.class));
+        assertNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getConstructor(), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getField("f1"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getField("f2"), TemplateAccessible.class));
+        assertNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getField("f3"), TemplateAccessible.class));
+        assertNotNull(_MethodUtil.getInheritableAnnotation(
+                C2.class, C2.class.getField("f4"), TemplateAccessible.class));
+    }
+
+    @Test
+    public void testMethodInheritanceWithSyntheticMethod() {
+        for (Method method : D2.class.getMethods()) {
+            if (method.getName().equals("m1")) {
+                assertNotNull(_MethodUtil.getInheritableAnnotation(
+                        C2.class, method, TemplateAccessible.class));
+            }
+        }
+    }
+
+    static public class C1 implements Serializable {
+        @TemplateAccessible
+        public int f1;
+
+        @TemplateAccessible
+        public int f2;
+
+        public int f3;
+
+        public int f4;
+
+        @TemplateAccessible
+        public C1(int x) {}
+
+        public C1(int x, int y) {}
+
+        @TemplateAccessible
+        public void m1() {}
+
+        public void m2() {}
+
+        public void m3() {}
+
+        @TemplateAccessible
+        public void m4() {}
+
+        @TemplateAccessible
+        public void m5() {}
+    }
+
+    static public class C2 extends C1 implements I1 {
+        public long f2;
+
+        public C2() {
+            super(0);
+        }
+
+        public C2(int x) {
+            super(x);
+        }
+
+        @Override
+        public void m1() {}
+
+        @TemplateAccessible
+        @Override
+        public void m3() {}
+    }
+
+    public interface I1 {
+        @TemplateAccessible
+        int f4 = 0;
+
+        @TemplateAccessible
+        void m2();
+
+        void m5();
+    }
+
+    public static class D1<T> {
+        @TemplateAccessible
+        public T m1() { return null; }
+    }
+
+    public static class D2 extends D1<String> {
+        @Override
+        public String m1() { return ""; }
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index c18e47c..4b77eef 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -22,6 +22,7 @@
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.lang.reflect.Field;
@@ -80,7 +81,11 @@
 import freemarker.core.XMLOutputFormat;
 import freemarker.core._CoreStringUtils;
 import freemarker.ext.beans.BeansWrapperBuilder;
+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;
 import freemarker.template.utility.NullWriter;
@@ -1896,6 +1901,39 @@
         assertFalse(cfg.getFallbackOnNullLoopVariable());
     }
 
+    public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY =
+            new WhitelistMemberAccessPolicy(MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+                    ImmutableList.<String>of(
+                            File.class.getName() + ".getName()",
+                            File.class.getName() + ".isFile()"),
+                    ConfigurationTest.class.getClassLoader()));
+
+    @Test
+    public void testMemberAccessPolicySetting() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+        cfg.setSetting(
+                "objectWrapper",
+                "DefaultObjectWrapper(2.3.30, "
+                        + "memberAccessPolicy="
+                        + ConfigurationTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY"
+                        + ")");
+        TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new File("x"));
+        assertNotNull(m.get("getName"));
+        assertNotNull(m.get("isFile"));
+        assertNull(m.get("delete"));
+    }
+
+    @Test
+    public void testMemberAccessPolicySetting2() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+        cfg.setSetting(
+                "objectWrapper",
+                "DefaultObjectWrapper(2.3.30, "
+                        + "memberAccessPolicy=" + LegacyDefaultMemberAccessPolicy.class.getName() + "())");
+        assertSame(((DefaultObjectWrapper) cfg.getObjectWrapper()).getMemberAccessPolicy(),
+                LegacyDefaultMemberAccessPolicy.INSTANCE);
+    }
+
     @Test
     public void testGetSettingNamesAreSorted() throws Exception {
         Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index 86d14bb..12f4954 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -27,6 +27,7 @@
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -56,6 +57,7 @@
 import freemarker.ext.beans.BeansWrapper;
 import freemarker.ext.beans.EnumerationModel;
 import freemarker.ext.beans.HashAdapter;
+import freemarker.ext.beans.WhitelistMemberAccessPolicy;
 import freemarker.ext.util.WrapperTemplateModel;
 
 public class DefaultObjectWrapperTest {
@@ -316,6 +318,30 @@
 
             assertTrue(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter);
         }
+
+        {
+            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
+
+            DefaultObjectWrapper bwDefault = builder.build();
+            assertSame(bwDefault, builder.build());
+
+            WhitelistMemberAccessPolicy memberAccessPolicy =
+                    new WhitelistMemberAccessPolicy(
+                            WhitelistMemberAccessPolicy.MemberSelector.parse(
+                                    Arrays.asList(SomeBean.class.getName() + ".getX()"),
+                                    DefaultObjectWrapperTest.class.getClassLoader()));
+            builder.setMemberAccessPolicy(memberAccessPolicy);
+            DefaultObjectWrapper bw = builder.build();
+            assertNotSame(bw, bwDefault);
+            assertSame(bw, builder.build());
+            assertSame(bw.getMemberAccessPolicy(), memberAccessPolicy);
+
+            TemplateHashModel m = (TemplateHashModel) bw.wrap(new SomeBean());
+            assertNotNull(m.get("x"));
+            assertNotNull(m.get("getX"));
+            assertNull(m.get("y"));
+            assertNull(m.get("getY"));
+        }
     }
     
     @Test
@@ -1191,5 +1217,14 @@
         }
         
     };
+
+    public static class SomeBean {
+        public int getX() {
+            return 1;
+        }
+        public int getY() {
+            return 1;
+        }
+    }
     
 }
diff --git a/src/test/java/freemarker/test/templatesuite/models/LegacyList.java b/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
index 06abaca..a02199b 100644
--- a/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
+++ b/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
@@ -24,6 +24,7 @@
 import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
+import freemarker.template._TemplateAPI;
 
 /**
  * A little bridge class that subclasses the new SimpleList
@@ -33,6 +34,10 @@
 
     private Iterator iterator;
 
+    public LegacyList() {
+        super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
+    }
+
     /**
      * Resets the cursor to the beginning of the list.
      */
diff --git a/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java b/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
index f9ac0ca..09d8a71 100644
--- a/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
+++ b/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
@@ -19,6 +19,9 @@
 
 package freemarker.test.templatesuite.models;
 
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
 import freemarker.template.SimpleHash;
 import freemarker.template.SimpleScalar;
 import freemarker.template.SimpleSequence;
@@ -34,19 +37,21 @@
 public class MultiModel1 implements TemplateHashModel,
         TemplateSequenceModel, TemplateScalarModel {
 
+    private static final DefaultObjectWrapper DEFAULT_OBJECT_WRAPPER =
+            new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_0).build();
     private TemplateModel m_cSubModel = new MultiModel2();
     private TemplateModel m_cListHashModel1 = new MultiModel4();
     private TemplateModel m_cListHashModel2 = new MultiModel5();
-    private TemplateSequenceModel m_cListModel = new SimpleSequence();
-    private TemplateHashModel m_cHashModel = new SimpleHash();
+    private TemplateSequenceModel m_cListModel = new SimpleSequence(DEFAULT_OBJECT_WRAPPER);
+    private TemplateHashModel m_cHashModel = new SimpleHash(DEFAULT_OBJECT_WRAPPER);
 
     /** Creates new MultiModel1 */
     public MultiModel1() {
         for ( int i = 0; i < 10; i++ ) {
             ((SimpleSequence) m_cListModel).add( "Model1 value: " + Integer.toString( i ));
         }
-        ((SimpleSequence) m_cListModel).add( new MultiModel3() );
-        ((SimpleHash) m_cHashModel).put( "nested", new MultiModel3() );
+        ((SimpleSequence) m_cListModel).add(new MultiModel3());
+        ((SimpleHash) m_cHashModel).put("nested", new MultiModel3());
     }
 
     /**
diff --git a/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java b/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
index f3de0a9..f317b18 100644
--- a/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
+++ b/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
@@ -24,6 +24,7 @@
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
 import freemarker.template.utility.HtmlEscape;
 import freemarker.template.utility.StandardCompress;
 
@@ -33,7 +34,7 @@
 public class TransformHashWrapper implements TemplateHashModel,
         TemplateScalarModel {
 
-    private SimpleHash m_cHashModel = new SimpleHash();
+    private SimpleHash m_cHashModel = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
 
     /** Creates new TransformHashWrapper */
     public TransformHashWrapper() {
diff --git a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
index 9819fa0..583b444 100644
--- a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
+++ b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
@@ -1,3 +1,21 @@
+<#--
+  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.
+-->
 <#macro m1 a b c>
   m1 does things with ${a}, ${b}, ${c}
 </#macro>
diff --git a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
index 54488c3..aa3a8ab 100644
--- a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
+++ b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
@@ -1,3 +1,21 @@
+/*
+ * 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.
+ */
 
   m2 does things with 1, 2, 3
   Delegate to m1:
diff --git a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
index 4494b13..9cea85f 100644
--- a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
+++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
@@ -1,3 +1,21 @@
+<#--
+  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.
+-->
 <#function f a b c d>
   <#return "a=${a}, b=${b}, c=${c}, d=${d}">
 </#function>
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
similarity index 63%
copy from src/main/java/freemarker/core/_Java6.java
copy to src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
index cd03fb2..c9d17b6 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
@@ -16,20 +16,34 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package freemarker.core;
 
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+a=2, b=3, c=1, d=2
+a=1, b=2, c=2, d=3
 
-/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
- */
-public interface _Java6 {
+  a=1
+  b=2
+  others:
+    e = 5
+    f = 6
+    c = 3
+    d = 4
+  a=1
+  b=2
+  others:
+    c = 3
+    d = 4
+    e = 5
+    f = 6
 
-    void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
-    void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-    
-}
+    a = 1
+    b = 2
+    e = 5
+    f = 6
+    c = 3
+    d = 4
+    a = 1
+    b = 2
+    c = 3
+    d = 4
+    e = 5
+    f = 6