Improve error message if store module doesn't support a well-known fragment interface.
We now throw a RepositoryCreationException (or subclass) when a repository cannot be created due to a missing fragment, a fragment without implementation or if a well-known fragment is not supported by the repository factory. Throw QueryCreationException if QueryExecutorMethodInterceptor cannot resolve a RepositoryQuery. Closes #2341 Original pull request: #2342.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
|
||||
/**
|
||||
* Exception thrown in the context of repository creation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.5
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class RepositoryCreationException extends InvalidDataAccessApiUsageException {
|
||||
|
||||
private final Class<?> repositoryInterface;
|
||||
|
||||
/**
|
||||
* Constructor for RepositoryCreationException.
|
||||
*
|
||||
* @param msg the detail message.
|
||||
* @param repositoryInterface the repository interface.
|
||||
*/
|
||||
public RepositoryCreationException(String msg, Class<?> repositoryInterface) {
|
||||
super(msg);
|
||||
this.repositoryInterface = repositoryInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for RepositoryException.
|
||||
*
|
||||
* @param msg the detail message.
|
||||
* @param cause the root cause from the data access API in use.
|
||||
* @param repositoryInterface the repository interface.
|
||||
*/
|
||||
public RepositoryCreationException(String msg, Throwable cause, Class<?> repositoryInterface) {
|
||||
super(msg, cause);
|
||||
this.repositoryInterface = repositoryInterface;
|
||||
}
|
||||
|
||||
public Class<?> getRepositoryInterface() {
|
||||
return repositoryInterface;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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 org.springframework.data.repository.core.RepositoryCreationException;
|
||||
|
||||
/**
|
||||
* Exception thrown during repository creation or repository method invocation when invoking a repository method on a
|
||||
* fragment without an implementation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.5
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class FragmentNotImplementedException extends RepositoryCreationException {
|
||||
|
||||
private final RepositoryFragment<?> fragment;
|
||||
|
||||
/**
|
||||
* Constructor for FragmentNotImplementedException.
|
||||
*
|
||||
* @param msg the detail message.
|
||||
* @param repositoryInterface the repository interface.
|
||||
* @param fragment the offending repository fragment.
|
||||
*/
|
||||
public FragmentNotImplementedException(String msg, Class<?> repositoryInterface, RepositoryFragment<?> fragment) {
|
||||
super(msg, repositoryInterface);
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
public RepositoryFragment<?> getFragment() {
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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 org.springframework.data.repository.core.RepositoryCreationException;
|
||||
|
||||
/**
|
||||
* Exception thrown during repository creation when a the repository has custom methods that are not backed by a
|
||||
* fragment or if no fragment could be found for a repository method invocation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.5
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class IncompleteRepositoryCompositionException extends RepositoryCreationException {
|
||||
|
||||
/**
|
||||
* Constructor for IncompleteRepositoryCompositionException.
|
||||
*
|
||||
* @param msg the detail message.
|
||||
* @param repositoryInterface the repository interface.
|
||||
*/
|
||||
public IncompleteRepositoryCompositionException(String msg, Class<?> repositoryInterface) {
|
||||
super(msg, repositoryInterface);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster;
|
||||
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
|
||||
import org.springframework.data.repository.query.QueryCreationException;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
@@ -97,7 +98,13 @@ class QueryExecutorMethodInterceptor implements MethodInterceptor {
|
||||
|
||||
private Pair<Method, RepositoryQuery> lookupQuery(Method method, RepositoryInformation information,
|
||||
QueryLookupStrategy strategy, ProjectionFactory projectionFactory) {
|
||||
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
|
||||
try {
|
||||
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
|
||||
} catch (QueryCreationException e) {
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
throw QueryCreationException.create(e.getMessage(), e, information.getRepositoryInterface(), method);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
|
||||
@@ -316,8 +316,12 @@ public class RepositoryComposition {
|
||||
public void validateImplementation() {
|
||||
|
||||
fragments.stream().forEach(it -> it.getImplementation() //
|
||||
.orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
|
||||
ClassUtils.getQualifiedName(it.getSignatureContributor())))));
|
||||
.orElseThrow(() -> {
|
||||
Class<?> repositoryInterface = metadata != null ? metadata.getRepositoryInterface() : Object.class;
|
||||
return new FragmentNotImplementedException(String.format("Fragment %s used in %s has no implementation.",
|
||||
ClassUtils.getQualifiedName(it.getSignatureContributor()),
|
||||
ClassUtils.getQualifiedName(repositoryInterface)), repositoryInterface, it);
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -28,6 +29,7 @@ import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -57,12 +59,12 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.util.ClassUtils;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.interceptor.TransactionalProxy;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -312,8 +314,6 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
|
||||
repositoryCompositionStep.end();
|
||||
|
||||
validate(information, composition);
|
||||
|
||||
StartupStep repositoryTargetStep = onEvent(applicationStartup, "spring.data.repository.target",
|
||||
repositoryInterface);
|
||||
Object target = getTargetRepository(information);
|
||||
@@ -321,6 +321,9 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
repositoryTargetStep.tag("target", target.getClass().getName());
|
||||
repositoryTargetStep.end();
|
||||
|
||||
RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
|
||||
validate(information, compositionToUse);
|
||||
|
||||
// Create proxy
|
||||
StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface);
|
||||
ProxyFactory result = new ProxyFactory();
|
||||
@@ -357,7 +360,6 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
|
||||
namedQueries, queryPostProcessors, methodInvocationListeners));
|
||||
|
||||
RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
|
||||
result.addAdvice(
|
||||
new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));
|
||||
|
||||
@@ -502,17 +504,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
*/
|
||||
private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) {
|
||||
|
||||
if (repositoryInformation.hasCustomMethod()) {
|
||||
|
||||
if (composition.isEmpty()) {
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("You have custom methods in %s but have not provided a custom implementation!",
|
||||
repositoryInformation.getRepositoryInterface()));
|
||||
}
|
||||
|
||||
composition.validateImplementation();
|
||||
}
|
||||
RepositoryValidator.validate(composition, getClass(), repositoryInformation);
|
||||
|
||||
validate(repositoryInformation);
|
||||
}
|
||||
@@ -606,7 +598,7 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
try {
|
||||
return composition.invoke(invocationMulticaster, method, arguments);
|
||||
} catch (Exception e) {
|
||||
ClassUtils.unwrapReflectionException(e);
|
||||
org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Should not occur!");
|
||||
@@ -715,4 +707,94 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware,
|
||||
+ this.getRepositoryInterfaceName() + ", compositionHash=" + this.getCompositionHash() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator utility to catch common mismatches with a proper error message instead of letting the query mechanism
|
||||
* attempt implementing a query method and fail with a less specific message.
|
||||
*/
|
||||
static class RepositoryValidator {
|
||||
|
||||
static Map<Class<?>, String> WELL_KNOWN_EXECUTORS = new HashMap<>();
|
||||
|
||||
static {
|
||||
|
||||
org.springframework.data.repository.util.ClassUtils.ifPresent(
|
||||
"org.springframework.data.querydsl.QuerydslPredicateExecutor", RepositoryValidator.class.getClassLoader(),
|
||||
it -> {
|
||||
WELL_KNOWN_EXECUTORS.put(it, "Querydsl");
|
||||
});
|
||||
|
||||
org.springframework.data.repository.util.ClassUtils.ifPresent(
|
||||
"org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor",
|
||||
RepositoryValidator.class.getClassLoader(), it -> {
|
||||
WELL_KNOWN_EXECUTORS.put(it, "Reactive Querydsl");
|
||||
});
|
||||
|
||||
org.springframework.data.repository.util.ClassUtils.ifPresent(
|
||||
"org.springframework.data.repository.query.QueryByExampleExecutor",
|
||||
RepositoryValidator.class.getClassLoader(), it -> {
|
||||
WELL_KNOWN_EXECUTORS.put(it, "Query by Example");
|
||||
});
|
||||
|
||||
org.springframework.data.repository.util.ClassUtils.ifPresent(
|
||||
"org.springframework.data.repository.query.ReactiveQueryByExampleExecutor",
|
||||
RepositoryValidator.class.getClassLoader(), it -> {
|
||||
WELL_KNOWN_EXECUTORS.put(it, "Reactive Query by Example");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the {@link RepositoryComposition} for custom implementations and well-known executors.
|
||||
*
|
||||
* @param composition
|
||||
* @param source
|
||||
* @param repositoryInformation
|
||||
*/
|
||||
public static void validate(RepositoryComposition composition, Class<?> source,
|
||||
RepositoryInformation repositoryInformation) {
|
||||
|
||||
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
|
||||
if (repositoryInformation.hasCustomMethod()) {
|
||||
|
||||
if (composition.isEmpty()) {
|
||||
|
||||
throw new IncompleteRepositoryCompositionException(
|
||||
String.format("You have custom methods in %s but have not provided a custom implementation!",
|
||||
org.springframework.util.ClassUtils.getQualifiedName(repositoryInterface)),
|
||||
repositoryInterface);
|
||||
}
|
||||
|
||||
composition.validateImplementation();
|
||||
}
|
||||
|
||||
for (Map.Entry<Class<?>, String> entry : WELL_KNOWN_EXECUTORS.entrySet()) {
|
||||
|
||||
Class<?> executorInterface = entry.getKey();
|
||||
if (!executorInterface.isAssignableFrom(repositoryInterface)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!containsFragmentImplementation(composition, executorInterface)) {
|
||||
throw new UnsupportedFragmentException(
|
||||
String.format("Repository %s implements %s but %s does not support %s!",
|
||||
ClassUtils.getQualifiedName(repositoryInterface), ClassUtils.getQualifiedName(executorInterface),
|
||||
ClassUtils.getShortName(source), entry.getValue()),
|
||||
repositoryInterface, executorInterface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsFragmentImplementation(RepositoryComposition composition,
|
||||
Class<?> executorInterface) {
|
||||
|
||||
for (RepositoryFragment<?> fragment : composition.getFragments()) {
|
||||
|
||||
if (fragment.getImplementation().filter(executorInterface::isInstance).isPresent()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2021 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
|
||||
*
|
||||
* https://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 org.springframework.data.repository.core.RepositoryCreationException;
|
||||
|
||||
/**
|
||||
* Exception thrown during repository creation when a well-known fragment interface is not supported by the repository
|
||||
* factory.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.5
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class UnsupportedFragmentException extends RepositoryCreationException {
|
||||
|
||||
private final Class<?> fragmentInterface;
|
||||
|
||||
/**
|
||||
* Constructor for UnsupportedFragmentException.
|
||||
*
|
||||
* @param msg the detail message.
|
||||
* @param repositoryInterface the repository interface.
|
||||
* @param fragmentInterface the offending fragment interface.
|
||||
*/
|
||||
public UnsupportedFragmentException(String msg, Class<?> repositoryInterface, Class<?> fragmentInterface) {
|
||||
super(msg, repositoryInterface);
|
||||
this.fragmentInterface = fragmentInterface;
|
||||
}
|
||||
|
||||
public Class<?> getFragmentInterface() {
|
||||
return fragmentInterface;
|
||||
}
|
||||
}
|
||||
@@ -15,24 +15,39 @@
|
||||
*/
|
||||
package org.springframework.data.repository.query;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.data.repository.core.RepositoryCreationException;
|
||||
|
||||
/**
|
||||
* Exception to be thrown if a query cannot be created from a {@link QueryMethod}.
|
||||
* Exception to be thrown if a query cannot be created from a {@link Method}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public final class QueryCreationException extends RuntimeException {
|
||||
public final class QueryCreationException extends RepositoryCreationException {
|
||||
|
||||
private static final long serialVersionUID = -1238456123580L;
|
||||
private static final String MESSAGE_TEMPLATE = "Could not create query for method %s! Could not find property %s on domain class %s.";
|
||||
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates a new {@link QueryCreationException}.
|
||||
*
|
||||
* @param method
|
||||
*/
|
||||
private QueryCreationException(String message) {
|
||||
private QueryCreationException(String message, QueryMethod method) {
|
||||
|
||||
super(message);
|
||||
super(message, method.getMetadata().getRepositoryInterface());
|
||||
this.method = method.getMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link QueryCreationException}.
|
||||
*/
|
||||
private QueryCreationException(String message, Throwable cause, Class<?> repositoryInterface, Method method) {
|
||||
|
||||
super(message, cause, repositoryInterface);
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +60,7 @@ public final class QueryCreationException extends RuntimeException {
|
||||
public static QueryCreationException invalidProperty(QueryMethod method, String propertyName) {
|
||||
|
||||
return new QueryCreationException(String.format(MESSAGE_TEMPLATE, method, propertyName, method.getDomainClass()
|
||||
.getName()));
|
||||
.getName()), method);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +72,8 @@ public final class QueryCreationException extends RuntimeException {
|
||||
*/
|
||||
public static QueryCreationException create(QueryMethod method, String message) {
|
||||
|
||||
return new QueryCreationException(String.format("Could not create query for %s! Reason: %s", method, message));
|
||||
return new QueryCreationException(String.format("Could not create query for %s! Reason: %s", method, message),
|
||||
method);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +84,29 @@ public final class QueryCreationException extends RuntimeException {
|
||||
* @return
|
||||
*/
|
||||
public static QueryCreationException create(QueryMethod method, Throwable cause) {
|
||||
return new QueryCreationException(cause.getMessage(), cause, method.getMetadata().getRepositoryInterface(),
|
||||
method.getMethod());
|
||||
}
|
||||
|
||||
return create(method, cause.getMessage());
|
||||
/**
|
||||
* Creates a new {@link QueryCreationException} for the given {@link QueryMethod} and {@link Throwable} as cause.
|
||||
*
|
||||
* @param method
|
||||
* @param cause
|
||||
* @return
|
||||
* @since 2.5
|
||||
*/
|
||||
public static QueryCreationException create(String message, Throwable cause, Class<?> repositoryInterface,
|
||||
Method method) {
|
||||
return new QueryCreationException(String.format("Could not create query for %s! Reason: %s", method, message),
|
||||
cause, repositoryInterface, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* @since 2.5
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +241,14 @@ public class QueryMethod {
|
||||
return resultProcessor;
|
||||
}
|
||||
|
||||
RepositoryMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
|
||||
Reference in New Issue
Block a user