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:
Mark Paluch
2021-03-25 12:19:27 +01:00
parent 1abd534ab0
commit e47faf8aa2
11 changed files with 434 additions and 31 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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" })

View File

@@ -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);
}));
}
/*

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -241,6 +241,14 @@ public class QueryMethod {
return resultProcessor;
}
RepositoryMetadata getMetadata() {
return metadata;
}
Method getMethod() {
return method;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()