Skip to content

Commit 8760383

Browse files
authored
Merge pull request #1631 from harawata/gh/1201-2
Ambiguous setters/getters should throw exception only when they are invoked
2 parents 1c0cad1 + 6e1b82f commit 8760383

File tree

3 files changed

+188
-46
lines changed

3 files changed

+188
-46
lines changed

src/main/java/org/apache/ibatis/reflection/Reflector.java

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.reflect.ParameterizedType;
2525
import java.lang.reflect.ReflectPermission;
2626
import java.lang.reflect.Type;
27+
import java.text.MessageFormat;
2728
import java.util.ArrayList;
2829
import java.util.Arrays;
2930
import java.util.Collection;
@@ -33,6 +34,7 @@
3334
import java.util.Map;
3435
import java.util.Map.Entry;
3536

37+
import org.apache.ibatis.reflection.invoker.AmbiguousMethodInvoker;
3638
import org.apache.ibatis.reflection.invoker.GetFieldInvoker;
3739
import org.apache.ibatis.reflection.invoker.Invoker;
3840
import org.apache.ibatis.reflection.invoker.MethodInvoker;
@@ -92,6 +94,7 @@ private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters
9294
for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
9395
Method winner = null;
9496
String propName = entry.getKey();
97+
boolean isAmbiguous = false;
9598
for (Method candidate : entry.getValue()) {
9699
if (winner == null) {
97100
winner = candidate;
@@ -101,10 +104,8 @@ private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters
101104
Class<?> candidateType = candidate.getReturnType();
102105
if (candidateType.equals(winnerType)) {
103106
if (!boolean.class.equals(candidateType)) {
104-
throw new ReflectionException(
105-
"Illegal overloaded getter method with ambiguous type for property "
106-
+ propName + " in class " + winner.getDeclaringClass()
107-
+ ". This breaks the JavaBeans specification and can cause unpredictable results.");
107+
isAmbiguous = true;
108+
break;
108109
} else if (candidate.getName().startsWith("is")) {
109110
winner = candidate;
110111
}
@@ -113,22 +114,23 @@ private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters
113114
} else if (winnerType.isAssignableFrom(candidateType)) {
114115
winner = candidate;
115116
} else {
116-
throw new ReflectionException(
117-
"Illegal overloaded getter method with ambiguous type for property "
118-
+ propName + " in class " + winner.getDeclaringClass()
119-
+ ". This breaks the JavaBeans specification and can cause unpredictable results.");
117+
isAmbiguous = true;
118+
break;
120119
}
121120
}
122-
addGetMethod(propName, winner);
121+
addGetMethod(propName, winner, isAmbiguous);
123122
}
124123
}
125124

