Skip to content

Commit e3aae61

Browse files
mp911deschauder
authored andcommitted
#98 - Add support for AbstractRoutingConnectionFactory.
We now provide an abstract base class for ConnectionFactory routing. Routing keys are typically obtained from a subscriber context. AbstractRoutingConnectionFactory is backed by either a map of string identifiers or connection factories. When using string identifiers, these can map agains e.g. Spring bean names that can be resolved using BeanFactoryConnectionFactoryLookup. class MyRoutingConnectionFactory extends AbstractRoutingConnectionFactory { @OverRide protected Mono<Object> determineCurrentLookupKey() { return Mono.subscriberContext().filter(it -> it.hasKey(ROUTING_KEY)).map(it -> it.get(ROUTING_KEY)); } } @bean public void routingConnectionFactory() { MyRoutingConnectionFactory router = new MyRoutingConnectionFactory(); Map<String, ConnectionFactory> factories = new HashMap<>(); ConnectionFactory myDefault = …; ConnectionFactory primary = …; ConnectionFactory secondary = …; factories.put("primary", primary); factories.put("secondary", secondary); router.setTargetConnectionFactories(factories); router.setDefaultTargetConnectionFactory(myDefault); return router; } Original pull request: #132.
1 parent 16d4a66 commit e3aae61

11 files changed

