Skip to content

Resolve type handler based on java.lang.reflect.Type instead of Class and respect runtime JDBC type #3379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
78276f3
Resolve type handler based on Type instead of Class
harawata Dec 20, 2022
5ceaaf4
Merge branch 'master' into type-based-handler-resolution
harawata Jan 3, 2025
960129d
License header, format
harawata Jan 3, 2025
321f46d
Fixed broken test
harawata Jan 3, 2025
2996817
Merge branch 'master' into type-based-handler-resolution
harawata Jan 28, 2025
c7805b4
Fixed a broken test
harawata Jan 28, 2025
8a79493
Merge branch 'master' into type-based-handler-resolution
harawata Jan 28, 2025
13f516a
Type handler resolution is now delayed so that JdbcType is taken into…
harawata Jan 28, 2025
84d1a4b
Suppress "unchecked" warning
harawata Jan 28, 2025
cb9e973
Resolved a TODO
harawata Jan 28, 2025
d2d9bac
Fallback for generic param names
harawata Jan 29, 2025
3e82947
Type handler mapped to Map is no longer invoked incorrectly
harawata Jan 29, 2025
c4228b1
Updated tests that are related to #591
harawata Jan 29, 2025
1c658c0
Proper test name
harawata Jan 29, 2025
bf72269
Drop TypeHandlerResolver for now
harawata Jan 29, 2025
4b8db6e
License and format
harawata Jan 29, 2025
c97c5c5
Proper type handler resolution for OUT parameters, drop UnknownTypeHa…
harawata Feb 1, 2025
c706462
Cleaned up a little bit
harawata Feb 2, 2025
7c8a3c1
Cleaned up a little bit more
harawata Feb 8, 2025
881cf64
Type handler should be applied to parameter objects
harawata Feb 9, 2025
a38d06c
Type handler should be applied to args for nested select
harawata Feb 9, 2025
a22361d
Making the behavior closer to the previous version
harawata Feb 11, 2025
1261a34
Drop declaringClass arg for now
harawata Feb 12, 2025
4ed48b1
`Jdbc3KeyGenerator` should use `Type` instead of `Class` in handler r…
harawata Feb 20, 2025
9e56276
Fill info for `@Deprecated` annotation
harawata Feb 21, 2025
bcb00f8
Revised default type handler registration
harawata Feb 26, 2025
29c8c16
Merge master into type-based-handler-resolution
harawata Mar 3, 2025
cce4726
Improve `TypeParameterResolver#toString()` for messaging
harawata Mar 5, 2025
0172c5b
Avoid corner case NPE when setting a parameter
harawata Mar 6, 2025
949ccd3
Avoid corner case NPEs when mapping results
harawata Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/java/org/apache/ibatis/binding/MapperMethod.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2024 the original author or authors.
* Copyright 2009-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -298,7 +298,7 @@ public MethodSignature(Configuration configuration, Class<?> mapperInterface, Me
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
this.paramNameResolver = new ParamNameResolver(configuration, method, mapperInterface);
}

public Object convertArgsToSqlCommandParam(Object[] args) {
Expand Down
40 changes: 23 additions & 17 deletions src/main/java/org/apache/ibatis/builder/BaseBuilder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2023 the original author or authors.
* Copyright 2009-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,7 @@
*/
package org.apache.ibatis.builder;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
Expand Down Expand Up @@ -104,28 +105,33 @@ protected <T> Class<? extends T> resolveClass(String alias) {
}
}

@Deprecated(since = "3.6.0", forRemoval = true)
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
if (typeHandlerAlias == null) {
return null;
}
Class<?> type = resolveClass(typeHandlerAlias);
if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
throw new BuilderException(
"Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
}
@SuppressWarnings("unchecked") // already verified it is a TypeHandler
Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
return resolveTypeHandler(javaType, typeHandlerType);
return resolveTypeHandler(null, javaType, null, typeHandlerAlias);
}

@Deprecated(since = "3.6.0", forRemoval = true)
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null) {
return resolveTypeHandler(javaType, null, typeHandlerType);
}

protected TypeHandler<?> resolveTypeHandler(Class<?> parameterType, Type propertyType, JdbcType jdbcType,
String typeHandlerAlias) {
Class<? extends TypeHandler<?>> typeHandlerType = null;
typeHandlerType = resolveClass(typeHandlerAlias);
if (typeHandlerType != null && !TypeHandler.class.isAssignableFrom(typeHandlerType)) {
throw new BuilderException("Type " + typeHandlerType.getName()
+ " is not a valid TypeHandler because it does not implement TypeHandler interface");
}
return resolveTypeHandler(propertyType, jdbcType, typeHandlerType);
}

protected TypeHandler<?> resolveTypeHandler(Type javaType, JdbcType jdbcType,
Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null && jdbcType == null) {
return null;
}
// javaType ignored for injected handlers see issue #746 for full detail
TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
// if handler not in registry, create a new one, otherwise return directly
return handler == null ? typeHandlerRegistry.getInstance(javaType, typeHandlerType) : handler;
return configuration.getTypeHandlerRegistry().getTypeHandler(javaType, jdbcType, typeHandlerType);
}

