Skip to content

Commit f5af2cf

Browse files
mp911dechristophstrobl
authored andcommitted
Replace Bound…Operations implementation with a proxy factory.
Original Pull Request: #2276
1 parent 33e8974 commit f5af2cf

14 files changed

+407
-1585
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* Copyright 2022 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.redis.core;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.Date;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.function.Function;
24+
25+
import org.aopalliance.intercept.MethodInterceptor;
26+
import org.aopalliance.intercept.MethodInvocation;
27+
28+
import org.springframework.aop.framework.ProxyFactory;
29+
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
30+
import org.springframework.data.redis.connection.DataType;
31+
import org.springframework.data.redis.connection.stream.ReadOffset;
32+
import org.springframework.data.redis.connection.stream.StreamOffset;
33+
import org.springframework.lang.Nullable;
34+
import org.springframework.util.ReflectionUtils;
35+
36+
/**
37+
* Utility to create implementation objects for {@code Bound…Operations} so that bound key interfaces can be implemented
38+
* automatically by translating interface calls to actual {@code …Operations} interfaces.
39+
*
40+
* @author Mark Paluch
41+
* @since 3.0
42+
*/
43+
class BoundOperationsProxyFactory {
44+
45+
private final Map<Method, Method> targetMethodCache = new ConcurrentHashMap<>();
46+
47+
/**
48+
* Create a proxy object that implements {@link Class boundOperationsInterface} using the given {@code key} and
49+
* {@link DataType}. Calls to {@code Bound…Operations} methods are bridged by forwarding these either to the
50+
* {@code operationsTarget} or a default implementation.
51+
*
52+
* @param boundOperationsInterface the {@code Bound…Operations} interface.
53+
* @param key the bound key.
54+
* @param type the {@link DataType} for which to create a proxy object.
55+
* @param operations the {@link RedisOperations} instance.
56+
* @param operationsTargetFunction function to extract the actual delegate for method calls.
57+
* @param <T>
58+
* @return the proxy object.
59+
*/
60+
@SuppressWarnings({ "unchecked", "rawtypes" })
61+
public <T> T createProxy(Class<T> boundOperationsInterface, Object key, DataType type,
62+
RedisOperations<?, ?> operations, Function<RedisOperations<?, ?>, Object> operationsTargetFunction) {
63+
64+
DefaultBoundKeyOperations delegate = new DefaultBoundKeyOperations(type, key, (RedisOperations) operations);
65+
Object operationsTarget = operationsTargetFunction.apply(operations);
66+
67+
ProxyFactory proxyFactory = new ProxyFactory();
68+
proxyFactory.addInterface(boundOperationsInterface);
69+
proxyFactory.addAdvice(new DefaultMethodInvokingMethodInterceptor());
70+
proxyFactory.addAdvice(
71+
new BoundOperationsMethodInterceptor(key, operations, boundOperationsInterface, operationsTarget, delegate));
72+
73+
return (T) proxyFactory.getProxy();
74+
}
75+
76+
Method lookupRequiredMethod(Method method, Class<?> targetClass, boolean considerKeyArgument) {
77+
78+
Method target = lookupMethod(method, targetClass, considerKeyArgument);
79+
80+
if (target == null) {
81+
throw new IllegalArgumentException("Cannot lookup target method for %s in class %s. This appears to be a bug."
82+
.formatted(method, targetClass.getName()));
83+
}
84+
85+
return target;
86+
}
87+
88+
@Nullable
89+
Method lookupMethod(Method method, Class<?> targetClass, boolean considerKeyArgument) {
90+
91+
return targetMethodCache.computeIfAbsent(method, it -> {
92+
93+
Class[] paramTypes;
94+
95+
if (isStreamRead(method)) {
96+
paramTypes = new Class[it.getParameterCount()];
97+
System.arraycopy(it.getParameterTypes(), 0, paramTypes, 0, paramTypes.length - 1);
98+
paramTypes[paramTypes.length - 1] = StreamOffset[].class;
99+
} else if (considerKeyArgument) {
100+
101+
paramTypes = new Class[it.getParameterCount() + 1];
102+
paramTypes[0] = Object.class;
103+
System.arraycopy(it.getParameterTypes(), 0, paramTypes, 1, paramTypes.length - 1);
104+
} else {
105+
paramTypes = it.getParameterTypes();
106+
}
107+
108+
return ReflectionUtils.findMethod(targetClass, method.getName(), paramTypes);
109+
});
110+
}
111+
112+
private boolean isStreamRead(Method method) {
113+
return method.getName().equals("read")
114+
&& method.getParameterTypes()[method.getParameterCount() - 1].equals(ReadOffset.class);
115+
}
116+
117+
/**
118+
* {@link MethodInterceptor} to delegate proxy calls to either {@link RedisOperations}, {@code key},
119+
* {@link DefaultBoundKeyOperations} or the {@code operationsTarget} such as {@link ValueOperations}.
120+
*/
121+
class BoundOperationsMethodInterceptor implements MethodInterceptor {
122+
123+
private final Class<?> boundOperationsInterface;
124+
private final Object operationsTarget;
125+
private final DefaultBoundKeyOperations delegate;
126+
127+
public BoundOperationsMethodInterceptor(Object key, RedisOperations<?, ?> operations,
128+
Class<?> boundOperationsInterface, Object operationsTarget, DefaultBoundKeyOperations delegate) {
129+
130+
this.boundOperationsInterface = boundOperationsInterface;
131+
this.operationsTarget = operationsTarget;
132+
this.delegate = delegate;
133+
}
134+
135+
@Override
136+
public Object invoke(MethodInvocation invocation) throws Throwable {
137+
138+
Method method = invocation.getMethod();
139+
140+
switch (method.getName()) {
141+
case "getKey":
142+
return delegate.getKey();
143+
144+
case "rename":
145+
delegate.rename(invocation.getArguments()[0]);
146+
return null;
147+
148+
case "getOperations":
149+
return delegate.getOps();
150+
}
151+
152+
if (method.getDeclaringClass() == boundOperationsInterface) {
153+
return doInvoke(invocation, method, operationsTarget, true);
154+
}
155+
156+
return doInvoke(invocation, method, delegate, false);
157+
}
158+
159+
@Nullable
160+
private Object doInvoke(MethodInvocation invocation, Method method, Object target, boolean considerKeyArgument) {
161+
162+
Method backingMethod = lookupRequiredMethod(method, target.getClass(), considerKeyArgument);
163+
164+
Object[] args;
165+
Object[] invocationArguments = invocation.getArguments();
166+
167+
if (isStreamRead(method)) {
168+
// stream.read requires translation to StreamOffset using the bound key.
169+
args = new Object[backingMethod.getParameterCount()];
170+
System.arraycopy(invocationArguments, 0, args, 0, args.length - 1);
171+
args[args.length - 1] = new StreamOffset[] {
172+
StreamOffset.create(delegate.getKey(), (ReadOffset) invocationArguments[invocationArguments.length - 1]) };
173+
} else if (backingMethod.getParameterCount() > 0 && backingMethod.getParameterTypes()[0].equals(Object.class)) {
174+
175+
args = new Object[backingMethod.getParameterCount()];
176+
args[0] = delegate.getKey();
177+
System.arraycopy(invocationArguments, 0, args, 1, args.length - 1);
178+
} else {
179+
args = invocationArguments;
180+
}
181+
182+
try {
183+
return backingMethod.invoke(target, args);
184+
} catch (ReflectiveOperationException e) {
185+
ReflectionUtils.handleReflectionException(e);
186+
throw new UnsupportedOperationException("Should not happen", e);
187+
}
188+
}
189+
}
190+
191+
/**
192+
* Default {@link BoundKeyOperations} implementation. Meant for internal usage.
193+
*
194+
* @author Costin Leau
195+
* @author Christoph Strobl
196+
*/
197+
static class DefaultBoundKeyOperations implements BoundKeyOperations<Object> {
198+
199+
private final DataType type;
200+
private Object key;
201+
private final RedisOperations<Object, ?> ops;
202+
203+
DefaultBoundKeyOperations(DataType type, Object key, RedisOperations<Object, ?> operations) {
204+
this.type = type;
205+
206+
this.key = key;
207+
this.ops = operations;
208+
}
209+
210+
@Override
211+
public Object getKey() {
212+
return key;
213+
}
214+
215+
@Override
216+
public Boolean expire(long timeout, TimeUnit unit) {
217+
return ops.expire(key, timeout, unit);
218+
}
219+
220+
@Override
221+
public Boolean expireAt(Date date) {
222+
return ops.expireAt(key, date);
223+
}
224+
225+
@Override
226+
public Long getExpire() {
227+
return ops.getExpire(key);
228+
}
229+
230+
@Override
231+
public Boolean persist() {
232+
return ops.persist(key);
233+
}
234+
235+
@Override
236+
public void rename(Object newKey) {
237+
if (ops.hasKey(key)) {
238+
ops.rename(key, newKey);
239+
}
240+
key = newKey;
241+
}
242+
243+
public DataType getType() {
244+
return type;
245+
}
246+
247+
public RedisOperations<Object, ?> getOps() {
248+
return ops;
249+
}
250+
}
251+
}

