Added freemarker.ext.beans.MemberAccessPolicy interface, and the memberAccessPolicy property to BeansWrapper, and subclasses like DefaultObjectWrapper. This allows users to implement their own program logic to decide what members of classes will be exposed to the templates. The legacy "unsafe methods" mechanism also builds on the same now, and by setting a custom MemberAccessPolicy you completely replace that.
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index 953c2f4..d014a69 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -96,7 +96,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;
     
@@ -858,9 +859,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
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
index 905bde9..f791d12 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
@@ -225,10 +225,18 @@
         classIntrospectorBuilder.setExposeFields(exposeFields);
     }
 
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return classIntrospectorBuilder.getMemberAccessPolicy();
+    }
+
+    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/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index c48a91b..72f26cb 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;
 
@@ -138,10 +139,11 @@
 
     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 #getHasSharedInstanceRestrictions()} */
     final private boolean hasSharedInstanceRestrictions;
@@ -178,10 +180,11 @@
 
         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;
 
@@ -264,25 +267,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);
 
-        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;
@@ -294,28 +298,30 @@
         }
     }
 
-    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<MethodSignature, 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) {
@@ -327,7 +333,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) {
@@ -344,7 +350,7 @@
                             (decision.getReplaceExistingProperty()
                                     || !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) {
                         addPropertyDescriptorToClassIntrospectionData(
-                                introspData, propDesc, clazz, accessibleMethods);
+                                introspData, propDesc, accessibleMethods, classMemberAccessPolicy);
                     }
 
                     String methodKey = decision.getExposeMethodAs();
@@ -352,7 +358,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);
@@ -652,9 +659,10 @@
     }
 
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
-            PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
+            PropertyDescriptor pd,
+            Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods);
-        if (readMethod != null && !isAllowedToExpose(readMethod)) {
+        if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) {
             readMethod = null;
         }
         
@@ -662,7 +670,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) {
@@ -679,31 +687,42 @@
     }
 
     private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
-            Map<MethodSignature, List<Method>> accessibleMethods) {
+            Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method genericGet = getFirstAccessibleMethod(
                 MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
         if (genericGet == null) {
             genericGet = getFirstAccessibleMethod(
                     MethodSignature.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);
@@ -800,8 +819,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) {
@@ -1035,7 +1074,11 @@
     boolean getExposeFields() {
         return exposeFields;
     }
-    
+
+    MemberAccessPolicy getMemberAccessPolicy() {
+        return memberAccessPolicy;
+    }
+
     boolean getTreatDefaultMethodsAsBeanMembers() {
         return treatDefaultMethodsAsBeanMembers;
     }
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 1b54958..1f2d5e0 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -26,21 +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<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;
@@ -51,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 {
@@ -81,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;
@@ -97,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;
         
@@ -137,6 +152,15 @@
         this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers;
     }
 
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return memberAccessPolicy;
+    }
+
+    public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+        NullArgumentException.check(memberAccessPolicy);
+        this.memberAccessPolicy = memberAccessPolicy;
+    }
+
     public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
         return methodAppearanceFineTuner;
     }
@@ -153,6 +177,13 @@
         this.methodSorter = methodSorter;
     }
 
