Added WhitelistMemberAccessPolicy and related internal classes
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index d014a69..d67c3ac 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -658,6 +658,29 @@
         }
     }
 
+    /**
+     * @since 2.3.30
+     */
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return classIntrospector.getMemberAccessPolicy();
+    }
+
+    /**
+     * Used to customize what  members will be hidden;
+     * see {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} for more.
+     *
+     * @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();
     }
@@ -1567,7 +1590,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/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index 72f26cb..630bf95 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -78,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.
@@ -205,7 +210,7 @@
         return new ClassIntrospectorBuilder(this);
     }
 
-    // ------------------------------------------------------------------------------------------------------------------
+    // -----------------------------------------------------------------------------------------------------------------
     // Introspection:
 
     /**
@@ -273,7 +278,7 @@
             addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
         }
 
-        final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+        final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
 
         addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy);
 
@@ -312,7 +317,8 @@
     }
 
     private void addBeanInfoToClassIntrospectionData(
-            Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods,
+            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);
@@ -660,7 +666,8 @@
 
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
             PropertyDescriptor pd,
-            Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods);
         if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) {
             readMethod = null;
@@ -687,12 +694,11 @@
     }
 
     private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
-            Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
-        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 && isMethodExposed(classMemberAccessPolicy, genericGet)) {
             introspData.put(GENERIC_GET_KEY, genericGet);
@@ -730,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
@@ -785,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;
@@ -802,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;
@@ -853,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:
 
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 1f2d5e0..e2847ab 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -156,6 +156,12 @@
         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;
diff --git a/src/main/java/freemarker/ext/beans/ConstructorMatcher.java b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
new file mode 100644
index 0000000..2c94576
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * {@link MemberMatcher} for constructors.
+ *
+ * @since 2.3.30
+ */
+final class ConstructorMatcher extends MemberMatcher<Constructor<?>, ExecutableMemberSignature> {
+    @Override
+    protected ExecutableMemberSignature toMemberSignature(Constructor<?> member) {
+        return new ExecutableMemberSignature(member);
+    }
+}
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/ext/beans/FieldMatcher.java b/src/main/java/freemarker/ext/beans/FieldMatcher.java
new file mode 100644
index 0000000..f67bf14
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/FieldMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * 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.Field;
+
+/**
+ * {@link MemberMatcher} for fields.
+ *
+ * @since 2.3.30
+ */
+final class FieldMatcher extends MemberMatcher<Field, String> {
+    @Override
+    protected String toMemberSignature(Field member) {
+        return member.getName();
+    }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
index 5d72fea..c8de56d 100644
--- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
+++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
@@ -19,18 +19,45 @@
 
 package freemarker.ext.beans;
 
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateModel;
+
 /**
- * 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).
+ * Implement this to specify what class members are accessible from templates.
+ *
+ * <p>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. An {@link MemberAccessPolicy} is a filter applied to
+ * the set of members that {@link BeansWrapper} intends to expose on the first place. (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}, and so the {@link MemberAccessPolicy} won't affect those.
+ *
+ * <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<?> containingClass);
+    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..2b52178
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberMatcher.java
@@ -0,0 +1,111 @@
+/*
+ * 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);
+
+    /**
+     * 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 && containsTypeOrSuperType(upperBoundTypes, contextClass);
+    }
+
+    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/MethodMatcher.java b/src/main/java/freemarker/ext/beans/MethodMatcher.java
new file mode 100644
index 0000000..7df56be
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MethodMatcher.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.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);
+    }
+}
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/WhitelistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
new file mode 100644
index 0000000..5e8945a
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
@@ -0,0 +1,411 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import freemarker.log.Logger;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Whitelist-based member access policy, that is, only members that you have explicitly whitelisted will be accessible.
+ * The whitelist content is application specific, and can be significant work to put together, but it's the only way
+ * you can achieve any practical safety if you don't fully trust the users who can edit templates. 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.
+ *
+ * <p>There are two ways you can add members to the whitelist:
+ * <ul>
+ *     <li>Via a list of member selectors passed to the constructor
+ *     <li>Via {@link TemplateAccessible} annotation
+ * </ul>
+ *
+ * <p>When a member is whitelisted, it's identified by the following data (with the example of
+ * {@code com.example.MyClass.myMethod(int, int)} being whitelisted):
+ * <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>Once you have whitelisted a member in the upper bound class, it will be automatically whitelisted in all
+ * subclasses of that, even if the whitelisted member is a field or constructor (which doesn't support overriding, but
+ * it will be treated as such if the field name or constructor parameter types match).
+ * It's called "upper bound" class, because the member will only be whitelisted in classes that are {@code instanceof}
+ * the upper bound class. That restriction stands even if the member was inherited from another class or
+ * interface, and it wasn't even overridden in the upper bound class; the member won't be whitelisted in the
+ * class/interface where it was inherited from, if that type is more generic than the upper bound class.
+ *
+ * <p>Note that the return type of methods aren't used in any way. So if you whitelist {@code myMethod(int, int)}, and
+ * it has multiple variants with different return types (which is possible on the bytecode level), then you have
+ * whitelisted all variants of it.
+ *
+ * @since 2.3.30
+ */
+public class WhitelistMemberAccessPolicy implements MemberAccessPolicy {
+    private static final Logger LOG = Logger.getLogger("freemarker.beans");
+
+    private final MethodMatcher methodMatcher;
+    private final ConstructorMatcher constructorMatcher;
+    private final FieldMatcher fieldMatcher;
+
+    /**
+     * A condition that matches some type members. See {@link WhitelistMemberAccessPolicy} 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 (missing closing ')'): "
+                            + 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)}.
+         */
+        public static List<MemberSelector> parse(Collection<String> memberSelectors,
+                ClassLoader classLoader) {
+            List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size());
+            for (String memberSelector : memberSelectors) {
+                parsedMemberSelectors.add(parse(memberSelector, classLoader));
+            }
+            return parsedMemberSelectors;
+        }
+    }
+
+    public WhitelistMemberAccessPolicy(Collection<MemberSelector> memberSelectors) {
+        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 ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
+        return new ClassMemberAccessPolicy() {
+            public boolean isMethodExposed(Method method) {
+                return methodMatcher.matches(contextClass, method)
+                        || _MethodUtil.getInheritableAnnotation(contextClass, method, TemplateAccessible.class) != null;
+            }
+
+            public boolean isConstructorExposed(Constructor<?> constructor) {
+                return constructorMatcher.matches(contextClass, constructor)
+                        || _MethodUtil.getInheritableAnnotation(contextClass, constructor, TemplateAccessible.class)
+                                != null;
+            }
+
+            public boolean isFieldExposed(Field field) {
+                return fieldMatcher.matches(contextClass, field)
+                        || _MethodUtil.getInheritableAnnotation(contextClass, field, TemplateAccessible.class) != null;
+            }
+        };
+    }
+
+    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/_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/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java
index 4c5a39d..8333a00 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapper.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java
@@ -32,6 +32,7 @@
 
 import freemarker.ext.beans.BeansWrapper;
 import freemarker.ext.beans.BeansWrapperConfiguration;
