Skip to content

Commit 4532bee

Browse files
authored
Merge pull request #3379 from harawata/type-based-handler-resolution
Resolve type handler based on `java.lang.reflect.Type` instead of `Class` and respect runtime JDBC type
2 parents 939c8c0 + 949ccd3 commit 4532bee

File tree

111 files changed

+3625
-761
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+3625
-761
lines changed

src/main/java/org/apache/ibatis/binding/MapperMethod.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -298,7 +298,7 @@ public MethodSignature(Configuration configuration, Class<?> mapperInterface, Me
298298
this.returnsMap = this.mapKey != null;
299299
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
300300
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
301-
this.paramNameResolver = new ParamNameResolver(configuration, method);
301+
this.paramNameResolver = new ParamNameResolver(configuration, method, mapperInterface);
302302
}
303303

304304
public Object convertArgsToSqlCommandParam(Object[] args) {

src/main/java/org/apache/ibatis/builder/BaseBuilder.java

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2023 the original author or authors.
2+
* Copyright 2009-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
*/
1616
package org.apache.ibatis.builder;
1717

18+
import java.lang.reflect.Type;
1819
import java.util.Arrays;
1920
import java.util.HashSet;
2021
import java.util.Set;
@@ -104,28 +105,33 @@ protected <T> Class<? extends T> resolveClass(String alias) {
104105
}
105106
}
106107

108+
@Deprecated(since = "3.6.0", forRemoval = true)
107109
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
108-
if (typeHandlerAlias == null) {
109-
return null;
110-
}
111-
Class<?> type = resolveClass(typeHandlerAlias);
112-
if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
113-
throw new BuilderException(
114-
"Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
115-
}
116-
@SuppressWarnings("unchecked") // already verified it is a TypeHandler
117-
Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
118-
return resolveTypeHandler(javaType, typeHandlerType);
110+
return resolveTypeHandler(null, javaType, null, typeHandlerAlias);
119111
}
120112

113+
@Deprecated(since = "3.6.0", forRemoval = true)
121114
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
122-
if (typeHandlerType == null) {
115+
return resolveTypeHandler(javaType, null, typeHandlerType);
116+
}
117+
118+
protected TypeHandler<?> resolveTypeHandler(Class<?> parameterType, Type propertyType, JdbcType jdbcType,
119+
String typeHandlerAlias) {
120+
Class<? extends TypeHandler<?>> typeHandlerType = null;
121+
typeHandlerType = resolveClass(typeHandlerAlias);
122+
if (typeHandlerType != null && !TypeHandler.class.isAssignableFrom(typeHandlerType)) {
123+
throw new BuilderException("Type " + typeHandlerType.getName()
124+
+ " is not a valid TypeHandler because it does not implement TypeHandler interface");
125+
}
126+
return resolveTypeHandler(propertyType, jdbcType, typeHandlerType);
127+
}
128+
129+
protected TypeHandler<?> resolveTypeHandler(Type javaType, JdbcType jdbcType,
130+
Class<? extends TypeHandler<?>> typeHandlerType) {
131+
if (typeHandlerType == null && jdbcType == null) {
123132
return null;
124133
}
125-
// javaType ignored for injected handlers see issue #746 for full detail
126-
TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
127-
// if handler not in registry, create a new one, otherwise return directly
128-
return handler == null ? typeHandlerRegistry.getInstance(javaType, typeHandlerType) : handler;
134+
return configuration.getTypeHandlerRegistry().getTypeHandler(javaType, jdbcType, typeHandlerType);
129135
}
130136

