diff --git a/src/main/asciidoc/repositories.adoc b/src/main/asciidoc/repositories.adoc index 0a811cb8a..94a184e53 100644 --- a/src/main/asciidoc/repositories.adoc +++ b/src/main/asciidoc/repositories.adoc @@ -528,10 +528,8 @@ The approach just shown works well if your custom implementation uses annotation [[repositories.custom-behaviour-for-all-repositories]] === Adding custom behavior to all repositories -The preceding approach is not feasible when you want to add a single method to all your repository interfaces. +The preceding approach is not feasible when you want to add a single method to all your repository interfaces. To add custom behavior to all repositories, you first add an intermediate interface to declare the shared behavior. -. To add custom behavior to all repositories, you first add an intermediate interface to declare the shared behavior. -+ .An interface declaring custom shared behavior ==== [source, java] @@ -545,10 +543,8 @@ public interface MyRepository ---- ==== -. Now your individual repository interfaces will extend this intermediate interface instead of the `Repository` interface to include the functionality declared. +Now your individual repository interfaces will extend this intermediate interface instead of the `Repository` interface to include the functionality declared. Next, create an implementation of the intermediate interface that extends the persistence technology-specific repository base class. This class will then act as a custom base class for the repository proxies. -. Next, create an implementation of the intermediate interface that extends the persistence technology-specific repository base class. This class will then act as a custom base class for the repository proxies. -+ .Custom repository base class ==== [source, java] @@ -571,62 +567,29 @@ public class MyRepositoryImpl } ---- ==== -+ + The default behavior of the Spring `` namespace is to provide an implementation for all interfaces that fall under the `base-package`. This means that if left in its current state, an implementation instance of `MyRepository` will be created by Spring. This is of course not desired as it is just supposed to act as an intermediary between `Repository` and the actual repository interfaces you want to define for each entity. To exclude an interface that extends `Repository` from being instantiated as a repository instance, you can either annotate it with `@NoRepositoryBean` (as seen above) or move it outside of the configured `base-package`. -. Then create a custom repository factory to replace the default `RepositoryFactoryBean` that will in turn produce a custom `RepositoryFactory`. The new repository factory will then provide your `MyRepositoryImpl` as the implementation of any interfaces that extend the `Repository` interface, replacing the `SimpleJpaRepository` implementation you just extended. -+ -.Custom repository factory bean +The final step is to make the Spring Data infrastructure aware of the customized repository base class. In JavaConfig this is achieved by using the `repositoryBaseClass` attribute of the `@Enable…Repositories` annotation: + +.Configuring a custom repository base class using JavaConfig ==== [source, java] ---- -public class MyRepositoryFactoryBean, T, - I extends Serializable> extends JpaRepositoryFactoryBean { - - protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) { - return new MyRepositoryFactory(em); - } - - private static class MyRepositoryFactory - extends JpaRepositoryFactory { - - private final EntityManager em; - - public MyRepositoryFactory(EntityManager em) { - - super(em); - this.em = em; - } - - protected Object getTargetRepository(RepositoryMetadata metadata) { - return new MyRepositoryImpl((Class) metadata.getDomainClass(), em); - } - - protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { - return MyRepositoryImpl.class; - } - } -} +@Configuration +@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class) +class ApplicationConfiguration { … } ---- ==== -. Finally, either declare beans of the custom factory directly or use the `factory-class` attribute of the Spring namespace or `@Enable…` annotation to instruct the repository infrastructure to use your custom factory implementation. -+ -.Using the custom factory with the namespace +A corresponding attribute is available in the XML namespace. + +.Configuring a custom repository base class using XML ==== [source, xml] ---- ----- -==== -+ -.Using the custom factory with the `@Enable…` annotation -==== -[source, java] ----- -@EnableJpaRepositories(factoryClass = "com.acme.MyRepositoryFactoryBean") -class Config {} + repository-base-class="….MyRepositoryImpl" /> ---- ==== diff --git a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java index 1c96d6beb..4702bd0d3 100644 --- a/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java +++ b/src/main/java/org/springframework/data/repository/config/AnnotationRepositoryConfigurationSource.java @@ -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 repositoryBaseClass = attributes.getClass(REPOSITORY_BASE_CLASS); + return DefaultRepositoryBaseClass.class.equals(repositoryBaseClass) ? null : repositoryBaseClass.getName(); + } + /** * Returns the {@link AnnotationAttributes} of the annotation configured. * diff --git a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryBaseClass.java b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryBaseClass.java new file mode 100644 index 000000000..7e06c9725 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryBaseClass.java @@ -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() {} +} diff --git a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java index f71c7f8ce..bbf858a6e 100644 --- a/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java +++ b/src/main/java/org/springframework/data/repository/config/DefaultRepositoryConfiguration.java @@ -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, private Key queryLookupStrategyKey; private Class repositoryInterface; + private Class repositoryBaseClass; private Object customImplementation; private NamedQueries namedQueries; private MappingContext mappingContext; @@ -74,6 +75,16 @@ public abstract class RepositoryFactoryBeanSupport, 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, this.factory.setNamedQueries(namedQueries); this.factory.setBeanClassLoader(classLoader); this.factory.setEvaluationContextProvider(evaluationContextProvider); + this.factory.setRepositoryBaseClass(repositoryBaseClass); this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface); diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 2de6b3175..99f76d34b 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -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 repositoryInformationCache = new HashMap(); private final List postProcessors = new ArrayList(); + private Class repositoryBaseClass; private QueryLookupStrategy.Key queryLookupStrategyKey; private List> queryPostProcessors = new ArrayList>(); 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 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); } diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 5790d5107..5a28940f0 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -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; + } } diff --git a/src/main/resources/META-INF/spring.schemas b/src/main/resources/META-INF/spring.schemas index 620dd602d..d3797779e 100644 --- a/src/main/resources/META-INF/spring.schemas +++ b/src/main/resources/META-INF/spring.schemas @@ -3,4 +3,6 @@ http\://www.springframework.org/schema/data/repository/spring-repository-1.4.xsd http\://www.springframework.org/schema/data/repository/spring-repository-1.5.xsd=org/springframework/data/repository/config/spring-repository-1.5.xsd http\://www.springframework.org/schema/data/repository/spring-repository-1.6.xsd=org/springframework/data/repository/config/spring-repository-1.6.xsd http\://www.springframework.org/schema/data/repository/spring-repository-1.7.xsd=org/springframework/data/repository/config/spring-repository-1.7.xsd -http\://www.springframework.org/schema/data/repository/spring-repository.xsd=org/springframework/data/repository/config/spring-repository-1.7.xsd +http\://www.springframework.org/schema/data/repository/spring-repository-1.8.xsd=org/springframework/data/repository/config/spring-repository-1.8.xsd +http\://www.springframework.org/schema/data/repository/spring-repository-1.11.xsd=org/springframework/data/repository/config/spring-repository-1.11.xsd +http\://www.springframework.org/schema/data/repository/spring-repository.xsd=org/springframework/data/repository/config/spring-repository-1.11.xsd diff --git a/src/main/resources/org/springframework/data/repository/config/spring-repository-1.11.xsd b/src/main/resources/org/springframework/data/repository/config/spring-repository-1.11.xsd new file mode 100644 index 000000000..da183f7c9 --- /dev/null +++ b/src/main/resources/org/springframework/data/repository/config/spring-repository-1.11.xsd @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Where to find the files to read the objects from the repository shall be populated with. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deprecated as of version 1.11. Configure a dedicated base-class instead. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/springframework/data/repository/config/EnableRepositories.java b/src/test/java/org/springframework/data/repository/config/EnableRepositories.java index 88214f621..41798ad26 100644 --- a/src/test/java/org/springframework/data/repository/config/EnableRepositories.java +++ b/src/test/java/org/springframework/data/repository/config/EnableRepositories.java @@ -21,6 +21,7 @@ import java.lang.annotation.RetentionPolicy; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupportUnitTests.DummyRegistrar; import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; @@ -41,6 +42,8 @@ public @interface EnableRepositories { Class repositoryFactoryBeanClass() default DummyRepositoryFactoryBean.class; + Class repositoryBaseClass() default PagingAndSortingRepository.class; + String namedQueriesLocation() default ""; String repositoryImplementationPostfix() default "Impl"; diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java index ee67e46cd..8279df80e 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import org.mockito.Mockito; 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.core.support.RepositoryFactorySupportUnitTests.MyRepositoryQuery; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -67,7 +68,7 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryMetadata) */ @Override - protected Object getTargetRepository(RepositoryMetadata metadata) { + protected Object getTargetRepository(RepositoryInformation information) { return repository; } diff --git a/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java b/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java index b39ef1288..a0cbc89e5 100644 --- a/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/util/ReflectionUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 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. @@ -18,6 +18,7 @@ package org.springframework.data.util; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import org.junit.Before; @@ -31,11 +32,13 @@ import org.springframework.util.ReflectionUtils.FieldFilter; */ public class ReflectionUtilsUnitTests { + @SuppressWarnings("rawtypes") Constructor constructor; Field reference; @Before public void setUp() throws Exception { this.reference = Sample.class.getField("field"); + this.constructor = ConstructorDetection.class.getConstructor(int.class, String.class); } @Test @@ -84,12 +87,43 @@ public class ReflectionUtilsUnitTests { assertThat(sample.first, is("foo")); } + /** + * @see DATACMNS-542 + */ + @Test + public void detectsConstructorForCompleteMatch() throws Exception { + assertThat(ReflectionUtils.findConstructor(ConstructorDetection.class, 2, "test"), is(constructor)); + } + + /** + * @see DATACMNS-542 + */ + @Test + public void detectsConstructorForMatchWithNulls() throws Exception { + assertThat(ReflectionUtils.findConstructor(ConstructorDetection.class, 2, null), is(constructor)); + } + + /** + * @see DATACMNS-542 + */ + @Test + public void rejectsConstructorIfNumberOfArgumentsDontMatch() throws Exception { + assertThat(ReflectionUtils.findConstructor(ConstructorDetection.class, 2, "test", "test"), is(nullValue())); + } + + /** + * @see DATACMNS-542 + */ + @Test + public void rejectsConstructorForNullForPrimitiveArgument() throws Exception { + assertThat(ReflectionUtils.findConstructor(ConstructorDetection.class, null, "test"), is(nullValue())); + } + static class Sample { public String field; - @Autowired - String first, second; + @Autowired String first, second; } static class FieldNameFieldFilter implements DescribedFieldFilter { @@ -108,4 +142,8 @@ public class ReflectionUtilsUnitTests { return String.format("Filter for fields named %s", name); } } + + static class ConstructorDetection { + public ConstructorDetection(int i, String string) {} + } }