Skip to content

Commit 9463751

Browse files
predic8christiangoerdesrrayst
authored
Router refactor to beanfactory (#2514)
* refactor: move globalInterceptor to beanFactory * refactor: streamline exception messages, improve logging, and enhance Javadoc comments across classes * feat: global interceptor support for YAML * refactor: streamline imports, remove unused methods, and improve exception formatting in `Router` * refactor: remove unused `setRouter` method from `Transport` and enhance Javadoc for `Router` settings * refactor: remove unused imports, simplify package structure, and clean up redundant code in YAML integration * refactor: remove unused imports, simplify package structure, and clean up redundant code in YAML integration * refactor: extend and streamline BeanRegistry integration in Router, improve exception formatting, enhance concurrency handling * refactor: streamline imports in `GenericYamlParserTest`, improve readability and maintainability * refactor: simplify `registerIfAbsent` usage in `Router` and `BeanRegistry`, streamline bean registration logic * refactor: simplify `registerIfAbsent` by removing `name` parameter, streamline bean registration * refactor: remove duplicate `openPorts` field from `Router` * refactor: enhance concurrency handling in `BeanRegistryImplementation`, improve logging and null checks, and streamline `activationRun` logic * refactor: replace `Router.init` with `Router.initByXML`, improve thread-safety in `BeanContainer`, and streamline `BeanRegistry` handling * refactor: remove redundant `Exception` declarations, simplify `tearDown` and `setUp` methods, and clean up `BeanContainer` by removing unused `setIfAbsent` method * refactor: simplify Router lifecycle methods, streamline test setup/teardown logic, and remove unused classes * refactor: streamline imports in test files and remove redundant `Exception` declarations * refactor: improve thread-safety in `registerIfAbsent` by introducing dedicated lock * refactor: update test lifecycle methods to use `@BeforeAll`/`@AfterAll`, remove redundant assertions, and improve object reuse * refactor: refactor `Router` lifecycle methods, remove `openPorts` handling, improve proxy addition & initialization logic * refactor: streamline imports, remove redundant declarations, and improve readability in test files * refactor: improve Router lifecycle management, update test setup/teardown methods, and streamline imports across test cases * refactor: replace `Router.initByXML` with `RouterBootstrap.initByXML`, improve HotDeployer lifecycle, and streamline `Router` initialization logic * refactor: make `router` field `final` in `RuleReinitializer` to ensure immutability and improve thread-safety * refactor: improve `Router` lifecycle methods, enhance thread-safety, and streamline `RuleReinitializer` logic * refactor: remove unnecessary `Exception` declaration from `initOasi` method in test file * refactor: enhance `Router` lifecycle handling, improve thread-safety, and update test setup/teardown methods * refactor: add TODO for replacing `backgroundInitializer` with virtual threads in `Router` to align with modern threading practices * refactor: replace `Router` with `HttpRouter` in test files and update initialization logic for consistency * refactor: improve thread-safety in `DefaultHotDeployer`, enhance logging, and fix `BeanContainer` string formatting * refactor: replace `Router` with `IRouter` for better abstraction and consistency across core components * refactor: introduce `AbstractRouter` for shared functionality, replace `Router` with `IRouter` or specialized implementations, and align tests accordingly * refactor: remove `IRouter` interface, replace with `DefaultRouter` or specialized implementations, and align related components and tests * refactor: replace `router.getConfig()` with `router.getConfiguration()` for improved naming consistency across core components and tests * refactor: replace `router.shutdown()` with `router.stop()` and introduce `DefaultMainComponents` to streamline router initialization and component management * refactor: improve thread-safety and synchronization in `BeanRegistryImplementation`, enhance initialization/logging in routers, and remove unused/deprecated methods * refactor: extend timeout in `MassivelyParallelTest` to improve test stability under high load * improved synchronization * doc * small improvements --------- Co-authored-by: Christian Gördes <118011644+christiangoerdes@users.noreply.github.com> Co-authored-by: Tobias Polley <mail@tobias-polley.de>
1 parent a5ecc1b commit 9463751

323 files changed

Lines changed: 3787 additions & 3266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanContainer.java

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,52 @@
1414

1515
package com.predic8.membrane.annot.beanregistry;
1616

17+
import com.predic8.membrane.annot.Grammar;
18+
import com.predic8.membrane.annot.bean.BeanFactory;
19+
import com.predic8.membrane.annot.yaml.GenericYamlParser;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
23+
import java.util.concurrent.atomic.*;
24+
1725
public class BeanContainer {
26+
private static final Logger log = LoggerFactory.getLogger(BeanContainer.class);
27+
1828
private final BeanDefinition definition;
1929
/**
2030
* Constructed bean after initialization.
2131
*/
22-
private volatile Object singleton;
32+
private final AtomicReference<Object> singleton = new AtomicReference<>();
2333

34+
/**
35+
* Creates a BeanDefinition where the bean has not yet been instantiated and initialized.
36+
*/
2437
public BeanContainer(BeanDefinition definition) {
2538
this.definition = definition;
2639
}
2740

41+
/**
42+
* Creates a BeanDefinition where the bean has already been instantiated and initialized.
43+
*/
44+
public BeanContainer(BeanDefinition definition, Object singleton) {
45+
this.definition = definition;
46+
this.singleton.set(singleton);
47+
}
2848

29-
public Object getSingleton() {
30-
return singleton;
49+
/**
50+
* Only to be used within this class.
51+
* Use {@link #getOrCreate(BeanRegistryImplementation, Grammar)} instead.
52+
*/
53+
private Object getSingleton() {
54+
return singleton.get();
3155
}
3256

33-
public void setSingleton(Object singleton) {
34-
this.singleton = singleton;
57+
/**
58+
* Only to be used within this class.
59+
* Use {@link #getOrCreate(BeanRegistryImplementation, Grammar)} instead.
60+
*/
61+
private void setSingleton(Object singleton) {
62+
this.singleton.set(singleton);
3563
}
3664

3765
public BeanDefinition getDefinition() {
@@ -40,6 +68,52 @@ public BeanDefinition getDefinition() {
4068

4169
@Override
4270
public String toString() {
43-
return "BeanContainer: %s of %s".formatted( definition.getName(),definition.getKind());
71+
return "BeanContainer: %s of %s singleton: %s".formatted( definition.getName(),definition.getKind(),singleton.get());
72+
}
73+
74+
private synchronized Object define(BeanRegistryImplementation registry, Grammar grammar) {
75+
log.debug("defining bean: {}", definition.getNode());
76+
try {
77+
if ("bean".equals(definition.getKind())) {
78+
return new BeanFactory(registry).create(definition.getNode().path("bean"));
79+
}
80+
return GenericYamlParser.readMembraneObject(definition.getKind(),
81+
grammar,
82+
definition.getNode(),
83+
registry);
84+
} catch (Exception e) {
85+
throw new RuntimeException(e);
86+
}
87+
}
88+
89+
public Object getOrCreate(BeanRegistryImplementation registry, Grammar grammar) {
90+
boolean prototype = isPrototypeScope(getDefinition());
91+
92+
// Prototypes are created anew every time.
93+
if (prototype) {
94+
return define(registry, grammar);
95+
}
96+
97+
// Singleton: ensure define() runs at most once per BeanContainer.
98+
synchronized (this) {
99+
Object existing = getSingleton();
100+
if (existing != null) {
101+
return existing;
102+
}
103+
104+
Object created = define(registry, grammar);
105+
setSingleton(created);
106+
return created;
107+
}
108+
}
109+
110+
private static boolean isPrototypeScope(BeanDefinition bd) {
111+
if (!bd.isBean())
112+
return bd.isPrototype();
113+
114+
return "PROTOTYPE".equalsIgnoreCase(
115+
bd.getNode().path("bean").path("scope").asText("SINGLETON")
116+
);
44117
}
118+
45119
}

annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanDefinition.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import com.fasterxml.jackson.databind.*;
1717
import com.predic8.membrane.annot.yaml.*;
1818

19+
/**
20+
* Immutable.
21+
*/
1922
public class BeanDefinition {
2023

2124
public static final String PROTOTYPE = "prototype";
@@ -76,6 +79,8 @@ public String getKind() {
7679
}
7780

7881
public String getScope() {
82+
if (node == null)
83+
return null;
7984
JsonNode meta = node.get("metadata");
8085
if (meta == null)
8186
return null;

annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.predic8.membrane.annot.*;
1717

1818
import java.util.*;
19+
import java.util.function.*;
1920

2021
public interface BeanRegistry {
2122

@@ -44,4 +45,15 @@ public interface BeanRegistry {
4445
* @param bean instance to register (must not be null)
4546
*/
4647
void register(String beanName, Object bean);
48+
49+
/**
50+
* Registers a bean of the specified type with the given name if it is not already registered.
51+
* If a bean with the given name is already present, the existing instance is returned.
52+
* Otherwise, the supplier is used to create and register a new instance.
53+
* @param type the class type of the bean
54+
* @param supplier a supplier that provides a new instance of the bean if not already registered
55+
* @param <T> the generic type of the bean
56+
* @return the existing or newly created and registered bean instance
57+
*/
58+
<T> T registerIfAbsent(Class<T> type, Supplier<T> supplier);
4759
}

annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,29 @@
1313
limitations under the License. */
1414
package com.predic8.membrane.annot.beanregistry;
1515

16-
import com.predic8.membrane.annot.Grammar;
17-
import com.predic8.membrane.annot.bean.BeanFactory;
18-
import com.predic8.membrane.annot.yaml.GenericYamlParser;
19-
import com.predic8.membrane.annot.yaml.WatchAction;
16+
import com.predic8.membrane.annot.*;
17+
import com.predic8.membrane.annot.bean.*;
18+
import com.predic8.membrane.annot.yaml.*;
2019
import org.jetbrains.annotations.*;
21-
import org.slf4j.Logger;
22-
import org.slf4j.LoggerFactory;
20+
import org.slf4j.*;
2321

22+
import javax.annotation.concurrent.*;
2423
import java.util.*;
25-
import java.util.concurrent.ConcurrentHashMap;
26-
24+
import java.util.concurrent.*;
25+
import java.util.function.*;
26+
27+
/**
28+
* TODO:
29+
* - More Tests
30+
* - Document
31+
* - For TB Unclear: Lifecycle activation/resolve
32+
* - oldbean
33+
* - a.) Revert to second list (singletonBeans)
34+
* - b.) Give proxies an unique id
35+
* <p>
36+
* For K8S UUID and name is needed cause name is only unique within a namespace.
37+
*
38+
*/
2739
public class BeanRegistryImplementation implements BeanRegistry, BeanCollector {
2840

2941
private static final Logger log = LoggerFactory.getLogger(BeanRegistryImplementation.class);
@@ -36,31 +48,24 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector {
3648

3749
// uid -> bean container
3850
private final Map<String, BeanContainer> bcs = new ConcurrentHashMap<>(); // Order is not critical. Order is determined by uidsToActivate
51+
52+
@GuardedBy("uidsToActivate")
3953
private final Set<UidAction> uidsToActivate = Collections.synchronizedSet(new LinkedHashSet<>()); // keeps order
4054

41-
record UidAction(String uid, WatchAction action) {}
55+
/**
56+
* Protects the initialization of beans, which are unique per class.
57+
*/
58+
private final Object uniqueClassInitialization = new Object();
59+
60+
record UidAction(String uid, WatchAction action) {
61+
}
4262

4363
public BeanRegistryImplementation(BeanCacheObserver observer, BeanRegistryAware registryAware, Grammar grammar) {
4464
this.observer = observer;
4565
this.grammar = grammar;
4666
registryAware.setRegistry(this);
4767
}
4868

49-
private Object define(BeanDefinition bd) {
50-
log.debug("defining bean: {}", bd.getNode());
51-
try {
52-
if ("bean".equals(bd.getKind())) {
53-
return new BeanFactory(this).create(bd.getNode().path("bean"));
54-
}
55-
return GenericYamlParser.readMembraneObject(bd.getKind(),
56-
grammar,
57-
bd.getNode(),
58-
this);
59-
} catch (Exception e) {
60-
throw new RuntimeException(e);
61-
}
62-
}
63-
6469
@Override
6570
public void start() {
6671
}
@@ -89,10 +94,9 @@ private void activationRun() {
8994
for (UidAction uidAction : uidsToActivate) {
9095
BeanContainer bc = bcs.get(uidAction.uid);
9196
try {
92-
Object bean = define(bc.getDefinition());
93-
bc.setSingleton(bean);
97+
Object bean = bc.getOrCreate(this, grammar);
9498

95-
// e.g. inform router about new proxy
99+
// e.g., inform router about a new ApiProxy or GlobalInterceptor
96100
observer.handleBeanEvent(new BeanDefinitionChanged(uidAction.action, bc.getDefinition()), bean, getOldBean(uidAction.action, bc.getDefinition()));
97101

98102
if (uidAction.action.isAdded() || uidAction.action.isModified())
@@ -121,19 +125,7 @@ private void activationRun() {
121125

122126
@Override
123127
public Object resolve(String url) {
124-
BeanContainer bc = getFirstByName(url).orElseThrow(() -> new RuntimeException("Reference %s not found".formatted(url)));
125-
126-
boolean prototype = isPrototypeScope(bc.getDefinition());
127-
128-
if (!prototype && bc.getSingleton() != null)
129-
return bc.getSingleton();
130-
131-
Object instance = define(bc.getDefinition());
132-
133-
if (!prototype)
134-
bc.setSingleton(instance);
135-
136-
return instance;
128+
return getFirstByName(url).orElseThrow(() -> new RuntimeException("Reference %s not found".formatted(url))).getOrCreate(this, grammar);
137129
}
138130

139131
private @NotNull Optional<BeanContainer> getFirstByName(String url) {
@@ -143,7 +135,7 @@ public Object resolve(String url) {
143135
@Override
144136
public List<Object> getBeans() {
145137
return bcs.values().stream().filter(bd -> !bd.getDefinition().isComponent())
146-
.map(BeanContainer::getSingleton)
138+
.map(bc -> bc.getOrCreate(this, grammar))
147139
.filter(Objects::nonNull)
148140
.toList();
149141
}
@@ -153,19 +145,10 @@ public Grammar getGrammar() {
153145
return grammar;
154146
}
155147

156-
private static boolean isPrototypeScope(BeanDefinition bd) {
157-
if (!bd.isBean())
158-
return bd.isPrototype();
159-
160-
return "PROTOTYPE".equalsIgnoreCase(
161-
bd.getNode().path("bean").path("scope").asText("SINGLETON")
162-
);
163-
}
164-
165148
@Override
166149
public <T> List<T> getBeans(Class<T> clazz) {
167150
return bcs.values().stream()
168-
.map(BeanContainer::getSingleton)
151+
.map(bc -> bc.getOrCreate(this, grammar))
169152
.filter(Objects::nonNull)
170153
.filter(clazz::isInstance)
171154
.map(clazz::cast)
@@ -175,18 +158,44 @@ public <T> List<T> getBeans(Class<T> clazz) {
175158
public <T> Optional<T> getBean(Class<T> clazz) {
176159
var beans = getBeans(clazz);
177160
if (beans.size() > 1) {
178-
var msg = "One bean was asked. But found %d beans of %s".formatted(beans.size(),clazz);
161+
var msg = "One bean was asked. But found %d beans of %s".formatted(beans.size(), clazz);
179162
log.error(msg);
180163
throw new RuntimeException(msg);
181164
}
182165
return beans.size() == 1 ? Optional.of(beans.getFirst()) : Optional.empty();
183166
}
184167

185168
public void register(String beanName, Object bean) {
169+
if (bean == null)
170+
throw new IllegalArgumentException("bean must not be null");
171+
186172
var uuid = UUID.randomUUID().toString();
187-
BeanContainer bc = new BeanContainer(new BeanDefinition("component", beanName,null, uuid, null));
188-
bc.setSingleton(bean);
189-
singletonBeans.put(uuid,bean);
190-
bcs.put(uuid, bc);
173+
bcs.put(uuid,
174+
new BeanContainer(
175+
new BeanDefinition(
176+
"component",
177+
computeBeanName(beanName, uuid),
178+
null,
179+
uuid,
180+
null),
181+
bean));
182+
singletonBeans.put(uuid, bean);
183+
// the return value of 'put' is ignored, since bean registration with
184+
// random keys should not yield duplicates anyway.
191185
}
186+
187+
public <T> T registerIfAbsent(Class<T> type, Supplier<T> supplier) {
188+
synchronized (uniqueClassInitialization) {
189+
return getBean(type).orElseGet(() -> getBean(type).orElseGet(() -> {
190+
T created = supplier.get();
191+
register(null, created);
192+
return created;
193+
}));
194+
}
195+
}
196+
197+
private static @NotNull String computeBeanName(String beanName, String uuid) {
198+
return beanName != null ? beanName : "#" + uuid;
199+
}
200+
192201
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
15+
package com.predic8.membrane.core;
16+
17+
import com.predic8.membrane.core.proxies.*;
18+
import org.slf4j.*;
19+
20+
public abstract class AbstractRouter implements Router, MainComponents {
21+
22+
private static final Logger log = LoggerFactory.getLogger(DefaultRouter.class);
23+
24+
protected void initProxies() {
25+
log.debug("Initializing proxies.");
26+
for (Proxy proxy : getRuleManager().getRules()) {
27+
log.debug("Initializing proxy {}.", proxy.getName());
28+
proxy.init(this);
29+
}
30+
}
31+
}

core/src/main/java/com/predic8/membrane/core/Configuration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Configuration {
3737

3838
private boolean retryInit = false;
3939

40-
private String jmxRouterName;
40+
private String jmxRouterName = "default";
4141

4242
private URIFactory uriFactory = new URIFactory(false);
4343

core/src/main/java/com/predic8/membrane/core/DefaultConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private void defineRouter(BeanDefinitionRegistry beanDefinitionRegistry) {
6666
if (!beanDefinitionRegistry.containsBeanDefinition("router")) {
6767
beanDefinitionRegistry.registerBeanDefinition(
6868
"router",
69-
root().clazz(Router.class).addRef("transport", "transport").addRef("exchangeStore", "memoryExchangeStore").build());
69+
root().clazz(DefaultRouter.class).addRef("transport", "transport").addRef("exchangeStore", "memoryExchangeStore").build());
7070
}
7171
}
7272

0 commit comments

Comments
 (0)