131137
protected <T> Class<? extends T> resolveAlias(String alias) {

src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
*/
1616
package org.apache.ibatis.builder;
1717

18+
import java.lang.reflect.Type;
1819
import java.sql.ResultSet;
1920
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.HashMap;
2223
import java.util.HashSet;
2324
import java.util.List;
2425
import java.util.Map;
26+
import java.util.Map.Entry;
2527
import java.util.Properties;
2628
import java.util.Set;
2729
import java.util.StringTokenizer;
@@ -45,6 +47,7 @@
4547
import org.apache.ibatis.mapping.SqlSource;
4648
import org.apache.ibatis.mapping.StatementType;
4749
import org.apache.ibatis.reflection.MetaClass;
50+
import org.apache.ibatis.reflection.ParamNameResolver;
4851
import org.apache.ibatis.scripting.LanguageDriver;
4952
import org.apache.ibatis.session.Configuration;
5053
import org.apache.ibatis.type.JdbcType;
@@ -146,7 +149,7 @@ public ParameterMapping buildParameterMapping(Class<?> parameterType, String pro
146149

147150
// Class parameterType = parameterMapBuilder.type();
148151
Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
149-
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
152+
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, jdbcType, typeHandler);
150153

151154
return new ParameterMapping.Builder(configuration, property, javaTypeClass).jdbcType(jdbcType)
152155
.resultMapId(resultMap).mode(parameterMode).numericScale(numericScale).typeHandler(typeHandlerInstance).build();
@@ -200,7 +203,7 @@ public MappedStatement addMappedStatement(String id, SqlSource sqlSource, Statem
200203
SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
201204
String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
202205
boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
203-
LanguageDriver lang, String resultSets, boolean dirtySelect) {
206+
LanguageDriver lang, String resultSets, boolean dirtySelect, ParamNameResolver paramNameResolver) {
204207

205208
if (unresolvedCacheRef) {
206209
throw new IncompleteElementException("Cache-ref not yet resolved");
@@ -213,7 +216,8 @@ public MappedStatement addMappedStatement(String id, SqlSource sqlSource, Statem
213216
.keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang)
214217
.resultOrdered(resultOrdered).resultSets(resultSets)
215218
.resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
216-
.flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);
219+
.flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect)
220+
.paramNameResolver(paramNameResolver);
217221

218222
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
219223
if (statementParameterMap != null) {
@@ -276,7 +280,7 @@ public MappedStatement addMappedStatement(String id, SqlSource sqlSource, Statem
276280
LanguageDriver lang, String resultSets) {
277281
return addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
278282
parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator,
279-
keyProperty, keyColumn, databaseId, lang, null, false);
283+
keyProperty, keyColumn, databaseId, lang, null, false, null);
280284
}
281285

282286
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
@@ -337,15 +341,15 @@ public ResultMapping buildResultMapping(Class<?> resultType, String property, St
337341
JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
338342
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
339343
boolean lazy) {
340-
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
341-
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
344+
Entry<Type, Class<?>> setterType = resolveSetterType(resultType, property, javaType);
345+
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(setterType.getKey(), jdbcType, typeHandler);
342346
List<ResultMapping> composites;
343347
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
344348
composites = Collections.emptyList();
345349
} else {
346350
composites = parseCompositeColumnName(column);
347351
}
348-
return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
352+
return new ResultMapping.Builder(configuration, property, column, setterType.getValue()).jdbcType(jdbcType)
349353
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
350354
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
351355
.typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags).composites(composites)
@@ -427,26 +431,26 @@ private List<ResultMapping> parseCompositeColumnName(String columnName) {
427431
String property = parser.nextToken();
428432
String column = parser.nextToken();
429433
ResultMapping complexResultMapping = new ResultMapping.Builder(configuration, property, column,
430-
configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
434+
(TypeHandler<?>) null).build();
431435
composites.add(complexResultMapping);
432436
}
433437
}
434438
return composites;
435439
}
436440

