Skip to content

Commit e47faf8

Browse files
committed
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.
1 parent 1abd534 commit e47faf8

11 files changed

+434
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.core;
17+
18+
import org.springframework.dao.InvalidDataAccessApiUsageException;
19+
20+
/**
21+
* Exception thrown in the context of repository creation.
22+
*
23+
* @author Mark Paluch
24+
* @since 2.5
25+
*/
26+
@SuppressWarnings("serial")
27+
public class RepositoryCreationException extends InvalidDataAccessApiUsageException {
28+
29+
private final Class<?> repositoryInterface;
30+
31+
/**
32+
* Constructor for RepositoryCreationException.
33+
*
34+
* @param msg the detail message.
35+
* @param repositoryInterface the repository interface.
36+
*/
37+
public RepositoryCreationException(String msg, Class<?> repositoryInterface) {
38+
super(msg);
39+
this.repositoryInterface = repositoryInterface;
40+
}
41+
42+
/**
43+
* Constructor for RepositoryException.
44+
*
45+
* @param msg the detail message.
46+
* @param cause the root cause from the data access API in use.
47+
* @param repositoryInterface the repository interface.
48+
*/
49+
public RepositoryCreationException(String msg, Throwable cause, Class<?> repositoryInterface) {
50+
super(msg, cause);
51+
this.repositoryInterface = repositoryInterface;
52+
}
53+
54+
public Class<?> getRepositoryInterface() {
55+
return repositoryInterface;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.core.support;
17+
18+
import org.springframework.data.repository.core.RepositoryCreationException;
19+
20+
/**
21+
* Exception thrown during repository creation or repository method invocation when invoking a repository method on a
22+
* fragment without an implementation.
23+
*
24+
* @author Mark Paluch
25+
* @since 2.5
26+
*/
27+
@SuppressWarnings("serial")
28+
public class FragmentNotImplementedException extends RepositoryCreationException {
29+
30+
private final RepositoryFragment<?> fragment;
31+
32+
/**
33+
* Constructor for FragmentNotImplementedException.
34+
*
35+
* @param msg the detail message.
36+
* @param repositoryInterface the repository interface.
37+
* @param fragment the offending repository fragment.
38+
*/
39+
public FragmentNotImplementedException(String msg, Class<?> repositoryInterface, RepositoryFragment<?> fragment) {
40+
super(msg, repositoryInterface);
41+
this.fragment = fragment;
42+
}
43+
44+
public RepositoryFragment<?> getFragment() {
45+
return fragment;
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.repository.core.support;
17+
18+
import org.springframework.data.repository.core.RepositoryCreationException;
19+
20+
/**
21+
* Exception thrown during repository creation when a the repository has custom methods that are not backed by a
22+
* fragment or if no fragment could be found for a repository method invocation.
23+
*
24+
* @author Mark Paluch
25+
* @since 2.5
26+
*/
27+
@SuppressWarnings("serial")
28+
public class IncompleteRepositoryCompositionException extends RepositoryCreationException {
29+
30+
/**
31+
* Constructor for IncompleteRepositoryCompositionException.
32+
*
33+
* @param msg the detail message.
34+
* @param repositoryInterface the repository interface.
35+
*/
36+
public IncompleteRepositoryCompositionException(String msg, Class<?> repositoryInterface) {
37+
super(msg, repositoryInterface);
38+
}
39+
}

src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.data.repository.core.RepositoryInformation;
3030
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster;
3131
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
32+
import org.springframework.data.repository.query.QueryCreationException;
3233
import org.springframework.data.repository.query.QueryLookupStrategy;
3334
import org.springframework.data.repository.query.QueryMethod;
3435
import org.springframework.data.repository.query.RepositoryQuery;
@@ -97,7 +98,13 @@ private Map<Method, RepositoryQuery> mapMethodsToQuery(RepositoryInformation rep
9798

9899
private Pair<Method, RepositoryQuery> lookupQuery(Method method, RepositoryInformation information,
99100
QueryLookupStrategy strategy, ProjectionFactory projectionFactory) {
100-
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
101+
try {
102+
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
103+
} catch (QueryCreationException e) {
104+
throw e;
105+
} catch (RuntimeException e) {
106+
throw QueryCreationException.create(e.getMessage(), e, information.getRepositoryInterface(), method);
107+
}
101108
}
102109

103110
@SuppressWarnings({ "rawtypes", "unchecked" })

src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,12 @@ Method getMethod(Method method) {
316316
public void validateImplementation() {
317317

318318
fragments.stream().forEach(it -> it.getImplementation() //
319-
.orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
320-
ClassUtils.getQualifiedName(it.getSignatureContributor())))));
319+
.orElseThrow(() -> {
320+
Class<?> repositoryInterface = metadata != null ? metadata.getRepositoryInterface() : Object.class;
321+
return new FragmentNotImplementedException(String.format("Fragment %s used in %s has no implementation.",
322+
ClassUtils.getQualifiedName(it.getSignatureContributor()),
323+
ClassUtils.getQualifiedName(repositoryInterface)), repositoryInterface, it);
324+
}));
321325
}
322326

323327
/*

src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

+98-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22+
import java.util.HashMap;
2223
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
@@ -28,6 +29,7 @@
2829
import org.aopalliance.intercept.MethodInvocation;
2930
import org.apache.commons.logging.Log;
3031
import org.apache.commons.logging.LogFactory;
32+
3133
import org.springframework.aop.framework.ProxyFactory;
3234
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
3335
import org.springframework.beans.BeanUtils;
@@ -57,12 +59,12 @@
5759
import org.springframework.data.repository.query.QueryMethod;
5860
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
5961
import org.springframework.data.repository.query.RepositoryQuery;
60-
import org.springframework.data.repository.util.ClassUtils;
6162
import org.springframework.data.repository.util.QueryExecutionConverters;
6263
import org.springframework.data.util.ReflectionUtils;
6364
import org.springframework.lang.Nullable;
6465
import org.springframework.transaction.interceptor.TransactionalProxy;
6566
import org.springframework.util.Assert;
67+
import org.springframework.util.ClassUtils;
6668
import org.springframework.util.ConcurrentReferenceHashMap;
6769
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
6870
import org.springframework.util.ObjectUtils;
@@ -312,15 +314,16 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra
312314

313315
repositoryCompositionStep.end();
314316

315-
validate(information, composition);
316-
317317
StartupStep repositoryTargetStep = onEvent(applicationStartup, "spring.data.repository.target",
318318
repositoryInterface);
319319
Object target = getTargetRepository(information);
320320

321321
repositoryTargetStep.tag("target", target.getClass().getName());
322322
repositoryTargetStep.end();
323323

324+
RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
325+
validate(information, compositionToUse);
326+
324327
// Create proxy
325328
StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface);
326329
ProxyFactory result = new ProxyFactory();
@@ -357,7 +360,6 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra
357360
result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
358361
namedQueries, queryPostProcessors, methodInvocationListeners));
359362

360-
RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
361363
result.addAdvice(
362364
new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));
363365

@@ -502,17 +504,7 @@ protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key
502504
*/
503505
private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) {
504506

505-
if (repositoryInformation.hasCustomMethod()) {
506-
507-
if (composition.isEmpty()) {
508-
509-
throw new IllegalArgumentException(
510-
String.format("You have custom methods in %s but have not provided a custom implementation!",
511-
repositoryInformation.getRepositoryInterface()));
512-
}
513-
514-
composition.validateImplementation();
515-
}
507+
RepositoryValidator.validate(composition, getClass(), repositoryInformation);
516508

517509
validate(repositoryInformation);
518510
}
@@ -606,7 +598,7 @@ public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) thro
606598
try {
607599
return composition.invoke(invocationMulticaster, method, arguments);
608600
} catch (Exception e) {
609-
ClassUtils.unwrapReflectionException(e);
601+
org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e);
610602
}
611603

