Java client¶
The Java client lets an external Java application publish real-time objects to a Relay, answer commands, and subscribe to Center tables in 3forge Enterprise, using classes that ship inside the install. Two classes divide the work and they differ in what they require:
| Class | Use it for | Needs a license |
|---|---|---|
AmiClient |
Publishing real-time objects to a Relay, and answering commands | No |
AmiCenterClient |
Subscribing to Center tables | Yes |
Prerequisites¶
| Item | Value |
|---|---|
| Java | JDK 8 or later |
| Classpath | lib/amicore.jar from the Enterprise install. There is no separate client-only jar |
| Relay host and port | The Relay's realtime port, default 3289, set by ami.port |
| Center port, for subscribing | Default 3270, set by ami.center.port |
| A login id | The identity the application connects as. Authentication is covered in Client library configuration |
| A client keystore | Step 1 of Connect creates one if it does not exist, and writes a sibling .pub beside it |
| Your public key registered on the server | Give that .pub line to whoever administers the server. It goes in the authorized-clients file, keys/3forge.auth, and the server must be restarted before it takes effect. See Connecting to a secured server |
| The server's public key | keys/3forge.pub from the server host, used to pin the server |
The realtime port expects a certificate from both sides
A default install sets both a keystore (ami.port.keystore.file) and a client allowlist
(ami.port.ssl.authorized.clients.file) on the realtime port, so the default posture is mutual
TLS. An application that connects without a client certificate is accepted, reports a
successful login, and is then disconnected.
Connect¶
The publishing client cannot build a mutual-TLS connection on its own, so the application builds the socket and hands it over. How the client authenticates explains why.
Substitute these values:
| Placeholder in the example | Meaning |
|---|---|
RELAY_HOST |
Host running the Relay |
RELAY_PORT |
The Relay's realtime port, default 3289 |
LOGIN_ID |
The identity the application connects as |
KEYSTORE_PATH |
Where your client keystore is, or should be created |
KEYSTORE_PASSWORD |
Password protecting that keystore |
SERVER_PUB_PATH |
The server's published public key, keys/3forge.pub on the server host |
-
Build an
SSLContext. One call does both halves: it presents the client's identity and pins the server against the public key the server publishes. If the keystore does not exist yet, it is created, along with a sibling.pubfile to hand to whoever administers the server.The file arguments are a platform file type rather than a path or a
java.io.File, which is why each one goes throughBasicResourceOpener.INSTANCE.toFile(...).Do not trust the server from your own keystore
It is tempting to initialise a
TrustManagerFactoryfrom the same keystore that holds your client key. That appears to work only if the keystore happens to contain the server's certificate, which is true of the server's own keystore and false of one you generated. Pin the server against its published.pubinstead, as above. -
Implement a listener.
AmiClientListenerdeclares six methods, and every one must be present.onMessageReceivedis the one that reports success or failure. -
Open the socket and complete the handshake.
-
Start the client on that socket.
ENABLE_AUTO_PROCESS_INCOMINGis a constant onAmiClient. It starts a thread that reads incoming messages and dispatches them to the listener. Without it, nothing arrives. Combine constants with|, for exampleAmiClient.ENABLE_AUTO_PROCESS_INCOMING | AmiClient.DISABLE_AUTO_RECONNECT.
Result¶
The listener prints one message:
That single line is the success signal, and nothing further arrives until the client sends something. A connection that stays open after it is working.
A successful login followed shortly by a disconnect is a failure, not a success: see Troubleshooting.
Publish data to a Relay¶
An object has a type, an identifier and named parameters. Build it, then flush it.
Result¶
The listener reports:
The object also reaches a Center table named after its type, where named parameters become columns
and the object's identifier lands in a column called I.
To confirm that from Java alone, subscribe to the same table from a second program, as described in Receive data from a Center. Running it in a separate process after the publisher has exited shows the row is held by the server rather than echoed locally:
This page does not cover running SQL from Java. If a SQL surface is available to you, the same
check is SELECT * FROM Trades.
The table is created on first publish
Publishing an object whose type the Center has not seen creates a table for it, so this round
trip completes on a default install with no routing configured in advance. A table created this
way adds unknown parameters as new columns. A table that already exists may instead be set to
reject them, in which case an unexpected parameter is refused rather than added. The setting is
the table's OnUndefColumn.
The data outlives the publisher. A row stays in the table after the publishing program exits, so a subscriber started later still receives it. Removing a table created this way is a server-side task, not something the client API does.
Receive data from a Center¶
Subscribing uses AmiCenterClient, which accepts an SSLContext directly and so needs no
hand-built socket.
BasicResourceOpener.INSTANCE is the default file-access strategy the client uses to resolve paths.
"C1" is a name the application chooses for the connection and then passes to subscribe.
AmiCenterClientListener declares six methods, and all six must be present:
Result¶
The callbacks arrive in this order:
onCenterConnect, once the connection is established.onSchemaChanged, before any subscription, describing every table in the Center rather than only the one subscribed to later. Use it to learn what is available.-
onCenterMessageaftersubscribe, carrying the subscribed table's rows. Each message states an operation, a sequence number, the table, and the row: -
onCenterMessageBatchDone, once per batch.
The initial delivery carries one message per existing row, so a table of 22 rows produces 22
messages in a single batch. Later changes arrive as further messages with their own operation, such
as UPD for an update to a row already delivered.
The identifier column is not in the message body
onSchemaChanged lists I among the table's columns, but the message text carries only the
named parameters, as {sym=IBM, qty=100}. A subscriber can see that the column exists and
cannot read back the identifier a publisher used. Where a subscriber needs the identifier, make
it one of the named parameters as well.
Subscribing requires a license; publishing does not
Creating AmiCenterClient starts a 3forge container inside the application, and that container
requires a license. Without one the application exits with code 23 and the message
License File not found, which also lists the accepted ways to supply a license file. Point it
at a license with -Df1.license.file=<path>. On acceptance it reports the file and its expiry
date, and subscription then proceeds normally.
AmiClient has no such requirement. An application that only sends data never meets this.
Respond to commands¶
The platform can invoke commands an application has registered, which is how an operator triggers
work inside a running client. Define the command, send the definition, and answer the callback in
onCommand.
Result¶
The listener reports:
The definition is registered centrally for as long as the client stays connected, and is removed when the client disconnects. A command is therefore scoped to the connection rather than stored, so it needs no cleaning up after a client exits, and a client must re-register its commands on every reconnect.
When the command is invoked, onCommand receives a request id, the command id, the invoking user,
the object type and id the command was run against, and a map of arguments. Reply by starting a
response message against that request id and flushing it:
Registering a command is covered here; invoking one is not
This page covers the client side: defining a command, sending the definition, and answering
onCommand. How an operator triggers a registered command from the platform is not described
here, so confirm that end of the round trip with whoever administers your deployment before
relying on it.
How the client authenticates¶
AmiClient offers four ways to start. Only the socket form can present a client certificate, which
on a default install makes it the only one that stays connected.
| Call | Behavior |
|---|---|
start(socket, loginId, options) |
The application builds the TLS socket, so it controls both the certificate presented and the trust material. |
start(host, port, loginId, options) |
Opens a plain connection. Against a TLS port it logs in, then is dropped. |
start(host, port, loginId, options, file, String password) |
The file is used to pin the server's public key. The password is not used. |
start(host, port, loginId, options, file, Password password) |
As above. Passing a keystore to the file parameter fails. |
The file parameter on the last two is not a path or a java.io.File: it is a platform file type
that must be obtained from a resource opener rather than constructed directly. Since neither form
sends a client certificate, there is no reason to call either, and this page does not show how.
The file-and-password forms do not send a client certificate
Their shape suggests the file is the application's keystore. It is not: it pins the server's public key, and the password argument is ignored. No host-and-port form presents a client certificate, which is why step 3 of Connect opens the socket directly.
AmiCenterClient is built differently and takes an SSLContext, so the limitation above applies
only to publishing.
Troubleshooting¶
| Symptom | Cause | What to do |
|---|---|---|
onLoggedIn fires, then onDisconnect. Most often with General error: missing @ for string ' ', sometimes with a socket abort |
A plain connection to a TLS port. The parser error is the client reading raw TLS bytes as protocol text, so grep for missing @ as well as for a socket error |
Build the socket as in Connect |
The handshake itself fails, with a socket write error. onConnect never fires |
The certificate's public key is not in the server's authorized-clients file, or it was added and the server has not been restarted since | Register the key and restart the server. See Connecting to a secured server |
| That sequence repeating | Automatic reconnection is on | Pass AmiClient.DISABLE_AUTO_RECONNECT while debugging so the failure holds still |
java.lang.RuntimeException: Error building sslcontext from public key |
A keystore was passed where a server public key is expected | Use the socket form instead |
Exit code 23, License File not found |
AmiCenterClient needs a license |
Supply a license by one of the routes the message lists, or publish only, which needs none |
| Nothing arrives on the listener | ENABLE_AUTO_PROCESS_INCOMING was not passed, so no thread is reading |
Add it to the options argument |
What this client does not do¶
- It does not run SQL. There is no JDBC or query entry point in these classes. Use the Python or C++ client where a program needs to query.
- Calls are asynchronous. Connecting and subscribing return immediately and the results arrive
on your listener, so there is no equivalent of the Python client's
sync=True. Wait on your own signal inside the callback rather than expecting the call to block. - A subscriber cannot read back a publisher's identifier, as described above.
Related¶
- Client library configuration for ports, authentication and mutual TLS.
- Python client and C++ client for the same tasks in those languages.
- Troubleshooting the client libraries for failures shared across languages.