src/main/java/org/springframework/data/redis/core/BoundSetOperations.java

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,37 +182,95 @@ public interface BoundSetOperations<K, V> extends BoundKeyOperations<K> {
182182
* @param key must not be {@literal null}.
183183
* @return {@literal null} when used in pipeline / transaction.
184184
* @see <a href="https://redis.io/commands/sdiff">Redis Documentation: SDIFF</a>
185+
* @deprecated since 3.0, use {@link #difference(Object)} instead to follow a consistent method naming scheme.
185186
*/
186187
@Nullable
187-
Set<V> diff(K key);
188+
default Set<V> diff(K key) {
189+
return difference(key);
190+
}
191+
192+
/**
193+
* Diff all sets for given the bound key and {@code key}.
194+
*
195+
* @param key must not be {@literal null}.
196+
* @return {@literal null} when used in pipeline / transaction.
197+
* @since 3.0
198+
* @see <a href="https://redis.io/commands/sdiff">Redis Documentation: SDIFF</a>
199+
*/
200+
@Nullable
201+
Set<V> difference(K key);
202+
203+
/**
204+
* Diff all sets for given the bound key and {@code keys}.
205+
*
206+
* @param keys must not be {@literal null}.
207+
* @return {@literal null} when used in pipeline / transaction.
208+
* @see <a href="https://redis.io/commands/sdiff">Redis Documentation: SDIFF</a>
209+
* @deprecated since 3.0, use {@link #difference(Collection)} instead to follow a consistent method naming scheme.
210+
*/
211+
@Nullable
212+
default Set<V> diff(Collection<K> keys) {
213+
return difference(keys);
214+
}
188215

189216
/**
190217
* Diff all sets for given the bound key and {@code keys}.
191218
*
192219
* @param keys must not be {@literal null}.
193220
* @return {@literal null} when used in pipeline / transaction.
221+
* @since 3.0
194222
* @see <a href="https://redis.io/commands/sdiff">Redis Documentation: SDIFF</a>
195223
*/
196224
@Nullable
197-
Set<V> diff(Collection<K> keys);
225+
Set<V> difference(Collection<K> keys);
226+
227+
/**
228+
* Diff all sets for given the bound key and {@code keys} and store result in {@code destKey}.
229+
*
230+
* @param keys must not be {@literal null}.
231+
* @param destKey must not be {@literal null}.
232+
* @see <a href="https://redis.io/commands/sdiffstore">Redis Documentation: SDIFFSTORE</a>
233+
* @deprecated since 3.0, use {@link #differenceAndStore(Object, Object)} instead to follow a consistent method naming
234+
* scheme..
235+
*/
236+
@Deprecated
237+
default void diffAndStore(K keys, K destKey) {
238+
differenceAndStore(keys, destKey);
239+
}
240+
241+
/**
242+
* Diff all sets for given the bound key and {@code keys} and store result in {@code destKey}.
243+
*
244+
* @param keys must not be {@literal null}.
245+
* @param destKey must not be {@literal null}.
246+
* @since 3.0
247+
* @see <a href="https://redis.io/commands/sdiffstore">Redis Documentation: SDIFFSTORE</a>
248+
*/
249+
void differenceAndStore(K keys, K destKey);
198250

199251
/**
200252
* Diff all sets for given the bound key and {@code keys} and store result in {@code destKey}.
201253
*
202254
* @param keys must not be {@literal null}.
203255
* @param destKey must not be {@literal null}.
204256
* @see <a href="https://redis.io/commands/sdiffstore">Redis Documentation: SDIFFSTORE</a>
257+
* @deprecated since 3.0, use {@link #differenceAndStore(Collection, Object)} instead to follow a consistent method
258+
* naming scheme.
205259
*/
206-
void diffAndStore(K keys, K destKey);
260+
@Deprecated
261+
default void diffAndStore(Collection<K> keys, K destKey) {
262+
differenceAndStore(keys, destKey);
263+
}
207264

208265
/**
209266
* Diff all sets for given the bound key and {@code keys} and store result in {@code destKey}.
210267
*
211268
* @param keys must not be {@literal null}.
212269
* @param destKey must not be {@literal null}.
270+
* @since 3.0
213271
* @see <a href="https://redis.io/commands/sdiffstore">Redis Documentation: SDIFFSTORE</a>
214272
*/
215-
void diffAndStore(Collection<K> keys, K destKey);
273+
void differenceAndStore(Collection<K> keys, K destKey);
216274

217275
/**
218276
* Get all elements of set at the bound key.

0 commit comments

Comments
 (0)