protected <T> Class<? extends T> resolveAlias(String alias) {
Expand Down
38 changes: 21 additions & 17 deletions src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package org.apache.ibatis.builder;

import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
Expand All @@ -45,6 +47,7 @@
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
Expand Down Expand Up @@ -146,7 +149,7 @@ public ParameterMapping buildParameterMapping(Class<?> parameterType, String pro

// Class parameterType = parameterMapBuilder.type();
Class<?> javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, jdbcType, typeHandler);

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

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

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

public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
Expand Down Expand Up @@ -337,15 +341,15 @@ public ResultMapping buildResultMapping(Class<?> resultType, String property, St
JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
boolean lazy) {
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
Entry<Type, Class<?>> setterType = resolveSetterType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(setterType.getKey(), jdbcType, typeHandler);
List<ResultMapping> composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
return new ResultMapping.Builder(configuration, property, column, setterType.getValue()).jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
.typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<>() : flags).composites(composites)
Expand Down Expand Up @@ -427,26 +431,26 @@ private List<ResultMapping> parseCompositeColumnName(String columnName) {
String property = parser.nextToken();
String column = parser.nextToken();
ResultMapping complexResultMapping = new ResultMapping.Builder(configuration, property, column,
configuration.getTypeHandlerRegistry().getUnknownTypeHandler()).build();
(TypeHandler<?>) null).build();
composites.add(complexResultMapping);
}
}
return composites;
}

private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
if (javaType == null && property != null) {
private Entry<Type, Class<?>> resolveSetterType(Class<?> resultType, String property, Class<?> javaType) {
if (javaType != null) {
return Map.entry(javaType, javaType);
}
if (property != null) {
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
try {
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
javaType = metaResultType.getSetterType(property);
return metaResultType.getGenericSetterType(property);
} catch (Exception e) {
// ignore, following null check statement will deal with the situation
// Not all property types are resolvable.
}
}
if (javaType == null) {
javaType = Object.class;
}
return javaType;
return Map.entry(Object.class, Object.class);
}

private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@
*/
package org.apache.ibatis.builder;

import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.parsing.TokenHandler;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

Expand All @@ -35,26 +41,33 @@ public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHa
private final MetaObject metaParameters;
private final Object parameterObject;
private final boolean paramExists;
private final ParamNameResolver paramNameResolver;

private Type genericType = null;
private TypeHandler<?> typeHandler = null;

public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
Object parameterObject, Class<?> parameterType, Map<String, Object> additionalParameters, boolean paramExists) {
Object parameterObject, Class<?> parameterType, Map<String, Object> additionalParameters,
ParamNameResolver paramNameResolver, boolean paramExists) {
super(configuration);
this.parameterType = parameterObject == null ? (parameterType == null ? Object.class : parameterType)
: parameterObject.getClass();
this.metaParameters = configuration.newMetaObject(additionalParameters);
this.parameterObject = parameterObject;
this.paramExists = paramExists;
this.parameterMappings = parameterMappings;
this.paramNameResolver = paramNameResolver;
}

public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
Class<?> parameterType, Map<String, Object> additionalParameters) {
Class<?> parameterType, Map<String, Object> additionalParameters, ParamNameResolver paramNameResolver) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
this.parameterObject = null;
this.paramExists = false;
this.parameterMappings = parameterMappings;
this.paramNameResolver = paramNameResolver;
}

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

private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");

final String property = propertiesMap.remove("property");
final JdbcType jdbcType = resolveJdbcType(propertiesMap.remove("jdbcType"));
final String typeHandlerAlias = propertiesMap.remove("typeHandler");

ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, (Class<?>) null);
PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property);
Class<?> propertyType;
if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
builder.jdbcType(jdbcType);
final Class<?> javaType = figureOutJavaType(propertiesMap, property, propertyTokenizer, jdbcType);
builder.javaType(javaType);
if (genericType == null) {
genericType = javaType;
}
if ((typeHandler == null || typeHandlerAlias != null) && genericType != null && genericType != Object.class) {
typeHandler = resolveTypeHandler(parameterType, genericType, jdbcType, typeHandlerAlias);
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
builder.typeHandler(typeHandler);

ParameterMode mode = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
if ("mode".equals(name)) {
mode = resolveParameterMode(value);
builder.mode(mode);
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
+ "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
if (!ParameterMode.OUT.equals(mode) && paramExists) {
if (metaParameters.hasGetter(propertyTokenizer.getName())) {
builder.value(metaParameters.getValue(property));
Expand All @@ -138,6 +135,52 @@ private ParameterMapping buildParameterMapping(String content) {
return builder.build();
}

private Class<?> figureOutJavaType(Map<String, String> propertiesMap, String property,
PropertyTokenizer propertyTokenizer, JdbcType jdbcType) {
Class<?> javaType = resolveClass(propertiesMap.remove("javaType"));
if (javaType != null) {
return javaType;
}
if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params
return metaParameters.getGetterType(property);
}
typeHandler = resolveTypeHandler(parameterType, jdbcType, (Class<? extends TypeHandler<?>>) null);
if (typeHandler != null) {
return parameterType;
}
if (JdbcType.CURSOR.equals(jdbcType)) {
return ResultSet.class;
}
if (paramNameResolver != null && ParamMap.class.equals(parameterType)) {
Type actualParamType = paramNameResolver.getType(property);
if (actualParamType instanceof Type) {
MetaClass metaClass = MetaClass.forClass(actualParamType, configuration.getReflectorFactory());
String multiParamsPropertyName;
if (propertyTokenizer.hasNext()) {
multiParamsPropertyName = propertyTokenizer.getChildren();
if (metaClass.hasGetter(multiParamsPropertyName)) {
Entry<Type, Class<?>> getterType = metaClass.getGenericGetterType(multiParamsPropertyName);
genericType = getterType.getKey();
return getterType.getValue();
}
} else {
genericType = actualParamType;
}
}
return Object.class;
}
if (Map.class.isAssignableFrom(parameterType)) {
return Object.class;
}
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
Entry<Type, Class<?>> getterType = metaClass.getGenericGetterType(property);
genericType = getterType.getKey();
return getterType.getValue();
}
return Object.class;
}

private Map<String, String> parseParameterMapping(String content) {
try {
return new ParameterExpression(content);
Expand Down
Loading