Skip to content
Open
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
52 changes: 52 additions & 0 deletions itara-common/src/main/java/io/itara/runtime/ItaraRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,20 @@ public void preRegister(String id, Object proxy) {
public void registerActivator(String id,
Class<? extends ItaraActivator<?>> activatorClass,
Class<?> contractClass) {
if (activatorClass == null) {
Comment thread
kissgabor94 marked this conversation as resolved.
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());
}

Expand Down Expand Up @@ -156,4 +168,44 @@ public <T> T get(String id, Class<T> 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> T get(Class<T> type) {

String matchedId = null;

// Search all known component contracts
for (Map.Entry<String, Class<?>> 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);
}
}
70 changes: 70 additions & 0 deletions itara-common/src/main/java/io/itara/runtime/ItaraSpring.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.itara.runtime;

/**
* Minimal Spring integration helper for Itara.
*
* <p>Purpose:
* Provides explicit access to Itara-managed component proxies so application
* developers can wire them into their own Spring configuration.
*
* <p>This is intentionally simple and explicit:
* <ul>
* <li>No Spring auto-configuration</li>
* <li>No automatic bean injection magic</li>
* <li>No dependency on Spring inside itara-common or itara-agent</li>
* </ul>
*
* <p>Typical usage:
*
* <pre>{@code
* @Configuration
* public class AppConfig {
*
* @Bean
* public UserClient userClient() {
* return ItaraSpring.get(UserClient.class);
* }
* }
* }</pre>
*
* <p>The returned instance is the Itara-wired proxy that was pre-registered
* by the agent during startup.
*/
public final class ItaraSpring {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the GitHub Issue required this class, but I'm wondering if we should just skip it. It suggests that this is spring specific, which isn't true, and if we do introduce spring specific support later on, the name will be confusing. Could you remove it and simply call the registry directly instead?


private ItaraSpring() {
}

/**
* Retrieve a component by explicit component id and contract type.
*
* <p>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> T get(String id, Class<T> type) {
ItaraRegistry registry = ItaraRegistry.instance();
return registry.get(id, type);
}

/**
* Retrieve a component by contract/interface type.
*
* <p>Designed for Spring {@code @Bean} methods where the developer
* explicitly chooses which Itara component to expose as a Spring bean.
*
* <p>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> T get(Class<T> type) {
ItaraRegistry registry = ItaraRegistry.instance();
return registry.get(type);
}
}
62 changes: 62 additions & 0 deletions itara-demo/gateway-springcomponent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.itara.demo</groupId>
<artifactId>itara-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>gateway-springcomponent</artifactId>

<dependencies>

<dependency>
<groupId>io.itara.demo</groupId>
<artifactId>gateway-api</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>io.itara.demo</groupId>
<artifactId>calculator-api</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>4.0.6</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-runtime-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to delete this in the other demo code, but it shouldn't be needed anymore, I think you can remove this sleep. When it's manually started, the user handles it, when docker-compose runs it, there is a dependency. It shouldn't be necessary.


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);
}
}
Original file line number Diff line number Diff line change
@@ -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<GatewayService> {

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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
component-id=gateway
activator=demo.gateway.springcomponent.GatewayActivator
1 change: 1 addition & 0 deletions itara-demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<module>calculator-component</module>
<module>gateway-api</module>
<module>gateway-component</module>
<module>gateway-springcomponent</module>
</modules>

<dependencyManagement>
Expand Down
15 changes: 15 additions & 0 deletions itara-demo/wiring-direct-spring.yaml
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 14 additions & 0 deletions itara-demo/wiring-http-spring.yaml
Original file line number Diff line number Diff line change
@@ -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"
Loading