Skip to content

Commit dbe79f8

Browse files
committed
DATACMNS-89 - Infrastructure for projections on repository queries.
QueryMethods now expose a ResourceProcessor which is exposes information about the final to be created object types which can either be DTOs containing a persistence constructor (see @PersistenceConstructor) or projection interfaces. The former are analyzed for constructor properties so that store implementations can use that information to create projected queries that return exactly the fields required for that DTO. Projection interfaces are inspected, their properties are considered input properties and the same projection queries can be issued against the data store. If a projection contains dynamically calculated properties (i.e. it uses SpEL expressions via @value) the original entities have to be queried and can be projected during post processing. ProjectionFactory now exposes a more advanced ProjectionInformation that has additional meta information about the projection type. ProxyProjectionFactory now refers to the BeanClassLoader instead of the ResourceLoader. RepositoryFactory(Bean)Support now also implement BeanFactoryAware to forward the BeanFactory to the SpelAwareProxyProjectionFactory which in turn now gets handed into the QueryLookupStrategy as well as the QueryMethod. Parameter now knows about a dynamic projection type, which is a query method parameter of type Class bound to a generic method parameter and will be used to determine the projection to be used on a per call basis. Original pull request: #150.
1 parent c60cd64 commit dbe79f8

28 files changed

+1523
-97
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2015 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+
* http://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.projection;
17+
18+
import java.beans.PropertyDescriptor;
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import org.springframework.beans.BeanUtils;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input
28+
* properties.
29+
*
30+
* @author Oliver Gierke
31+
* @since 1.12
32+
*/
33+
class DefaultProjectionInformation implements ProjectionInformation {
34+
35+
private final Class<?> projectionType;
36+
private final List<PropertyDescriptor> properties;
37+
38+
/**
39+
* Creates a new {@link DefaultProjectionInformation} for the given type.
40+
*
41+
* @param type must not be {@literal null}.
42+
*/
43+
public DefaultProjectionInformation(Class<?> type) {
44+
45+
Assert.notNull(type, "Projection type must not be null!");
46+
47+
this.projectionType = type;
48+
this.properties = collectDescriptors(type);
49+
}
50+
51+
/*
52+
* (non-Javadoc)
53+
* @see org.springframework.data.projection.ProjectionInformation#getType()
54+
*/
55+
@Override
56+
public Class<?> getType() {
57+
return projectionType;
58+
}
59+
60+
/*
61+
* (non-Javadoc)
62+
* @see org.springframework.data.projection.ProjectionInformation#getInputProperties()
63+
*/
64+
public List<PropertyDescriptor> getInputProperties() {
65+
66+
List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();
67+
68+
for (PropertyDescriptor descriptor : properties) {
69+
if (isInputProperty(descriptor)) {
70+
result.add(descriptor);
71+
}
72+
}
73+
74+
return result;
75+
}
76+
77+
/*
78+
* (non-Javadoc)
79+
* @see org.springframework.data.projection.ProjectionInformation#isDynamic()
80+
*/
81+
@Override
82+
public boolean isClosed() {
83+
return this.properties.equals(getInputProperties());
84+
}
85+
86+
/**
87+
* Returns whether the given {@link PropertyDescriptor} describes an input property for the projection, i.e. a
88+
* property that needs to be present on the source to be able to create reasonable projections for the type the
89+
* descriptor was looked up on.
90+
*
91+
* @param descriptor will never be {@literal null}.
92+
* @return
93+
*/
94+
protected boolean isInputProperty(PropertyDescriptor descriptor) {
95+
return true;
96+
}
97+
98+
/**
99+
* Collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces.
100+
*
101+
* @param type must not be {@literal null}.
102+
* @return
103+
*/
104+
private static List<PropertyDescriptor> collectDescriptors(Class<?> type) {
105+
106+
List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();
107+
result.addAll(Arrays.asList(BeanUtils.getPropertyDescriptors(type)));
108+
109+
for (Class<?> interfaze : type.getInterfaces()) {
110+
result.addAll(collectDescriptors(interfaze));
111+
}
112+
113+
return result;
114+
}
115+
}

