From 78276f3933f69e1d3b53f15863dafb7aaf3240aa Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Wed, 21 Dec 2022 05:05:41 +0900 Subject: [PATCH 01/26] Resolve type handler based on Type instead of Class --- .../apache/ibatis/binding/MapperMethod.java | 2 +- .../apache/ibatis/builder/BaseBuilder.java | 41 ++- .../builder/MapperBuilderAssistant.java | 82 ++++- .../ibatis/builder/SqlSourceBuilder.java | 119 +++++-- .../annotation/MapperAnnotationBuilder.java | 54 +-- .../builder/annotation/ProviderSqlSource.java | 8 +- .../ibatis/builder/xml/XMLMapperBuilder.java | 8 +- .../builder/xml/XMLStatementBuilder.java | 37 +- .../resultset/DefaultResultSetHandler.java | 86 +++-- .../ibatis/mapping/MappedStatement.java | 11 + .../ibatis/mapping/ParameterMapping.java | 26 +- .../apache/ibatis/mapping/ResultMapping.java | 11 +- .../reflection/DefaultReflectorFactory.java | 5 +- .../apache/ibatis/reflection/MetaClass.java | 68 ++-- .../apache/ibatis/reflection/MetaObject.java | 10 + .../ibatis/reflection/ParamNameResolver.java | 52 ++- .../apache/ibatis/reflection/Reflector.java | 74 ++-- .../ibatis/reflection/ReflectorFactory.java | 6 +- .../reflection/TypeParameterResolver.java | 33 ++ .../reflection/wrapper/BeanWrapper.java | 32 ++ .../ibatis/reflection/wrapper/MapWrapper.java | 16 + .../reflection/wrapper/ObjectWrapper.java | 10 + .../ibatis/scripting/LanguageDriver.java | 9 + .../defaults/DefaultParameterHandler.java | 124 ++++++- .../scripting/defaults/RawLanguageDriver.java | 5 +- .../scripting/defaults/RawSqlSource.java | 13 +- .../scripting/xmltags/DynamicSqlSource.java | 9 +- .../scripting/xmltags/XMLLanguageDriver.java | 15 +- .../scripting/xmltags/XMLScriptBuilder.java | 9 +- .../AutoMappingUnknownColumnBehavior.java | 14 +- .../apache/ibatis/session/Configuration.java | 7 + .../apache/ibatis/type/BaseTypeHandler.java | 2 +- .../ibatis/type/ConflictedTypeHandler.java | 76 +++++ .../type/DefaultTypeHandlerResolver.java | 36 ++ .../ibatis/type/TypeHandlerRegistry.java | 243 +++++++++---- .../ibatis/type/TypeHandlerResolver.java | 25 ++ .../org/apache/ibatis/type/TypeReference.java | 8 +- .../ibatis/builder/XmlMapperBuilderTest.java | 5 +- .../reflection/ParamNameResolverTest.java | 63 ++++ .../ibatis/reflection/ReflectorTest.java | 40 ++- .../reflection/TypeParameterResolverTest.java | 43 +++ .../defaults/DefaultParameterHandlerTest.java | 2 + .../BooleanCharTypeHandler.java | 52 +++ .../BooleanIntTypeHandler.java | 51 +++ .../handle_by_jdbc_type/BoolsBean.java | 57 ++++ .../HandlerByJdbcTypeTest.java | 96 ++++++ .../submitted/handle_by_jdbc_type/Mapper.java | 39 +++ .../language/VelocitySqlSourceBuilder.java | 10 +- .../maptypehandler/MapTypeHandlerTest.java | 2 + .../CsvTypeHandler.java | 86 +++++ .../FuzzyBean.java | 37 ++ .../GloballyRegisteredHandlerMapper.java | 42 +++ ...lyRegisteredTypeHandlerResolutionTest.java | 190 +++++++++++ .../LocallySpecifiedHandlerMapper.java | 92 +++++ ...llySpecifiedTypeHandlerResolutionTest.java | 318 ++++++++++++++++++ .../TypeAwareTypeHandler.java | 89 +++++ .../typebasedtypehandlerresolution/User.java | 86 +++++ .../typehandler/TypeHandlerTest.java | 4 + .../unknownobject/UnknownObjectTest.java | 24 +- ...ricTypeSupportedInHierarchiesTestCase.java | 49 --- .../ibatis/type/TypeHandlerRegistryTest.java | 58 +++- .../apache/ibatis/type/TypeReferenceTest.java | 55 +++ .../org/apache/ibatis/builder/BlogMapper.xml | 2 +- .../handle_by_jdbc_type/CreateDB.sql | 25 ++ .../submitted/handle_by_jdbc_type/Mapper.xml | 41 +++ .../handle_by_jdbc_type/mybatis-config.xml | 51 +++ .../CreateDB.sql | 32 ++ .../GloballyRegisteredHandlerMapper.xml | 51 +++ .../LocallySpecifiedHandlerMapper.xml | 105 ++++++ .../mybatis-config.xml | 43 +++ 70 files changed, 2945 insertions(+), 381 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/type/ConflictedTypeHandler.java create mode 100644 src/main/java/org/apache/ibatis/type/DefaultTypeHandlerResolver.java create mode 100644 src/main/java/org/apache/ibatis/type/TypeHandlerResolver.java create mode 100644 src/test/java/org/apache/ibatis/reflection/ParamNameResolverTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BooleanCharTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BooleanIntTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/BoolsBean.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/HandlerByJdbcTypeTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/handle_by_jdbc_type/Mapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/CsvTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/FuzzyBean.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredHandlerMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredTypeHandlerResolutionTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedHandlerMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedTypeHandlerResolutionTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/TypeAwareTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/typebasedtypehandlerresolution/User.java delete mode 100644 src/test/java/org/apache/ibatis/type/GenericTypeSupportedInHierarchiesTestCase.java create mode 100644 src/test/java/org/apache/ibatis/type/TypeReferenceTest.java create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/Mapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/handle_by_jdbc_type/mybatis-config.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/GloballyRegisteredHandlerMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/LocallySpecifiedHandlerMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/typebasedtypehandlerresolution/mybatis-config.xml diff --git a/src/main/java/org/apache/ibatis/binding/MapperMethod.java b/src/main/java/org/apache/ibatis/binding/MapperMethod.java index 17b6cd9f863..f0eaf317ed0 100644 --- a/src/main/java/org/apache/ibatis/binding/MapperMethod.java +++ b/src/main/java/org/apache/ibatis/binding/MapperMethod.java @@ -302,7 +302,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 7e7a443726b..43efd8f5446 100644 --- a/src/main/java/org/apache/ibatis/builder/BaseBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/BaseBuilder.java @@ -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; @@ -119,30 +120,34 @@ protected Class resolveClass(String alias) { } } + @Deprecated 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, null, javaType, null, typeHandlerAlias); } + @Deprecated protected TypeHandler resolveTypeHandler(Class javaType, Class> typeHandlerType) { - if (typeHandlerType == null) { - return null; + return resolveTypeHandler(null, null, javaType, null, typeHandlerType); + } + + protected TypeHandler resolveTypeHandler(Class parameterType, String propertyName, 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"); } - // javaType ignored for injected handlers see issue #746 for full detail - TypeHandler handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType); - if (handler == null) { - // not in registry, create a new one - handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType); + return resolveTypeHandler(parameterType, propertyName, propertyType, jdbcType, typeHandlerType); + } + + protected TypeHandler resolveTypeHandler(Class parameterType, String propertyName, Type propertyType, + JdbcType jdbcType, Class> typeHandlerType) { + if (typeHandlerType == null && jdbcType == null) { + return null; } - return handler; + return configuration.getTypeHandlerResolver().resolve(parameterType, propertyType, propertyName, 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 4c9341a16a9..1605c899cf3 100644 --- a/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java +++ b/src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java @@ -15,12 +15,15 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; @@ -44,10 +47,12 @@ 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; import org.apache.ibatis.type.TypeHandler; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -162,7 +167,7 @@ public ParameterMapping buildParameterMapping( // Class parameterType = parameterMapBuilder.type(); Class javaTypeClass = resolveParameterJavaType(parameterType, property, javaType, jdbcType); - TypeHandler typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler); + TypeHandler typeHandlerInstance = resolveTypeHandler(parameterType, property, javaTypeClass, jdbcType, typeHandler); return new ParameterMapping.Builder(configuration, property, javaTypeClass) .jdbcType(jdbcType) @@ -241,6 +246,49 @@ public Discriminator buildDiscriminator( return new Discriminator.Builder(configuration, resultMapping, namespaceDiscriminatorMap).build(); } + /** + * @param id + * the id + * @param sqlSource + * the sql source + * @param statementType + * the statement type + * @param sqlCommandType + * the sql command type + * @param fetchSize + * the fetch size + * @param timeout + * the timeout + * @param parameterMap + * the parameter map + * @param parameterType + * the parameter type + * @param resultMap + * the result map + * @param resultType + * the result type + * @param resultSetType + * the result set type + * @param flushCache + * the flush cache + * @param useCache + * the use cache + * @param resultOrdered + * the result ordered + * @param keyGenerator + * the key generator + * @param keyProperty + * the key property + * @param keyColumn + * the key column + * @param databaseId + * the database id + * @param lang + * the lang + * @param paramNameResolver + * the param name resolver + * @return the mapped statement + */ public MappedStatement addMappedStatement( String id, SqlSource sqlSource, @@ -262,7 +310,8 @@ public MappedStatement addMappedStatement( String databaseId, LanguageDriver lang, String resultSets, - boolean dirtySelect) { + boolean dirtySelect, + ParamNameResolver paramNameResolver) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); @@ -287,7 +336,8 @@ public MappedStatement addMappedStatement( .flushCacheRequired(flushCache) .useCache(useCache) .cache(currentCache) - .dirtySelect(dirtySelect); + .dirtySelect(dirtySelect) + .paramNameResolver(paramNameResolver); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { @@ -351,7 +401,7 @@ public MappedStatement addMappedStatement(String id, SqlSource sqlSource, Statem id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterType, resultMap, resultType, resultSetType, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, - keyColumn, databaseId, lang, null, false); + keyColumn, databaseId, lang, null, false, null); } public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, @@ -436,15 +486,15 @@ public ResultMapping buildResultMapping( 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(resultType, property, 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) + return new ResultMapping.Builder(configuration, property, column, setterType.getValue()) .jdbcType(jdbcType) .nestedQueryId(applyCurrentNamespace(nestedSelect, true)) .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)) @@ -538,19 +588,19 @@ private List parseCompositeColumnName(String columnName) { 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 MapUtil.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 MapUtil.entry(Object.class, Object.class); } private Class resolveParameterJavaType(Class resultType, String property, Class javaType, JdbcType jdbcType) { diff --git a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java index 9c6b3fb2765..263b885585c 100644 --- a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java @@ -15,19 +15,25 @@ */ package org.apache.ibatis.builder; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.StringTokenizer; +import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.parsing.GenericTokenParser; 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; /** * @author Clinton Begin @@ -41,7 +47,13 @@ public SqlSourceBuilder(Configuration configuration) { } public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { - ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); + return parse(originalSql, parameterType, additionalParameters, null); + } + + public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters, + ParamNameResolver paramNameResolver) { + ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, + additionalParameters, paramNameResolver); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql; if (configuration.isShrinkWhitespacesInSql()) { @@ -71,11 +83,17 @@ private static class ParameterMappingTokenHandler extends BaseBuilder implements private final List parameterMappings = new ArrayList<>(); private final Class parameterType; private final MetaObject metaParameters; + private final ParamNameResolver paramNameResolver; + + private Type genericType = null; + private TypeHandler typeHandler = null; - public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, Map additionalParameters) { + public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, + Map additionalParameters, ParamNameResolver paramNameResolver) { super(configuration); this.parameterType = parameterType; this.metaParameters = configuration.newMetaObject(additionalParameters); + this.paramNameResolver = paramNameResolver; } public List getParameterMappings() { @@ -90,59 +108,88 @@ public String handleToken(String content) { private ParameterMapping buildParameterMapping(String content) { Map propertiesMap = parseParameterMapping(content); - String property = propertiesMap.get("property"); - Class propertyType; - if (metaParameters.hasGetter(property)) { // 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; - } + 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); + builder.jdbcType(jdbcType); + final Class javaType = figureOutJavaType(propertiesMap, property, jdbcType); + builder.javaType(javaType); + if (genericType == null) { + genericType = javaType; + } + if (typeHandler == null || typeHandlerAlias != null) { + typeHandler = resolveTypeHandler(parameterType, property, genericType, jdbcType, typeHandlerAlias); } - ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); - Class javaType = propertyType; - String typeHandlerAlias = null; + builder.typeHandler(typeHandler); + 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)) { builder.mode(resolveParameterMode(value)); } else if ("numericScale".equals(name)) { builder.numericScale(Integer.valueOf(value)); } else if ("resultMap".equals(name)) { builder.resultMapId(value); - } else if ("typeHandler".equals(name)) { - typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) { builder.jdbcTypeName(value); - } else if ("property".equals(name)) { - // Do Nothing } else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { - throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES); + throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + + "}. Valid properties are " + PARAMETER_PROPERTIES); } } - if (typeHandlerAlias != null) { - builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); - } return builder.build(); } + private Class figureOutJavaType(Map propertiesMap, String property, JdbcType jdbcType) { + Class javaType = resolveClass(propertiesMap.remove("javaType")); + if (javaType != null) { + return javaType; + } + if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params + return metaParameters.getGetterType(property); + } + typeHandler = resolveTypeHandler(parameterType, null, null, jdbcType, (Class>)null); + if (typeHandler != null) { + return parameterType; + } + if (JdbcType.CURSOR.equals(jdbcType)) { + return java.sql.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()); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property); + 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 af32cafc2dc..4bfe091f32c 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; @@ -48,6 +49,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; @@ -175,7 +178,7 @@ private void loadXmlResource() { } } if (inputStream != null) { - XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); + XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type); xmlParser.parse(); } } @@ -295,10 +298,11 @@ 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); + final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, paramNameResolver, languageDriver, method); final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType(); final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null); final String mappedStatementId = type.getName() + "." + method.getName(); @@ -310,7 +314,7 @@ void parseStatement(Method method) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null); if (selectKey != null) { - keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); + keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), paramNameResolver, languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; @@ -379,7 +383,8 @@ void parseStatement(Method method) { languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null, - statementAnnotation.isDirtySelect()); + statementAnnotation.isDirtySelect(), + paramNameResolver); }); } @@ -394,15 +399,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; @@ -581,7 +587,7 @@ private String nullOrEmpty(String value) { return value == null || value.trim().length() == 0 ? null : value; } - private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class parameterTypeClass, LanguageDriver languageDriver) { + private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX; Class resultTypeClass = selectKeyAnnotation.resultType(); StatementType statementType = selectKeyAnnotation.statementType(); @@ -600,12 +606,12 @@ 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); + keyGenerator, keyProperty, keyColumn, databaseId, languageDriver, null, false, paramNameResolver); id = assistant.applyCurrentNamespace(id, false); @@ -615,25 +621,25 @@ private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, St return answer; } - private SqlSource buildSqlSource(Annotation annotation, Class parameterType, LanguageDriver languageDriver, + private SqlSource buildSqlSource(Annotation annotation, Class parameterType, ParamNameResolver paramNameResolver, LanguageDriver languageDriver, Method method) { if (annotation instanceof Select) { - return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver); + return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, paramNameResolver, languageDriver); } else if (annotation instanceof Update) { - return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver); + 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, + private SqlSource buildSqlSourceFromStrings(String[] strings, Class parameterTypeClass, ParamNameResolver paramNameResolver, LanguageDriver languageDriver) { - return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass); + 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 7b8781b26b1..f462a2e6d03 100644 --- a/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java +++ b/src/main/java/org/apache/ibatis/builder/annotation/ProviderSqlSource.java @@ -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; @@ -130,7 +130,7 @@ public ProviderSqlSource(Configuration configuration, Annotation provider, Class + candidateProviderMethodName + "' 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; @@ -168,7 +168,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 if (providerMethodParameterTypes.length == 0) { sql = invokeProviderMethod(); @@ -186,7 +186,7 @@ private SqlSource createSqlSource(Object parameterObject) { + "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination."); } 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 cbff63e334e..6595f93ef52 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java @@ -59,6 +59,7 @@ public class XMLMapperBuilder extends BaseBuilder { private final MapperBuilderAssistant builderAssistant; private final Map sqlFragments; private final String resource; + private Class mapperClass; @Deprecated public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map sqlFragments, String namespace) { @@ -72,6 +73,11 @@ public XMLMapperBuilder(Reader reader, Configuration configuration, String resou configuration, 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); this.builderAssistant.setCurrentNamespace(namespace); @@ -133,7 +139,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); + final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, 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 82316bb49c9..97d33679d6f 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLStatementBuilder.java @@ -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.executor.keygen.Jdbc3KeyGenerator; @@ -30,6 +35,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; @@ -41,16 +47,22 @@ 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); } 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() { @@ -74,6 +86,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); @@ -93,7 +124,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"); Integer timeout = context.getIntAttribute("timeout"); @@ -114,7 +145,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) { @@ -161,7 +192,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/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 22af84642e8..80beceff313 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; @@ -64,6 +65,7 @@ import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeException; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; import org.apache.ibatis.util.MapUtil; @@ -102,6 +104,8 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final Map> autoMappingsCache = new HashMap<>(); private final Map> constructorAutoMappingColumns = new HashMap<>(); + private final Map> typeHandlerCache = new HashMap<>(); + // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage) private boolean useConstructorMappings; @@ -357,7 +361,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); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } @@ -484,7 +488,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) { @@ -505,20 +509,52 @@ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, return foundValues; } - private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) + private Object getPropertyMappingValue(ResultSetWrapper rsw, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { + final ResultSet rs = rsw.getResultSet(); + final String property = propertyMapping.getProperty(); if (propertyMapping.getNestedQueryId() != null) { return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } else if (propertyMapping.getResultSet() != null) { 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) { + typeHandler = resolvePropertyTypeHandler(rsw, metaResultObject, property, column); + } return typeHandler.getResult(rs, column); } } + private TypeHandler resolvePropertyTypeHandler(ResultSetWrapper rsw, MetaObject metaResultObject, + final String property, final String column) { + CacheKey typeHandlerCacheKey = new CacheKey(); + Class metaResultObjectClass = metaResultObject.getOriginalObject().getClass(); + typeHandlerCacheKey.update(metaResultObjectClass); + typeHandlerCacheKey.update(column); + typeHandlerCacheKey.update(property); + return typeHandlerCache.computeIfAbsent(typeHandlerCacheKey, k -> { + final JdbcType jdbcType = rsw.getJdbcType(column); + final TypeHandler th; + if (property == null) { + th = typeHandlerRegistry.getTypeHandler(jdbcType); + } else { + Type classToHandle = metaResultObject.getGenericSetterType(property).getKey(); + th = configuration.getTypeHandlerResolver().resolve(metaResultObjectClass, + classToHandle, property, jdbcType, null); + if (th == null) { + throw new TypeException("No usable type handler found for mapping the result of column '" + column + + "' to property '" + property + + "'. It was either not specified and/or could not be found for the javaType (" + + classToHandle + ") : jdbcType (" + jdbcType + ") combination."); + } + } + return th; + }); + } + private List createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { final String mapKey = resultMap.getId() + ":" + columnPrefix; List autoMapping = autoMappingsCache.get(mapKey); @@ -546,10 +582,12 @@ 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(); + Class metaObjectClass = metaObject.getOriginalObject().getClass(); + TypeHandler typeHandler = configuration.getTypeHandlerResolver().resolve(metaObjectClass, propertyType, + property, rsw.getJdbcType(columnName), null); + if (typeHandler != null) { + autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType instanceof Class && ((Class)propertyType).isPrimitive())); } else { configuration.getAutoMappingUnknownColumnBehavior() .doAction(mappedStatement, columnName, property, propertyType); @@ -685,7 +723,10 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId()); value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping)); } 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) { @@ -931,11 +972,11 @@ private Object instantiateParameterObject(Class parameterType) { // DISCRIMINATOR // - public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException { + 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)) { resultMap = configuration.getResultMap(discriminatedMapId); @@ -951,10 +992,14 @@ public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap return resultMap; } - private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException { + 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) { @@ -974,7 +1019,7 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r skipRows(resultSet, rowBounds); 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); Object partialObject = nestedResultObjects.get(rowKey); // issue #577 && #542 @@ -1010,7 +1055,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) @@ -1075,9 +1120,9 @@ private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String col return true; } - private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix) throws SQLException { + 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); } // @@ -1129,10 +1174,13 @@ 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(); List 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)); + } 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/mapping/MappedStatement.java b/src/main/java/org/apache/ibatis/mapping/MappedStatement.java index c5269af470f..539a3fdbf7d 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() { @@ -180,6 +182,11 @@ public Builder dirtySelect(boolean dirtySelect) { return this; } + public Builder paramNameResolver(ParamNameResolver paramNameResolver) { + mappedStatement.paramNameResolver = paramNameResolver; + return this; + } + /** * Resul sets. * @@ -300,6 +307,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 94abab3bbb4..4cb80d5ca08 100644 --- a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java @@ -100,35 +100,17 @@ public Builder expression(String expression) { } 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 80d5f113968..2bd73dbc0ff 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMapping.java @@ -136,7 +136,6 @@ public ResultMapping build() { // lock down collections resultMapping.flags = Collections.unmodifiableList(resultMapping.flags); resultMapping.composites = Collections.unmodifiableList(resultMapping.composites); - resolveTypeHandler(); validate(); return resultMapping; } @@ -148,7 +147,7 @@ private void validate() { } // 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); + // 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()) { @@ -169,14 +168,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 987e721e19a..205d61964a7 100644 --- a/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/DefaultReflectorFactory.java @@ -15,6 +15,7 @@ */ package org.apache.ibatis.reflection; +import java.lang.reflect.Type; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -22,7 +23,7 @@ public class DefaultReflectorFactory implements ReflectorFactory { private boolean classCacheEnabled = true; - private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + private final ConcurrentMap reflectorMap = new ConcurrentHashMap<>(); public DefaultReflectorFactory() { } @@ -38,7 +39,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 MapUtil.computeIfAbsent(reflectorMap, 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 abbdaadbfa2..134d64a4a14 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaClass.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaClass.java @@ -15,16 +15,15 @@ */ 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.AbstractMap; import java.util.Collection; +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; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -34,12 +33,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); } @@ -78,58 +77,57 @@ public Class getSetterType(String name) { } } + 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 MapUtil.entry(returnType, (Class)returnType); } else if (returnType instanceof ParameterizedType) { - type = (Class) ((ParameterizedType) returnType).getRawType(); + return MapUtil.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()); - } else 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 e95dd527d09..1a765fb1022 100644 --- a/src/main/java/org/apache/ibatis/reflection/MetaObject.java +++ b/src/main/java/org/apache/ibatis/reflection/MetaObject.java @@ -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; @@ -97,10 +99,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 e47c51ca218..1f2eff830bf 100644 --- a/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/ParamNameResolver.java @@ -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; @@ -51,14 +56,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++) { @@ -70,6 +78,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; } @@ -86,8 +95,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) { @@ -143,6 +175,21 @@ public Object getNamedParams(Object[] args) { } } + public Type getType(String name) { + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(name); + Type type = typeMap.get(propertyTokenizer.getName()); + 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(); + } + } + // TODO: param1, param2 + return type; + } + /** * Wrap to a {@link ParamMap} if object is {@link Collection} or array. * @@ -170,4 +217,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 a326b70bb12..f6801f51588 100644 --- a/src/main/java/org/apache/ibatis/reflection/Reflector.java +++ b/src/main/java/org/apache/ibatis/reflection/Reflector.java @@ -54,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 Map caseInsensitivePropertyMap = new HashMap<>(); - public Reflector(Class clazz) { - type = clazz; + private static final Entry> nullEntry = MapUtil.entry(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); @@ -144,7 +152,7 @@ private void addGetMethod(String name, Method method, boolean isAmbiguous) { : new MethodInvoker(method); getMethods.put(name, invoker); Type returnType = TypeParameterResolver.resolveReturnType(method, type); - getTypes.put(name, typeToClass(returnType)); + getTypes.put(name, MapUtil.entry(returnType, typeToClass(returnType))); } private void addSetMethods(Method[] methods) { @@ -165,7 +173,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; @@ -203,7 +211,7 @@ private Method pickBetterSetter(Method setter1, Method setter2, String property) 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, MapUtil.entry(paramTypes[0], typeToClass(paramTypes[0]))); return null; } @@ -211,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, MapUtil.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) { @@ -260,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(), MapUtil.entry(fieldType, typeToClass(fieldType))); } } @@ -268,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(), MapUtil.entry(fieldType, typeToClass(fieldType))); } } @@ -358,14 +362,14 @@ public static boolean canControlMemberAccessible() { * @return The class name */ public Class getType() { - return type; + return clazz; } public Constructor getDefaultConstructor() { if (defaultConstructor != null) { return defaultConstructor; } else { - throw new ReflectionException("There is no default constructor for " + type); + throw new ReflectionException("There is no default constructor for " + clazz); } } @@ -376,7 +380,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; } @@ -384,7 +388,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; } @@ -396,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. * @@ -410,13 +420,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 33e4b108f1c..f50b4b440d2 100644 --- a/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java +++ b/src/main/java/org/apache/ibatis/reflection/ReflectorFactory.java @@ -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); -} \ No newline at end of file + 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 9f2df476c93..5b866d4a4ba 100644 --- a/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java +++ b/src/main/java/org/apache/ibatis/reflection/TypeParameterResolver.java @@ -24,12 +24,22 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.Arrays; +import java.util.Objects; /** * @author Iwao AVE! */ 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. * @@ -160,6 +170,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,6 +284,21 @@ public Type getRawType() { return rawType; } + @Override + public int hashCode() { + return (ownerType == null ? 0 : ownerType.hashCode()) ^ Arrays.hashCode(actualTypeArguments) ^ rawType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParameterizedType)) { + return false; + } + ParameterizedType other = (ParameterizedType) obj; + return rawType.equals(other.getRawType()) && Objects.equals(ownerType, other.getOwnerType()) + && Arrays.equals(actualTypeArguments, other.getActualTypeArguments()); + } + @Override public String toString() { return "ParameterizedTypeImpl [rawType=" + rawType + ", ownerType=" + ownerType + ", actualTypeArguments=" + Arrays.toString(actualTypeArguments) + "]"; 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 cae43e0f0e4..421a7ed9579 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/BeanWrapper.java @@ -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) { } } + @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); @@ -105,6 +122,21 @@ public Class getGetterType(String name) { } } + @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 c59b42f6b44..3cd0426ef11 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/MapWrapper.java @@ -15,14 +15,18 @@ */ package org.apache.ibatis.reflection.wrapper; +import java.lang.reflect.Type; +import java.util.AbstractMap; 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; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.property.PropertyTokenizer; +import org.apache.ibatis.util.MapUtil; /** * @author Clinton Begin @@ -90,6 +94,12 @@ public Class getSetterType(String name) { } } + @Override + public Entry> getGenericSetterType(String name) { + Class setterType = getSetterType(name); + return MapUtil.entry(setterType, setterType); + } + @Override public Class getGetterType(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); @@ -109,6 +119,12 @@ public Class getGetterType(String name) { } } + @Override + public Entry> getGenericGetterType(String name) { + Class getterType = getGetterType(name); + return MapUtil.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..3d2b18a92aa 100644 --- a/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java +++ b/src/main/java/org/apache/ibatis/reflection/wrapper/ObjectWrapper.java @@ -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,14 @@ 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 ed681587cc8..2c93f72bec5 100644 --- a/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/LanguageDriver.java @@ -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; @@ -48,6 +49,10 @@ 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. @@ -59,4 +64,8 @@ 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 a9f5a91a0b8..93eb520aeef 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,31 @@ */ package org.apache.ibatis.scripting.defaults; +import java.lang.reflect.Type; +import java.sql.CallableStatement; +import java.sql.ParameterMetaData; import java.sql.PreparedStatement; +import java.sql.ResultSet; 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.BaseTypeHandler; 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,6 +57,26 @@ public class DefaultParameterHandler implements ParameterHandler { private final BoundSql boundSql; private final Configuration configuration; + private ParameterMetaData paramMetaData; + private MetaObject paramMetaObject; + private HashMap, MetaClass> metaClassCache = new HashMap<>(); + private static final TypeHandler nullTypeHandler = new ObjectTypeHandler(); + private static final ParameterMetaData nullParameterMetaData = 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; this.configuration = mappedStatement.getConfiguration(); @@ -58,30 +90,83 @@ public Object getParameterObject() { return parameterObject; } + @SuppressWarnings("rawtypes") @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; String propertyName = parameterMapping.getProperty(); + JdbcType jdbcType = parameterMapping.getJdbcType(); + JdbcType actualJdbcType = jdbcType == null ? getParamJdbcType(ps, i + 1) : jdbcType; + Type propertyGenericType = null; + TypeHandler typeHandler = parameterMapping.getTypeHandler(); if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); + if (typeHandler == null) { + typeHandler = configuration.getTypeHandlerResolver().resolve(value.getClass(), null, null, actualJdbcType, + null); + } } else if (parameterObject == null) { value = null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - value = parameterObject; } else { - MetaObject metaObject = configuration.newMetaObject(parameterObject); - value = metaObject.getValue(propertyName); + Class parameterClass = parameterObject.getClass(); + TypeHandler paramTypeHandler = typeHandlerRegistry.getTypeHandler(parameterClass, actualJdbcType); + if (paramTypeHandler != null) { + value = parameterObject; + typeHandler = paramTypeHandler; + } else { + MetaObject metaObject = getParamMetaObject(); + value = metaObject.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 { + try { + propertyGenericType = metaObject.getGenericGetterType(propertyName).getKey(); + typeHandler = configuration.getTypeHandlerResolver().resolve(parameterClass, propertyGenericType, + propertyName, actualJdbcType, null); + } catch (Exception e) { + // Not always resolvable + } + } + } + } } - TypeHandler typeHandler = parameterMapping.getTypeHandler(); - JdbcType jdbcType = parameterMapping.getJdbcType(); - if (value == null && jdbcType == null) { - jdbcType = configuration.getJdbcTypeForNull(); + if (value == null) { + if (jdbcType == null) { + jdbcType = configuration.getJdbcTypeForNull(); + } + if (typeHandler == null) { + typeHandler = nullTypeHandler; + } + } else if (typeHandler == null) { + if (propertyGenericType == null) { + propertyGenericType = value.getClass(); + } + typeHandler = configuration.getTypeHandlerResolver().resolve(parameterObject.getClass(), + propertyGenericType, propertyName, actualJdbcType, null); } try { typeHandler.setParameter(ps, i + 1, value, jdbcType); @@ -93,4 +178,27 @@ public void setParameters(PreparedStatement ps) { } } + private MetaObject getParamMetaObject() { + if (paramMetaObject != null) { + return paramMetaObject; + } + paramMetaObject = configuration.newMetaObject(parameterObject); + return paramMetaObject; + } + + private JdbcType getParamJdbcType(PreparedStatement ps, int paramIndex) { + try { + if (paramMetaData == null) { + try { + paramMetaData = ps.getParameterMetaData(); + } catch (SQLException e) { + paramMetaData = nullParameterMetaData; + } + } + return JdbcType.forCode(paramMetaData.getParameterType(paramIndex)); + } catch (SQLException e) { + 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 731b364517b..043e4e0d062 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawLanguageDriver.java @@ -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,8 @@ 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 556712e5501..ad54df5d301 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java @@ -20,6 +20,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.scripting.xmltags.DynamicContext; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.scripting.xmltags.SqlNode; @@ -37,13 +38,21 @@ public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) { - this(configuration, getSql(configuration, rootSqlNode), parameterType); + this(configuration, rootSqlNode, parameterType, null); + } + + public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType, ParamNameResolver paramNameResolver) { + this(configuration, getSql(configuration, rootSqlNode), parameterType, paramNameResolver); } public RawSqlSource(Configuration configuration, String sql, Class parameterType) { + this(configuration, sql, parameterType, null); + } + + public RawSqlSource(Configuration configuration, String sql, Class parameterType, ParamNameResolver paramNameResolver) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class clazz = parameterType == null ? Object.class : parameterType; - sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); + sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>(), paramNameResolver); } private static String getSql(Configuration configuration, SqlNode rootSqlNode) { 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 fce0bfa5396..1e6edf6d87f 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java @@ -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,10 +28,16 @@ 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 @@ -39,7 +46,7 @@ public BoundSql getBoundSql(Object parameterObject) { rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings(), paramNameResolver); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; 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 8a1521d59a6..aaf132e8618 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/XMLLanguageDriver.java @@ -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; @@ -40,12 +41,22 @@ 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("