Skip to content

Commit d1a1864

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. Closes #2341
1 parent 19c9d1a commit d1a1864

File tree

8 files changed

+328
-25
lines changed

8 files changed

+328
-25
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
/**
19+
* Exception thrown when a fragment remains with without an implementation during repository creation.
20+
*
21+
* @author Mark Paluch
22+
* @since 2.5
23+
*/
24+
@SuppressWarnings("serial")
25+
public class FragmentNotImplementedException extends RepositoryCreationException {
26+
27+
private final RepositoryFragment<?> fragment;
28+
29+
/**
30+
* Constructor for FragmentNotImplementedException.
31+
*
32+
* @param msg the detail message.
33+
* @param repositoryInterface the repository interface.
34+
* @param fragment the offending repository fragment.
35+
*/
36+
public FragmentNotImplementedException(String msg, Class<?> repositoryInterface, RepositoryFragment<?> fragment) {
37+
super(msg, repositoryInterface);
38+
this.fragment = fragment;
39+
}
40+
41+
public RepositoryFragment<?> getFragment() {
42+
return fragment;
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
/**
19+
* Exception thrown during repository creation when a fragment implementation is missing.
20+
*
21+
* @author Mark Paluch
22+
* @since 2.5
23+
*/
24+
@SuppressWarnings("serial")
25+
public class MissingFragmentException extends RepositoryCreationException {
26+
27+
/**
28+
* Constructor for MissingFragmentException.
29+
*
30+
* @param msg the detail message.
31+
* @param repositoryInterface the repository interface.
32+
*/
33+
public MissingFragmentException(String msg, Class<?> repositoryInterface) {
34+
super(msg, repositoryInterface);
35+
}
36+
}

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

+13-6
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
/*
@@ -506,11 +510,13 @@ public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) t
506510
Object invoke(Class<?> repositoryInterface, RepositoryInvocationMulticaster listener, Method invokedMethod,
507511
Method methodToCall, Object[] args) throws Throwable {
508512

509-
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
513+
RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall,
514+
key -> findImplementationFragment(key, repositoryInterface));
510515
Optional<?> optional = fragment.getImplementation();
511516

512517
if (!optional.isPresent()) {
513-
throw new IllegalArgumentException(String.format("No implementation found for method %s", methodToCall));
518+
throw new FragmentNotImplementedException(String.format("No implementation found for method %s", methodToCall),
519+
repositoryInterface, fragment);
514520
}
515521

516522
RepositoryMethodInvoker repositoryMethodInvoker = invocationMetadataCache.get(invokedMethod);
@@ -525,12 +531,13 @@ Object invoke(Class<?> repositoryInterface, RepositoryInvocationMulticaster list
525531
return repositoryMethodInvoker.invoke(repositoryInterface, listener, args);
526532
}
527533

528-
private RepositoryFragment<?> findImplementationFragment(Method key) {
534+
private RepositoryFragment<?> findImplementationFragment(Method key, Class<?> repositoryInterface) {
529535

530536
return stream().filter(it -> it.hasMethod(key)) //
531537
.filter(it -> it.getImplementation().isPresent()) //
532538
.findFirst()
533-
.orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", key)));
539+
.orElseThrow(() -> new MissingFragmentException(String.format("No fragment found for method %s", key),
540+
repositoryInterface));
534541
}
535542

536543
@Nullable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.dao.InvalidDataAccessApiUsageException;
19+
20+
/**
21+
* Exception thrown when the repository instance cannot be created.
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+
public Class<?> getRepositoryInterface() {
43+
return repositoryInterface;
44+
}
45+
}

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.util.ReflectionUtils.handleReflectionException(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 MissingFragmentException(
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)