DATACMNS-542 - Simplified way to customize repository base class.

RepositoryFactorySupport now exposes a setRepositoryBaseClass(…) to take a custom type that instances will be created reflectively for. This removes the need for a boilerplate repository factory and FactoryBean implementation to hook custom repository base class implementations into the infrastructure.

RepositoryFactorySupport now exposes a getTargetRepositoryViaReflection(…) method to allow sub-classes to create repository instances and consider customized repository base classes nonetheless. getTargetRepository(…) needs a tiny signature change which unfortunately requires imple

Fixed schema registration for version 1.8 schema. Added new XSD containing a base-class attribute on the <repositories /> element to customize the repository base class via XML.

Removed outdated section of how to create a custom base repository class from the reference documentation and replaced it with new simplified instructions.

Deprecated usage of factory-class attribute in configuration. Point users to newly introduced way of configuring a base class directly to avoid the need for a boilerplate repository factory and factory bean implementation.

DefaultRepositoryInformation now makes target class methods assignable before caching and returning them, so that they can always be invoked reflectively. Made this aspect part of the contract of RepositoryInformation.getTargetClassMethod(…).
This commit is contained in:
Oliver Gierke
2015-02-24 21:08:23 +01:00
parent af2e1c50c3
commit 2d62c149bb
18 changed files with 522 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@@ -54,6 +54,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
private static final String NAMED_QUERIES_LOCATION = "namedQueriesLocation";
private static final String QUERY_LOOKUP_STRATEGY = "queryLookupStrategy";
private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass";
private static final String REPOSITORY_BASE_CLASS = "repositoryBaseClass";
private static final String CONSIDER_NESTED_REPOSITORIES = "considerNestedRepositories";
private final AnnotationMetadata configMetadata;
@@ -213,6 +214,17 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
return attributes.getClass(REPOSITORY_FACTORY_BEAN_CLASS).getName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#getRepositoryBaseClassName()
*/
@Override
public String getRepositoryBaseClassName() {
Class<? extends Object> repositoryBaseClass = attributes.getClass(REPOSITORY_BASE_CLASS);
return DefaultRepositoryBaseClass.class.equals(repositoryBaseClass) ? null : repositoryBaseClass.getName();
}
/**
* Returns the {@link AnnotationAttributes} of the annotation configured.
*

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2015 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.springframework.data.repository.config;
/**
* Placeholder class to be used in {@code @Enable} annotation's {@code repositoryBaseClass} attribute. The configuration
* evaluation infrastructure can use this type to find out no special repository base class was configured and apply
* defaults.
*
* @author Oliver Gierke
* @soundtrack Elen - Sink like a stone (Elen)
* @since 1.11
*/
public final class DefaultRepositoryBaseClass {
private DefaultRepositoryBaseClass() {}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@@ -150,6 +150,15 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou
return configurationSource.getRepositoryFactoryBeanName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfiguration#getRepositoryBaseClassName()
*/
@Override
public String getRepositoryBaseClassName() {
return configurationSource.getRepositoryBaseClassName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfiguration#isLazyInit()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@@ -65,7 +65,8 @@ class RepositoryBeanDefinitionBuilder {
this.extension = extension;
this.resourceLoader = resourceLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader);
this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment,
resourceLoader);
}
/**
@@ -90,6 +91,7 @@ class RepositoryBeanDefinitionBuilder {
builder.addPropertyValue("repositoryInterface", configuration.getRepositoryInterface());
builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey());
builder.addPropertyValue("lazyInit", configuration.isLazyInit());
builder.addPropertyValue("repositoryBaseClass", configuration.getRepositoryBaseClassName());
NamedQueriesBeanDefinitionBuilder definitionBuilder = new NamedQueriesBeanDefinitionBuilder(
extension.getDefaultNamedQueryLocation());
@@ -143,6 +145,4 @@ class RepositoryBeanDefinitionBuilder {
return beanName;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2015 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.
@@ -72,9 +72,20 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
* Returns the name of the {@link FactoryBean} class to be used to create repository instances.
*
* @return
* @deprecated as of 1.11 in favor of a dedicated repository class name, see {@link #getRepositoryBaseClassName()}.
*/
@Deprecated
String getRepositoryFactoryBeanName();
/**
* Returns the name of the repository base class to be used or {@literal null} if the store specific defaults shall be
* applied.
*
* @return
* @since 1.11
*/
String getRepositoryBaseClassName();
/**
* Returns the source of the {@link RepositoryConfiguration}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@@ -68,9 +68,21 @@ public interface RepositoryConfigurationSource {
* Returns the name of the class of the {@link FactoryBean} to actually create repository instances.
*
* @return
* @deprecated as of 1.11 in favor of using a dedicated repository base class name, see
* {@link #getRepositoryBaseClassName()}.
*/
@Deprecated
String getRepositoryFactoryBeanName();
/**
* Returns the name of the repository base class to be used or {@literal null} if the store specific defaults shall be
* applied.
*
* @return
* @since 1.11
*/
String getRepositoryBaseClassName();
/**
* Returns the source {@link BeanDefinition}s of the repository interfaces to create repository instances for.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@@ -42,6 +42,7 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
private static final String NAMED_QUERIES_LOCATION = "named-queries-location";
private static final String REPOSITORY_IMPL_POSTFIX = "repository-impl-postfix";
private static final String REPOSITORY_FACTORY_BEAN_CLASS_NAME = "factory-class";
private static final String REPOSITORY_BASE_CLASS_NAME = "base-class";
private static final String CONSIDER_NESTED_REPOSITORIES = "consider-nested-repositories";
private final Element element;
@@ -148,6 +149,15 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
return getNullDefaultedAttribute(element, REPOSITORY_FACTORY_BEAN_CLASS_NAME);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#getRepositoryBaseClassName()
*/
@Override
public String getRepositoryBaseClassName() {
return getNullDefaultedAttribute(element, REPOSITORY_BASE_CLASS_NAME);
}
private String getNullDefaultedAttribute(Element element, String attributeName) {
String attribute = element.getAttribute(attributeName);
return StringUtils.hasText(attribute) ? attribute : null;

View File

@@ -74,9 +74,10 @@ public interface RepositoryInformation extends RepositoryMetadata {
/**
* Returns the target class method that is backing the given method. This can be necessary if a repository interface
* redeclares a method of the core repository interface (e.g. for transaction behaviour customization). Returns the
* method itself if the target class does not implement the given method.
* method itself if the target class does not implement the given method. Implementations need to make sure the
* {@link Method} returned can be invoked via reflection, i.e. needs to be accessible.
*
* @param method
* @param method must not be {@literal null}.
* @return
*/
Method getTargetClassMethod(Method method);

View File

@@ -17,6 +17,7 @@ package org.springframework.data.repository.core.support;
import static org.springframework.core.GenericTypeResolver.*;
import static org.springframework.data.repository.util.ClassUtils.*;
import static org.springframework.util.ReflectionUtils.*;
import java.io.Serializable;
import java.lang.reflect.Method;
@@ -117,13 +118,20 @@ class DefaultRepositoryInformation implements RepositoryInformation {
Method result = getTargetClassMethod(method, customImplementationClass);
if (!result.equals(method)) {
methodCache.put(method, result);
return result;
return cacheAndReturn(method, result);
}
result = getTargetClassMethod(method, repositoryBaseClass);
methodCache.put(method, result);
return result;
return cacheAndReturn(method, getTargetClassMethod(method, repositoryBaseClass));
}
private Method cacheAndReturn(Method key, Method value) {
if (value != null) {
makeAccessible(value);
}
methodCache.put(key, value);
return value;
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2014 the original author or authors.
* Copyright 2008-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,11 +29,11 @@ import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
import org.springframework.util.Assert;
/**
@@ -51,6 +51,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
private Key queryLookupStrategyKey;
private Class<? extends T> repositoryInterface;
private Class<?> repositoryBaseClass;
private Object customImplementation;
private NamedQueries namedQueries;
private MappingContext<?, ?> mappingContext;
@@ -74,6 +75,16 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.repositoryInterface = repositoryInterface;
}
/**
* Configures the repository base class to be used.
*
* @param repositoryBaseClass the repositoryBaseClass to set, can be {@literal null}.
* @since 1.11
*/
public void setRepositoryBaseClass(Class<?> repositoryBaseClass) {
this.repositoryBaseClass = repositoryBaseClass;
}
/**
* Set the {@link QueryLookupStrategy.Key} to be used.
*
@@ -218,6 +229,7 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>,
this.factory.setNamedQueries(namedQueries);
this.factory.setBeanClassLoader(classLoader);
this.factory.setEvaluationContextProvider(evaluationContextProvider);
this.factory.setRepositoryBaseClass(repositoryBaseClass);
this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.data.repository.core.support;
import static org.springframework.util.ReflectionUtils.*;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
@@ -28,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
@@ -45,6 +45,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -63,6 +64,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache = new HashMap<RepositoryInformationCacheKey, RepositoryInformation>();
private final List<RepositoryProxyPostProcessor> postProcessors = new ArrayList<RepositoryProxyPostProcessor>();
private Class<?> repositoryBaseClass;
private QueryLookupStrategy.Key queryLookupStrategyKey;
private List<QueryCreationListener<?>> queryPostProcessors = new ArrayList<QueryCreationListener<?>>();
private NamedQueries namedQueries = PropertiesBasedNamedQueries.EMPTY;
@@ -113,6 +115,17 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
: evaluationContextProvider;
}
/**
* Configures the repository base class to use when creating the repository proxy. If not set, the factory will use
* the type returned by {@link #getRepositoryBaseClass(RepositoryMetadata)} by default.
*
* @param repositoryBaseClass the repository base class to back the repository proxy, can be {@literal null}.
* @since 1.11
*/
public void setRepositoryBaseClass(Class<?> repositoryBaseClass) {
this.repositoryBaseClass = repositoryBaseClass;
}
/**
* Adds a {@link QueryCreationListener} to the factory to plug in functionality triggered right after creation of
* {@link RepositoryQuery} instances.
@@ -214,8 +227,10 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
return repositoryInformation;
}
repositoryInformation = new DefaultRepositoryInformation(metadata, getRepositoryBaseClass(metadata),
customImplementationClass);
Class<?> repositoryBaseClass = this.repositoryBaseClass == null ? getRepositoryBaseClass(metadata)
: this.repositoryBaseClass;
repositoryInformation = new DefaultRepositoryInformation(metadata, repositoryBaseClass, customImplementationClass);
repositoryInformationCache.put(cacheKey, repositoryInformation);
return repositoryInformation;
}
@@ -240,7 +255,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
* @param metadata
* @return
*/
protected abstract Object getTargetRepository(RepositoryMetadata metadata);
protected abstract Object getTargetRepository(RepositoryInformation metadata);
/**
* Returns the base class backing the actual repository instance. Make sure
@@ -296,6 +311,29 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
}
/**
* Creates a repository of the repository base class defined in the given {@link RepositoryInformation} using
* reflection.
*
* @param information
* @param constructorArguments
* @return
*/
@SuppressWarnings("unchecked")
protected final <R> R getTargetRepositoryViaReflection(RepositoryInformation information,
Object... constructorArguments) {
Class<?> baseClass = information.getRepositoryBaseClass();
Constructor<?> constructor = ReflectionUtils.findConstructor(baseClass, constructorArguments);
if (constructor == null) {
throw new IllegalStateException(String.format(
"No suitable constructor found on %s to match the given arguments: %s", baseClass, constructorArguments));
}
return (R) BeanUtils.instantiateClass(constructor, constructorArguments);
}
/**
* This {@code MethodInterceptor} intercepts calls to methods of the custom implementation and delegates the to it if
* configured. Furthermore it resolves method calls to finders and triggers execution of them. You can rely on having
@@ -384,8 +422,8 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
Object[] arguments = invocation.getArguments();
if (isCustomMethodInvocation(invocation)) {
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
makeAccessible(actualMethod);
return executeMethodOn(customImplementation, actualMethod, arguments);
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -238,4 +239,56 @@ public abstract class ReflectionUtils {
return JAVA8_STREAM_TYPE.isAssignableFrom(type);
}
/**
* Finds a constructoron the given type that matches the given constructor arguments.
*
* @param type must not be {@literal null}.
* @param constructorArguments must not be {@literal null}.
* @return a {@link Constructor} that is compatible with the given arguments or {@literal null} if none found.
*/
public static Constructor<?> findConstructor(Class<?> type, Object... constructorArguments) {
Assert.notNull(type, "Target type must not be null!");
Assert.notNull(constructorArguments, "Constructor arguments must not be null!");
for (Constructor<?> candidate : type.getDeclaredConstructors()) {
Class<?>[] parameterTypes = candidate.getParameterTypes();
if (argumentsMatch(parameterTypes, constructorArguments)) {
return candidate;
}
}
return null;
}
private static final boolean argumentsMatch(Class<?>[] parameterTypes, Object[] arguments) {
if (parameterTypes.length != arguments.length) {
return false;
}
int index = 0;
for (Class<?> argumentType : parameterTypes) {
Object argument = arguments[index];
// Reject nulls for primitives
if (argumentType.isPrimitive() && argument == null) {
return false;
}
// Type check if argument is not null
if (argument != null && !ClassUtils.isAssignableValue(argumentType, argument)) {
return false;
}
index++;
}
return true;
}
}