+import freemarker.ext.beans.DefaultMemberAccessPolicy;
 import freemarker.ext.beans.EnumerationModel;
 import freemarker.ext.dom.NodeModel;
 import freemarker.log.Logger;
@@ -252,7 +253,8 @@
      * 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}, 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 +265,10 @@
         if (obj instanceof Node) {
             return wrapDomNode(obj);
         }
-        if (JYTHON_WRAPPER != null  && JYTHON_OBJ_CLASS.isInstance(obj)) {
-            return JYTHON_WRAPPER.wrap(obj);
+        if (getMemberAccessPolicy() instanceof DefaultMemberAccessPolicy) {
+            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/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/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 4b10a92..a2732b6 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>
 
@@ -28887,10 +28884,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 +28913,17 @@
                 <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>Also, don't forget about the <link
                 linkend="ref_buitin_api_and_has_api"><literal>?api</literal>
@@ -28935,16 +28939,23 @@
                 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 +28997,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>
@@ -29197,6 +29214,23 @@
 
             <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>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
index 355a769..eafbaf2 100644
--- a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
+++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -22,24 +22,32 @@
 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 {
 
@@ -166,7 +174,7 @@
     public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException {
         DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         String name = method.getName();
@@ -208,7 +216,7 @@
         DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
         owb.setExposeFields(true);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         return true;
@@ -238,7 +246,7 @@
     public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException {
         DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         return false;
@@ -264,7 +272,7 @@
     public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException {
         DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         return true;
@@ -305,6 +313,93 @@
                         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 {
+        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());
+        }
+
+        cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build());
+        {
+            StringWriter out = new StringWriter();
+            template.process(null, out);
+            assertEquals("1 2", out.toString());
+        }
+    }
+
     private static DefaultObjectWrapper createDefaultMemberAccessPolicyObjectWrapper() {
         return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
     }
@@ -406,4 +501,16 @@
         }
     }
 
+    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/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/ext/beans/WhitelistMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java
new file mode 100644
index 0000000..0edee07
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java
@@ -0,0 +1,558 @@
+/*
+ * 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;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+
+public class WhitelistMemberAccessPolicyTest {
+
+    @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)));
+        assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor(int.class)));
+
+        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 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("missing closing ')'"));
+        }
+        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)));
+    }
+
+    public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY =
+            newWhitelistMemberAccessPolicy(
+                    C1.class.getName() + ".m1()",
+                    C1.class.getName() + ".m3()");
+
+    @Test
+    public void stringBasedConfigurationTest() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+        cfg.setSetting(
+                "objectWrapper",
+                "DefaultObjectWrapper(2.3.30, " +
+                        "memberAccessPolicy="
+                        + WhitelistMemberAccessPolicyTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY"
+                        + ")");
+        TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new C1());
+        assertNotNull(m.get("m1"));
+        assertNull(m.get("m2"));
+        assertNotNull(m.get("m3"));
+    }
+
+    private static WhitelistMemberAccessPolicy newWhitelistMemberAccessPolicy(String... memberSelectors) {
+        return new WhitelistMemberAccessPolicy(
+                WhitelistMemberAccessPolicy.MemberSelector.parse(
+                        Arrays.asList(memberSelectors),
+                        WhitelistMemberAccessPolicyTest.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);
+        }
+
+        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/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;
+        }
+    }
     
 }