/* * Copyright 2009-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.builder.annotation; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.ibatis.annotations.Arg; import org.apache.ibatis.annotations.CacheNamespace; import org.apache.ibatis.annotations.CacheNamespaceRef; import org.apache.ibatis.annotations.Case; import org.apache.ibatis.annotations.ConstructorArgs; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.DeleteProvider; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.InsertProvider; import org.apache.ibatis.annotations.Lang; import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; import org.apache.ibatis.annotations.ResultType; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.SelectKey; import org.apache.ibatis.annotations.SelectProvider; import org.apache.ibatis.annotations.TypeDiscriminator; import org.apache.ibatis.annotations.Update; import org.apache.ibatis.annotations.UpdateProvider; import org.apache.ibatis.binding.BindingException; import org.apache.ibatis.binding.MapperMethod.ParamMap; import org.apache.ibatis.builder.BuilderException; import org.apache.ibatis.builder.IncompleteElementException; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; import org.apache.ibatis.executor.keygen.KeyGenerator; import org.apache.ibatis.executor.keygen.NoKeyGenerator; import org.apache.ibatis.executor.keygen.SelectKeyGenerator; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.Discriminator; import org.apache.ibatis.mapping.FetchType; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultFlag; import org.apache.ibatis.mapping.ResultMapping; import org.apache.ibatis.mapping.ResultSetType; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.scripting.LanguageDriver; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.UnknownTypeHandler; /** * @author Clinton Begin */ /** * 注解方式构建mapper,一般不用,可以暂时忽略 */ public class MapperAnnotationBuilder { private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>(); private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<Class<? extends Annotation>>(); private Configuration configuration; private MapperBuilderAssistant assistant; private Class<?> type; public MapperAnnotationBuilder(Configuration configuration, Class<?> type) { String resource = type.getName().replace('.', '/') + ".java (best guess)"; this.assistant = new MapperBuilderAssistant(configuration, resource); this.configuration = configuration; this.type = type; sqlAnnotationTypes.add(Select.class); sqlAnnotationTypes.add(Insert.class); sqlAnnotationTypes.add(Update.class); sqlAnnotationTypes.add(Delete.class); sqlProviderAnnotationTypes.add(SelectProvider.class); sqlProviderAnnotationTypes.add(InsertProvider.class); sqlProviderAnnotationTypes.add(UpdateProvider.class); sqlProviderAnnotationTypes.add(DeleteProvider.class); } public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); } private void parsePendingMethods() { Collection<MethodResolver> incompleteMethods = configuration.getIncompleteMethods(); synchronized (incompleteMethods) { Iterator<MethodResolver> iter = incompleteMethods.iterator(); while (iter.hasNext()) { try { iter.next().resolve(); iter.remove(); } catch (IncompleteElementException e) { // This method is still missing a resource } } } } private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } } private void parseCache() { CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class); if (cacheDomain != null) { Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size(); Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval(); assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), null); } } private void parseCacheRef() { CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class); if (cacheDomainRef != null) { assistant.useCacheRef(cacheDomainRef.value().getName()); } } private String parseResultMap(Method method) { Class<?> returnType = getReturnType(method); ConstructorArgs args = method.getAnnotation(ConstructorArgs.class); Results results = method.getAnnotation(Results.class); TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class); String resultMapId = generateResultMapName(method); applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator); return resultMapId; } private String generateResultMapName(Method method) { StringBuilder suffix = new StringBuilder(); for (Class<?> c : method.getParameterTypes()) { suffix.append("-"); suffix.append(c.getSimpleName()); } if (suffix.length() < 1) { suffix.append("-void"); } return type.getName() + "." + method.getName() + suffix; } private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results, TypeDiscriminator discriminator) { List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); applyConstructorArgs(args, returnType, resultMappings); applyResults(results, returnType, resultMappings); Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator); // TODO add AutoMappingBehaviour assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null); createDiscriminatorResultMaps(resultMapId, returnType, discriminator); } private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) { if (discriminator != null) { for (Case c : discriminator.cases()) { String caseResultMapId = resultMapId + "-" + c.value(); List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); // issue #136 applyConstructorArgs(c.constructArgs(), resultType, resultMappings); applyResults(c.results(), resultType, resultMappings); // TODO add AutoMappingBehaviour assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null); } } } private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) { if (discriminator != null) { String column = discriminator.column(); Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType(); JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType(); Class<? extends TypeHandler<?>> typeHandler = discriminator.typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler(); Case[] cases = discriminator.cases(); Map<String, String> discriminatorMap = new HashMap<String, String>(); for (Case c : cases) { String value = c.value(); String caseResultMapId = resultMapId + "-" + value; discriminatorMap.put(value, caseResultMapId); } return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap); } return null; } void parseStatement(Method method) { Class<?> parameterTypeClass = getParameterType(method); LanguageDriver languageDriver = getLanguageDriver(method); SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver); if (sqlSource != null) { Options options = method.getAnnotation(Options.class); final String mappedStatementId = type.getName() + "." + method.getName(); Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = ResultSetType.FORWARD_ONLY; SqlCommandType sqlCommandType = getSqlCommandType(method); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; KeyGenerator keyGenerator; String keyProperty = "id"; String keyColumn = null; if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = method.getAnnotation(SelectKey.class); if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); } else { keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { keyGenerator = new NoKeyGenerator(); } if (options != null) { flushCache = options.flushCache(); useCache = options.useCache(); fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348 timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); resultSetType = options.resultSetType(); } String resultMapId = null; ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { String[] resultMaps = resultMapAnnotation.value(); StringBuilder sb = new StringBuilder(); for (String resultMap : resultMaps) { if (sb.length() > 0) { sb.append(","); } sb.append(resultMap); } resultMapId = sb.toString(); } else if (isSelect) { resultMapId = parseResultMap(method); } assistant.addMappedStatement( mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, // ParameterMapID null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, // TODO issue #577 false, keyGenerator, keyProperty, keyColumn, // DatabaseID null, languageDriver, // ResultSets null); } } private LanguageDriver getLanguageDriver(Method method) { Lang lang = method.getAnnotation(Lang.class); Class<?> langClass = null; if (lang != null) { langClass = lang.value(); } return assistant.getLanguageDriver(langClass); } private Class<?> getParameterType(Method method) { Class<?> parameterType = null; Class<?>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { if (!RowBounds.class.isAssignableFrom(parameterTypes[i]) && !ResultHandler.class.isAssignableFrom(parameterTypes[i])) { if (parameterType == null) { parameterType = parameterTypes[i]; } else { // issue #135 parameterType = ParamMap.class; } } } return parameterType; } private Class<?> getReturnType(Method method) { Class<?> returnType = method.getReturnType(); // issue #508 if (void.class.equals(returnType)) { ResultType rt = method.getAnnotation(ResultType.class); if (rt != null) { returnType = rt.value(); } } else if (Collection.class.isAssignableFrom(returnType)) { Type returnTypeParameter = method.getGenericReturnType(); if (returnTypeParameter instanceof ParameterizedType) { Type[] actualTypeArguments = ((ParameterizedType) returnTypeParameter).getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length == 1) { returnTypeParameter = actualTypeArguments[0]; if (returnTypeParameter instanceof Class) { returnType = (Class<?>) returnTypeParameter; } else if (returnTypeParameter instanceof ParameterizedType) { // (issue #443) actual type can be a also a parameterized type returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType(); } else if (returnTypeParameter instanceof GenericArrayType) { Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType(); // (issue #525) support List<byte[]> returnType = Array.newInstance(componentType, 0).getClass(); } } } } else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(returnType)) { // (issue 504) Do not look into Maps if there is not MapKey annotation Type returnTypeParameter = method.getGenericReturnType(); if (returnTypeParameter instanceof ParameterizedType) { Type[] actualTypeArguments = ((ParameterizedType) returnTypeParameter).getActualTypeArguments(); if (actualTypeArguments != null && actualTypeArguments.length == 2) { returnTypeParameter = actualTypeArguments[1]; if (returnTypeParameter instanceof Class) { returnType = (Class<?>) returnTypeParameter; } else if (returnTypeParameter instanceof ParameterizedType) { // (issue 443) actual type can be a also a parameterized type returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType(); } } } } return returnType; } private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) { try { Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method); Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method); if (sqlAnnotationType != null) { if (sqlProviderAnnotationType != null) { throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName()); } Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType); final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation); return buildSqlSourceFromStrings(strings, parameterType, languageDriver); } else if (sqlProviderAnnotationType != null) { Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType); return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation); } return null; } catch (Exception e) { throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e); } } private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) { final StringBuilder sql = new StringBuilder(); for (String fragment : strings) { sql.append(fragment); sql.append(" "); } return languageDriver.createSqlSource(configuration, sql.toString(), parameterTypeClass); } private SqlCommandType getSqlCommandType(Method method) { Class<? extends Annotation> type = getSqlAnnotationType(method); if (type == null) { type = getSqlProviderAnnotationType(method); if (type == null) { return SqlCommandType.UNKNOWN; } if (type == SelectProvider.class) { type = Select.class; } else if (type == InsertProvider.class) { type = Insert.class; } else if (type == UpdateProvider.class) { type = Update.class; } else if (type == DeleteProvider.class) { type = Delete.class; } } return SqlCommandType.valueOf(type.getSimpleName().toUpperCase(Locale.ENGLISH)); } private Class<? extends Annotation> getSqlAnnotationType(Method method) { return chooseAnnotationType(method, sqlAnnotationTypes); } private Class<? extends Annotation> getSqlProviderAnnotationType(Method method) { return chooseAnnotationType(method, sqlProviderAnnotationTypes); } private Class<? extends Annotation> chooseAnnotationType(Method method, Set<Class<? extends Annotation>> types) { for (Class<? extends Annotation> type : types) { Annotation annotation = method.getAnnotation(type); if (annotation != null) { return type; } } return null; } private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) { for (Result result : results) { List<ResultFlag> flags = new ArrayList<ResultFlag>(); if (result.id()) { flags.add(ResultFlag.ID); } ResultMapping resultMapping = assistant.buildResultMapping( resultType, nullOrEmpty(result.property()), nullOrEmpty(result.column()), result.javaType() == void.class ? null : result.javaType(), result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(), hasNestedSelect(result) ? nestedSelectId(result) : null, null, null, null, result.typeHandler() == UnknownTypeHandler.class ? null : result.typeHandler(), flags, null, null, isLazy(result)); resultMappings.add(resultMapping); } } private String nestedSelectId(Result result) { String nestedSelect = result.one().select(); if (nestedSelect.length() < 1) { nestedSelect = result.many().select(); } if (!nestedSelect.contains(".")) { nestedSelect = type.getName() + "." + nestedSelect; } return nestedSelect; } private boolean isLazy(Result result) { boolean isLazy = configuration.isLazyLoadingEnabled(); if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) { isLazy = (result.one().fetchType() == FetchType.LAZY); } else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) { isLazy = (result.many().fetchType() == FetchType.LAZY); } return isLazy; } private boolean hasNestedSelect(Result result) { if (result.one().select().length() > 0 && result.many().select().length() > 0) { throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result"); } return result.one().select().length() > 0 || result.many().select().length() > 0; } private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) { for (Arg arg : args) { List<ResultFlag> flags = new ArrayList<ResultFlag>(); flags.add(ResultFlag.CONSTRUCTOR); if (arg.id()) { flags.add(ResultFlag.ID); } ResultMapping resultMapping = assistant.buildResultMapping( resultType, null, nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(), arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()), nullOrEmpty(arg.resultMap()), null, null, arg.typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler(), flags, null, null, false); resultMappings.add(resultMapping); } } private String nullOrEmpty(String value) { return value == null || value.trim().length() == 0 ? null : value; } private Result[] resultsIf(Results results) { return results == null ? new Result[0] : results.value(); } private Arg[] argsIf(ConstructorArgs args) { return args == null ? new Arg[0] : args.value(); } private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class<?> parameterTypeClass, LanguageDriver languageDriver) { String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX; Class<?> resultTypeClass = selectKeyAnnotation.resultType(); StatementType statementType = selectKeyAnnotation.statementType(); String keyProperty = selectKeyAnnotation.keyProperty(); String keyColumn = selectKeyAnnotation.keyColumn(); boolean executeBefore = selectKeyAnnotation.before(); // defaults boolean useCache = false; KeyGenerator keyGenerator = new NoKeyGenerator(); Integer fetchSize = null; Integer timeout = null; boolean flushCache = false; String parameterMap = null; String resultMap = null; ResultSetType resultSetTypeEnum = null; SqlSource sqlSource = buildSqlSourceFromStrings(selectKeyAnnotation.statement(), parameterTypeClass, languageDriver); SqlCommandType sqlCommandType = SqlCommandType.SELECT; assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, false, keyGenerator, keyProperty, keyColumn, null, languageDriver, null); id = assistant.applyCurrentNamespace(id, false); MappedStatement keyStatement = configuration.getMappedStatement(id, false); SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore); configuration.addKeyGenerator(id, answer); return answer; } }