437-
private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
438-
if (javaType == null && property != null) {
441+
private Entry<Type, Class<?>> resolveSetterType(Class<?> resultType, String property, Class<?> javaType) {
442+
if (javaType != null) {
443+
return Map.entry(javaType, javaType);
444+
}
445+
if (property != null) {
446+
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
439447
try {
440-
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
441-
javaType = metaResultType.getSetterType(property);
448+
return metaResultType.getGenericSetterType(property);
442449
} catch (Exception e) {
443-
// ignore, following null check statement will deal with the situation
450+
// Not all property types are resolvable.
444451
}
445452
}
446-
if (javaType == null) {
447-
javaType = Object.class;
448-
}
449-
return javaType;
453+
return Map.entry(Object.class, Object.class);
450454
}
451455

452456
private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType,

src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@
1515
*/
1616
package org.apache.ibatis.builder;
1717

18+
import java.lang.reflect.Type;
19+
import java.sql.ResultSet;
1820
import java.util.List;
1921
import java.util.Map;
22+
import java.util.Map.Entry;
2023

24+
import org.apache.ibatis.binding.MapperMethod.ParamMap;
2125
import org.apache.ibatis.mapping.ParameterMapping;
2226
import org.apache.ibatis.mapping.ParameterMode;
2327
import org.apache.ibatis.parsing.TokenHandler;
2428
import org.apache.ibatis.reflection.MetaClass;
2529
import org.apache.ibatis.reflection.MetaObject;
30+
import org.apache.ibatis.reflection.ParamNameResolver;
2631
import org.apache.ibatis.reflection.property.PropertyTokenizer;
2732
import org.apache.ibatis.session.Configuration;
2833
import org.apache.ibatis.type.JdbcType;
34+
import org.apache.ibatis.type.TypeHandler;
2935

3036
public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
3137

@@ -35,26 +41,33 @@ public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHa
3541
private final MetaObject metaParameters;
3642
private final Object parameterObject;
3743
private final boolean paramExists;
44+
private final ParamNameResolver paramNameResolver;
45+
46+
private Type genericType = null;
47+
private TypeHandler<?> typeHandler = null;
3848

3949
public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
40-
Object parameterObject, Class<?> parameterType, Map<String, Object> additionalParameters, boolean paramExists) {
50+
Object parameterObject, Class<?> parameterType, Map<String, Object> additionalParameters,
51+
ParamNameResolver paramNameResolver, boolean paramExists) {
4152
super(configuration);
4253
this.parameterType = parameterObject == null ? (parameterType == null ? Object.class : parameterType)
4354
: parameterObject.getClass();
4455
this.metaParameters = configuration.newMetaObject(additionalParameters);
4556
this.parameterObject = parameterObject;
4657
this.paramExists = paramExists;
4758
this.parameterMappings = parameterMappings;
59+
this.paramNameResolver = paramNameResolver;
4860
}
4961

5062
public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
51-
Class<?> parameterType, Map<String, Object> additionalParameters) {
63+
Class<?> parameterType, Map<String, Object> additionalParameters, ParamNameResolver paramNameResolver) {
5264
super(configuration);
5365
this.parameterType = parameterType;
5466
this.metaParameters = configuration.newMetaObject(additionalParameters);
5567
this.parameterObject = null;
5668
this.paramExists = false;
5769
this.parameterMappings = parameterMappings;
70+
this.paramNameResolver = paramNameResolver;
5871
}
5972