126-
private void addGetMethod(String name, Method method) {
127-
if (isValidPropertyName(name)) {
128-
getMethods.put(name, new MethodInvoker(method));
129-
Type returnType = TypeParameterResolver.resolveReturnType(method, type);
130-
getTypes.put(name, typeToClass(returnType));
131-
}
125+
private void addGetMethod(String name, Method method, boolean isAmbiguous) {
126+
MethodInvoker invoker = isAmbiguous
127+
? new AmbiguousMethodInvoker(method, MessageFormat.format(
128+
"Illegal overloaded getter method with ambiguous type for property ''{0}'' in class ''{1}''. This breaks the JavaBeans specification and can cause unpredictable results.",
129+
name, method.getDeclaringClass().getName()))
130+
: new MethodInvoker(method);
131+
getMethods.put(name, invoker);
132+
Type returnType = TypeParameterResolver.resolveReturnType(method, type);
133+
getTypes.put(name, typeToClass(returnType));
132134
}
133135

134136
private void addSetMethods(Class<?> clazz) {
@@ -140,35 +142,31 @@ private void addSetMethods(Class<?> clazz) {
140142
}
141143

142144
private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
143-
List<Method> list = conflictingMethods.computeIfAbsent(name, k -> new ArrayList<>());
144-
list.add(method);
145+
if (isValidPropertyName(name)) {
146+
List<Method> list = conflictingMethods.computeIfAbsent(name, k -> new ArrayList<>());
147+
list.add(method);
148+
}
145149
}
146150

147151
private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
148152
for (String propName : conflictingSetters.keySet()) {
149153
List<Method> setters = conflictingSetters.get(propName);
150154
Class<?> getterType = getTypes.get(propName);
155+
boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker;
156+
boolean isSetterAmbiguous = false;
151157
Method match = null;
152-
ReflectionException exception = null;
153158
for (Method setter : setters) {
154-
if (setter.getParameterTypes()[0].equals(getterType)) {
159+
if (!isGetterAmbiguous && setter.getParameterTypes()[0].equals(getterType)) {
155160
// should be the best match
156161
match = setter;
157162
break;
158163
}
159-
if (exception == null) {
160-
try {
161-
match = pickBetterSetter(match, setter, propName);
162-
} catch (ReflectionException e) {
163-
// there could still be the 'best match'
164-
match = null;
165-
exception = e;
166-
}
164+
if (!isSetterAmbiguous) {
165+
match = pickBetterSetter(match, setter, propName);
166+
isSetterAmbiguous = match == null;
167167
}
168168
}
169-
if (match == null) {
170-
throw exception;
171-
} else {
169+
if (match != null) {
172170
addSetMethod(propName, match);
173171
}
174172
}
@@ -185,17 +183,21 @@ private Method pickBetterSetter(Method setter1, Method setter2, String property)
185183
} else if (paramType2.isAssignableFrom(paramType1)) {
186184
return setter1;
187185
}
188-
throw new ReflectionException("Ambiguous setters defined for property '" + property + "' in class '"
189-
+ setter2.getDeclaringClass() + "' with types '" + paramType1.getName() + "' and '"
190-
+ paramType2.getName() + "'.");
186+
MethodInvoker invoker = new AmbiguousMethodInvoker(setter1,
187+
MessageFormat.format(
188+
"Ambiguous setters defined for property ''{0}'' in class ''{1}'' with types ''{2}'' and ''{3}''.",
189+
property, setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName()));
190+
setMethods.put(property, invoker);
191+
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type);
192+
setTypes.put(property, typeToClass(paramTypes[0]));
193+
return null;
191194
}
192195

193196
private void addSetMethod(String name, Method method) {
194-
if (isValidPropertyName(name)) {
195-
setMethods.put(name, new MethodInvoker(method));
196-
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
197-
setTypes.put(name, typeToClass(paramTypes[0]));
198-
}
197+
MethodInvoker invoker = new MethodInvoker(method);
198+
setMethods.put(name, invoker);
199+
Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
200+
setTypes.put(name, typeToClass(paramTypes[0]));
199201
}
200202

201203
private Class<?> typeToClass(Type src) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright 2009-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.apache.ibatis.reflection.invoker;
18+
19+
import java.lang.reflect.InvocationTargetException;
20+
import java.lang.reflect.Method;
21+
22+
import org.apache.ibatis.reflection.ReflectionException;
23+
24+
public class AmbiguousMethodInvoker extends MethodInvoker {
25+
private final String exceptionMessage;
26+
27+
public AmbiguousMethodInvoker(Method method, String exceptionMessage) {
28+
super(method);
29+
this.exceptionMessage = exceptionMessage;
30+
}
31+
32+
@Override
33+
public Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException {
34+
throw new ReflectionException(exceptionMessage);
35+
}
36+
}

src/test/java/org/apache/ibatis/reflection/ReflectorTest.java

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
import static org.junit.jupiter.api.Assertions.*;
1919

2020
import java.io.Serializable;
21+
import java.util.Arrays;
2122
import java.util.List;
2223

24+
import org.apache.ibatis.reflection.invoker.Invoker;
2325
import org.junit.jupiter.api.Assertions;
2426
import org.junit.jupiter.api.Test;
27+
2528
import static com.googlecode.catchexception.apis.BDDCatchException.*;
2629
import static org.assertj.core.api.BDDAssertions.then;
2730

@@ -189,20 +192,100 @@ interface BeanInterface<T> {
189192
}
190193

191194
@Test
192-
void shouldSettersWithUnrelatedArgTypesThrowException() {
195+
void shouldSettersWithUnrelatedArgTypesThrowException() throws Exception {
193196
@SuppressWarnings("unused")
194197
class BeanClass {
195-
public void setTheProp(String arg) {}
196-
public void setTheProp(Integer arg) {}
198+
public void setProp1(String arg) {}
199+
public void setProp2(String arg) {}
200+
public void setProp2(Integer arg) {}
201+
public void setProp2(boolean arg) {}
197202
}
203+
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
204+
Reflector reflector = reflectorFactory.findForClass(BeanClass.class);
205+
206+
List<String> setableProps = Arrays.asList(reflector.getSetablePropertyNames());
207+
assertTrue(setableProps.contains("prop1"));
208+
assertTrue(setableProps.contains("prop2"));
209+
assertEquals("prop1", reflector.findPropertyName("PROP1"));
210+
assertEquals("prop2", reflector.findPropertyName("PROP2"));
211+
212+
assertEquals(String.class, reflector.getSetterType("prop1"));
213+
assertNotNull(reflector.getSetInvoker("prop1"));
214+
215+
Class<?> paramType = reflector.getSetterType("prop2");
216+
assertTrue(String.class.equals(paramType) || Integer.class.equals(paramType));
198217

218+
Invoker ambiguousInvoker = reflector.getSetInvoker("prop2");
219+
Object[] param = String.class.equals(paramType)? new String[]{"x"} : new Integer[]{1};
220+
when(ambiguousInvoker).invoke(new BeanClass(), param);
221+
then(caughtException()).isInstanceOf(ReflectionException.class)
222+
.hasMessageMatching(
223+
"Ambiguous setters defined for property 'prop2' in class '" + BeanClass.class.getName().replace("$", "\\$")
224+
+ "' with types '(java.lang.String|java.lang.Integer|boolean)' and '(java.lang.String|java.lang.Integer|boolean)'\\.");
225+
}
226+
227+
@Test
228+
public void shouldTwoGettersForNonBooleanPropertyThrowException() throws Exception {
229+
@SuppressWarnings("unused")
230+
class BeanClass {
231+
public Integer getProp1() {return 1;}
232+
public int getProp2() {return 0;}
233+
public int isProp2() {return 0;}
234+
}
199235
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
200-
when(reflectorFactory).findForClass(BeanClass.class);
236+
Reflector reflector = reflectorFactory.findForClass(BeanClass.class);
237+
238+
List<String> getableProps = Arrays.asList(reflector.getGetablePropertyNames());
239+
assertTrue(getableProps.contains("prop1"));
240+
assertTrue(getableProps.contains("prop2"));
241+
assertEquals("prop1", reflector.findPropertyName("PROP1"));
242+
assertEquals("prop2", reflector.findPropertyName("PROP2"));
243+
244+
assertEquals(Integer.class, reflector.getGetterType("prop1"));
245+
Invoker getInvoker = reflector.getGetInvoker("prop1");
246+
assertEquals(Integer.valueOf(1), getInvoker.invoke(new BeanClass(), null));
247+
248+
Class<?> paramType = reflector.getGetterType("prop2");
249+
assertEquals(int.class, paramType);
250+
251+
Invoker ambiguousInvoker = reflector.getGetInvoker("prop2");
252+
when(ambiguousInvoker).invoke(new BeanClass(), new Integer[] {1});
201253
then(caughtException()).isInstanceOf(ReflectionException.class)
202-
.hasMessageContaining("theProp")
203-
.hasMessageContaining("BeanClass")
204-
.hasMessageContaining("java.lang.String")
205-
.hasMessageContaining("java.lang.Integer");
254+
.hasMessageContaining("Illegal overloaded getter method with ambiguous type for property 'prop2' in class '"
255+
+ BeanClass.class.getName()
256+
+ "'. This breaks the JavaBeans specification and can cause unpredictable results.");
257+
}
258+
259+
@Test
260+
public void shouldTwoGettersWithDifferentTypesThrowException() throws Exception {
261+
@SuppressWarnings("unused")
262+
class BeanClass {
263+
public Integer getProp1() {return 1;}
264+
public Integer getProp2() {return 1;}
265+
public boolean isProp2() {return false;}
266+
}
267+
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
268+
Reflector reflector = reflectorFactory.findForClass(BeanClass.class);
269+
270+
List<String> getableProps = Arrays.asList(reflector.getGetablePropertyNames());
271+
assertTrue(getableProps.contains("prop1"));
272+
assertTrue(getableProps.contains("prop2"));
273+
assertEquals("prop1", reflector.findPropertyName("PROP1"));
274+
assertEquals("prop2", reflector.findPropertyName("PROP2"));
275+
276+
assertEquals(Integer.class, reflector.getGetterType("prop1"));
277+
Invoker getInvoker = reflector.getGetInvoker("prop1");
278+
assertEquals(Integer.valueOf(1), getInvoker.invoke(new BeanClass(), null));
279+
280+
Class<?> returnType = reflector.getGetterType("prop2");
281+
assertTrue(Integer.class.equals(returnType) || boolean.class.equals(returnType));
282+
283+
Invoker ambiguousInvoker = reflector.getGetInvoker("prop2");
284+
when(ambiguousInvoker).invoke(new BeanClass(), null);
285+
then(caughtException()).isInstanceOf(ReflectionException.class)
286+
.hasMessageContaining("Illegal overloaded getter method with ambiguous type for property 'prop2' in class '"
287+
+ BeanClass.class.getName()
288+
+ "'. This breaks the JavaBeans specification and can cause unpredictable results.");
206289
}
207290

208291
@Test
@@ -218,4 +301,25 @@ public void setBool(boolean bool) {}
218301
Reflector reflector = reflectorFactory.findForClass(Bean.class);
219302
assertTrue((Boolean)reflector.getGetInvoker("bool").invoke(new Bean(), new Byte[0]));
220303
}
304+
305+
@Test
306+
void shouldIgnoreBestMatchSetterIfGetterIsAmbiguous() throws Exception {
307+
@SuppressWarnings("unused")
308+
class Bean {
309+
public Integer isBool() {return Integer.valueOf(1);}
310+
public Integer getBool() {return Integer.valueOf(2);}
311+
public void setBool(boolean bool) {}
312+
public void setBool(Integer bool) {}
313+
}
314+
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
315+
Reflector reflector = reflectorFactory.findForClass(Bean.class);
316+
Class<?> paramType = reflector.getSetterType("bool");
317+
Object[] param = boolean.class.equals(paramType) ? new Boolean[] { true } : new Integer[] { 1 };
318+
Invoker ambiguousInvoker = reflector.getSetInvoker("bool");
319+
when(ambiguousInvoker).invoke(new Bean(), param);
320+
then(caughtException()).isInstanceOf(ReflectionException.class)
321+
.hasMessageMatching(
322+
"Ambiguous setters defined for property 'bool' in class '" + Bean.class.getName().replace("$", "\\$")
323+
+ "' with types '(java.lang.Integer|boolean)' and '(java.lang.Integer|boolean)'\\.");
324+
}
221325
}

0 commit comments

Comments
 (0)