Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ public interface BeanCacheObserver {
* Called for an add/modify/delete event of a bean.
*
* @param bd the bean definition changed event
* @param bean the current instance (on ADD/MODIFY) or {@code null} (on DELETE)
* @param newBean the current instance (on ADD/MODIFY) or {@code null} (on DELETE)
* @param oldBean the previous instance (on MODIFY) or {@code null}
* @throws IOException if handling the event performs I/O and it fails
*
*/
void handleBeanEvent(BeanDefinitionChanged bd, Object bean, Object oldBean) throws IOException;
void handleBeanEvent(BeanDefinitionChanged bd, Object newBean, Object oldBean) throws IOException;

/**
* Whether beans of the given definition should be considered activatable/usable
* by the runtime.
*
* @param bd the bean definition
* @param bd the bean container
* @return {@code true} if activatable, {@code false} otherwise
*/
boolean isActivatable(BeanDefinition bd);
boolean isActivatable(BeanContainer bd);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public class BeanContainer {
private static final Logger log = LoggerFactory.getLogger(BeanContainer.class);

private final BeanDefinition definition;
/**
* The class of the bean.
*/
private final Class clazz;
/**
* Constructed bean after initialization.
*/
Expand All @@ -35,16 +39,18 @@ public class BeanContainer {
/**
* Creates a BeanDefinition where the bean has not yet been instantiated and initialized.
*/
public BeanContainer(BeanDefinition definition) {
public BeanContainer(BeanDefinition definition, Grammar grammar) {
this.definition = definition;
this.clazz = GenericYamlParser.decideClazz(definition.getKind(), grammar, definition.getNode());
}

/**
* Creates a BeanDefinition where the bean has already been instantiated and initialized.
*/
public BeanContainer(BeanDefinition definition, Object singleton) {
public BeanContainer(BeanDefinition definition, @NotNull Object singleton) {
this.definition = definition;
this.singleton.set(singleton);
this.clazz = singleton.getClass();
}

/**
Expand Down Expand Up @@ -95,6 +101,7 @@ public String toString() {
return define(registry, grammar);
}


// Singleton: ensure define() runs at most once per BeanContainer.
synchronized (this) {
Object existing = getSingleton();
Expand All @@ -117,4 +124,10 @@ private static boolean isPrototypeScope(BeanDefinition bd) {
);
}

/**
* Checks whether this bean definition will produce a bean which can be assigned to the clazz.
*/
public <T> boolean produces(Class<T> clazz) {
return clazz.isAssignableFrom(this.clazz);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import org.slf4j.*;

import javax.annotation.concurrent.*;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;

import static com.predic8.membrane.annot.yaml.WatchAction.ADDED;

/**
* TODO:
* - More Tests
Expand All @@ -41,7 +44,7 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector,

private static final Logger log = LoggerFactory.getLogger(BeanRegistryImplementation.class);

private final BeanCacheObserver observer;
private final List<BeanCacheObserver> observers = Collections.synchronizedList(new ArrayList<>());
private final Grammar grammar;

// uid -> bean
Expand All @@ -67,44 +70,54 @@ record UidAction(String uid, WatchAction action) {
record PreDestroyCallback(Object bean, Method method) {
}

public BeanRegistryImplementation(BeanCacheObserver observer, BeanRegistryAware registryAware, Grammar grammar) {
this.observer = observer;
public BeanRegistryImplementation(Grammar grammar) {
this.grammar = grammar;
registryAware.setRegistry(this);
}

@Override
public void start() {
for (BeanContainer bc : bcs.values()) {
bc.getOrCreate(this, grammar);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Override
public void handle(ChangeEvent changeEvent, boolean isLast) {
if (changeEvent instanceof StaticConfigurationLoaded) {
activationRun();
observer.handleAsynchronousInitializationResult(uidsToActivate.isEmpty());
handleAsynchronousInitializationResult(uidsToActivate.isEmpty());
}
if (changeEvent instanceof BeanDefinitionChanged(WatchAction action, BeanDefinition bd)) {
// Keep the latest BeanDefinition for all actions so activationRun
// can see both metadata and the action (including DELETED).
bcs.put(bd.getUid(), new BeanContainer(bd));

if (!bd.isComponent() && observer.isActivatable(bd)) {
uidsToActivate.add(new UidAction(bd.getUid(), action));
}
handleBeanContainerChange(action, new BeanContainer(bd, grammar));
if (isLast)
activationRun();
}
}

private void handleBeanContainerChange(WatchAction action, BeanContainer bc) {
bcs.put(bc.getDefinition().getUid(), bc);

if (bc.produces(BeanCacheObserver.class)) {
observers.add((BeanCacheObserver) bc.getOrCreate(this, grammar));
log.debug("Registered BeanRegistry observer: " + bc);
}

if (!bc.getDefinition().isComponent() && isActivatable(bc)) {
uidsToActivate.add(new UidAction(bc.getDefinition().getUid(), action));
}
}


private void activationRun() {
Set<UidAction> uidsToRemove = new HashSet<>();
for (UidAction uidAction : uidsToActivate) {
BeanContainer bc = bcs.get(uidAction.uid);
try {
Object bean = bc.getOrCreate(this, grammar);

// e.g., inform router about a new ApiProxy or GlobalInterceptor
observer.handleBeanEvent(new BeanDefinitionChanged(uidAction.action, bc.getDefinition()), bean, getOldBean(uidAction.action, bc.getDefinition()));
handleBeanEvent(new BeanDefinitionChanged(uidAction.action, bc.getDefinition()), bean, getOldBean(uidAction.action, bc.getDefinition()));

if (uidAction.action.isAdded() || uidAction.action.isModified())
singletonBeans.put(bc.getDefinition().getUid(), bean);
Expand Down Expand Up @@ -155,6 +168,7 @@ public Grammar getGrammar() {
@Override
public <T> List<T> getBeans(Class<T> clazz) {
return bcs.values().stream()
.filter(bd -> bd.produces(clazz))
.map(bc -> bc.getOrCreate(this, grammar))
.filter(Objects::nonNull)
.filter(clazz::isInstance)
Expand Down Expand Up @@ -183,15 +197,14 @@ public void register(String beanName, Object bean) {
throw new IllegalArgumentException("bean must not be null");

var uuid = UUID.randomUUID().toString();
bcs.put(uuid,
new BeanContainer(
new BeanDefinition(
"component",
computeBeanName(beanName, uuid),
null,
uuid,
null),
bean));
handleBeanContainerChange(ADDED, new BeanContainer(
new BeanDefinition(
"component",
computeBeanName(beanName, uuid),
null,
uuid,
null),
bean));
singletonBeans.put(uuid, bean);
// the return value of 'put' is ignored, since bean registration with
// random keys should not yield duplicates anyway.
Expand Down Expand Up @@ -235,4 +248,38 @@ public void close() {
});
}

/**
* Checks whether any registered Observer is interested in the given bean.
*/
private boolean isActivatable(BeanContainer bd) {
synchronized (observers) {
for (BeanCacheObserver observer : observers) {
if (observer.isActivatable(bd)) return true;
}
}
return false;
}

/**
* Notifies all registered Observers about the result of the asynchronous initialization.
*/
private void handleAsynchronousInitializationResult(boolean empty) {
synchronized (observers) {
for (BeanCacheObserver observer : observers) {
observer.handleAsynchronousInitializationResult(empty);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Notifies all registered Observers about a bean change.
*/
private void handleBeanEvent(BeanDefinitionChanged event, Object newBean, @Nullable Object oldBean) throws IOException {
synchronized (observers) {
for (BeanCacheObserver observer : observers)
// e.g., inform router about a new ApiProxy or GlobalInterceptor
observer.handleBeanEvent(event, newBean, oldBean);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,18 @@ private static void validate(Grammar grammar, JsonNode input) throws YamlSchemaV
* grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.</p>
*/
public static <R extends BeanRegistry & BeanLifecycleManager> Object readMembraneObject(String kind, Grammar grammar, JsonNode node, R registry) throws ParsingException {
return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar), decideClazz(kind, grammar, node), node.get(kind));
}

/**
* Detects the class that will be selected to represent the node in Java.
*/
public static Class<?> decideClazz(String kind, Grammar grammar, JsonNode node) {
ensureSingleKey(node);
Class<?> clazz = grammar.getElement(kind);
if (clazz == null)
throw new ParsingException("Did not find java class for kind '%s'.".formatted(kind), node);
return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar), clazz, node.get(kind));
return clazz;
}

/**
Expand Down Expand Up @@ -306,6 +313,9 @@ private static Object parseMapToObj(ParsingContext<?> ctx, JsonNode node, String
* registered within the registry.
*/
private static <T> T handlePostConstructAndPreDestroy(ParsingContext<?> ctx, T bean) {
if (bean instanceof BeanRegistryAware beanRegistryAware) {
beanRegistryAware.setRegistry(ctx.registry());
}
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
if (method.isAnnotationPresent(PostConstruct.class)) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@ void prototypeResolveReference() {

@Test
void missingClassFailsFastOnResolve() {
BeanRegistry r = parse("""
var ex = assertThrows(RuntimeException.class, () -> {
BeanRegistry r = parse("""
components:
x:
bean:
scope: singleton
""");

var ex = assertThrows(RuntimeException.class, () -> r.resolve("#/components/x"));
assertAnyErrorContains(ex, "Missing/blank 'class'");
});
assertAnyErrorContains(ex, "Missing/blank 'class' in bean spec.");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class BeanRegistryImplementationTest {
@BeforeEach
void setup() {
aware = Mockito.mock(BeanRegistryAware.class);
registry = new BeanRegistryImplementation(null, aware, null);
registry = new BeanRegistryImplementation(null);
registry.register("aware", aware);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ public class YamlParser {
*/
private final BeanRegistry beanRegistry;

private static class TestRouter implements BeanRegistryAware {

@Override
public void setRegistry(BeanRegistry registry) {

}
}

public YamlParser(String resourceName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, InterruptedException {
Grammar generator = getGrammar();

Expand All @@ -55,11 +47,11 @@ public YamlParser(String resourceName) throws ClassNotFoundException, NoSuchMeth
String normalized = resourceName.startsWith("/") ?
resourceName.substring(1) : resourceName;

BeanRegistryImplementation impl = new BeanRegistryImplementation(getLatchObserver(cdl), new TestRouter(), generator);
BeanRegistryImplementation impl = new BeanRegistryImplementation(generator);
impl.parseYamls(requireNonNull(cl.getResourceAsStream(normalized)), generator);
beanRegistry = impl;
impl.start();

cdl.await();
beanRegistry = impl;
}

private @NotNull Grammar getGrammar()
Expand All @@ -78,28 +70,6 @@ public YamlParser(String resourceName) throws ClassNotFoundException, NoSuchMeth
return (Grammar) grammarClass.getConstructor().newInstance();
}

/**
* Used to get notification about termination of parsing
*/
private static @NotNull BeanCacheObserver getLatchObserver(CountDownLatch cdl) {
return new BeanCacheObserver() {
@Override
public void handleAsynchronousInitializationResult(boolean empty) {
cdl.countDown();
}

@Override
public void handleBeanEvent(BeanDefinitionChanged bdc, Object bean, Object oldBean) {

}

@Override
public boolean isActivatable(BeanDefinition bd) {
return true;
}
};
}

/**
* Called by reflection from the YAML parser in CompilerHelper
* @return BeanRegistry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,15 @@ private static Router initRouterByYAML(String location) throws Exception {
router.getConfiguration().setBaseLocation(location);

GrammarAutoGenerated grammar = new GrammarAutoGenerated();
BeanRegistryImplementation reg = new BeanRegistryImplementation(router, router, grammar);
BeanRegistryImplementation reg = new BeanRegistryImplementation(grammar);
Comment thread
rrayst marked this conversation as resolved.
reg.register("router", router);

getConfigDefinition(reg.parseYamlBeanDefinitions(router.getResolverMap().resolve(location), grammar))
.ifPresent(beanDefinition -> router.applyConfiguration((Configuration) reg.resolve(beanDefinition.getName())));

reg.finishStaticConfiguration();

reg.start();
router.start();
logStartupMessage();
return router;
Expand Down
Loading
Loading