src/main/java/org/springframework/data/projection/ProjectionFactory.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ public interface ProjectionFactory {
4949
*
5050
* @param projectionType must not be {@literal null}.
5151
* @return
52+
* @deprecated use {@link #getProjectionInformation(Class)}
5253
*/
54+
@Deprecated
5355
List<String> getInputProperties(Class<?> projectionType);
56+
57+
/**
58+
* Returns the {@link ProjectionInformation} for the given projection type.
59+
*
60+
* @param projectionType must not be {@literal null}.
61+
* @return
62+
* @since 1.12
63+
*/
64+
ProjectionInformation getProjectionInformation(Class<?> projectionType);
5465
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2015 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+
* http://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.projection;
17+
18+
import java.beans.PropertyDescriptor;
19+
import java.util.List;
20+
21+
/**
22+
* Information about a projection type.
23+
*
24+
* @author Oliver Gierke
25+
* @since 1.12
26+
*/
27+
public interface ProjectionInformation {
28+
29+
/**
30+
* Returns the projection type.
31+
*
32+
* @return will never be {@literal null}.
33+
*/
34+
Class<?> getType();
35+
36+
/**
37+
* Returns the properties that will be consumed by the projection type.
38+
*
39+
* @return will never be {@literal null}.
40+
*/
41+
List<PropertyDescriptor> getInputProperties();
42+
43+
/**
44+
* Returns whether supplying values for the properties returned via {@link #getInputProperties()} is sufficient to
45+
* create a working proxy instance. This will usually be used to determine whether the projection uses any dynamically
46+
* resolved properties.
47+
*
48+
* @return
49+
*/
50+
boolean isClosed();
51+
}

src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import org.aopalliance.intercept.MethodInvocation;
2727
import org.springframework.aop.framework.Advised;
2828
import org.springframework.aop.framework.ProxyFactory;
29-
import org.springframework.beans.BeanUtils;
29+
import org.springframework.beans.factory.BeanClassLoaderAware;
3030
import org.springframework.context.ResourceLoaderAware;
3131
import org.springframework.core.io.ResourceLoader;
3232
import org.springframework.util.Assert;
@@ -42,20 +42,30 @@
4242
* @see SpelAwareProxyProjectionFactory
4343
* @since 1.10
4444
*/
45-
class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware {
45+
class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware, BeanClassLoaderAware {
4646

4747
private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional",
4848
ProxyProjectionFactory.class.getClassLoader());
4949

50-
private ResourceLoader resourceLoader;
50+
private ClassLoader classLoader;
5151

52-
/*
53-
* (non-Javadoc)
52+
/**
5453
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
54+
* @deprecated rather set the {@link ClassLoader} directly via {@link #setBeanClassLoader(ClassLoader)}.
5555
*/
5656
@Override
57+
@Deprecated
5758
public void setResourceLoader(ResourceLoader resourceLoader) {
58-
this.resourceLoader = resourceLoader;
59+
this.classLoader = resourceLoader.getClassLoader();
60+
}
61+
62+
/*
63+
* (non-Javadoc)
64+
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
65+
*/
66+
@Override
67+
public void setBeanClassLoader(ClassLoader classLoader) {
68+
this.classLoader = classLoader;
5969
}
6070

6171
/*
@@ -85,8 +95,7 @@ public <T> T createProjection(Class<T> projectionType, Object source) {
8595
factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass()));
8696
factory.addAdvice(getMethodInterceptor(source, projectionType));
8797

88-
return (T) factory
89-
.getProxy(resourceLoader == null ? ClassUtils.getDefaultClassLoader() : resourceLoader.getClassLoader());
98+
return (T) factory.getProxy(classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader);
9099
}
91100

92101
/*
@@ -110,18 +119,24 @@ public List<String> getInputProperties(Class<?> projectionType) {
110119

111120
Assert.notNull(projectionType, "Projection type must not be null!");
112121

113-
PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(projectionType);
114-
List<String> result = new ArrayList<String>(descriptors.length);
122+
List<String> result = new ArrayList<String>();
115123

116-
for (PropertyDescriptor descriptor : descriptors) {
117-
if (isInputProperty(descriptor)) {
118-
result.add(descriptor.getName());
119-
}
124+
for (PropertyDescriptor descriptor : getProjectionInformation(projectionType).getInputProperties()) {
125+
result.add(descriptor.getName());
120126
}
121127

122128
return result;
123129
}
124130

131+
/*
132+
* (non-Javadoc)
133+
* @see org.springframework.data.projection.ProjectionFactory#getProjectionInformation(java.lang.Class)
134+
*/
135+
@Override
136+
public ProjectionInformation getProjectionInformation(Class<?> projectionType) {
137+
return new DefaultProjectionInformation(projectionType);
138+
}
139+
125140
/**
126141
* Returns the {@link MethodInterceptor} to add to the proxy.
127142
*
@@ -132,11 +147,12 @@ public List<String> getInputProperties(Class<?> projectionType) {
132147
@SuppressWarnings("unchecked")
133148
private MethodInterceptor getMethodInterceptor(Object source, Class<?> projectionType) {
134149

135-
MethodInterceptor propertyInvocationInterceptor = source instanceof Map ? new MapAccessingMethodInterceptor(
136-
(Map<String, Object>) source) : new PropertyAccessingMethodInterceptor(source);
150+
MethodInterceptor propertyInvocationInterceptor = source instanceof Map
151+
? new MapAccessingMethodInterceptor((Map<String, Object>) source)
152+
: new PropertyAccessingMethodInterceptor(source);
137153

138-
return new ProjectingMethodInterceptor(this, postProcessAccessorInterceptor(propertyInvocationInterceptor, source,
139-
projectionType));
154+
return new ProjectingMethodInterceptor(this,
155+
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType));
140156
}
141157

142158
/**
@@ -153,18 +169,6 @@ protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor int
153169
return interceptor;
154170
}
155171

156-
/**
157-
* Returns whether the given {@link PropertyDescriptor} describes an input property for the projection, i.e. a
158-
* property that needs to be present on the source to be able to create reasonable projections for the type the
159-
* descriptor was looked up on.
160-
*
161-
* @param descriptor will never be {@literal null}.
162-
* @return
163-
*/
164-
protected boolean isInputProperty(PropertyDescriptor descriptor) {
165-
return true;
166-
}
167-
168172
/**
169173
* Custom {@link MethodInterceptor} to expose the proxy target class even if we set
170174
* {@link ProxyFactory#setOpaque(boolean)} to true to prevent properties on {@link Advised} to be rendered.

src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,23 +75,34 @@ protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor int
7575
typeCache.put(projectionType, callback.hasFoundAnnotation());
7676
}
7777

78-
return typeCache.get(projectionType) ? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory,
79-
parser, projectionType) : interceptor;
78+
return typeCache.get(projectionType)
79+
? new SpelEvaluatingMethodInterceptor(interceptor, source, beanFactory, parser, projectionType) : interceptor;
8080
}
8181

8282
/*
8383
* (non-Javadoc)
84-
* @see org.springframework.data.projection.ProxyProjectionFactory#isProperty(java.beans.PropertyDescriptor)
84+
* @see org.springframework.data.projection.ProxyProjectionFactory#getProjectionInformation(java.lang.Class)
8585
*/
8686
@Override
87-
protected boolean isInputProperty(PropertyDescriptor descriptor) {
87+
public ProjectionInformation getProjectionInformation(Class<?> projectionType) {
8888

89-
Method readMethod = descriptor.getReadMethod();
89+
return new DefaultProjectionInformation(projectionType) {
9090

91-
if (readMethod == null) {
92-
return false;
93-
}
91+
/*
92+
* (non-Javadoc)
93+
* @see org.springframework.data.projection.DefaultProjectionInformation#isInputProperty(java.beans.PropertyDescriptor)
94+
*/
95+
@Override
96+
protected boolean isInputProperty(PropertyDescriptor descriptor) {
97+
98+
Method readMethod = descriptor.getReadMethod();
99+
100+
if (readMethod == null) {
101+
return false;
102+
}
94103

95-
return AnnotationUtils.findAnnotation(readMethod, Value.class) == null;
104+
return AnnotationUtils.findAnnotation(readMethod, Value.class) == null;
105+
}
106+
};
96107
}
97108
}

0 commit comments

Comments
 (0)