diff --git a/itara-common/src/main/java/io/itara/runtime/ItaraRegistry.java b/itara-common/src/main/java/io/itara/runtime/ItaraRegistry.java index a44e924..cf1ba33 100644 --- a/itara-common/src/main/java/io/itara/runtime/ItaraRegistry.java +++ b/itara-common/src/main/java/io/itara/runtime/ItaraRegistry.java @@ -73,8 +73,20 @@ public void preRegister(String id, Object proxy) { public void registerActivator(String id, Class> activatorClass, Class contractClass) { + if (activatorClass == null) { + throw new IllegalArgumentException(String.format( + "[Itara] Received null value for activator class to be registered for component with id: %s%n" + + "Check ComponentInterface definitions", id)); + } activators.put(id, activatorClass); + + if (contractClass == null) { + throw new IllegalArgumentException(String.format( + "[Itara] Received null value for contract class to be registered for component with id: %s%n" + + "Check ComponentInterface definitions", id)); + } contracts.put(id, contractClass); + log.info("[Itara] Registered activator for: " + id + " -> " + activatorClass.getName()); } @@ -156,4 +168,44 @@ public T get(String id, Class type) { activating.remove(id); } } + + /** + * Try retrieve a component by contract/interface type. + * + * If exactly one component matches the type, returns it. + * If none match, throws IllegalStateException. + * If multiple match, throws IllegalStateException. + */ + @SuppressWarnings("unchecked") + public T get(Class type) { + + String matchedId = null; + + // Search all known component contracts + for (Map.Entry> entry : contracts.entrySet()) { + + String id = entry.getKey(); + Class contract = entry.getValue(); + + if (type.isAssignableFrom(contract)) { + + if (matchedId != null) { + throw new IllegalStateException( + "[Itara] Multiple components match type: " + + type.getName() + + " -> '" + matchedId + "', '" + id + "'"); + } + + matchedId = id; + } + } + + if (matchedId == null) { + throw new IllegalStateException( + "[Itara] No component registered for type: " + + type.getName()); + } + + return get(matchedId, type); + } } diff --git a/itara-common/src/main/java/io/itara/runtime/ItaraSpring.java b/itara-common/src/main/java/io/itara/runtime/ItaraSpring.java new file mode 100644 index 0000000..d7e05f5 --- /dev/null +++ b/itara-common/src/main/java/io/itara/runtime/ItaraSpring.java @@ -0,0 +1,70 @@ +package io.itara.runtime; + +/** + * Minimal Spring integration helper for Itara. + * + *

Purpose: + * Provides explicit access to Itara-managed component proxies so application + * developers can wire them into their own Spring configuration. + * + *

This is intentionally simple and explicit: + *

    + *
  • No Spring auto-configuration
  • + *
  • No automatic bean injection magic
  • + *
  • No dependency on Spring inside itara-common or itara-agent
  • + *
+ * + *

Typical usage: + * + *

{@code
+ * @Configuration
+ * public class AppConfig {
+ *
+ *     @Bean
+ *     public UserClient userClient() {
+ *         return ItaraSpring.get(UserClient.class);
+ *     }
+ * }
+ * }
+ * + *

The returned instance is the Itara-wired proxy that was pre-registered + * by the agent during startup. + */ +public final class ItaraSpring { + + private ItaraSpring() { + } + + /** + * Retrieve a component by explicit component id and contract type. + * + *

Usually used internally or in advanced wiring scenarios where + * multiple implementations may exist. + * + * @param id component id from the topology configuration + * @param type expected contract/interface type + * @return the Itara-managed component instance or proxy + */ + public static T get(String id, Class type) { + ItaraRegistry registry = ItaraRegistry.instance(); + return registry.get(id, type); + } + + /** + * Retrieve a component by contract/interface type. + * + *

Designed for Spring {@code @Bean} methods where the developer + * explicitly chooses which Itara component to expose as a Spring bean. + * + *

The registry must contain exactly one component matching the type. + * If multiple components match, an exception is thrown to avoid ambiguous + * wiring. + * + * @param type contract/interface type + * @return the pre-registered Itara proxy or activated component + */ + public static T get(Class type) { + ItaraRegistry registry = ItaraRegistry.instance(); + return registry.get(type); + } +} \ No newline at end of file diff --git a/itara-demo/gateway-springcomponent/pom.xml b/itara-demo/gateway-springcomponent/pom.xml new file mode 100644 index 0000000..f52101d --- /dev/null +++ b/itara-demo/gateway-springcomponent/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + + io.itara.demo + itara-demo + 1.0-SNAPSHOT + + + gateway-springcomponent + + + + + io.itara.demo + gateway-api + compile + + + + io.itara.demo + calculator-api + compile + + + + org.springframework.boot + spring-boot-starter + 4.0.6 + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-runtime-dependencies + package + + copy-dependencies + + + + ${project.build.directory}/lib + + runtime + + + + + + + \ No newline at end of file diff --git a/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/DemoMain.java b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/DemoMain.java new file mode 100644 index 0000000..33edfa7 --- /dev/null +++ b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/DemoMain.java @@ -0,0 +1,74 @@ +package demo.gateway.springcomponent; + +import demo.gateway.api.GatewayService; +import io.itara.runtime.ItaraSpring; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.logging.Logger; + +/** + * Demo entry point. + * + * The agent has already wired everything before this runs. + * All we do is obtain gateway from ItaraSpring and make a few calls. + * + * This same main works for both topologies: + * - Direct: calculator runs in this JVM, called as a method + * - HTTP: calculator runs in a separate JVM, called over the network + * + * The code here does not change. The wiring config changes. + * + * Run with: + * java -Ditara.lib.dir=itara-libs \ + * -Ditara.config=itara-demo/wiring-direct-spring.yaml \ + * -Ditara.nodes=calculatorNode,gatewayNodeSpring \ + * -javaagent:itara-agent/target/itara-agent-1.0-SNAPSHOT.jar \ + * -cp "itara-demo/calculator-component/target/calculator-component-1.0-SNAPSHOT.jar;itara-demo/gateway-springcomponent/target/lib/*;itara-demo/gateway-springcomponent/target/gateway-springcomponent-1.0-SNAPSHOT.jar" \ + * demo.gateway.springcomponent.DemoMain + */ +@SpringBootApplication +public class DemoMain { + + private static final Logger log = Logger.getLogger(DemoMain.class.getName()); + + + public static void main(String[] args) throws Exception { + SpringApplication.run(DemoMain.class, args); + } + + @Bean + public CommandLineRunner runner(final GatewayService gateway) { + return args -> { + log.info("=".repeat(50)); + log.info("Itara Demo starting..."); + log.info("=".repeat(50)); + + // Small pause to let the agent's HTTP server start if needed + Thread.sleep(500); + + log.info(""); + log.info("--- Making calls ---"); + log.info(""); + + log.info(gateway.calculate(3, 4)); + log.info(""); + log.info(gateway.calculate(10, 25)); + log.info(""); + log.info(gateway.calculate(100, 200)); + + log.info(""); + log.info("=".repeat(50)); + log.info("Done."); + log.info("=".repeat(50)); + }; + } + + @Bean + public GatewayService gateway() { + log.info("Making bean from: " + GatewayService.class.getName()); + return ItaraSpring.get(GatewayService.class); + } +} diff --git a/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayActivator.java b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayActivator.java new file mode 100644 index 0000000..e10cdb1 --- /dev/null +++ b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayActivator.java @@ -0,0 +1,28 @@ +package demo.gateway.springcomponent; + +import demo.calculator.api.CalculatorService; +import demo.gateway.api.GatewayService; +import io.itara.api.ItaraActivator; +import io.itara.runtime.ItaraRegistry; + +import java.util.logging.Logger; + +/** + * Activator for the gateway component. + * + * Pulls CalculatorService from the registry — the registry returns either + * a direct instance (collocated topology) or an HTTP proxy (remote topology). + * This code does not change between the two topologies. That is the point. + */ +public class GatewayActivator implements ItaraActivator { + + private static final Logger log = Logger.getLogger(GatewayActivator.class.getName()); + + @Override + public GatewayService activate(ItaraRegistry registry) { + log.info("[GatewayActivator] Pulling calculator from registry..."); + CalculatorService calculator = registry.get("calculator", CalculatorService.class); + log.info("[GatewayActivator] Creating GatewayServiceImpl"); + return new GatewayServiceImpl(calculator); + } +} diff --git a/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayServiceImpl.java b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayServiceImpl.java new file mode 100644 index 0000000..f6de3a8 --- /dev/null +++ b/itara-demo/gateway-springcomponent/src/main/java/demo/gateway/springcomponent/GatewayServiceImpl.java @@ -0,0 +1,33 @@ +package demo.gateway.springcomponent; + +import demo.calculator.api.CalculatorService; +import demo.gateway.api.GatewayService; + +import java.util.logging.Logger; + +/** + * The gateway implementation. + * Accepts a request, delegates to CalculatorService, prints and returns the result. + * + * Has no knowledge of whether CalculatorService is a direct call or HTTP. + * The registry provides whichever the topology config dictates. + */ +public class GatewayServiceImpl implements GatewayService { + + private static final Logger log = Logger.getLogger(GatewayServiceImpl.class.getName()); + + private final CalculatorService calculator; + + public GatewayServiceImpl(CalculatorService calculator) { + this.calculator = calculator; + } + + @Override + public String calculate(int a, int b) { + log.info("[Gateway] Received request: add(" + a + ", " + b + ")"); + int result = calculator.add(a, b); + String message = "The result of " + a + " + " + b + " = " + result; + log.info("[Gateway] Returning: " + message); + return message; + } +} diff --git a/itara-demo/gateway-springcomponent/src/main/resources/META-INF/itara/activator b/itara-demo/gateway-springcomponent/src/main/resources/META-INF/itara/activator new file mode 100644 index 0000000..bad3d09 --- /dev/null +++ b/itara-demo/gateway-springcomponent/src/main/resources/META-INF/itara/activator @@ -0,0 +1,2 @@ +component-id=gateway +activator=demo.gateway.springcomponent.GatewayActivator \ No newline at end of file diff --git a/itara-demo/pom.xml b/itara-demo/pom.xml index c65bd8b..d1ee9f7 100644 --- a/itara-demo/pom.xml +++ b/itara-demo/pom.xml @@ -21,6 +21,7 @@ calculator-component gateway-api gateway-component + gateway-springcomponent diff --git a/itara-demo/wiring-direct-spring.yaml b/itara-demo/wiring-direct-spring.yaml new file mode 100644 index 0000000..faee35c --- /dev/null +++ b/itara-demo/wiring-direct-spring.yaml @@ -0,0 +1,15 @@ +# wiring-direct.yaml +# Both components run in the same JVM. +# Gateway calls Calculator as a direct method call — no network involved. + +nodes: + - id: "gatewayNodeSpring" + component: "gateway" + + - id: "calculatorNode" + component: "calculator" + +connections: + - from: "gatewayNodeSpring" + to: "calculatorNode" + type: "direct" diff --git a/itara-demo/wiring-http-spring.yaml b/itara-demo/wiring-http-spring.yaml new file mode 100644 index 0000000..ccfc545 --- /dev/null +++ b/itara-demo/wiring-http-spring.yaml @@ -0,0 +1,14 @@ +nodes: + - id: "gatewayNodeSpring" + component: "gateway" + + - id: "calculatorNode" + component: "calculator" + +connections: + - from: "gatewayNodeSpring" + to: "calculatorNode" + type: http + host: "localhost" + port: 8081 + serializer: "json" \ No newline at end of file