6073
public List<ParameterMapping> getParameterMappings() {
@@ -69,60 +82,44 @@ public String handleToken(String content) {
6982

7083
private ParameterMapping buildParameterMapping(String content) {
7184
Map<String, String> propertiesMap = parseParameterMapping(content);
72-
String property = propertiesMap.get("property");
85+
86+
final String property = propertiesMap.remove("property");
87+
final JdbcType jdbcType = resolveJdbcType(propertiesMap.remove("jdbcType"));
88+
final String typeHandlerAlias = propertiesMap.remove("typeHandler");
89+
90+
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, (Class<?>) null);
7391
PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property);
74-
Class<?> propertyType;
75-
if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params
76-
propertyType = metaParameters.getGetterType(property);
77-
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
78-
propertyType = parameterType;
79-
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
80-
propertyType = java.sql.ResultSet.class;
81-
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
82-
propertyType = Object.class;
83-
} else {
84-
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
85-
if (metaClass.hasGetter(property)) {
86-
propertyType = metaClass.getGetterType(property);
87-
} else {
88-
propertyType = Object.class;
89-
}
92+
builder.jdbcType(jdbcType);
93+
final Class<?> javaType = figureOutJavaType(propertiesMap, property, propertyTokenizer, jdbcType);
94+
builder.javaType(javaType);
95+
if (genericType == null) {
96+
genericType = javaType;
97+
}
98+
if ((typeHandler == null || typeHandlerAlias != null) && genericType != null && genericType != Object.class) {
99+
typeHandler = resolveTypeHandler(parameterType, genericType, jdbcType, typeHandlerAlias);
90100
}
91-
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
92-
Class<?> javaType = propertyType;
93-
String typeHandlerAlias = null;
101+
builder.typeHandler(typeHandler);
102+
94103
ParameterMode mode = null;
95104
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
96105
String name = entry.getKey();
97106
String value = entry.getValue();
98-
if ("javaType".equals(name)) {
99-
javaType = resolveClass(value);
100-
builder.javaType(javaType);
101-
} else if ("jdbcType".equals(name)) {
102-
builder.jdbcType(resolveJdbcType(value));
103-
} else if ("mode".equals(name)) {
107+
if ("mode".equals(name)) {
104108
mode = resolveParameterMode(value);
105109
builder.mode(mode);
106110
} else if ("numericScale".equals(name)) {
107111
builder.numericScale(Integer.valueOf(value));
108112
} else if ("resultMap".equals(name)) {
109113
builder.resultMapId(value);
110-
} else if ("typeHandler".equals(name)) {
111-
typeHandlerAlias = value;
112114
} else if ("jdbcTypeName".equals(name)) {
113115
builder.jdbcTypeName(value);
114-
} else if ("property".equals(name)) {
115-
// Do Nothing
116116
} else if ("expression".equals(name)) {
117117
throw new BuilderException("Expression based parameters are not supported yet");
118118
} else {
119119
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
120120
+ "}. Valid properties are " + PARAMETER_PROPERTIES);
121121
}
122122
}
123-
if (typeHandlerAlias != null) {
124-
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
125-
}
126123
if (!ParameterMode.OUT.equals(mode) && paramExists) {
127124
if (metaParameters.hasGetter(propertyTokenizer.getName())) {
128125
builder.value(metaParameters.getValue(property));
@@ -138,6 +135,52 @@ private ParameterMapping buildParameterMapping(String content) {
138135
return builder.build();
139136
}
140137

138+
private Class<?> figureOutJavaType(Map<String, String> propertiesMap, String property,
139+
PropertyTokenizer propertyTokenizer, JdbcType jdbcType) {
140+
Class<?> javaType = resolveClass(propertiesMap.remove("javaType"));
141+
if (javaType != null) {
142+
return javaType;
143+
}
144+
if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params
145+
return metaParameters.getGetterType(property);
146+
}
147+
typeHandler = resolveTypeHandler(parameterType, jdbcType, (Class<? extends TypeHandler<?>>) null);
148+
if (typeHandler != null) {
149+
return parameterType;
150+
}
151+
if (JdbcType.CURSOR.equals(jdbcType)) {
152+
return ResultSet.class;
153+
}
154+
if (paramNameResolver != null && ParamMap.class.equals(parameterType)) {
155+
Type actualParamType = paramNameResolver.getType(property);
156+
if (actualParamType instanceof Type) {
157+
MetaClass metaClass = MetaClass.forClass(actualParamType, configuration.getReflectorFactory());
158+
String multiParamsPropertyName;
159+
if (propertyTokenizer.hasNext()) {
160+
multiParamsPropertyName = propertyTokenizer.getChildren();
161+
if (metaClass.hasGetter(multiParamsPropertyName)) {
162+
Entry<Type, Class<?>> getterType = metaClass.getGenericGetterType(multiParamsPropertyName);
163+
genericType = getterType.getKey();
164+
return getterType.getValue();
165+
}
166+
} else {
167+
genericType = actualParamType;
168+
}
169+
}
170+
return Object.class;
171+
}
172+
if (Map.class.isAssignableFrom(parameterType)) {
173+
return Object.class;
174+
}
175+
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
176+
if (metaClass.hasGetter(property)) {
177+
Entry<Type, Class<?>> getterType = metaClass.getGenericGetterType(property);
178+
genericType = getterType.getKey();
179+
return getterType.getValue();
180+
}
181+
return Object.class;
182+
}
183+
141184
private Map<String, String> parseParameterMapping(String content) {
142185
try {
143186
return new ParameterExpression(content);

0 commit comments

Comments
 (0)