612604
throw new IllegalStateException("Should not occur!");
@@ -715,4 +707,94 @@ public String toString() {
715707
+ this.getRepositoryInterfaceName() + ", compositionHash=" + this.getCompositionHash() + ")";
716708
}
717709
}
710+
711+
/**
712+
* Validator utility to catch common mismatches with a proper error message instead of letting the query mechanism
713+
* attempt implementing a query method and fail with a less specific message.
714+
*/
715+
static class RepositoryValidator {
716+
717+
static Map<Class<?>, String> WELL_KNOWN_EXECUTORS = new HashMap<>();
718+
719+
static {
720+
721+
org.springframework.data.repository.util.ClassUtils.ifPresent(
722+
"org.springframework.data.querydsl.QuerydslPredicateExecutor", RepositoryValidator.class.getClassLoader(),
723+
it -> {
724+
WELL_KNOWN_EXECUTORS.put(it, "Querydsl");
725+
});
726+
727+
org.springframework.data.repository.util.ClassUtils.ifPresent(
728+
"org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor",
729+
RepositoryValidator.class.getClassLoader(), it -> {
730+
WELL_KNOWN_EXECUTORS.put(it, "Reactive Querydsl");
731+
});
732+
733+
org.springframework.data.repository.util.ClassUtils.ifPresent(
734+
"org.springframework.data.repository.query.QueryByExampleExecutor",
735+
RepositoryValidator.class.getClassLoader(), it -> {
736+
WELL_KNOWN_EXECUTORS.put(it, "Query by Example");
737+
});
738+
739+
org.springframework.data.repository.util.ClassUtils.ifPresent(
740+
"org.springframework.data.repository.query.ReactiveQueryByExampleExecutor",
741+
RepositoryValidator.class.getClassLoader(), it -> {
742+
WELL_KNOWN_EXECUTORS.put(it, "Reactive Query by Example");
743+
});
744+
}
745+
746+
/**
747+
* Validate the {@link RepositoryComposition} for custom implementations and well-known executors.
748+
*
749+
* @param composition
750+
* @param source
751+
* @param repositoryInformation
752+
*/
753+
public static void validate(RepositoryComposition composition, Class<?> source,
754+
RepositoryInformation repositoryInformation) {
755+
756+
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
757+
if (repositoryInformation.hasCustomMethod()) {
758+
759+
if (composition.isEmpty()) {
760+
761+
throw new IncompleteRepositoryCompositionException(
762+
String.format("You have custom methods in %s but have not provided a custom implementation!",
763+
org.springframework.util.ClassUtils.getQualifiedName(repositoryInterface)),
764+
repositoryInterface);
765+
}
766+
767+
composition.validateImplementation();
768+
}
769+
770+
for (Map.Entry<Class<?>, String> entry : WELL_KNOWN_EXECUTORS.entrySet()) {
771+
772+
Class<?> executorInterface = entry.getKey();
773+
if (!executorInterface.isAssignableFrom(repositoryInterface)) {
774+
continue;
775+
}
776+
777+
if (!containsFragmentImplementation(composition, executorInterface)) {
778+
throw new UnsupportedFragmentException(
779+
String.format("Repository %s implements %s but %s does not support %s!",
780+
ClassUtils.getQualifiedName(repositoryInterface), ClassUtils.getQualifiedName(executorInterface),
781+
ClassUtils.getShortName(source), entry.getValue()),
782+
repositoryInterface, executorInterface);
783+
}
784+
}
785+
}
786+
787+
private static boolean containsFragmentImplementation(RepositoryComposition composition,
788+
Class<?> executorInterface) {
789+
790+
for (RepositoryFragment<?> fragment : composition.getFragments()) {
791+
792+
if (fragment.getImplementation().filter(executorInterface::isInstance).isPresent()) {
793+
return true;
794+
}
795+
}
796+
797+
return false;
798+
}
799+
}
718800
}

0 commit comments

Comments
 (0)