+    /**
+     * Returns the normalized incompatible improvements.
+     */
+    public Version getIncompatibleImprovements() {
+        return incompatibleImprovements;
+    }
+
     private static void removeClearedReferencesFromInstanceCache() {
         Reference<? extends ClassIntrospector> clearedRef;
         while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
@@ -210,8 +241,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/ext/beans/DefaultMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
new file mode 100644
index 0000000..8c1186d
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
@@ -0,0 +1,140 @@
+/*
+ * 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.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
+import freemarker.template.utility.ClassUtil;
+
+/**
+ * Legacy black list 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!
+ *
+ * @since 2.3.30
+ */
+public final class DefaultMemberAccessPolicy implements MemberAccessPolicy {
+
+    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);
+            Map<String, Class<?>> 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<String, Class<?>> 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] = primClasses.get(argClassName);
+            if (argTypes[i] == null) {
+                argTypes[i] = ClassUtil.forName(argClassName);
+            }
+        }
+        return clazz.getMethod(methodName, argTypes);
+    }
+
+    private static Map<String, Class<?>> createPrimitiveClassesMap() {
+        Map<String, Class<?>> map = new HashMap<String, Class<?>>();
+        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;
+    }
+
+    private static final DefaultMemberAccessPolicy INSTANCE = new DefaultMemberAccessPolicy();
+
+    private DefaultMemberAccessPolicy() {
+    }
+
+    /**
+     * 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;
+    }
+
+    public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+        return CLASS_MEMBER_ACCESS_POLICY_INSTANCE;
+    }
+
+    private static final BacklistClassMemberAccessPolicy CLASS_MEMBER_ACCESS_POLICY_INSTANCE
+            = new BacklistClassMemberAccessPolicy();
+    private static class BacklistClassMemberAccessPolicy 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..5d72fea
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Implement this to specify what class members are accessible from templates. Implementations must be thread
+ * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache
+ * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the
+ * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to
+ * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself).
+ *
+ * @since 2.3.30
+ */
+public interface MemberAccessPolicy {
+    /**
+     * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class.
+     */
+    ClassMemberAccessPolicy forClass(Class<?> containingClass);
+}
diff --git a/src/main/java/freemarker/ext/beans/StaticModel.java b/src/main/java/freemarker/ext/beans/StaticModel.java
index 28c84bb..1b9e0f5 100644
--- a/src/main/java/freemarker/ext/beans/StaticModel.java
+++ b/src/main/java/freemarker/ext/beans/StaticModel.java
@@ -126,12 +126,14 @@
             }
         }
         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) {
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/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/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 30227ca..1b7bb0b 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -52,6 +52,7 @@
     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);
     
     public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index c8a2aab..4b10a92 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -29187,6 +29187,16 @@
           <itemizedlist>
             <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>Environment.getDataModelOrSharedVariable(String)</literal>.</para>
             </listitem>
 
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..355a769
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -0,0 +1,409 @@
+/*
+ * 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.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 org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapperAndUnwrapper;
+import freemarker.template.SimpleNumber;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+public class DefaultObjectWrapperMemberAccessPolicyTest {
+
+    @Test
+    public void testMethodsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper();
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+
+        assertNotNull(objM.get("m1"));
+        assertEquals("m2(true)", exec(ow, objM.get("m2"), true));
+        assertEquals("staticM()", exec(ow, objM.get("staticM")));
+
+        assertEquals("x", getHashValue(ow, objM, "x"));
+        assertNotNull(objM.get("getX"));
+        assertNotNull(objM.get("setX"));
+
+        assertNull(objM.get("notPublic"));
+
+        assertNull(objM.get("notify"));
+
+        // Because it was overridden, we allow it historically.
+        assertNotNull(objM.get("run"));
+
+        assertEquals("safe wait(1)", exec(ow, objM.get("wait"), 1L));
+        try {
+            exec(ow, 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 {
+        DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper();
+        TemplateHashModel objM = (TemplateHashModel) ow.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 {
+        DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper();
+
+        TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CWithGenericGet());
+
+        assertEquals("get(x)", getHashValue(ow, objM, "x"));
+    }
+
+    @Test
+    public void testConstructorsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+        DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper();
+        assertNonPublicConstructorNotExposed(ow);
+
+        assertEquals(CWithConstructor.class, ow.newInstance(CWithConstructor.class, Collections.emptyList())
+                .getClass());
+
+        assertEquals(CWithOverloadedConstructor.class,
+                ow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList())
+                        .getClass());
+
+        assertEquals(CWithOverloadedConstructor.class,
+                ow.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<?> containingClass) {
+                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<?> containingClass) {
+                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<?> containingClass) {
+                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<?> containingClass) {
+                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());
+    }
+
+    private static DefaultObjectWrapper createDefaultMemberAccessPolicyObjectWrapper() {
+        return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
+    }
+
+    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 extends Thread {
+        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 + ")";
+        }
+
+        @Override
+        public void run() {
+            return;
+        }
+    }
+
+    public static class CExtended extends C {
+        public int publicField3 = 3;
+    }
+
+    public static class CWithGenericGet extends Thread {
+        public String get(String key) {
+            return "get(" + key + ")";
+        }
+    }
+
+    public static class CWithConstructor implements TemplateModel {
+        public CWithConstructor() {
+        }
+    }
+
+    public static class CWithOverloadedConstructor implements TemplateModel {
+        public CWithOverloadedConstructor() {
+        }
+
+        public CWithOverloadedConstructor(int x) {
+        }
+    }
+
+}