diff --git a/src/main/java/org/apache/ibatis/binding/MapperMethod.java b/src/main/java/org/apache/ibatis/binding/MapperMethod.java index 6990b4f4f63..aeac7afea31 100644 --- a/src/main/java/org/apache/ibatis/binding/MapperMethod.java +++ b/src/main/java/org/apache/ibatis/binding/MapperMethod.java @@ -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. @@ -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) { diff --git a/src/main/java/org/apache/ibatis/builder/BaseBuilder.java b/src/main/java/org/apache/ibatis/builder/BaseBuilder.java index 5c63c99ca43..0bc49151406 100644 --- a/src/main/java/org/apache/ibatis/builder/BaseBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/BaseBuilder.java @@ -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. @@ -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; @@ -104,28 +105,33 @@ protected Class 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> typeHandlerType = (Class>) type; - return resolveTypeHandler(javaType, typeHandlerType); + return resolveTypeHandler(null, javaType, null, typeHandlerAlias); } + @Deprecated(since = "3.6.0", forRemoval = true) protected TypeHandler resolveTypeHandler(Class javaType, Class> typeHandlerType) { - if (typeHandlerType == null) { + return resolveTypeHandler(javaType, null, typeHandlerType); + } + + protected TypeHandler resolveTypeHandler(Class parameterType, Type propertyType, JdbcType jdbcType, + String typeHandlerAlias) { + Class> 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> 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 Class resolveAlias(String alias) { diff --git a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java index 4067e81d899..f78dacfc3ee 100644 --- a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java +++ b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java @@ -15,6 +15,7 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collections; @@ -22,6 +23,7 @@ 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; @@ -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; @@ -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(); @@ -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"); @@ -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) { @@ -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, @@ -337,15 +341,15 @@ public ResultMapping buildResultMapping(Class resultType, String property, St JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix, Class> typeHandler, List flags, String resultSet, String foreignColumn, boolean lazy) { - Class javaTypeClass = resolveResultJavaType(resultType, property, javaType); - TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + Entry> setterType = resolveSetterType(resultType, property, javaType); + TypeHandler typeHandlerInstance = resolveTypeHandler(setterType.getKey(), jdbcType, typeHandler); List 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) @@ -427,26 +431,26 @@ private List 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> 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, diff --git a/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java index 83b76279c8a..c9946a76949 100644 --- a/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java +++ b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java @@ -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 { @@ -35,9 +41,14 @@ 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 parameterMappings, Configuration configuration, - Object parameterObject, Class parameterType, Map additionalParameters, boolean paramExists) { + Object parameterObject, Class parameterType, Map additionalParameters, + ParamNameResolver paramNameResolver, boolean paramExists) { super(configuration); this.parameterType = parameterObject == null ? (parameterType == null ? Object.class : parameterType) : parameterObject.getClass(); @@ -45,16 +56,18 @@ public ParameterMappingTokenHandler(List parameterMappings, Co this.parameterObject = parameterObject; this.paramExists = paramExists; this.parameterMappings = parameterMappings; + this.paramNameResolver = paramNameResolver; } public ParameterMappingTokenHandler(List parameterMappings, Configuration configuration, - Class parameterType, Map additionalParameters) { + Class parameterType, Map 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 getParameterMappings() { @@ -69,50 +82,37 @@ public String handleToken(String content) { private ParameterMapping buildParameterMapping(String content) { Map 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 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 { @@ -120,9 +120,6 @@ private ParameterMapping buildParameterMapping(String 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)); @@ -138,6 +135,52 @@ private ParameterMapping buildParameterMapping(String content) { return builder.build(); } + private Class figureOutJavaType(Map 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>) 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> 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> getterType = metaClass.getGenericGetterType(property); + genericType = getterType.getKey(); + return getterType.getValue(); + } + return Object.class; + } + private Map parseParameterMapping(String content) { try { return new ParameterExpression(content); diff --git a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java index a0d802f3d19..def3c898155 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -47,6 +48,7 @@ import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Options.FlushCachePolicy; +import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Property; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; @@ -81,6 +83,7 @@ import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.parsing.PropertyParser; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.reflection.TypeParameterResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -161,7 +164,7 @@ private void loadXmlResource() { } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, - configuration.getSqlFragments(), type.getName()); + configuration.getSqlFragments(), type); xmlParser.parse(); } } @@ -282,11 +285,12 @@ private Discriminator applyDiscriminator(String resultMapId, Class resultType void parseStatement(Method method) { final Class parameterTypeClass = getParameterType(method); + final ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, method, type); final LanguageDriver languageDriver = getLanguageDriver(method); getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> { final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, - languageDriver, method); + paramNameResolver, languageDriver, method); final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType(); final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation()) .orElse(null); @@ -301,7 +305,7 @@ void parseStatement(Method method) { .map(x -> (SelectKey) x.getAnnotation()).orElse(null); if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), - languageDriver); + paramNameResolver, languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; @@ -353,7 +357,8 @@ null, parameterTypeClass, resultMapId, getReturnType(method, type), resultSetTyp // TODO gcode issue #577 false, keyGenerator, keyProperty, keyColumn, statementAnnotation.getDatabaseId(), languageDriver, // ResultSets - options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect()); + options != null ? nullOrEmpty(options.resultSets()) : null, statementAnnotation.isDirtySelect(), + paramNameResolver); }); } @@ -368,16 +373,16 @@ private LanguageDriver getLanguageDriver(Method method) { private Class getParameterType(Method method) { Class parameterType = null; - Class[] parameterTypes = method.getParameterTypes(); - for (Class currentParameterType : parameterTypes) { - if (!RowBounds.class.isAssignableFrom(currentParameterType) - && !ResultHandler.class.isAssignableFrom(currentParameterType)) { - if (parameterType == null) { - parameterType = currentParameterType; - } else { - // issue #135 - parameterType = ParamMap.class; - } + Parameter[] parameters = method.getParameters(); + for (Parameter param : parameters) { + Class paramType = param.getType(); + if (RowBounds.class.isAssignableFrom(paramType) || ResultHandler.class.isAssignableFrom(paramType)) { + continue; + } + if (parameterType == null && param.getAnnotation(Param.class) == null) { + parameterType = paramType; + } else { + return ParamMap.class; } } return parameterType; @@ -543,7 +548,7 @@ private String nullOrEmpty(String value) { } private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, - Class parameterTypeClass, LanguageDriver languageDriver) { + Class parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX; Class resultTypeClass = selectKeyAnnotation.resultType(); StatementType statementType = selectKeyAnnotation.statementType(); @@ -562,12 +567,13 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St ResultSetType resultSetTypeEnum = null; String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId(); - SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null); + SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass, + paramNameResolver, languageDriver); SqlCommandType sqlCommandType = SqlCommandType.SELECT; assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator, - keyProperty, keyColumn, databaseId, languageDriver, null, false); + keyProperty, keyColumn, databaseId, languageDriver, null, false, paramNameResolver); id = assistant.applyCurrentNamespace(id, false); @@ -577,26 +583,27 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St return answer; } - private SqlSource buildSqlSource(Annotation annotation, Class parameterType, LanguageDriver languageDriver, - Method method) { + private SqlSource buildSqlSource(Annotation annotation, Class parameterType, ParamNameResolver paramNameResolver, + LanguageDriver languageDriver, Method method) { if (annotation instanceof Select) { - return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver); - } - if (annotation instanceof Update) { - return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, paramNameResolver, languageDriver); + } else if (annotation instanceof Update) { + return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Insert) { - return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Delete) { - return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof SelectKey) { - return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, paramNameResolver, + languageDriver); } return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method); } private SqlSource buildSqlSourceFromStrings(String[] strings, Class parameterTypeClass, - LanguageDriver languageDriver) { - return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass); + ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { + return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass, + paramNameResolver); } @SafeVarargs diff --git a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java index 499292e1880..85e6e67862d 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java @@ -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. @@ -40,7 +40,7 @@ public class ProviderSqlSource implements SqlSource { private final LanguageDriver languageDriver; private final Method mapperMethod; private final Method providerMethod; - private final String[] providerMethodArgumentNames; + private final ParamNameResolver paramNameResolver; private final Class[] providerMethodParameterTypes; private final ProviderContext providerContext; private final Integer providerContextIndex; @@ -138,7 +138,7 @@ public ProviderSqlSource(Configuration configuration, Annotation provider, Class + "' not found in SqlProvider '" + this.providerType.getName() + "'."); } this.providerMethod = candidateProviderMethod; - this.providerMethodArgumentNames = new ParamNameResolver(configuration, this.providerMethod).getNames(); + this.paramNameResolver = new ParamNameResolver(configuration, this.providerMethod, mapperType); this.providerMethodParameterTypes = this.providerMethod.getParameterTypes(); ProviderContext candidateProviderContext = null; @@ -178,7 +178,7 @@ private SqlSource createSqlSource(Object parameterObject) { } else { @SuppressWarnings("unchecked") Map params = (Map) parameterObject; - sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames)); + sql = invokeProviderMethod(extractProviderMethodArguments(params, paramNameResolver.getNames())); } } else { switch (providerMethodParameterTypes.length) { @@ -202,7 +202,7 @@ private SqlSource createSqlSource(Object parameterObject) { } } Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - return languageDriver.createSqlSource(configuration, sql, parameterType); + return languageDriver.createSqlSource(configuration, sql, parameterType, paramNameResolver); } catch (BuilderException e) { throw e; } catch (Exception e) { diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java index 98e9c221c6b..1de56ae6d78 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java @@ -58,8 +58,9 @@ public class XMLMapperBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final Map sqlFragments; private final String resource; + private Class mapperClass; - @Deprecated + @Deprecated(since = "3.6.0", forRemoval = true) public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map sqlFragments, String namespace) { this(reader, configuration, resource, sqlFragments); @@ -73,6 +74,12 @@ public XMLMapperBuilder(Reader reader, Configuration configuration, String resou resource, sqlFragments); } + public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, + Map sqlFragments, Class mapperClass) { + this(inputStream, configuration, resource, sqlFragments, mapperClass.getName()); + this.mapperClass = mapperClass; + } + public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map sqlFragments, String namespace) { this(inputStream, configuration, resource, sqlFragments); @@ -137,7 +144,7 @@ private void buildStatementFromContext(List list) { private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, - requiredDatabaseId); + requiredDatabaseId, mapperClass); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java index 4ebca8917a8..103d8644417 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java @@ -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. @@ -15,9 +15,14 @@ */ package org.apache.ibatis.builder.xml; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.builder.BaseBuilder; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder; @@ -31,6 +36,7 @@ import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -42,6 +48,7 @@ public class XMLStatementBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final XNode context; private final String requiredDatabaseId; + private final Class mapperClass; public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) { this(configuration, builderAssistant, context, null); @@ -49,10 +56,16 @@ public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant b public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) { + this(configuration, builderAssistant, context, null, null); + } + + public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, + String databaseId, Class mapperClass) { super(configuration); this.builderAssistant = builderAssistant; this.context = context; this.requiredDatabaseId = databaseId; + this.mapperClass = mapperClass; } public void parseStatementNode() { @@ -76,6 +89,25 @@ public void parseStatementNode() { String parameterType = context.getStringAttribute("parameterType"); Class parameterTypeClass = resolveClass(parameterType); + ParamNameResolver paramNameResolver = null; + if (parameterTypeClass == null && mapperClass != null) { + List mapperMethods = Arrays.stream(mapperClass.getMethods()) + .filter(m -> m.getName().equals(id) && !m.isDefault() && !m.isBridge()).collect(Collectors.toList()); + if (mapperMethods.size() == 1) { + paramNameResolver = new ParamNameResolver(configuration, mapperMethods.get(0), mapperClass); + if (paramNameResolver.isUseParamMap()) { + parameterTypeClass = ParamMap.class; + } else { + String[] paramNames = paramNameResolver.getNames(); + if (paramNames.length == 1) { + Type paramType = paramNameResolver.getType(paramNames[0]); + if (paramType instanceof Class) { + parameterTypeClass = (Class) paramType; + } + } + } + } + } String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); @@ -95,7 +127,7 @@ public void parseStatementNode() { ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } - SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass, paramNameResolver); StatementType statementType = StatementType .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); Integer fetchSize = context.getIntAttribute("fetchSize"); @@ -119,7 +151,7 @@ public void parseStatementNode() { builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect); + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect, paramNameResolver); } private void processSelectKeyNodes(String id, Class parameterTypeClass, LanguageDriver langDriver) { @@ -168,7 +200,7 @@ private void parseSelectKeyNode(String id, XNode nodeToHandle, Class paramete builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, - keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null, false); + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null, false, null); id = builderAssistant.applyCurrentNamespace(id, false); diff --git a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java index 509f4e84fcd..e70f157b123 100644 --- a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java @@ -36,6 +36,7 @@ import org.apache.ibatis.mapping.ParameterMode; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.LocalCacheScope; @@ -218,13 +219,18 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - value = parameterObject; } else { - if (metaObject == null) { - metaObject = configuration.newMetaObject(parameterObject); + ParamNameResolver paramNameResolver = ms.getParamNameResolver(); + if (paramNameResolver != null + && typeHandlerRegistry.hasTypeHandler(paramNameResolver.getType(paramNameResolver.getNames()[0])) + || typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + value = parameterObject; + } else { + if (metaObject == null) { + metaObject = configuration.newMetaObject(parameterObject); + } + value = metaObject.getValue(propertyName); } - value = metaObject.getValue(propertyName); } cacheKey.update(value); } diff --git a/src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java b/src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java index 69056a90384..7c7ef3cb8ee 100644 --- a/src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java +++ b/src/main/java/org/apache/ibatis/executor/keygen/Jdbc3KeyGenerator.java @@ -15,6 +15,7 @@ */ package org.apache.ibatis.executor.keygen; +import java.lang.reflect.Type; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -259,9 +260,12 @@ protected void assign(ResultSet rs, Object param) { throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '" + metaParam.getOriginalObject().getClass().getName() + "'."); } - Class propertyType = metaParam.getSetterType(propertyName); - typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, - JdbcType.forCode(rsmd.getColumnType(columnPosition))); + Type propertyType = metaParam.getGenericSetterType(propertyName).getKey(); + JdbcType jdbcType = JdbcType.forCode(rsmd.getColumnType(columnPosition)); + typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(jdbcType); + } } if (typeHandler == null) { // Error? diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index c99ea625be9..7c1c8cd348c 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -17,6 +17,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Parameter; +import java.lang.reflect.Type; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -66,6 +67,7 @@ import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.ObjectTypeHandler; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; @@ -158,8 +160,23 @@ public void handleOutputParameters(CallableStatement cs) throws SQLException { if (ResultSet.class.equals(parameterMapping.getJavaType())) { handleRefCursorOutputParameter((ResultSet) cs.getObject(i + 1), parameterMapping, metaParam); } else { - final TypeHandler typeHandler = parameterMapping.getTypeHandler(); - metaParam.setValue(parameterMapping.getProperty(), typeHandler.getResult(cs, i + 1)); + final String property = parameterMapping.getProperty(); + TypeHandler typeHandler = parameterMapping.getTypeHandler(); + if (typeHandler == null) { + Type javaType = parameterMapping.getJavaType(); + if (javaType == null || javaType == Object.class) { + javaType = metaParam.getGenericSetterType(property).getKey(); + } + JdbcType jdbcType = parameterMapping.getJdbcType(); + typeHandler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType, null); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(jdbcType); + if (typeHandler == null) { + typeHandler = ObjectTypeHandler.INSTANCE; + } + } + } + metaParam.setValue(property, typeHandler.getResult(cs, i + 1)); } } } @@ -384,7 +401,7 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { - ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); + ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw, resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap, null, null); if (!useCollectionConstructorInjection) { storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); @@ -539,8 +556,7 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, if (propertyMapping.isCompositeResult() || column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)) || propertyMapping.getResultSet() != null) { - Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, - columnPrefix); + Object value = getPropertyMappingValue(rsw, metaObject, propertyMapping, lazyLoader, columnPrefix); // issue #541 make property optional final String property = propertyMapping.getProperty(); if (property == null) { @@ -563,13 +579,14 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, return foundValues; } - private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, - ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + private Object getPropertyMappingValue(ResultSetWrapper rsw, MetaObject metaResultObject, + ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + final ResultSet rs = rsw.getResultSet(); if (propertyMapping.getNestedQueryId() != null) { - return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); + return getNestedQueryMappingValue(rsw, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } if (JdbcType.CURSOR.equals(propertyMapping.getJdbcType())) { - List results = getNestedCursorValue(rs, propertyMapping, columnPrefix); + List results = getNestedCursorValue(rsw, propertyMapping, columnPrefix); linkObjects(metaResultObject, propertyMapping, results.get(0), true); return metaResultObject.getValue(propertyMapping.getProperty()); } @@ -577,21 +594,31 @@ private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? return DEFERRED; } else { - final TypeHandler typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); + TypeHandler typeHandler = propertyMapping.getTypeHandler(); + if (typeHandler == null) { + final String property = propertyMapping.getProperty(); + final Type javaType = property == null ? null : metaResultObject.getGenericSetterType(property).getKey(); + typeHandler = rsw.getTypeHandler(javaType, column); + if (typeHandler == null) { + throw new ExecutorException( + "No type handler found for '" + javaType + "' and JDBC type '" + rsw.getJdbcType(column) + "'"); + } + } return typeHandler.getResult(rs, column); } } - private List getNestedCursorValue(ResultSet rs, ResultMapping propertyMapping, String parentColumnPrefix) - throws SQLException { + private List getNestedCursorValue(ResultSetWrapper rsw, ResultMapping propertyMapping, + String parentColumnPrefix) throws SQLException { final String column = prependPrefix(propertyMapping.getColumn(), parentColumnPrefix); - ResultMap nestedResultMap = resolveDiscriminatedResultMap(rs, + ResultMap nestedResultMap = resolveDiscriminatedResultMap(rsw, configuration.getResultMap(propertyMapping.getNestedResultMapId()), getColumnPrefix(parentColumnPrefix, propertyMapping)); - ResultSetWrapper rsw = new ResultSetWrapper(rs.getObject(column, ResultSet.class), configuration); + ResultSetWrapper nestedRsw = new ResultSetWrapper(rsw.getResultSet().getObject(column, ResultSet.class), + configuration); List results = new ArrayList<>(); - handleResultSet(rsw, nestedResultMap, results, null); + handleResultSet(nestedRsw, nestedResultMap, results, null); return results; } @@ -622,11 +649,11 @@ private List createAutomaticMappings(ResultSetWrapper if (resultMap.getMappedProperties().contains(property)) { continue; } - final Class propertyType = metaObject.getSetterType(property); - if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) { - final TypeHandler typeHandler = rsw.getTypeHandler(propertyType, columnName); - autoMapping - .add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive())); + final Type propertyType = metaObject.getGenericSetterType(property).getKey(); + TypeHandler typeHandler = rsw.getTypeHandler(propertyType, columnName); + if (typeHandler != null) { + autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, + propertyType instanceof Class && ((Class) propertyType).isPrimitive())); } else { configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property, propertyType); @@ -777,9 +804,9 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType final Object value; try { if (constructorMapping.getNestedQueryId() != null) { - value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix); + value = getNestedQueryConstructorValue(rsw, constructorMapping, columnPrefix); } else if (JdbcType.CURSOR.equals(constructorMapping.getJdbcType())) { - List result = (List) getNestedCursorValue(rsw.getResultSet(), constructorMapping, columnPrefix).get(0); + List result = (List) getNestedCursorValue(rsw, constructorMapping, columnPrefix).get(0); if (objectFactory.isCollection(parameterType)) { MetaObject collection = configuration.newMetaObject(objectFactory.create(parameterType)); collection.addAll((List) result); @@ -789,12 +816,15 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType } } else if (constructorMapping.getNestedResultMapId() != null) { final String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); - final ResultMap resultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), + final ResultMap resultMap = resolveDiscriminatedResultMap(rsw, configuration.getResultMap(constructorMapping.getNestedResultMapId()), constructorColumnPrefix); value = getRowValue(rsw, resultMap, constructorColumnPrefix, useCollectionConstructorInjection ? parentRowKey : null); } else { - final TypeHandler typeHandler = constructorMapping.getTypeHandler(); + TypeHandler typeHandler = constructorMapping.getTypeHandler(); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(constructorMapping.getJavaType(), rsw.getJdbcType(column)); + } value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix)); } } catch (ResultMapException | SQLException e) { @@ -969,12 +999,12 @@ private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resul // NESTED QUERY // - private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix) - throws SQLException { + private Object getNestedQueryConstructorValue(ResultSetWrapper rsw, ResultMapping constructorMapping, + String columnPrefix) throws SQLException { final String nestedQueryId = constructorMapping.getNestedQueryId(); final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId); final Class nestedQueryParameterType = nestedQuery.getParameterMap().getType(); - final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping, + final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rsw, constructorMapping, nestedQueryParameterType, columnPrefix); Object value = null; if (nestedQueryParameterObject != null) { @@ -989,13 +1019,13 @@ private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constr return value; } - private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, - ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + private Object getNestedQueryMappingValue(ResultSetWrapper rsw, MetaObject metaResultObject, + ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { final String nestedQueryId = propertyMapping.getNestedQueryId(); final String property = propertyMapping.getProperty(); final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId); final Class nestedQueryParameterType = nestedQuery.getParameterMap().getType(); - final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, + final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rsw, propertyMapping, nestedQueryParameterType, columnPrefix); Object value = null; if (nestedQueryParameterObject != null) { @@ -1020,34 +1050,33 @@ private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObj return value; } - private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class parameterType, - String columnPrefix) throws SQLException { + private Object prepareParameterForNestedQuery(ResultSetWrapper rsw, ResultMapping resultMapping, + Class parameterType, String columnPrefix) throws SQLException { if (resultMapping.isCompositeResult()) { - return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix); + return prepareCompositeKeyParameter(rsw, resultMapping, parameterType, columnPrefix); } - return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix); + return prepareSimpleKeyParameter(rsw, resultMapping, parameterType, columnPrefix); } - private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class parameterType, + private Object prepareSimpleKeyParameter(ResultSetWrapper rsw, ResultMapping resultMapping, Class parameterType, String columnPrefix) throws SQLException { - final TypeHandler typeHandler; - if (typeHandlerRegistry.hasTypeHandler(parameterType)) { - typeHandler = typeHandlerRegistry.getTypeHandler(parameterType); - } else { - typeHandler = typeHandlerRegistry.getUnknownTypeHandler(); - } - return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix)); + // parameterType is ignored in this case + final String columnName = prependPrefix(resultMapping.getColumn(), columnPrefix); + final TypeHandler typeHandler = rsw.getTypeHandler(null, columnName); + return typeHandler.getResult(rsw.getResultSet(), columnName); } - private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class parameterType, + private Object prepareCompositeKeyParameter(ResultSetWrapper rsw, ResultMapping resultMapping, Class parameterType, String columnPrefix) throws SQLException { + // Map is used if parameterType is not specified final Object parameterObject = instantiateParameterObject(parameterType); final MetaObject metaObject = configuration.newMetaObject(parameterObject); boolean foundValues = false; for (ResultMapping innerResultMapping : resultMapping.getComposites()) { - final Class propType = metaObject.getSetterType(innerResultMapping.getProperty()); - final TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(propType); - final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix)); + final String columnName = prependPrefix(innerResultMapping.getColumn(), columnPrefix); + final TypeHandler typeHandler = rsw + .getTypeHandler(metaObject.getGenericSetterType(innerResultMapping.getProperty()).getKey(), columnName); + final Object propValue = typeHandler.getResult(rsw.getResultSet(), columnName); // issue #353 & #560 do not execute nested query if key is null if (propValue != null) { metaObject.setValue(innerResultMapping.getProperty(), propValue); @@ -1072,12 +1101,12 @@ private Object instantiateParameterObject(Class parameterType) { // DISCRIMINATOR // - public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) + public ResultMap resolveDiscriminatedResultMap(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { Set pastDiscriminators = new HashSet<>(); Discriminator discriminator = resultMap.getDiscriminator(); while (discriminator != null) { - final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix); + final Object value = getDiscriminatorValue(rsw, discriminator, columnPrefix); final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value)); if (!configuration.hasResultMap(discriminatedMapId)) { break; @@ -1092,11 +1121,15 @@ public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap return resultMap; } - private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) + private Object getDiscriminatorValue(ResultSetWrapper rsw, Discriminator discriminator, String columnPrefix) throws SQLException { final ResultMapping resultMapping = discriminator.getResultMapping(); - final TypeHandler typeHandler = resultMapping.getTypeHandler(); - return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix)); + String column = prependPrefix(resultMapping.getColumn(), columnPrefix); + TypeHandler typeHandler = resultMapping.getTypeHandler(); + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.getJavaType(), rsw.getJdbcType(column)); + } + return typeHandler.getResult(rsw.getResultSet(), column); } private String prependPrefix(String columnName, String prefix) { @@ -1124,7 +1157,7 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r Object rowValue = previousRowValue; while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { - final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); + final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw, resultMap, null); final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); final Object partialObject = nestedResultObjects.get(rowKey); @@ -1199,7 +1232,7 @@ private void linkNestedPendingCreations(ResultSetWrapper rsw, ResultMap resultMa } final String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); - final ResultMap nestedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), + final ResultMap nestedResultMap = resolveDiscriminatedResultMap(rsw, configuration.getResultMap(constructorMapping.getNestedResultMapId()), constructorColumnPrefix); final Object actualValue = constructorArgs.get(index); @@ -1255,7 +1288,7 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res try { final String columnPrefix = getColumnPrefix(parentPrefix, constructorMapping); - final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix); + final ResultMap nestedResultMap = getNestedResultMap(rsw, nestedResultMapId, columnPrefix); final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix); final CacheKey combinedKey = combineKeys(rowKey, parentRowKey); @@ -1373,7 +1406,7 @@ private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap result if (nestedResultMapId != null && resultMapping.getResultSet() == null) { try { final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping); - final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix); + final ResultMap nestedResultMap = getNestedResultMap(rsw, nestedResultMapId, columnPrefix); if (resultMapping.getColumnPrefix() == null) { // try to fill circular reference only when columnPrefix // is not specified for the nested result map (issue #215) @@ -1448,10 +1481,10 @@ private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String col return true; } - private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix) + private ResultMap getNestedResultMap(ResultSetWrapper rsw, String nestedResultMapId, String columnPrefix) throws SQLException { ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId); - return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix); + return resolveDiscriminatedResultMap(rsw, nestedResultMap, columnPrefix); } // @@ -1504,10 +1537,16 @@ private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapp for (ResultMapping resultMapping : resultMappings) { if (resultMapping.isSimple()) { final String column = prependPrefix(resultMapping.getColumn(), columnPrefix); - final TypeHandler th = resultMapping.getTypeHandler(); + TypeHandler th = resultMapping.getTypeHandler(); Set mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix); // Issue #114 if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) { + if (th == null) { + th = typeHandlerRegistry.getTypeHandler(rsw.getJdbcType(column)); + } + if (th == null) { + th = ObjectTypeHandler.INSTANCE; + } final Object value = th.getResult(rsw.getResultSet(), column); if (value != null || configuration.isReturnInstanceForEmptyRow()) { cacheKey.update(column); diff --git a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java index 5623f1f29c5..12242503dab 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/ResultSetWrapper.java @@ -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. @@ -15,6 +15,7 @@ */ package org.apache.ibatis.executor.resultset; +import java.lang.reflect.Type; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -34,7 +35,6 @@ import org.apache.ibatis.type.ObjectTypeHandler; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; -import org.apache.ibatis.type.UnknownTypeHandler; /** * @author Iwao AVE! @@ -46,7 +46,7 @@ public class ResultSetWrapper { private final List columnNames = new ArrayList<>(); private final List classNames = new ArrayList<>(); private final List jdbcTypes = new ArrayList<>(); - private final Map, TypeHandler>> typeHandlerMap = new HashMap<>(); + private final Map>> typeHandlerMap = new HashMap<>(); private final Map> mappedColumnNamesMap = new HashMap<>(); private final Map> unMappedColumnNamesMap = new HashMap<>(); @@ -79,12 +79,8 @@ public List getJdbcTypes() { } public JdbcType getJdbcType(String columnName) { - for (int i = 0; i < columnNames.size(); i++) { - if (columnNames.get(i).equalsIgnoreCase(columnName)) { - return jdbcTypes.get(i); - } - } - return null; + int columnIndex = getColumnIndex(columnName); + return columnIndex == -1 ? null : jdbcTypes.get(columnIndex); } /** @@ -98,40 +94,34 @@ public JdbcType getJdbcType(String columnName) { * * @return the type handler */ - public TypeHandler getTypeHandler(Class propertyType, String columnName) { - TypeHandler handler = null; - Map, TypeHandler> columnHandlers = typeHandlerMap.get(columnName); - if (columnHandlers == null) { - columnHandlers = new HashMap<>(); - typeHandlerMap.put(columnName, columnHandlers); - } else { - handler = columnHandlers.get(propertyType); - } - if (handler == null) { - JdbcType jdbcType = getJdbcType(columnName); - handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType); - // Replicate logic of UnknownTypeHandler#resolveTypeHandler - // See issue #59 comment 10 - if (handler == null || handler instanceof UnknownTypeHandler) { - final int index = columnNames.indexOf(columnName); - final Class javaType = resolveClass(classNames.get(index)); - if (javaType != null && jdbcType != null) { - handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType); - } else if (javaType != null) { - handler = typeHandlerRegistry.getTypeHandler(javaType); - } else if (jdbcType != null) { - handler = typeHandlerRegistry.getTypeHandler(jdbcType); - } + public TypeHandler getTypeHandler(Type propertyType, String columnName) { + return typeHandlerMap.computeIfAbsent(columnName, k -> new HashMap<>()).computeIfAbsent(propertyType, k -> { + int index = getColumnIndex(columnName); + if (index == -1) { + return ObjectTypeHandler.INSTANCE; } - if (handler == null || handler instanceof UnknownTypeHandler) { - handler = new ObjectTypeHandler(); + + JdbcType jdbcType = jdbcTypes.get(index); + TypeHandler handler = typeHandlerRegistry.getTypeHandler(k, jdbcType, null); + if (handler != null) { + return handler; } - columnHandlers.put(propertyType, handler); - } - return handler; + + Class javaType = resolveClass(classNames.get(index)); + if (!(k instanceof Class && ((Class) k).isAssignableFrom(javaType))) { + // Clearly incompatible + return null; + } + + handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType, null); + if (handler == null) { + handler = typeHandlerRegistry.getTypeHandler(jdbcType); + } + return handler == null ? ObjectTypeHandler.INSTANCE : handler; + }); } - private Class resolveClass(String className) { + static Class resolveClass(String className) { try { // #699 className could be null if (className != null) { @@ -143,6 +133,15 @@ private Class resolveClass(String className) { return null; } + private int getColumnIndex(String columnName) { + for (int i = 0; i < columnNames.size(); i++) { + if (columnNames.get(i).equalsIgnoreCase(columnName)) { + return i; + } + } + return -1; + } + private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException { Set mappedColumnNames = new HashSet<>(); List unmappedColumnNames = new ArrayList<>(); diff --git a/src/main/java/org/apache/ibatis/jdbc/Null.java b/src/main/java/org/apache/ibatis/jdbc/Null.java index 5843cbd6c81..12b42ee0f7c 100644 --- a/src/main/java/org/apache/ibatis/jdbc/Null.java +++ b/src/main/java/org/apache/ibatis/jdbc/Null.java @@ -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. @@ -70,9 +70,9 @@ public enum Null { LONGVARBINARY(new BlobTypeHandler(), JdbcType.LONGVARBINARY), - OBJECT(new ObjectTypeHandler(), JdbcType.OTHER), + OBJECT(ObjectTypeHandler.INSTANCE, JdbcType.OTHER), - OTHER(new ObjectTypeHandler(), JdbcType.OTHER), + OTHER(ObjectTypeHandler.INSTANCE, JdbcType.OTHER), TIMESTAMP(new DateTypeHandler(), JdbcType.TIMESTAMP), diff --git a/src/main/java/org/apache/ibatis/jdbc/SqlRunner.java b/src/main/java/org/apache/ibatis/jdbc/SqlRunner.java index aef4c0f478b..e3cc53c6cb9 100644 --- a/src/main/java/org/apache/ibatis/jdbc/SqlRunner.java +++ b/src/main/java/org/apache/ibatis/jdbc/SqlRunner.java @@ -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. @@ -29,6 +29,7 @@ import java.util.Map; import org.apache.ibatis.io.Resources; +import org.apache.ibatis.type.ObjectTypeHandler; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; @@ -242,11 +243,11 @@ private List> getResults(ResultSet rs) throws SQLException { Class type = Resources.classForName(rsmd.getColumnClassName(i + 1)); TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(type); if (typeHandler == null) { - typeHandler = typeHandlerRegistry.getTypeHandler(Object.class); + typeHandler = ObjectTypeHandler.INSTANCE; } typeHandlers.add(typeHandler); } catch (Exception e) { - typeHandlers.add(typeHandlerRegistry.getTypeHandler(Object.class)); + typeHandlers.add(ObjectTypeHandler.INSTANCE); } } while (rs.next()) { diff --git a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java index 413e9f6ddd1..39ad6174ee1 100644 --- a/src/main/java/org/apache/ibatis/mapping/MappedStatement.java +++ b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java @@ -25,6 +25,7 @@ import org.apache.ibatis.executor.keygen.NoKeyGenerator; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; @@ -56,6 +57,7 @@ public final class MappedStatement { private Log statementLog; private LanguageDriver lang; private String[] resultSets; + private ParamNameResolver paramNameResolver; private boolean dirtySelect; MappedStatement() { @@ -182,6 +184,11 @@ public Builder dirtySelect(boolean dirtySelect) { return this; } + public Builder paramNameResolver(ParamNameResolver paramNameResolver) { + mappedStatement.paramNameResolver = paramNameResolver; + return this; + } + /** * Resul sets. * @@ -304,6 +311,10 @@ public boolean isDirtySelect() { return dirtySelect; } + public ParamNameResolver getParamNameResolver() { + return paramNameResolver; + } + /** * Gets the resul sets. * diff --git a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java index 7265499c600..bbe9e5f5c89 100644 --- a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java @@ -20,7 +20,6 @@ import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; -import org.apache.ibatis.type.TypeHandlerRegistry; /** * @author Clinton Begin @@ -107,33 +106,16 @@ public Builder value(Object value) { } public ParameterMapping build() { - resolveTypeHandler(); validate(); return parameterMapping; } private void validate() { - if (ResultSet.class.equals(parameterMapping.javaType)) { - if (parameterMapping.resultMapId == null) { - throw new IllegalStateException("Missing resultmap in property '" + parameterMapping.property + "'. " - + "Parameters of type java.sql.ResultSet require a resultmap."); - } - } else if (parameterMapping.typeHandler == null) { - throw new IllegalStateException("Type handler was null on parameter mapping for property '" - + parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType (" - + parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination."); + if (ResultSet.class.equals(parameterMapping.javaType) && parameterMapping.resultMapId == null) { + throw new IllegalStateException("Missing resultmap in property '" + parameterMapping.property + "'. " + + "Parameters of type java.sql.ResultSet require a resultmap."); } } - - private void resolveTypeHandler() { - if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) { - Configuration configuration = parameterMapping.configuration; - TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); - parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, - parameterMapping.jdbcType); - } - } - } public String getProperty() { diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java index a0968a8cf64..625d3afb816 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java @@ -23,7 +23,6 @@ import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; -import org.apache.ibatis.type.TypeHandlerRegistry; /** * @author Clinton Begin @@ -155,7 +154,6 @@ public ResultMapping build() { // lock down collections resultMapping.flags = Collections.unmodifiableList(resultMapping.flags); resultMapping.composites = Collections.unmodifiableList(resultMapping.composites); - resolveTypeHandler(); validate(); return resultMapping; } @@ -166,11 +164,6 @@ private void validate() { throw new IllegalStateException( "Cannot define both nestedQueryId and nestedResultMapId in property " + resultMapping.property); } - // Issue #5: there should be no mappings without typehandler - if (resultMapping.nestedQueryId == null && resultMapping.nestedResultMapId == null - && resultMapping.typeHandler == null) { - throw new IllegalStateException("No typehandler found for property " + resultMapping.property); - } // Issue #4 and GH #39: column is optional only in nested resultmaps but not in the rest if (resultMapping.nestedResultMapId == null && resultMapping.column == null && resultMapping.composites.isEmpty()) { @@ -192,14 +185,6 @@ private void validate() { } } - private void resolveTypeHandler() { - if (resultMapping.typeHandler == null && resultMapping.javaType != null) { - Configuration configuration = resultMapping.configuration; - TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); - resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType); - } - } - public Builder column(String column) { resultMapping.column = column; return this; diff --git a/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java b/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java index fa7886d6275..d1fdff88dd4 100644 --- a/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java @@ -15,12 +15,13 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class DefaultReflectorFactory implements ReflectorFactory { private boolean classCacheEnabled = true; - private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + private final ConcurrentMap reflectorMap = new ConcurrentHashMap<>(); public DefaultReflectorFactory() { } @@ -36,7 +37,7 @@ public void setClassCacheEnabled(boolean classCacheEnabled) { } @Override - public Reflector findForClass(Class type) { + public Reflector findForClass(Type type) { if (classCacheEnabled) { // synchronized (type) removed see issue #461 return reflectorMap.computeIfAbsent(type, Reflector::new); diff --git a/src/main/java/org/apache/ibatis/reflection/MetaClass.java b/src/main/java/org/apache/ibatis/reflection/MetaClass.java index 538238b3940..ae62b17efaa 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaClass.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaClass.java @@ -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. @@ -15,15 +15,13 @@ */ package org.apache.ibatis.reflection; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; -import org.apache.ibatis.reflection.invoker.GetFieldInvoker; import org.apache.ibatis.reflection.invoker.Invoker; -import org.apache.ibatis.reflection.invoker.MethodInvoker; import org.apache.ibatis.reflection.property.PropertyTokenizer; /** @@ -34,12 +32,12 @@ public class MetaClass { private final ReflectorFactory reflectorFactory; private final Reflector reflector; - private MetaClass(Class type, ReflectorFactory reflectorFactory) { + private MetaClass(Type type, ReflectorFactory reflectorFactory) { this.reflectorFactory = reflectorFactory; this.reflector = reflectorFactory.findForClass(type); } - public static MetaClass forClass(Class type, ReflectorFactory reflectorFactory) { + public static MetaClass forClass(Type type, ReflectorFactory reflectorFactory) { return new MetaClass(type, reflectorFactory); } @@ -77,59 +75,57 @@ public Class getSetterType(String name) { return reflector.getSetterType(prop.getName()); } + public Entry> getGenericSetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaClass metaProp = metaClassForProperty(prop); + return metaProp.getGenericSetterType(prop.getChildren()); + } + return reflector.getGenericSetterType(prop.getName()); + } + public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); if (prop.hasNext()) { MetaClass metaProp = metaClassForProperty(prop); return metaProp.getGetterType(prop.getChildren()); } - // issue #506. Resolve the type inside a Collection Object + return getGetterType(prop).getValue(); + } + + public Entry> getGenericGetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaClass metaProp = metaClassForProperty(prop); + return metaProp.getGenericGetterType(prop.getChildren()); + } return getGetterType(prop); } private MetaClass metaClassForProperty(PropertyTokenizer prop) { - Class propType = getGetterType(prop); + Class propType = getGetterType(prop).getValue(); return MetaClass.forClass(propType, reflectorFactory); } - private Class getGetterType(PropertyTokenizer prop) { - Class type = reflector.getGetterType(prop.getName()); - if (prop.getIndex() != null && Collection.class.isAssignableFrom(type)) { - Type returnType = getGenericGetterType(prop.getName()); + private Entry> getGetterType(PropertyTokenizer prop) { + // Resolve the type inside a Collection Object + // https://github.com/mybatis/old-google-code-issues/issues/506 + Entry> pair = reflector.getGenericGetterType(prop.getName()); + if (prop.getIndex() != null && Collection.class.isAssignableFrom(pair.getValue())) { + Type returnType = pair.getKey(); if (returnType instanceof ParameterizedType) { Type[] actualTypeArguments = ((ParameterizedType) returnType).getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length == 1) { returnType = actualTypeArguments[0]; if (returnType instanceof Class) { - type = (Class) returnType; + return Map.entry(returnType, (Class) returnType); } else if (returnType instanceof ParameterizedType) { - type = (Class) ((ParameterizedType) returnType).getRawType(); + return Map.entry(returnType, (Class) ((ParameterizedType) returnType).getRawType()); } } } } - return type; - } - - private Type getGenericGetterType(String propertyName) { - try { - Invoker invoker = reflector.getGetInvoker(propertyName); - if (invoker instanceof MethodInvoker) { - Field declaredMethod = MethodInvoker.class.getDeclaredField("method"); - declaredMethod.setAccessible(true); - Method method = (Method) declaredMethod.get(invoker); - return TypeParameterResolver.resolveReturnType(method, reflector.getType()); - } - if (invoker instanceof GetFieldInvoker) { - Field declaredField = GetFieldInvoker.class.getDeclaredField("field"); - declaredField.setAccessible(true); - Field field = (Field) declaredField.get(invoker); - return TypeParameterResolver.resolveFieldType(field, reflector.getType()); - } - } catch (NoSuchFieldException | IllegalAccessException e) { - // Ignored - } - return null; + return pair; } public boolean hasSetter(String name) { diff --git a/src/main/java/org/apache/ibatis/reflection/MetaObject.java b/src/main/java/org/apache/ibatis/reflection/MetaObject.java index 0bfcb669387..3365d3af09d 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaObject.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaObject.java @@ -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. @@ -15,9 +15,11 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.property.PropertyTokenizer; @@ -98,10 +100,18 @@ public Class getSetterType(String name) { return objectWrapper.getSetterType(name); } + public Entry> getGenericSetterType(String name) { + return objectWrapper.getGenericSetterType(name); + } + public Class getGetterType(String name) { return objectWrapper.getGetterType(name); } + public Entry> getGenericGetterType(String name) { + return objectWrapper.getGenericGetterType(name); + } + public boolean hasSetter(String name) { return objectWrapper.hasSetter(name); } diff --git a/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java b/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java index aab8e3c91da..60df6520d04 100644 --- a/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java @@ -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. @@ -16,9 +16,13 @@ package org.apache.ibatis.reflection; import java.lang.annotation.Annotation; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,6 +31,7 @@ import org.apache.ibatis.annotations.Param; import org.apache.ibatis.binding.MapperMethod.ParamMap; +import org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @@ -57,14 +62,17 @@ public class ParamNameResolver { * */ private final SortedMap names; + private final Map typeMap = new HashMap<>(); private boolean hasParamAnnotation; + private boolean useParamMap; - public ParamNameResolver(Configuration config, Method method) { + public ParamNameResolver(Configuration config, Method method, Class mapperClass) { this.useActualParamName = config.isUseActualParamName(); final Class[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap map = new TreeMap<>(); + Type[] actualParamTypes = TypeParameterResolver.resolveParamTypes(method, mapperClass); int paramCount = paramAnnotations.length; // get names from @Param annotations for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { @@ -76,6 +84,7 @@ public ParamNameResolver(Configuration config, Method method) { for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; + useParamMap = true; name = ((Param) annotation).value(); break; } @@ -92,8 +101,31 @@ public ParamNameResolver(Configuration config, Method method) { } } map.put(paramIndex, name); + typeMap.put(name, actualParamTypes[paramIndex]); } names = Collections.unmodifiableSortedMap(map); + if (names.size() > 1) { + useParamMap = true; + } + if (names.size() == 1) { + Type soleParamType = actualParamTypes[0]; + if (soleParamType instanceof GenericArrayType) { + typeMap.put("array", soleParamType); + } else { + Class soleParamClass = null; + if (soleParamType instanceof ParameterizedType) { + soleParamClass = (Class) ((ParameterizedType) soleParamType).getRawType(); + } else if (soleParamType instanceof Class) { + soleParamClass = (Class) soleParamType; + } + if (Collection.class.isAssignableFrom(soleParamClass)) { + typeMap.put("collection", soleParamType); + if (List.class.isAssignableFrom(soleParamClass)) { + typeMap.put("list", soleParamType); + } + } + } + } } private String getActualParamName(Method method, int paramIndex) { @@ -147,6 +179,34 @@ public Object getNamedParams(Object[] args) { } } + public Type getType(String name) { + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name); + String unindexed = propertyTokenizer.getName(); + Type type = typeMap.get(unindexed); + + if (type == null && unindexed.startsWith(GENERIC_NAME_PREFIX)) { + try { + Integer paramIndex = Integer.valueOf(unindexed.substring(GENERIC_NAME_PREFIX.length())) - 1; + unindexed = names.get(paramIndex); + if (unindexed != null) { + type = typeMap.get(unindexed); + } + } catch (NumberFormatException e) { + // user mistake + } + } + + if (propertyTokenizer.getIndex() != null) { + if (type instanceof ParameterizedType) { + Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments(); + return typeArgs[0]; + } else if (type instanceof Class && ((Class) type).isArray()) { + return ((Class) type).getComponentType(); + } + } + return type; + } + /** * Wrap to a {@link ParamMap} if object is {@link Collection} or array. * @@ -178,4 +238,7 @@ public static Object wrapToMapIfCollection(Object object, String actualParamName return object; } + public boolean isUseParamMap() { + return useParamMap; + } } diff --git a/src/main/java/org/apache/ibatis/reflection/Reflector.java b/src/main/java/org/apache/ibatis/reflection/Reflector.java index 2599c53dbf7..35f1a93c14f 100644 --- a/src/main/java/org/apache/ibatis/reflection/Reflector.java +++ b/src/main/java/org/apache/ibatis/reflection/Reflector.java @@ -28,6 +28,7 @@ import java.lang.reflect.ReflectPermission; import java.lang.reflect.Type; import java.text.MessageFormat; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -53,22 +54,30 @@ public class Reflector { private static final MethodHandle isRecordMethodHandle = getIsRecordMethodHandle(); - private final Class type; + private final Type type; + private final Class clazz; private final String[] readablePropertyNames; private final String[] writablePropertyNames; private final Map setMethods = new HashMap<>(); private final Map getMethods = new HashMap<>(); - private final Map> setTypes = new HashMap<>(); - private final Map> getTypes = new HashMap<>(); + private final Map>> setTypes = new HashMap<>(); + private final Map>> getTypes = new HashMap<>(); private Constructor defaultConstructor; private final Map caseInsensitivePropertyMap = new HashMap<>(); - public Reflector(Class clazz) { - type = clazz; + private static final Entry> nullEntry = new AbstractMap.SimpleImmutableEntry<>(null, null); + + public Reflector(Type type) { + this.type = type; + if (type instanceof ParameterizedType) { + this.clazz = (Class) ((ParameterizedType) type).getRawType(); + } else { + this.clazz = (Class) type; + } addDefaultConstructor(clazz); Method[] classMethods = getClassMethods(clazz); - if (isRecord(type)) { + if (isRecord(clazz)) { addRecordGetMethods(classMethods); } else { addGetMethods(classMethods); @@ -142,7 +151,7 @@ private void addGetMethod(String name, Method method, boolean isAmbiguous) { name, method.getDeclaringClass().getName())) : new MethodInvoker(method); getMethods.put(name, invoker); Type returnType = TypeParameterResolver.resolveReturnType(method, type); - getTypes.put(name, typeToClass(returnType)); + getTypes.put(name, Map.entry(returnType, typeToClass(returnType))); } private void addSetMethods(Method[] methods) { @@ -163,7 +172,7 @@ private void resolveSetterConflicts(Map> conflictingSetters for (Entry> entry : conflictingSetters.entrySet()) { String propName = entry.getKey(); List setters = entry.getValue(); - Class getterType = getTypes.get(propName); + Class getterType = getTypes.getOrDefault(propName, nullEntry).getValue(); boolean isGetterAmbiguous = getMethods.get(propName) instanceof AmbiguousMethodInvoker; boolean isSetterAmbiguous = false; Method match = null; @@ -202,7 +211,7 @@ private Method pickBetterSetter(Method setter1, Method setter2, String property) setter2.getDeclaringClass().getName(), paramType1.getName(), paramType2.getName())); setMethods.put(property, invoker); Type[] paramTypes = TypeParameterResolver.resolveParamTypes(setter1, type); - setTypes.put(property, typeToClass(paramTypes[0])); + setTypes.put(property, Map.entry(paramTypes[0], typeToClass(paramTypes[0]))); return null; } @@ -210,28 +219,24 @@ private void addSetMethod(String name, Method method) { MethodInvoker invoker = new MethodInvoker(method); setMethods.put(name, invoker); Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type); - setTypes.put(name, typeToClass(paramTypes[0])); + setTypes.put(name, Map.entry(paramTypes[0], typeToClass(paramTypes[0]))); } private Class typeToClass(Type src) { - Class result = null; if (src instanceof Class) { - result = (Class) src; + return (Class) src; } else if (src instanceof ParameterizedType) { - result = (Class) ((ParameterizedType) src).getRawType(); + return (Class) ((ParameterizedType) src).getRawType(); } else if (src instanceof GenericArrayType) { Type componentType = ((GenericArrayType) src).getGenericComponentType(); if (componentType instanceof Class) { - result = Array.newInstance((Class) componentType, 0).getClass(); + return Array.newInstance((Class) componentType, 0).getClass(); } else { Class componentClass = typeToClass(componentType); - result = Array.newInstance(componentClass, 0).getClass(); + return Array.newInstance(componentClass, 0).getClass(); } } - if (result == null) { - result = Object.class; - } - return result; + return Object.class; } private void addFields(Class clazz) { @@ -259,7 +264,7 @@ private void addSetField(Field field) { if (isValidPropertyName(field.getName())) { setMethods.put(field.getName(), new SetFieldInvoker(field)); Type fieldType = TypeParameterResolver.resolveFieldType(field, type); - setTypes.put(field.getName(), typeToClass(fieldType)); + setTypes.put(field.getName(), Map.entry(fieldType, typeToClass(fieldType))); } } @@ -267,7 +272,7 @@ private void addGetField(Field field) { if (isValidPropertyName(field.getName())) { getMethods.put(field.getName(), new GetFieldInvoker(field)); Type fieldType = TypeParameterResolver.resolveFieldType(field, type); - getTypes.put(field.getName(), typeToClass(fieldType)); + getTypes.put(field.getName(), Map.entry(fieldType, typeToClass(fieldType))); } } @@ -356,14 +361,14 @@ public static boolean canControlMemberAccessible() { * @return The class name */ public Class getType() { - return type; + return clazz; } public Constructor getDefaultConstructor() { if (defaultConstructor != null) { return defaultConstructor; } - throw new ReflectionException("There is no default constructor for " + type); + throw new ReflectionException("There is no default constructor for " + clazz); } public boolean hasDefaultConstructor() { @@ -373,7 +378,7 @@ public boolean hasDefaultConstructor() { public Invoker getSetInvoker(String propertyName) { Invoker method = setMethods.get(propertyName); if (method == null) { - throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + clazz + "'"); } return method; } @@ -381,7 +386,7 @@ public Invoker getSetInvoker(String propertyName) { public Invoker getGetInvoker(String propertyName) { Invoker method = getMethods.get(propertyName); if (method == null) { - throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + clazz + "'"); } return method; } @@ -395,13 +400,19 @@ public Invoker getGetInvoker(String propertyName) { * @return The Class of the property setter */ public Class getSetterType(String propertyName) { - Class clazz = setTypes.get(propertyName); + Class clazz = setTypes.get(propertyName).getValue(); if (clazz == null) { - throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no setter for property named '" + propertyName + "' in '" + clazz + "'"); } return clazz; } + public Entry> getGenericSetterType(String propertyName) { + return setTypes.computeIfAbsent(propertyName, k -> { + throw new ReflectionException("There is no setter for property named '" + k + "' in '" + clazz + "'"); + }); + } + /** * Gets the type for a property getter. * @@ -411,13 +422,19 @@ public Class getSetterType(String propertyName) { * @return The Class of the property getter */ public Class getGetterType(String propertyName) { - Class clazz = getTypes.get(propertyName); + Class clazz = getTypes.getOrDefault(propertyName, nullEntry).getValue(); if (clazz == null) { - throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'"); + throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + clazz + "'"); } return clazz; } + public Entry> getGenericGetterType(String propertyName) { + return getTypes.computeIfAbsent(propertyName, k -> { + throw new ReflectionException("There is no getter for property named '" + k + "' in '" + clazz + "'"); + }); + } + /** * Gets an array of the readable properties for an object. * diff --git a/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java b/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java index 18eeac3be78..513c08c3998 100644 --- a/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java @@ -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. @@ -15,11 +15,13 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; + public interface ReflectorFactory { boolean isClassCacheEnabled(); void setClassCacheEnabled(boolean classCacheEnabled); - Reflector findForClass(Class type); + Reflector findForClass(Type type); } diff --git a/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java b/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java index d05ff45d2d5..aba6a7f3e4d 100644 --- a/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java @@ -32,6 +32,15 @@ */ public class TypeParameterResolver { + public static Type[] resolveClassTypeParams(Class classWithTypeParams, Class childClass) { + TypeVariable[] typeArgs = classWithTypeParams.getTypeParameters(); + Type[] result = new Type[typeArgs.length]; + for (int i = 0; i < typeArgs.length; i++) { + result[i] = resolveTypeVar(typeArgs[i], childClass, classWithTypeParams); + } + return result; + } + /** * Resolve field type. * @@ -139,6 +148,14 @@ private static Type resolveTypeVar(TypeVariable typeVar, Type srcType, Class< } else if (srcType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) srcType; clazz = (Class) parameterizedType.getRawType(); + if (clazz == declaringClass) { + TypeVariable[] typeVars = declaringClass.getTypeParameters(); + for (int i = 0; i < typeVars.length; i++) { + if (typeVar.equals(typeVars[i])) { + return parameterizedType.getActualTypeArguments()[i]; + } + } + } } else { throw new IllegalArgumentException( "The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass()); @@ -266,8 +283,14 @@ public boolean equals(Object obj) { @Override public String toString() { - return "ParameterizedTypeImpl [rawType=" + rawType + ", ownerType=" + ownerType + ", actualTypeArguments=" - + Arrays.toString(actualTypeArguments) + "]"; + StringBuilder s = new StringBuilder().append(rawType.getName()).append("<"); + for (int i = 0; i < actualTypeArguments.length; i++) { + if (i > 0) { + s.append(", "); + } + s.append(actualTypeArguments[i].getTypeName()); + } + return s.append(">").toString(); } } @@ -312,6 +335,17 @@ public boolean equals(Object obj) { WildcardTypeImpl other = (WildcardTypeImpl) obj; return Arrays.equals(lowerBounds, other.lowerBounds) && Arrays.equals(upperBounds, other.upperBounds); } + + @Override + public String toString() { + StringBuilder s = new StringBuilder().append("?"); + if (lowerBounds.length > 0) { + s.append(" super ").append(lowerBounds[0].getTypeName()); + } else if (upperBounds.length > 0 && upperBounds[0] != Object.class) { + s.append(" extends ").append(upperBounds[0].getTypeName()); + } + return s.toString(); + } } static class GenericArrayTypeImpl implements GenericArrayType { @@ -343,5 +377,10 @@ public boolean equals(Object obj) { GenericArrayTypeImpl other = (GenericArrayTypeImpl) obj; return Objects.equals(genericComponentType, other.genericComponentType); } + + @Override + public String toString() { + return new StringBuilder().append(genericComponentType.toString()).append("[]").toString(); + } } } diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java index 9afa9584d79..458bac5e77e 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java @@ -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. @@ -15,7 +15,9 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map.Entry; import org.apache.ibatis.reflection.ExceptionUtil; import org.apache.ibatis.reflection.MetaClass; @@ -90,6 +92,21 @@ public Class getSetterType(String name) { return metaValue.getSetterType(prop.getChildren()); } + @Override + public Entry> getGenericSetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return metaClass.getGenericSetterType(name); + } else { + return metaValue.getGenericSetterType(prop.getChildren()); + } + } else { + return metaClass.getGenericSetterType(name); + } + } + @Override public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); @@ -103,6 +120,21 @@ public Class getGetterType(String name) { return metaValue.getGetterType(prop.getChildren()); } + @Override + public Entry> getGenericGetterType(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + MetaObject metaValue = metaObject.metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return metaClass.getGenericGetterType(name); + } else { + return metaValue.getGenericGetterType(prop.getChildren()); + } + } else { + return metaClass.getGenericGetterType(name); + } + } + @Override public boolean hasSetter(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java index 5ed9233ce04..30f5791890b 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java @@ -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. @@ -15,9 +15,11 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; @@ -91,6 +93,12 @@ public Class getSetterType(String name) { } } + @Override + public Entry> getGenericSetterType(String name) { + Class setterType = getSetterType(name); + return Map.entry(setterType, setterType); + } + @Override public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); @@ -109,6 +117,12 @@ public Class getGetterType(String name) { } } + @Override + public Entry> getGenericGetterType(String name) { + Class getterType = getGetterType(name); + return Map.entry(getterType, getterType); + } + @Override public boolean hasSetter(String name) { return true; diff --git a/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java b/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java index 295abdd6e7a..8cb0c54efdf 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2022 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. @@ -15,7 +15,9 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map.Entry; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.factory.ObjectFactory; @@ -40,6 +42,16 @@ public interface ObjectWrapper { Class getGetterType(String name); + default Entry> getGenericSetterType(String name) { + throw new UnsupportedOperationException( + "'" + this.getClass() + "' must override the default method 'getGenericSetterType()'."); + } + + default Entry> getGenericGetterType(String name) { + throw new UnsupportedOperationException( + "'" + this.getClass() + "' must override the default method 'getGenericGetterType()'."); + } + boolean hasSetter(String name); boolean hasGetter(String name); diff --git a/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java index a008a130484..2c43735162d 100644 --- a/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java @@ -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. @@ -20,6 +20,7 @@ import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.session.Configuration; @@ -59,6 +60,11 @@ public interface LanguageDriver { */ SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType); + default SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType, + ParamNameResolver paramNameResolver) { + return createSqlSource(configuration, script, parameterType); + } + /** * Creates an {@link SqlSource} that will hold the statement read from an annotation. It is called during startup, * when the mapped statement is read from a class or an xml file. @@ -75,4 +81,9 @@ public interface LanguageDriver { */ SqlSource createSqlSource(Configuration configuration, String script, Class parameterType); + default SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, + ParamNameResolver paramNameResolver) { + return createSqlSource(configuration, script, parameterType); + } + } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java index 678c791f92d..f50b9908cf1 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java @@ -15,19 +15,28 @@ */ package org.apache.ibatis.scripting.defaults; +import java.lang.reflect.Type; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; +import java.util.Map.Entry; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.ParameterMode; +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.ObjectTypeHandler; import org.apache.ibatis.type.TypeException; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; @@ -45,7 +54,24 @@ public class DefaultParameterHandler implements ParameterHandler { private final BoundSql boundSql; private final Configuration configuration; - private MetaObject metaObject; + private ParameterMetaData paramMetaData; + private MetaObject paramMetaObject; + private HashMap, MetaClass> metaClassCache = new HashMap<>(); + private static final ParameterMetaData NULL_PARAM_METADATA = new ParameterMetaData() { + // @formatter:off + public T unwrap(Class iface) throws SQLException { return null; } + public boolean isWrapperFor(Class iface) throws SQLException { return false; } + public boolean isSigned(int param) throws SQLException { return false; } + public int isNullable(int param) throws SQLException { return 0; } + public int getScale(int param) throws SQLException { return 0; } + public int getPrecision(int param) throws SQLException { return 0; } + public String getParameterTypeName(int param) throws SQLException { return null; } + public int getParameterType(int param) throws SQLException { return 0; } + public int getParameterMode(int param) throws SQLException { return 0; } + public int getParameterCount() throws SQLException { return 0; } + public String getParameterClassName(int param) throws SQLException { return null; } + // @formatter:on + }; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; @@ -60,19 +86,89 @@ public Object getParameterObject() { return parameterObject; } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { + ParamNameResolver paramNameResolver = mappedStatement.getParamNameResolver(); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { - Object value = getValue(parameterMapping); - TypeHandler typeHandler = parameterMapping.getTypeHandler(); + Object value; + String propertyName = parameterMapping.getProperty(); JdbcType jdbcType = parameterMapping.getJdbcType(); - if (value == null && jdbcType == null) { - jdbcType = configuration.getJdbcTypeForNull(); + JdbcType actualJdbcType = jdbcType == null ? getParamJdbcType(ps, i + 1) : jdbcType; + Type propertyGenericType = null; + TypeHandler typeHandler = parameterMapping.getTypeHandler(); + if (parameterMapping.hasValue()) { + value = parameterMapping.getValue(); + } else if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else { + Class parameterClass = parameterObject.getClass(); + TypeHandler paramTypeHandler = typeHandlerRegistry.getTypeHandler(parameterClass, actualJdbcType); + if (paramTypeHandler != null) { + value = parameterObject; + typeHandler = paramTypeHandler; + } else { + paramMetaObject = getParamMetaObject(); + value = paramMetaObject.getValue(propertyName); + if (typeHandler == null && value != null) { + if (paramNameResolver != null && ParamMap.class.equals(parameterClass)) { + Type actualParamType = paramNameResolver.getType(propertyName); + if (actualParamType instanceof Class) { + Class actualParamClass = (Class) actualParamType; + MetaClass metaClass = metaClassCache.computeIfAbsent(actualParamClass, + k -> MetaClass.forClass(k, configuration.getReflectorFactory())); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(propertyName); + String multiParamsPropertyName; + if (propertyTokenizer.hasNext()) { + multiParamsPropertyName = propertyTokenizer.getChildren(); + if (metaClass.hasGetter(multiParamsPropertyName)) { + Entry> getterType = metaClass.getGenericGetterType(multiParamsPropertyName); + propertyGenericType = getterType.getKey(); + } + } else { + propertyGenericType = actualParamClass; + } + } else { + propertyGenericType = actualParamType; + } + } else { + try { + propertyGenericType = paramMetaObject.getGenericGetterType(propertyName).getKey(); + typeHandler = configuration.getTypeHandlerRegistry().getTypeHandler(propertyGenericType, + actualJdbcType, null); + } catch (Exception e) { + // Not always resolvable + } + } + } + } + } + if (value == null) { + if (jdbcType == null) { + jdbcType = configuration.getJdbcTypeForNull(); + } + if (typeHandler == null) { + typeHandler = ObjectTypeHandler.INSTANCE; + } + } else if (typeHandler == null) { + if (propertyGenericType == null) { + propertyGenericType = value.getClass(); + } + typeHandler = typeHandlerRegistry.getTypeHandler(propertyGenericType, actualJdbcType, null); + } + if (typeHandler == null) { + typeHandler = typeHandlerRegistry.getTypeHandler(actualJdbcType); + } + if (typeHandler == null) { + throw new TypeException("Could not find type handler for Java type '" + propertyGenericType.getTypeName() + + "' nor JDBC type '" + actualJdbcType + "'"); } try { typeHandler.setParameter(ps, i + 1, value, jdbcType); @@ -84,22 +180,25 @@ public void setParameters(PreparedStatement ps) { } } - private Object getValue(ParameterMapping parameterMapping) { - if (parameterMapping.hasValue()) { - return parameterMapping.getValue(); + private MetaObject getParamMetaObject() { + if (paramMetaObject != null) { + return paramMetaObject; } - String propertyName = parameterMapping.getProperty(); - if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params - return boundSql.getAdditionalParameter(propertyName); - } else if (parameterObject == null) { - return null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - return parameterObject; - } else { - if (metaObject == null) { - metaObject = configuration.newMetaObject(parameterObject); + paramMetaObject = configuration.newMetaObject(parameterObject); + return paramMetaObject; + } + + private JdbcType getParamJdbcType(PreparedStatement ps, int paramIndex) { + try { + if (paramMetaData == null) { + paramMetaData = ps.getParameterMetaData(); } - return metaObject.getValue(propertyName); + return paramMetaData == NULL_PARAM_METADATA ? null : JdbcType.forCode(paramMetaData.getParameterType(paramIndex)); + } catch (SQLException e) { + if (paramMetaData == null) { + paramMetaData = NULL_PARAM_METADATA; + } + return null; } } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java index bb0a9a16ea7..e2a5350db60 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java @@ -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. @@ -18,6 +18,7 @@ import org.apache.ibatis.builder.BuilderException; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.XNode; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; import org.apache.ibatis.session.Configuration; @@ -39,8 +40,9 @@ public SqlSource createSqlSource(Configuration configuration, XNode script, Clas } @Override - public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { - SqlSource source = super.createSqlSource(configuration, script, parameterType); + public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, + ParamNameResolver paramNameResolver) { + SqlSource source = super.createSqlSource(configuration, script, parameterType, paramNameResolver); checkIsNotDynamic(source); return source; } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java index ab96078fe50..8ac7313aac9 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java @@ -25,6 +25,7 @@ import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.GenericTokenParser; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.xmltags.DynamicContext; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.scripting.xmltags.SqlNode; @@ -42,17 +43,27 @@ public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) { - DynamicContext context = new DynamicContext(configuration, parameterType); + this(configuration, rootSqlNode, parameterType, null); + } + + public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType, + ParamNameResolver paramNameResolver) { + DynamicContext context = new DynamicContext(configuration, parameterType, paramNameResolver); rootSqlNode.apply(context); String sql = context.getSql(); sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); } public RawSqlSource(Configuration configuration, String sql, Class parameterType) { + this(configuration, sql, parameterType, null); + } + + public RawSqlSource(Configuration configuration, String sql, Class parameterType, + ParamNameResolver paramNameResolver) { Class clazz = parameterType == null ? Object.class : parameterType; List parameterMappings = new ArrayList<>(); ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler(parameterMappings, configuration, - clazz, new HashMap<>()); + clazz, new HashMap<>(), paramNameResolver); GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler); sqlSource = SqlSourceBuilder.buildSqlSource(configuration, parser.parse(sql), parameterMappings); } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java index 195f9b2cfbb..082b6efd28f 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java @@ -29,6 +29,7 @@ import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.session.Configuration; /** @@ -49,17 +50,18 @@ public class DynamicContext { private final Configuration configuration; private final Object parameterObject; private final Class parameterType; + private final ParamNameResolver paramNameResolver; private final boolean paramExists; private GenericTokenParser tokenParser; private ParameterMappingTokenHandler tokenHandler; - public DynamicContext(Configuration configuration, Class parameterType) { - this(configuration, null, parameterType, false); + public DynamicContext(Configuration configuration, Class parameterType, ParamNameResolver paramNameResolver) { + this(configuration, null, parameterType, paramNameResolver, false); } public DynamicContext(Configuration configuration, Object parameterObject, Class parameterType, - boolean paramExists) { + ParamNameResolver paramNameResolver, boolean paramExists) { if (parameterObject == null || parameterObject instanceof Map) { bindings = new ContextMap(null, false); } else { @@ -73,6 +75,7 @@ public DynamicContext(Configuration configuration, Object parameterObject, Class this.parameterObject = parameterObject; this.paramExists = paramExists; this.parameterType = parameterType; + this.paramNameResolver = paramNameResolver; } public Map getBindings() { @@ -94,7 +97,7 @@ public String getSql() { private void initTokenParser(List parameterMappings) { if (tokenParser == null) { tokenHandler = new ParameterMappingTokenHandler(parameterMappings != null ? parameterMappings : new ArrayList<>(), - configuration, parameterObject, parameterType, bindings, paramExists); + configuration, parameterObject, parameterType, bindings, paramNameResolver, paramExists); tokenParser = new GenericTokenParser("#{", "}", tokenHandler); } } @@ -117,6 +120,10 @@ protected Class getParameterType() { return parameterType; } + protected ParamNameResolver getParamNameResolver() { + return paramNameResolver; + } + protected boolean isParamExists() { return paramExists; } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java index 03ab703b2b7..b2fe4a76080 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2022 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. @@ -18,6 +18,7 @@ import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.session.Configuration; /** @@ -27,15 +28,21 @@ public class DynamicSqlSource implements SqlSource { private final Configuration configuration; private final SqlNode rootSqlNode; + private final ParamNameResolver paramNameResolver; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { + this(configuration, rootSqlNode, null); + } + + public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode, ParamNameResolver paramNameResolver) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; + this.paramNameResolver = paramNameResolver; } @Override public BoundSql getBoundSql(Object parameterObject) { - DynamicContext context = new DynamicContext(configuration, parameterObject, null, true); + DynamicContext context = new DynamicContext(configuration, parameterObject, null, paramNameResolver, true); rootSqlNode.apply(context); String sql = context.getSql(); SqlSource sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java index 0c843faa284..e3f9048d80b 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java @@ -132,7 +132,8 @@ private class PrefixedContext extends DynamicContext { private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) { - super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.getParamNameResolver(), + delegate.isParamExists()); this.delegate = delegate; this.prefix = prefix; this.prefixApplied = false; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java index 1a1053794ec..79605600bb6 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java @@ -79,7 +79,8 @@ private class FilteredDynamicContext extends DynamicContext { private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) { - super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.getParamNameResolver(), + delegate.isParamExists()); this.delegate = delegate; this.prefixApplied = false; this.suffixApplied = false; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java b/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java index b223e772197..69561c32343 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java @@ -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. @@ -23,6 +23,7 @@ import org.apache.ibatis.parsing.PropertyParser; import org.apache.ibatis.parsing.XNode; import org.apache.ibatis.parsing.XPathParser; +import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.scripting.defaults.RawSqlSource; @@ -41,12 +42,24 @@ public ParameterHandler createParameterHandler(MappedStatement mappedStatement, @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) { - XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); + return createSqlSource(configuration, script, parameterType, null); + } + + @Override + public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType, + ParamNameResolver paramNameResolver) { + XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType, paramNameResolver); return builder.parseScriptNode(); } @Override public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { + return createSqlSource(configuration, script, parameterType, null); + } + + @Override + public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType, + ParamNameResolver paramNameResolver) { // issue #3 if (script.startsWith("