+1014
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright 2019 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.r2dbc.connectionfactory.lookup;
17+
18+
import io.r2dbc.spi.Connection;
19+
import io.r2dbc.spi.ConnectionFactory;
20+
import io.r2dbc.spi.ConnectionFactoryMetadata;
21+
import reactor.core.publisher.Mono;
22+
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
import org.springframework.beans.factory.InitializingBean;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Abstract {@link ConnectionFactory} implementation that routes {@link #create()} calls to one of various target
32+
* {@link ConnectionFactory factories} based on a lookup key. The latter is typically (but not necessarily) determined
33+
* from some subscriber context.
34+
* <p>
35+
* Allows to configure a {@link #setDefaultTargetConnectionFactory(Object) default ConnectionFactory} as fallback.
36+
* <p>
37+
* Calls to {@link #getMetadata()} are routed to the {@link #setDefaultTargetConnectionFactory(Object) default
38+
* ConnectionFactory} if configured.
39+
*
40+
* @author Mark Paluch
41+
* @see #setTargetConnectionFactories
42+
* @see #setDefaultTargetConnectionFactory
43+
* @see #determineCurrentLookupKey()
44+
*/
45+
public abstract class AbstractRoutingConnectionFactory implements ConnectionFactory, InitializingBean {
46+
47+
private static final Object FALLBACK_MARKER = new Object();
48+
49+
private @Nullable Map<Object, Object> targetConnectionFactories;
50+
51+
private @Nullable Object defaultTargetConnectionFactory;
52+
53+
private boolean lenientFallback = true;
54+
55+
private ConnectionFactoryLookup connectionFactoryLookup = new MapConnectionFactoryLookup();
56+
57+
private @Nullable Map<Object, ConnectionFactory> resolvedConnectionFactories;
58+
59+
private @Nullable ConnectionFactory resolvedDefaultConnectionFactory;
60+
61+
/**
62+
* Specify the map of target {@link ConnectionFactory ConnectionFactories}, with the lookup key as key. The mapped
63+
* value can either be a corresponding {@link ConnectionFactory} instance or a connection factory name String (to be
64+
* resolved via a {@link #setConnectionFactoryLookup ConnectionFactoryLookup}).
65+
* <p>
66+
* The key can be of arbitrary type; this class implements the generic lookup process only. The concrete key
67+
* representation will be handled by {@link #resolveSpecifiedLookupKey(Object)} and
68+
* {@link #determineCurrentLookupKey()}.
69+
*/
70+
@SuppressWarnings("unchecked")
71+
public void setTargetConnectionFactories(Map<?, ?> targetConnectionFactories) {
72+
this.targetConnectionFactories = (Map) targetConnectionFactories;
73+
}
74+
75+
/**
76+
* Specify the default target {@link ConnectionFactory}, if any.
77+
* <p>
78+
* The mapped value can either be a corresponding {@link ConnectionFactory} instance or a connection factory name
79+
* {@link String} (to be resolved via a {@link #setConnectionFactoryLookup ConnectionFactoryLookup}).
80+
* <p>
81+
* This {@link ConnectionFactory} will be used as target if none of the keyed {@link #setTargetConnectionFactories
82+
* targetConnectionFactories} match the {@link #determineCurrentLookupKey()} current lookup key.
83+
*/
84+
public void setDefaultTargetConnectionFactory(Object defaultTargetConnectionFactory) {
85+
this.defaultTargetConnectionFactory = defaultTargetConnectionFactory;
86+
}
87+
88+
/**
89+
* Specify whether to apply a lenient fallback to the default {@link ConnectionFactory} if no specific
90+
* {@link ConnectionFactory} could be found for the current lookup key.
91+
* <p>
92+
* Default is {@literal true}, accepting lookup keys without a corresponding entry in the target
93+
* {@link ConnectionFactory} map - simply falling back to the default {@link ConnectionFactory} in that case.
94+
* <p>
95+
* Switch this flag to {@literal false} if you would prefer the fallback to only apply no lookup key was emitted.
96+
* Lookup keys without a {@link ConnectionFactory} entry will then lead to an {@link IllegalStateException}.
97+
*
98+
* @see #setTargetConnectionFactories
99+
* @see #setDefaultTargetConnectionFactory
100+
* @see #determineCurrentLookupKey()
101+
*/
102+
public void setLenientFallback(boolean lenientFallback) {
103+
this.lenientFallback = lenientFallback;
104+
}
105+
106+
/**
107+
* Set the {@link ConnectionFactoryLookup} implementation to use for resolving connection factory name Strings in the
108+
* {@link #setTargetConnectionFactories targetConnectionFactories} map.
109+
*/
110+
public void setConnectionFactoryLookup(ConnectionFactoryLookup connectionFactoryLookup) {
111+
112+
Assert.notNull(connectionFactoryLookup, "ConnectionFactoryLookup must not be null!");
113+
114+
this.connectionFactoryLookup = connectionFactoryLookup;
115+
}
116+
117+
/*
118+
* (non-Javadoc)
119+
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
120+
*/
121+
@Override
122+
public void afterPropertiesSet() {
123+
124+
Assert.notNull(this.targetConnectionFactories, "Property 'targetConnectionFactories' must not be null!");
125+
126+
this.resolvedConnectionFactories = new HashMap<>(this.targetConnectionFactories.size());
127+
this.targetConnectionFactories.forEach((key, value) -> {
128+
Object lookupKey = resolveSpecifiedLookupKey(key);
129+
ConnectionFactory connectionFactory = resolveSpecifiedConnectionFactory(value);
130+
this.resolvedConnectionFactories.put(lookupKey, connectionFactory);
131+
});
132+
133+
if (this.defaultTargetConnectionFactory != null) {
134+
this.resolvedDefaultConnectionFactory = resolveSpecifiedConnectionFactory(this.defaultTargetConnectionFactory);
135+
}
136+
}
137+
138+
/**
139+
* Resolve the given lookup key object, as specified in the {@link #setTargetConnectionFactories
140+
* targetConnectionFactories} map, into the actual lookup key to be used for matching with the
141+
* {@link #determineCurrentLookupKey() current lookup key}.
142+
* <p>
143+
* The default implementation simply returns the given key as-is.
144+
*
145+
* @param lookupKey the lookup key object as specified by the user.
146+
* @return the lookup key as needed for matching.
147+
*/
148+
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
149+
return lookupKey;
150+
}
151+
152+
/**
153+
* Resolve the specified connection factory object into a {@link ConnectionFactory} instance.
154+
* <p>
155+
* The default implementation handles {@link ConnectionFactory} instances and connection factory names (to be resolved
156+
* via a {@link #setConnectionFactoryLookup ConnectionFactoryLookup}).
157+
*
158+
* @param connectionFactory the connection factory value object as specified in the
159+
* {@link #setTargetConnectionFactories targetConnectionFactories} map.
160+
* @return the resolved {@link ConnectionFactory} (never {@literal null}).
161+
* @throws IllegalArgumentException in case of an unsupported value type.
162+
*/
163+
protected ConnectionFactory resolveSpecifiedConnectionFactory(Object connectionFactory)
164+
throws IllegalArgumentException {
165+
166+
if (connectionFactory instanceof ConnectionFactory) {
167+
return (ConnectionFactory) connectionFactory;
168+
} else if (connectionFactory instanceof String) {
169+
return this.connectionFactoryLookup.getConnectionFactory((String) connectionFactory);
170+
} else {
171+
throw new IllegalArgumentException(
172+
"Illegal connection factory value - only 'io.r2dbc.spi.ConnectionFactory' and 'String' supported: "
173+
+ connectionFactory);
174+
}
175+
}
176+
177+
/*
178+
* (non-Javadoc)
179+
* @see io.r2dbc.spi.ConnectionFactory#create()
180+
*/
181+
@Override
182+
public Mono<Connection> create() {
183+
184+
return determineTargetConnectionFactory() //
185+
.map(ConnectionFactory::create) //
186+
.flatMap(Mono::from);
187+
}
188+
189+
/*
190+
* (non-Javadoc)
191+
* @see io.r2dbc.spi.ConnectionFactory#getMetadata()
192+
*/
193+
@Override
194+
public ConnectionFactoryMetadata getMetadata() {
195+
196+
if (this.resolvedDefaultConnectionFactory != null) {
197+
return this.resolvedDefaultConnectionFactory.getMetadata();
198+
}
199+
200+
throw new UnsupportedOperationException(
201+
"No default ConnectionFactory configured to retrieve ConnectionFactoryMetadata");
202+
}
203+
204+
/**
205+
* Retrieve the current target {@link ConnectionFactory}. Determines the {@link #determineCurrentLookupKey() current
206+
* lookup key}, performs a lookup in the {@link #setTargetConnectionFactories targetConnectionFactories} map, falls
207+
* back to the specified {@link #setDefaultTargetConnectionFactory default target ConnectionFactory} if necessary.
208+
*
209+
* @see #determineCurrentLookupKey()
210+
* @return {@link Mono} emitting the current {@link ConnectionFactory} as per {@link #determineCurrentLookupKey()}.
211+
*/
212+
protected Mono<ConnectionFactory> determineTargetConnectionFactory() {
213+
214+
Assert.state(this.resolvedConnectionFactories != null, "ConnectionFactory router not initialized");
215+
216+
Mono<Object> lookupKey = determineCurrentLookupKey().defaultIfEmpty(FALLBACK_MARKER);
217+
218+
return lookupKey.handle((key, sink) -> {
219+
220+
ConnectionFactory connectionFactory = this.resolvedConnectionFactories.get(key);
221+
222+
if (connectionFactory == null && (key == FALLBACK_MARKER || this.lenientFallback)) {
223+
connectionFactory = this.resolvedDefaultConnectionFactory;
224+
}
225+
226+
if (connectionFactory == null) {
227+
sink.error(new IllegalStateException(String.format(
228+
"Cannot determine target ConnectionFactory for lookup key '%s'", key == FALLBACK_MARKER ? null : key)));
229+
return;
230+
}
231+
232+
sink.next(connectionFactory);
233+
});
234+
}
235+
236+
/**
237+
* Determine the current lookup key. This will typically be implemented to check a subscriber context. Allows for
238+
* arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the
239+
* {@link #resolveSpecifiedLookupKey} method.
240+
*
241+
* @return {@link Mono} emitting the lookup key. May complete without emitting a value if no lookup key available.
242+
*/
243+
protected abstract Mono<Object> determineCurrentLookupKey();
244+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2019 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.r2dbc.connectionfactory.lookup;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
20+
import org.springframework.beans.BeansException;
21+
import org.springframework.beans.factory.BeanFactory;
22+
import org.springframework.beans.factory.BeanFactoryAware;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* {@link ConnectionFactoryLookup} implementation based on a Spring {@link BeanFactory}.
28+
* <p>
29+
* Will lookup Spring managed beans identified by bean name, expecting them to be of type {@link ConnectionFactory}.
30+
*
31+
* @author Mark Paluch
32+
* @see BeanFactory
33+
*/
34+
public class BeanFactoryConnectionFactoryLookup implements ConnectionFactoryLookup, BeanFactoryAware {
35+
36+
@Nullable private BeanFactory beanFactory;
37+
38+
/**
39+
* Creates a new {@link BeanFactoryConnectionFactoryLookup} instance.
40+
* <p>
41+
* The {@link BeanFactory} to access must be set via {@code setBeanFactory}.
42+
*
43+
* @see #setBeanFactory
44+
*/
45+
public BeanFactoryConnectionFactoryLookup() {}
46+
47+
/**
48+
* Create a new instance of the {@link BeanFactoryConnectionFactoryLookup} class.
49+
* <p>
50+
* Use of this constructor is redundant if this object is being created by a Spring IoC container, as the supplied
51+
* {@link BeanFactory} will be replaced by the {@link BeanFactory} that creates it (see the {@link BeanFactoryAware}
52+
* contract). So only use this constructor if you are using this class outside the context of a Spring IoC container.
53+
*
54+
* @param beanFactory the bean factory to be used to lookup {@link ConnectionFactory ConnectionFactories}.
55+
*/
56+
public BeanFactoryConnectionFactoryLookup(BeanFactory beanFactory) {
57+
58+
Assert.notNull(beanFactory, "BeanFactory must not be null!");
59+
60+
this.beanFactory = beanFactory;
61+
}
62+
63+
/*
64+
* (non-Javadoc)
65+
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
66+
*/
67+
@Override
68+
public void setBeanFactory(BeanFactory beanFactory) {
69+
this.beanFactory = beanFactory;
70+
}
71+
72+
/*
73+
* (non-Javadoc)
74+
* @see org.springframework.data.r2dbc.connectionfactory.lookup.ConnectionFactoryLookup#getConnectionFactory(java.lang.String)
75+
*/
76+
@Override
77+
public ConnectionFactory getConnectionFactory(String connectionFactoryName)
78+
throws ConnectionFactoryLookupFailureException {
79+
80+
Assert.state(this.beanFactory != null, "BeanFactory must not be null!");
81+
82+
try {
83+
return this.beanFactory.getBean(connectionFactoryName, ConnectionFactory.class);
84+
} catch (BeansException ex) {
85+
throw new ConnectionFactoryLookupFailureException(
86+
String.format("Failed to look up ConnectionFactory bean with name '%s'", connectionFactoryName), ex);
87+
}
88+
}
89+
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2019 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.r2dbc.connectionfactory.lookup;
17+
18+
import io.r2dbc.spi.ConnectionFactory;
19+
20+
/**
21+
* Strategy interface for looking up {@link ConnectionFactory} by name.
22+
*
23+
* @author Mark Paluch
24+
*/
25+
@FunctionalInterface
26+
public interface ConnectionFactoryLookup {
27+
28+
/**
29+
* Retrieve the {@link ConnectionFactory} identified by the given name.
30+
*
31+
* @param connectionFactoryName the name of the {@link ConnectionFactory}.
32+
* @return the {@link ConnectionFactory} (never {@literal null}).
33+
* @throws ConnectionFactoryLookupFailureException if the lookup failed.
34+
*/
35+
ConnectionFactory getConnectionFactory(String connectionFactoryName) throws ConnectionFactoryLookupFailureException;
36+
}

0 commit comments

Comments
 (0)