/*
* Copyright 2008-2017 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.core.support;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
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.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.Pair;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.transaction.interceptor.TransactionalProxy;
import org.springframework.util.Assert;
/**
* Factory bean to create instances of a given repository interface. Creates a proxy implementing the configured
* repository interface and apply an advice handing the control to the {@code QueryExecuterMethodInterceptor}. Query
* detection strategy can be configured by setting {@link QueryLookupStrategy.Key}.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
*/
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache;
private final List<RepositoryProxyPostProcessor> postProcessors;
private Optional<Class<?>> repositoryBaseClass;
private QueryLookupStrategy.Key queryLookupStrategyKey;
private List<QueryCreationListener<?>> queryPostProcessors;
private NamedQueries namedQueries;
private ClassLoader classLoader;
private EvaluationContextProvider evaluationContextProvider;
private BeanFactory beanFactory;
private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
public RepositoryFactorySupport() {
this.repositoryInformationCache = new HashMap<>();
this.postProcessors = new ArrayList<>();
this.repositoryBaseClass = Optional.empty();
this.namedQueries = PropertiesBasedNamedQueries.EMPTY;
this.classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
this.evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE;
this.queryPostProcessors = new ArrayList<>();
this.queryPostProcessors.add(collectingListener);
}
/**
* Sets the strategy of how to lookup a query to execute finders.
*
* @param key
*/
public void setQueryLookupStrategyKey(Key key) {
this.queryLookupStrategyKey = key;
}
/**
* Configures a {@link NamedQueries} instance to be handed to the {@link QueryLookupStrategy} for query creation.
*
* @param namedQueries the namedQueries to set
*/
public void setNamedQueries(NamedQueries namedQueries) {
this.namedQueries = namedQueries == null ? PropertiesBasedNamedQueries.EMPTY : namedQueries;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
*/
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Sets the {@link EvaluationContextProvider} to be used to evaluate SpEL expressions in manually defined queries.
*
* @param evaluationContextProvider can be {@literal null}, defaults to
* {@link DefaultEvaluationContextProvider#INSTANCE}.
*/
public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
this.evaluationContextProvider = evaluationContextProvider == null ? DefaultEvaluationContextProvider.INSTANCE
: 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 = Optional.ofNullable(repositoryBaseClass);
}
/**
* Adds a {@link QueryCreationListener} to the factory to plug in functionality triggered right after creation of
* {@link RepositoryQuery} instances.
*
* @param listener
*/
public void addQueryCreationListener(QueryCreationListener<?> listener) {
Assert.notNull(listener, "Listener must not be null!");
this.queryPostProcessors.add(listener);
}
/**
* Adds {@link RepositoryProxyPostProcessor}s to the factory to allow manipulation of the {@link ProxyFactory} before
* the proxy gets created. Note that the {@link QueryExecutorMethodInterceptor} will be added to the proxy
* <em>after</em> the {@link RepositoryProxyPostProcessor}s are considered.
*
* @param processor
*/
public void addRepositoryProxyPostProcessor(RepositoryProxyPostProcessor processor) {
Assert.notNull(processor, "RepositoryProxyPostProcessor must not be null!");
this.postProcessors.add(processor);
}
/**
* Returns a repository instance for the given interface.
*
* @param <T>
* @param repositoryInterface
* @return
*/
public <T> T getRepository(Class<T> repositoryInterface) {
return getRepository(repositoryInterface, Optional.empty());
}
/**
* Returns a repository instance for the given interface backed by an instance providing implementation logic for
* custom logic.
*
* @param <T>
* @param repositoryInterface
* @param customImplementation
* @return
*/
public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {
return getRepository(repositoryInterface, Optional.of(customImplementation));
}
/**
* Returns a repository instance for the given interface backed by an instance providing implementation logic for
* custom logic.
*
* @param <T>
* @param repositoryInterface
* @param customImplementation
* @return
*/
@SuppressWarnings({ "unchecked" })
protected <T> T getRepository(Class<T> repositoryInterface, Optional<Object> customImplementation) {
RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
RepositoryInformation information = getRepositoryInformation(metadata, customImplementation.map(Object::getClass));
validate(information, customImplementation);
Object target = getTargetRepository(information);
// Create proxy
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(new Class[] { repositoryInterface, Repository.class, TransactionalProxy.class });
result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
postProcessors.forEach(processor -> processor.postProcess(result, information));
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
result.addAdvice(new QueryExecutorMethodInterceptor(information));
result.addAdvice(information.isReactiveRepository()
? new ConvertingImplementationMethodExecutionInterceptor(information, customImplementation, target)
: new ImplementationMethodExecutionInterceptor(information, customImplementation, target));
return (T) result.getProxy(classLoader);
}
/**
* Returns the {@link RepositoryMetadata} for the given repository interface.
*
* @param repositoryInterface will never be {@literal null}.
* @return
*/
protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
return AbstractRepositoryMetadata.getMetadata(repositoryInterface);
}
/**
* Returns the {@link RepositoryInformation} for the given repository interface.
*
* @param metadata
* @param customImplementationClass
* @return
*/
protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
Optional<Class<?>> customImplementationClass) {
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, customImplementationClass);
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));
return metadata.isReactiveRepository()
? new ReactiveRepositoryInformation(metadata, baseClass, customImplementationClass)
: new DefaultRepositoryInformation(metadata, baseClass, customImplementationClass);
});
}
protected List<QueryMethod> getQueryMethods() {
return collectingListener.getQueryMethods();
}
/**
* Returns the {@link EntityInformation} for the given domain class.
*
* @param <T> the entity type
* @param <ID> the id type
* @param domainClass
* @return
*/
public abstract <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass);
/**
* Create a repository instance as backing for the query proxy.
*
* @param metadata
* @return
*/
protected abstract Object getTargetRepository(RepositoryInformation metadata);
/**
* Returns the base class backing the actual repository instance. Make sure
* {@link #getTargetRepository(RepositoryMetadata)} returns an instance of this class.
*
* @param metadata
* @return
*/
protected abstract Class<?> getRepositoryBaseClass(RepositoryMetadata metadata);
/**
* Returns the {@link QueryLookupStrategy} for the given {@link Key} and {@link EvaluationContextProvider}.
*
* @param key can be {@literal null}.
* @param evaluationContextProvider will never be {@literal null}.
* @return the {@link QueryLookupStrategy} to use or {@literal null} if no queries should be looked up.
* @since 1.9
*/
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
EvaluationContextProvider evaluationContextProvider) {
return Optional.empty();
}
/**
* Validates the given repository interface as well as the given custom implementation.
*
* @param repositoryInformation
* @param customImplementation
*/
private void validate(RepositoryInformation repositoryInformation, Optional<Object> customImplementation) {
customImplementation.orElseGet(() -> {
if (!repositoryInformation.hasCustomMethod()) {
return null;
}
throw new IllegalArgumentException(
String.format("You have custom methods in %s but not provided a custom implementation!",
repositoryInformation.getRepositoryInterface()));
});
validate(repositoryInformation);
}
protected void validate(RepositoryMetadata repositoryMetadata) {
}
/**
* 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();
Optional<Constructor<?>> constructor = ReflectionUtils.findConstructor(baseClass, constructorArguments);
return constructor.map(it -> (R) BeanUtils.instantiateClass(it, constructorArguments))
.orElseThrow(() -> new IllegalStateException(String.format(
"No suitable constructor found on %s to match the given arguments: %s. Make sure you implement a constructor taking these",
baseClass, Arrays.stream(constructorArguments).map(Object::getClass).collect(Collectors.toList()))));
}
/**
* 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
* a custom repository implementation instance set if this returns true.
*
* @author Oliver Gierke
*/
public class QueryExecutorMethodInterceptor implements MethodInterceptor {
private final Map<Method, RepositoryQuery> queries;
private final QueryExecutionResultHandler resultHandler;
/**
* Creates a new {@link QueryExecutorMethodInterceptor}. Builds a model of {@link QueryMethod}s to be invoked on
* execution of repository interface methods.
*/
public QueryExecutorMethodInterceptor(RepositoryInformation repositoryInformation) {
this.resultHandler = new QueryExecutionResultHandler();
Optional<QueryLookupStrategy> lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
RepositoryFactorySupport.this.evaluationContextProvider);
if (!lookupStrategy.isPresent() && repositoryInformation.hasQueryMethods()) {
throw new IllegalStateException("You have defined query method in the repository but "
+ "you don't have any query lookup strategy defined. The "
+ "infrastructure apparently does not support query methods!");
}
this.queries = lookupStrategy.map(it -> {
SpelAwareProxyProjectionFactory factory = new SpelAwareProxyProjectionFactory();
factory.setBeanClassLoader(classLoader);
factory.setBeanFactory(beanFactory);
return repositoryInformation.getQueryMethods().stream()//
.map(method -> Pair.of(method, it.resolveQuery(method, repositoryInformation, factory, namedQueries)))//
.peek(pair -> invokeListeners(pair.getSecond()))//
.collect(Pair.toMap());
}).orElse(Collections.emptyMap());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void invokeListeners(RepositoryQuery query) {
for (QueryCreationListener listener : queryPostProcessors) {
ResolvableType typeArgument = ResolvableType.forClass(QueryCreationListener.class, listener.getClass())
.getGeneric(0);
if (typeArgument != null && typeArgument.isAssignableFrom(ResolvableType.forClass(query.getClass()))) {
listener.onCreation(query);
}
}
}
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result = doInvoke(invocation);
// Looking up the TypeDescriptor for the return type - yes, this way o.O
Method method = invocation.getMethod();
MethodParameter parameter = new MethodParameter(method, -1);
TypeDescriptor methodReturnTypeDescriptor = TypeDescriptor.nested(parameter, 0);
return resultHandler.postProcessInvocationResult(result, methodReturnTypeDescriptor);
}
private Object doInvoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
if (hasQueryFor(method)) {
return queries.get(method).execute(arguments);
}
return invocation.proceed();
}
/**
* Returns whether we know of a query to execute for the given {@link Method};
*
* @param method
* @return
*/
private boolean hasQueryFor(Method method) {
return queries.containsKey(method);
}
}
/**
* Method interceptor that calls methods on either the base implementation or the custom repository implementation.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class ImplementationMethodExecutionInterceptor implements MethodInterceptor {
private final RepositoryInformation repositoryInformation;
private final Optional<Object> customImplementation;
private final Object target;
/* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
if (isCustomMethodInvocation(invocation)) {
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
return executeMethodOn(customImplementation.get(), actualMethod, arguments);
}
// Lookup actual method as it might be redeclared in the interface
// and we have to use the repository instance nevertheless
Method actualMethod = repositoryInformation.getTargetClassMethod(method);
return executeMethodOn(target, actualMethod, arguments);
}
/**
* Executes the given method on the given target. Correctly unwraps exceptions not caused by the reflection magic.
*
* @param target
* @param method
* @param parameters
* @return
* @throws Throwable
*/
protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
try {
return method.invoke(target, parameters);
} catch (Exception e) {
ClassUtils.unwrapReflectionException(e);
}
throw new IllegalStateException("Should not occur!");
}
/**
* Returns whether the given {@link MethodInvocation} is considered to be targeted as an invocation of a custom
* method.
*
* @param method
* @return
*/
private boolean isCustomMethodInvocation(MethodInvocation invocation) {
return customImplementation.map(it -> repositoryInformation.isCustomMethod(invocation.getMethod())).orElse(false);
}
}
/**
* Method interceptor that converts parameters before invoking a method.
*
* @author Mark Paluch
*/
public class ConvertingImplementationMethodExecutionInterceptor extends ImplementationMethodExecutionInterceptor {
/**
* @param repositoryInformation
* @param customImplementation
* @param target
*/
public ConvertingImplementationMethodExecutionInterceptor(RepositoryInformation repositoryInformation,
Optional<Object> customImplementation, Object target) {
super(repositoryInformation, customImplementation, target);
}
/* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport.ImplementationMethodExecutionInterceptor#executeMethodOn(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
*/
@Override
protected Object executeMethodOn(Object target, Method method, Object[] parameters) throws Throwable {
return super.executeMethodOn(target, method, convertParameters(method.getParameterTypes(), parameters));
}
/**
* @param parameterTypes
* @param parameters
* @return
*/
private Object[] convertParameters(Class<?>[] parameterTypes, Object[] parameters) {
if (parameters.length == 0) {
return parameters;
}
Object[] result = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
if (parameters[i] == null) {
continue;
}
if (!parameterTypes[i].isAssignableFrom(parameters[i].getClass()) && ReactiveWrappers.isAvailable()
&& ReactiveWrapperConverters.canConvert(parameters[i].getClass(), parameterTypes[i])) {
result[i] = ReactiveWrapperConverters.toWrapper(parameters[i], parameterTypes[i]);
} else {
result[i] = parameters[i];
}
}
return result;
}
}
/**
* {@link QueryCreationListener} collecting the {@link QueryMethod}s created for all query methods of the repository
* interface.
*
* @author Oliver Gierke
*/
@Getter
private static class QueryCollectingQueryCreationListener implements QueryCreationListener<RepositoryQuery> {
/**
* All {@link QueryMethod}s.
*/
private List<QueryMethod> queryMethods = new ArrayList<>();
/* (non-Javadoc)
* @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery)
*/
public void onCreation(RepositoryQuery query) {
this.queryMethods.add(query.getQueryMethod());
}
}
/**
* Simple value object to build up keys to cache {@link RepositoryInformation} instances.
*
* @author Oliver Gierke
*/
@EqualsAndHashCode
private static class RepositoryInformationCacheKey {
private final String repositoryInterfaceName;
private final String customImplementationClassName;
/**
* Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and cuytom
* implementation type.
*
* @param repositoryInterfaceName must not be {@literal null}.
* @param customImplementationClassName
*/
public RepositoryInformationCacheKey(RepositoryMetadata metadata, Optional<Class<?>> customImplementationType) {
this.repositoryInterfaceName = metadata.getRepositoryInterface().getName();
this.customImplementationClassName = customImplementationType.map(Class::getName).orElse(null);
}
}
}