We are going to explain our first example with Xorcery called Greeter, which you will find in the following Github repository.
With Xorcery we can implement the API of service as a REST API (using JSON-API as a content type) for request/response needs or reactive stream web sockets (server publishers or subscribers) for streaming needs (like event sourcing or projections or log collection, etc.).
Important fact: The entire implementation of Xorcery uses the Jakarta EE APIs and important libraries in the Java world. Xorcery for example uses HK2 a lightweight and dynamic injection framework.
Why JSON:API
The election in JSON-API and JSONSchema is that then makes it possible to create a REST client, and not an HTTP client, which applications can use.
When done properly it leads to the situation that all application code interactions with REST clients are one of these two: follow links and submit forms. That's it. There's no constructing URLs, or figuring out what method to use, or any of that. This is all happening behind the scenes as it is defined by JSON: API and JSONSchema on the server. Application code just needs to bother with what link rel's it cares about, what information it needs to extract from resources in JSON: API, and how to submit forms (which know the URI and method to use).
We currently support URI templates in the JSON:API schemas and HTML sandbox. This way it is easier to test the API in a browser, as well as simplify filling out the URLs as a form.
Why Jetty Websockets + Disruptor
Xorcery makes a custom variation of the ReactiveStreams API with WebSockets to send events as headers plus bytes, as well as an integration with the Disruptor API. In the xorcery source code you will find services where the architecture is applied such as publisher+subscriber, metrics events publisher+subscriber, and domain event publisher+subscriber (subscriber maps into hashmap “database”).
Greeter
<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.source>17</maven.compiler.source><maven.compiler.release>17</maven.compiler.release><xorcery.version>0.79.2</xorcery.version><hk2.version>3.0.5</hk2.version><jersey.version>3.1.3</jersey.version><slf4j.version>2.0.7</slf4j.version><log4j.version>2.22.0</log4j.version><junit.version>5.10.0</junit.version><junit.platform.version>1.9.0</junit.platform.version></properties>
<modules>
<module>xorcery-examples-greeter</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-bom</artifactId>
<version>${xorcery.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><annotationProcessorPaths><path><groupId>org.glassfish.hk2</groupId><artifactId>hk2-metadata-generator</artifactId><version>${hk2.version}</version></path></annotationProcessorPaths></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.6.1</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.2.2</version><dependencies><dependency><groupId>me.fabriciorby</groupId><artifactId>maven-surefire-junit5-tree-reporter</artifactId><version>1.2.1</version></dependency></dependencies><configuration><reportFormat>plain</reportFormat><consoleOutputReporter><disable>true</disable></consoleOutputReporter><statelessTestsetInfoReporterimplementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter"/></configuration></plugin></plugins></pluginManagement></build>
<repositories><repository><id>cantara-releases</id><name>Cantara Release Repository</name><url>https://mvnrepo.cantara.no/content/repositories/releases/</url></repository><repository><id>cantara-snapshots</id><name>Cantara Snapshot Repository</name><url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url></repository></repositories><distributionManagement><repository><id>cantara</id><name>Cantara Release Repository</name><url>https://mvnrepo.cantara.no/content/repositories/releases/</url></repository><snapshotRepository><id>cantara</id><name>Cantara Snapshot Repository</name><url>https://mvnrepo.cantara.no/content/repositories/snapshots/</url></snapshotRepository></distributionManagement>
Greeter Implementation
<parent><groupId>com.exoreaction.xorcery.examples</groupId><artifactId>xorcery-examples</artifactId><version>1.0-SNAPSHOT</version></parent>
<!-- Core dependencies --><dependency><groupId>com.exoreaction.xorcery</groupId><artifactId>xorcery-core</artifactId></dependency><dependency><groupId>com.exoreaction.xorcery</groupId><artifactId>xorcery-runner</artifactId></dependency><dependency><groupId>com.exoreaction.xorcery</groupId><artifactId>xorcery-metadata</artifactId></dependency><dependency><groupId>com.exoreaction.xorcery</groupId><artifactId>xorcery-configuration-api</artifactId></dependency><dependency><groupId>com.exoreaction.xorcery</groupId><artifactId>xorcery-json</artifactId></dependency>
The dependencies for REST API:
<!-- REST API -->
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-jsonapi-jaxrs</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-jersey-server</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-handlebars</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-status-server</artifactId>
</dependency>
Xorcery works with Jetty and Jersey as a Jakarta JAX-RS implementation.
The dependencies for service integration and implementation of reactive streams:
<!-- Integration -->
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-jersey-client</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-reactivestreams-client</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-reactivestreams-server</artifactId>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-neo4j</artifactId>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-domainevents-neo4jprojection</artifactId>
</dependency>
Dependencies for Logging:
<!-- Logging -->
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-log4j</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
Integration with Junit:
<!-- Test -->
<dependency>
<groupId>com.exoreaction.xorcery</groupId>
<artifactId>xorcery-junit</artifactId>
<scope>test</scope>
</dependency>
Finally, we add the profile to use jpackage introduced in Java 14 and create the image of an application and install it.
<profiles>
<profile>
<id>jpackage</id>
<activation>
<os>
<name>linux</name>
<arch>amd64</arch>
</os>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>compile</includeScope>
<outputDirectory>${project.build.directory}/app/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-modularized-jar</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy file="${project.build.directory}/${project.build.finalName}.jar"
tofile="${project.build.directory}/app/lib/${project.build.finalName}.jar"
overwrite="true"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>copy-app-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/app</outputDirectory>
<resources>
<resource>
<directory>src/jpackage/app</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.akman</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>0.1.5</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jpackage</goal>
</goals>
<configuration>
<resourcedir>${project.basedir}/src/jpackage/resources</resourcedir>
<input>${project.build.directory}/app</input>
<mainjar>lib/${project.artifactId}-${project.version}.jar</mainjar>
<mainclass>com.exoreaction.xorcery.examples.greeter.Main</mainclass>
<name>greeter</name>
<appversion>${project.version}</appversion>
<copyright>Copyright eXOReaction AS</copyright>
<description>Description</description>
<vendor>eXOReaction AS</vendor>
<installdir>/opt/exoreaction</installdir>
<javaoptions>-Dfile.encoding=UTF-8 -Xms256m -Xmx512m</javaoptions>
<dest>${project.build.directory}/jpackage</dest>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
At GreeterResourceTest we use the Xorcery extension for the configuration that tests that use Xorcery need via @RegisterExtension:
class GreeterResourceTest {
@RegisterExtension
static XorceryExtension xorceryExtension = XorceryExtension.xorcery()
.configuration(ConfigurationBuilder::addTestDefaults)
.addYaml(String.format("""
jetty.server.http.enabled: false
jetty.server.ssl.port: %d
""", Sockets.nextFreePort()))
.build();
We tested the GET first of a Rest API by instantiating an Xorcery client.
@Test
void updateGreeting() throws Exception {
Configuration configuration = xorceryExtension.getServiceLocator().getService(Configuration.class);
URI baseUri = InstanceConfiguration.get(configuration).getURI();
Client client = xorceryExtension.getServiceLocator().getService(ClientBuilder.class).build();
{
String content = client.target(baseUri)
.path("/api/greeter")
.request()
.get(String.class);
System.out.println(content);
}
This is a configuration where we indicate that we want to generate an SSL certificate for the local hostname, the REST API resources, and the log4j2 configuration are declared. Its configuration is located in resources/xorcery.yaml.
instance.name: "greeter"
instance.home: "{{ SYSTEM.jpackage_app-path ? jpackage.app | SYSTEM.user_dir}}"
jpackage.app: "{{ SYSTEM.jpackage_app-path }}/../../lib/app"
# So that we can generate a SSL certificate for the local hostname. Replace with whatever domain name you actually use
instance.domain: local
# Add local convenience names for your own computer into the SSL cert
certificates:
dnsNames:
- localhost
- "{{ instance.host }}"
ipAddresses:
- 127.0.0.1
- "{{ instance.ip }}"
# REST API resources
jersey.server.register:
- com.exoreaction.xorcery.examples.greeter.resources.api.GreeterResource
keystores:
enabled: true
keystore:
path: "{{ home }}/keystore.p12"
password: "password"
template: "META-INF/intermediatecakeystore.p12"
log4jpublisher.enabled: false
log4jsubscriber.enabled: false
domainevents.subscriber.configuration.projection: "greeter"
jsondomaineventprojection.enabled: false
log4j2:
Configuration: ...
The service GreeterApplication permits queries and domain events that are generated and projected into the database (in our example neo4j).
@Service
@Named(GreeterApplication.SERVICE_TYPE)
public class GreeterApplication {
public static final String SERVICE_TYPE = "greeter";
private final DomainEventPublisher domainEventPublisher;
private final DomainEventMetadata domainEventMetadata;
private final GraphDatabase graphDatabase;
@Inject
public GreeterApplication(DomainEventPublisher domainEventPublisher,
GraphDatabase graphDatabase) {
this.domainEventPublisher = domainEventPublisher;
this.graphDatabase = graphDatabase;
this.domainEventMetadata = new DomainEventMetadata(new Metadata.Builder()
.add("domain", "greeter")
.build());
}
// Reads
public CompletionStage<String> get(String name) {
return graphDatabase.execute("MATCH (greeter:Greeter {id:$id}) RETURN greeter.greeting as greeting",
Map.ofEntries(entry("id", "greeter")), 30)
.thenApply(r ->
{
try (GraphResult result = r) {
return result.getResult().stream().findFirst().map(m -> m.get("greeting").toString()).orElse("Hello World");
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
In GreeterResource see a full cycle of creating domain events, publishing, then subscribing with Neo4jDomainEventsService and projecting into the database, which then can be read through the GraphDatabase wrapper.
Reads
@Path("api/greeter")
public class GreeterResource
extends JsonApiResource {
private GreeterApplication application;
@Inject
public GreeterResource(GreeterApplication application) {
this.application = application;
}
@GET
public CompletionStage<Context> get() {
return application.get("greeting").handle((g, t)->
{
if (t != null)
{
LogManager.getLogger(getClass()).error("Could not get greeting", t);
return "";
} else
{
return g;
}
}).thenApply(Context::newContext);
}
If we run the test we will see that the call to /api/greeter returns the content of the API HTML template:
Tests result:
Writes
We now test publishing a domain event and projection to neo4j.
// Writes
public CompletionStage<Metadata> handle(Record command) {
Metadata.Builder metadata = new DomainEventMetadata.Builder(new Metadata.Builder().add(domainEventMetadata.context()))
.timestamp(System.currentTimeMillis()).builder();
try {
List<DomainEvent> events = (List<DomainEvent>) getClass().getDeclaredMethod("handle", command.getClass()).invoke(this, command);
Metadata md = metadata.add(Model.Metadata.commandName, command.getClass().getName()).build();
CommandEvents commandEvents = new CommandEvents(md, events);
return domainEventPublisher.publish(commandEvents);
} catch (Throwable e) {
return CompletableFuture.failedStage(e);
}
}
private List<DomainEvent> handle(UpdateGreeting updateGreeting) {
return Collections.singletonList(JsonDomainEvent.event("UpdatedGreeting").updated("Greeter", "greeter")
.attribute("greeting", updateGreeting.newGreeting())
.build());
}
We add the command:
public record UpdateGreeting(String newGreeting) {
}
Finally, if we want to see the HTML sandbox generated thanks to JSON:API and JSONSchema, we have to add the Main.java to use the runner.
public class Main {
public static void main(String[] args) {
com.exoreaction.xorcery.runner.Main.main(args);
}
}
2023-11-13 12:24:18,506 [RunLevelControllerThread-1699896252353] INFO c.e.x.j.s.JettyLifecycleService: Started Jetty server
2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896252353] DEBUG c.e.x.c.RunLevelLogger: Reached run level 18
2023-11-13 12:24:18,507 [RunLevelControllerThread-1699896253228] DEBUG c.e.x.c.RunLevelLogger: Reached run level 19
2023-11-13 12:24:18,508 [RunLevelControllerThread-1699896253228] DEBUG c.e.x.c.RunLevelLogger: Reached run level 20
2023-11-13 12:24:18,509 [main] DEBUG macbook-pro-de-jose.local.genericserver c.e.x.c.Xorcery: Services:
Note: Observe how xorcery executes the level settings when launching the project:
The HTML sandbox:
If we want to publish the domain event and project it to neo4j we can try this URL http://localhost/api/greeter
In subsequent posts, we will continue examining more examples of using Xorcery.
Enjoy!
Jose
0 comentarios:
Publicar un comentario