Skip to content

Commit eed4c2a

Browse files
mp911dechristophstrobl
authored andcommitted
Consider intermediate command interface in ConnectionSplittingInterceptor.
We now consider requests to command API objects such as RedisConnection.keyCommands() in ConnectionSplittingInterceptor to identify the correct command and route it accordingly. Closes: #2886 Original Pull Request: #2887
1 parent 6f28b53 commit eed4c2a

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

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

+37-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import org.springframework.transaction.support.TransactionSynchronization;
3333
import org.springframework.transaction.support.TransactionSynchronizationManager;
3434
import org.springframework.util.Assert;
35+
import org.springframework.util.ReflectionUtils;
36+
import org.springframework.util.StringUtils;
3537

3638
/**
3739
* Helper class that provides static methods for obtaining {@link RedisConnection} from a
@@ -320,8 +322,8 @@ private static RedisConnection getTargetConnection(RedisConnection connection) {
320322
*/
321323
public static void unbindConnection(RedisConnectionFactory factory) {
322324

323-
RedisConnectionHolder connectionHolder =
324-
(RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
325+
RedisConnectionHolder connectionHolder = (RedisConnectionHolder) TransactionSynchronizationManager
326+
.getResource(factory);
325327

326328
if (connectionHolder == null) {
327329
return;
@@ -358,7 +360,8 @@ public static void unbindConnection(RedisConnectionFactory factory) {
358360
* @param connectionFactory Redis connection factory that the connection was created with
359361
* @return whether the connection is transactional or not
360362
*/
361-
public static boolean isConnectionTransactional(RedisConnection connection, RedisConnectionFactory connectionFactory) {
363+
public static boolean isConnectionTransactional(RedisConnection connection,
364+
RedisConnectionFactory connectionFactory) {
362365

363366
Assert.notNull(connectionFactory, "No RedisConnectionFactory specified");
364367

@@ -448,9 +451,16 @@ public void afterCompletion(int status) {
448451
static class ConnectionSplittingInterceptor implements MethodInterceptor {
449452

450453
private final RedisConnectionFactory factory;
454+
private final @Nullable String commandInterface;
451455

452456
public ConnectionSplittingInterceptor(RedisConnectionFactory factory) {
453457
this.factory = factory;
458+
this.commandInterface = null;
459+
}
460+
461+
public ConnectionSplittingInterceptor(RedisConnectionFactory factory, String commandInterface) {
462+
this.factory = factory;
463+
this.commandInterface = commandInterface;
454464
}
455465

456466
@Override
@@ -465,6 +475,22 @@ public Object intercept(Object obj, Method method, Object[] args) throws Throwab
465475
return obj;
466476
}
467477

478+
Class<?> returnType = method.getReturnType();
479+
String returnTypeName = returnType.getSimpleName();
480+
481+
// bridge keyCommands etc. to defer target invocations
482+
if (returnType.isInterface() && returnType.getPackageName().equals("org.springframework.data.redis.connection")
483+
&& returnTypeName.startsWith("Redis") && returnTypeName.endsWith("Commands")) {
484+
485+
ProxyFactory proxyFactory = new ProxyFactory(ReflectionUtils.invokeMethod(method, obj));
486+
487+
proxyFactory.addAdvice(new ConnectionSplittingInterceptor(factory, method.getName()));
488+
proxyFactory.addInterface(RedisConnectionProxy.class);
489+
proxyFactory.addInterface(returnType);
490+
491+
return proxyFactory.getProxy();
492+
}
493+
468494
RedisCommand commandToExecute = RedisCommand.failsafeCommandLookup(method.getName());
469495

470496
if (isPotentiallyThreadBoundCommand(commandToExecute)) {
@@ -481,9 +507,15 @@ public Object intercept(Object obj, Method method, Object[] args) throws Throwab
481507
}
482508

483509
RedisConnection connection = factory.getConnection();
484-
510+
Object target = connection;
485511
try {
486-
return invoke(method, connection, args);
512+
513+
if (StringUtils.hasText(commandInterface)) {
514+
target = ReflectionUtils.invokeMethod(ReflectionUtils.findMethod(RedisConnection.class, commandInterface),
515+
connection);
516+
}
517+
518+
return invoke(method, target, args);
487519
} finally {
488520
// properly close the unbound connection after executing command
489521
if (!connection.isClosed()) {

src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java

+13
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.beans.factory.annotation.Autowired;
3333
import org.springframework.context.annotation.Bean;
3434
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.data.redis.core.RedisTemplate;
3536
import org.springframework.data.redis.core.StringRedisTemplate;
3637
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
3738
import org.springframework.test.annotation.Rollback;
@@ -132,6 +133,18 @@ public void valueOperationSetShouldBeCommittedCorrectly() {
132133
}
133134
}
134135

136+
@Rollback(false)
137+
@Test // GH-2886
138+
public void shouldReturnReadOnlyCommandResultInTransaction() {
139+
140+
RedisTemplate<String, String> template = new RedisTemplate<>();
141+
template.setConnectionFactory(factory);
142+
template.setEnableTransactionSupport(true);
143+
template.afterPropertiesSet();
144+
145+
assertThat(template.hasKey("foo")).isFalse();
146+
}
147+
135148
@Test // DATAREDIS-548
136149
@Transactional(readOnly = true)
137150
public void valueOperationShouldWorkWithReadOnlyTransactions() {

src/test/java/org/springframework/data/redis/core/RedisConnectionUtilsUnitTests.java

+40
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.data.redis.connection.RedisConnection;
2626
import org.springframework.data.redis.connection.RedisConnectionFactory;
27+
import org.springframework.data.redis.connection.RedisKeyCommands;
2728
import org.springframework.transaction.TransactionDefinition;
2829
import org.springframework.transaction.TransactionException;
2930
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
@@ -201,6 +202,45 @@ void bindConnectionShouldNotBindConnectionToTransaction() {
201202
assertThat(TransactionSynchronizationManager.hasResource(factoryMock)).isFalse();
202203
}
203204

205+
@Test // GH-2886
206+
void connectionProxyShouldInvokeReadOnlyMethods() {
207+
208+
TransactionTemplate template = new TransactionTemplate(new DummyTransactionManager());
209+
210+
byte[] anyBytes = new byte[] { 1, 2, 3 };
211+
when(connectionMock2.exists(anyBytes)).thenReturn(true);
212+
213+
template.executeWithoutResult(status -> {
214+
215+
RedisConnection connection = RedisConnectionUtils.getConnection(factoryMock, true);
216+
217+
assertThat(connection.exists(anyBytes)).isEqualTo(true);
218+
});
219+
}
220+
221+
@Test // GH-2886
222+
void connectionProxyShouldConsiderCommandInterfaces() {
223+
224+
TransactionTemplate template = new TransactionTemplate(new DummyTransactionManager());
225+
226+
byte[] anyBytes = new byte[] { 1, 2, 3 };
227+
228+
RedisKeyCommands commandsMock = mock(RedisKeyCommands.class);
229+
230+
when(connectionMock1.keyCommands()).thenReturn(commandsMock);
231+
when(connectionMock2.keyCommands()).thenReturn(commandsMock);
232+
when(commandsMock.exists(anyBytes)).thenReturn(true);
233+
when(commandsMock.del(anyBytes)).thenReturn(42L);
234+
235+
template.executeWithoutResult(status -> {
236+
237+
RedisConnection connection = RedisConnectionUtils.getConnection(factoryMock, true);
238+
239+
assertThat(connection.keyCommands().exists(anyBytes)).isEqualTo(true);
240+
assertThat(connection.keyCommands().del(anyBytes)).isEqualTo(42L);
241+
});
242+
}
243+
204244
static class DummyTransactionManager extends AbstractPlatformTransactionManager {
205245

206246
@Override

0 commit comments

Comments
 (0)