Repository: jdbi/jdbi
Issue: BindMethods doesn't work well with package-private implementations

I have an [`@AutoValue`d](https://github.com/google/auto/tree/master/value) class.

    @AutoValue
    public abstract class Person {
      public static Person create(String name) { return new AutoValue_Person(name); }
      public abstract String name();
    }

This creates a package-private class named `AutoValue_Person`.

I'm trying to use it with the following SqlObject method:

    @SqlUpdate("INSERT INTO person (name) VALUES (:name)")
    void addPerson(@BindMethods Person person);

But it fails with the following message:

    java.lang.IllegalAccessException: Class org.jdbi.v3.core.argument.MethodReturnValueNamedArgumentFinder can not access a member of class example.model.AutoValue_Person with modifiers "public"
      at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
      at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
      at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
      at java.lang.reflect.Method.invoke(Method.java:491)
      at org.jdbi.v3.core.argument.MethodReturnValueNamedArgumentFinder.invokeMethod(MethodReturnValueNamedArgumentFinder.java:37)

The reason is that the methods are extracted from the actual object, not from the given interface. I want the methods from `Person` to be called, not from `AutoValue_Person`: calling `AutoValue_Person`'s method is Java's job, not yours, especially since jdbi wants to work in a SecurityManager-ed environment.

So please use the interface I provide in my SqlObject to call the methods, not the object themselves.

To prove my point that there is a difference between the two and that calling the interface works, I created the following test case that you can adapt so you don't have to use Google's `@AutoValue`:

**`example/Controller.java`**

    package example;
    import example.model.*;
    import java.lang.reflect.*;
    public class Controller {
      public static void main(String[] args) throws Exception {
        Person jack = Person.create("Jack");
        System.out.print("Call using jack.name(): ");
        System.out.println(jack.name());
        call(jack, Person.class, "name");
        call(jack, jack.getClass(), "name");
      }
      private static void call(Object instance, Class type, String methodName) throws Exception {
        Method method = type.getMethod(methodName);
        System.out.printf("Call using %s.getMethod(%s).invoke(instance): ", type, methodName);
        System.out.println(method.invoke(instance));
      }
    }

**`example/model/Person.java`**

    package example.model;
    public abstract class Person {
      public static Person create(String name) { return new PersonImpl(name); }
      public abstract String name();
    }

**`example/model/PersonImpl.java`**

    package example.model;
    class PersonImpl extends Person {
      private String name;
      PersonImpl(String name) { this.name = name; }
      @Override public String name() { return name; }
    }

The result is the following:

    Call using jack.name(): Jack
    Call using class example.model.Person.getMethod(name).invoke(instance): Jack
    Call using class example.model.PersonImpl.getMethod(name).invoke(instance): Exception in thread "main" java.lang.IllegalAccessException: Class example.Controller can not access a member of class example.model.PersonImpl with modifiers "public"
      at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
      at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
      at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
      at java.lang.reflect.Method.invoke(Method.java:491)
      at example.Controller.call(Controller.java:15)
      at example.Controller.main(Controller.java:10)

So when you call the method, please use the method I provide in my interface (`Person`) and not the method provided in my implementation (`PersonImpl` or `AutoValue_Person`). This will work without any issue without ever calling `setAccessible(true)`.

Changed files:
core/src/test/java/org/jdbi/v3/core/argument/TestBeanArguments.java, core/src/main/java/org/jdbi/v3/core/argument/ObjectMethodArguments.java

diff --git a/core/src/main/java/org/jdbi/v3/core/argument/ObjectMethodArguments.java b/core/src/main/java/org/jdbi/v3/core/argument/ObjectMethodArguments.java
index 073a4cc70..c40cbcd70 100644
--- a/core/src/main/java/org/jdbi/v3/core/argument/ObjectMethodArguments.java
+++ b/core/src/main/java/org/jdbi/v3/core/argument/ObjectMethodArguments.java
@@ -14,8 +14,10 @@
 package org.jdbi.v3.core.argument;
 
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
@@ -32,10 +34,7 @@ public class ObjectMethodArguments extends MethodReturnValueNamedArgumentFinder
     private static final Map<Class<?>, Map<String, Method>> CLASS_METHODS = ExpiringMap.builder()
         .expiration(10, TimeUnit.MINUTES)
         .expirationPolicy(ExpirationPolicy.ACCESSED)
-        .entryLoader((Class<?> type) ->
-            Arrays.stream(type.getMethods())
-                .filter(m -> m.getParameterCount() == 0)
-                .collect(Collectors.toMap(Method::getName, Function.identity(), ObjectMethodArguments::bridgeMethodMerge)))
+        .entryLoader(ObjectMethodArguments::load)
         .build();
 
     private final Map<String, Method> methods;
@@ -50,6 +49,20 @@ public class ObjectMethodArguments extends MethodReturnValueNamedArgumentFinder
         this.methods = CLASS_METHODS.get(object.getClass());
     }
 
+    private static Map<String, Method> load(Class<?> type) {
+        // The package check could be removed if jdbi's tests would only use public classes for argument testing
+        if (Modifier.isPublic(type.getModifiers()) || type.getPackage().equals(ObjectMethodArguments.class.getPackage())) {
+            return Arrays.stream(type.getMethods())
+                .filter(m -> m.getParameterCount() == 0)
+                .collect(Collectors.toMap(Method::getName, Function.identity(), ObjectMethodArguments::bridgeMethodMerge));
+        } else {
+            final HashMap<String, Method> methodMap = new HashMap<>();
+            Optional.ofNullable(type.getSuperclass()).ifPresent(superclass -> methodMap.putAll(load(superclass)));
+            Arrays.stream(type.getInterfaces()).forEach(interfaceClass -> methodMap.putAll(load(interfaceClass)));
+            return methodMap;
+        }
+    }
+
     @Override
     Optional<TypedValue> getValue(String name, StatementContext ctx) {
         Method method = methods.get(name);
diff --git a/core/src/test/java/org/jdbi/v3/core/argument/TestBeanArguments.java b/core/src/test/java/org/jdbi/v3/core/argument/TestBeanArguments.java
index f71590291..e3b3e99b4 100644
--- a/core/src/test/java/org/jdbi/v3/core/argument/TestBeanArguments.java
+++ b/core/src/test/java/org/jdbi/v3/core/argument/TestBeanArguments.java
@@ -204,4 +204,58 @@ public class TestBeanArguments {
 
         verify(stmt).setLong(3, 69);
     }
+
+    @Test
+    public void testPrivateClass() throws Exception {
+        new ObjectMethodArguments(null, Person.create("hello")).find("name", ctx).get().apply(4, stmt, null);
+        verify(stmt).setString(4, "hello");
+    }
+
+    @Test
+    public void testPrivateInterfaceClass() throws Exception {
+        new ObjectMethodArguments(null, Car.create("hello")).find("name", ctx).get().apply(4, stmt, null);
+        verify(stmt).setString(4, "hello");
+    }
+
+    public abstract static class Person {
+        public static Person create(String name) {
+            return new PersonImpl(name);
+        }
+
+        public abstract String name();
+
+        private static class PersonImpl extends Person {
+            private String name;
+
+            PersonImpl(String name) {
+                this.name = name;
+            }
+
+            @Override
+            public String name() {
+                return name;
+            }
+        }
+    }
+
+    public interface Car {
+        static Car create(String name) {
+            return new CarImpl(name);
+        }
+
+        String name();
+    }
+
+    private static class CarImpl implements Car {
+        private String name;
+
+        CarImpl(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+    }
 }