Java Bindings¶
Introduction¶
OpenDDS provides JNI bindings. Java applications can make use of the complete OpenDDS middleware just like C++ applications.
See the java/INSTALL
file for information on getting started, including the prerequisites and dependencies.
Java versions 9 and up use the Java Platform Module System. To use OpenDDS with one of these Java versions, set the MPC feature java_pre_jpms to 0. OpenDDS’s configure script will attempt to detect the Java version and set this automatically.
See the java/FAQ
file for information on common issues encountered while developing applications with the Java bindings.
IDL and Code Generation¶
The OpenDDS Java binding is more than just a library that lives in one or two .jar files. The DDS specification defines the interaction between a DDS application and the DDS middleware. In particular, DDS applications send and receive messages that are strongly-typed and those types are defined by the application developer in IDL.
In order for the application to interact with the middleware in terms of these user-defined types, code must be generated at compile-time based on this IDL.
C++, Java, and even some additional IDL code is generated.
In most cases, application developers do not need to be concerned with the details of all the generated files.
Scripts included with OpenDDS automate this process so that the end result is a native library (.so
or .dll
) and a Java library (.jar
or just a classes
directory) that together contain all of the generated code.
Below is a description of the generated files and which tools generate them.
In this example, Foo.idl
contains a single struct Bar
contained in module Baz
(IDL modules are similar to C++ namespaces and Java packages).
To the right of each file name is the name of the tool that generates it, followed by some notes on its purpose.
File |
Generation Tool |
---|---|
|
Developer-written description of the DDS sample type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Foo.idl:
module Baz {
@topic
struct Bar {
long x;
};
};
Setting up an OpenDDS Java Project¶
These instructions assume you have completed the installation steps in the java/INSTALL
document, including having the various environment variables defined.
Start with an empty directory that will be used for your IDL and the code generated from it.
java/tests/messenger/messenger_idl/
is set up this way.Create an IDL file describing the data structure you will be using with OpenDDS. See
Messenger.idl
for an example. This file will contain at least struct/union annotated with@topic
. For the sake of these instructions, we will call the fileFoo.idl
.The C++ generated classes will be packaged in a shared library to be loaded at run-time by the JVM. This requires the packaged classes to be exported for external visibility. ACE provides a utility script for generating the correct export macros. The script usage is shown here:
$ACE_ROOT/bin/generate_export_file.pl Foo > Foo_Export.h
%ACE_ROOT%\bin\generate_export_file.pl Foo > Foo_Export.h
Create an MPC file, Foo.mpc, from this template:
project: dcps_java { idlflags += -Wb,stub_export_include=Foo_Export.h \ -Wb,stub_export_macro=Foo_Export dcps_ts_flags += -Wb,export_macro=Foo_Export idl2jniflags += -Wb,stub_export_include=Foo_Export.h \ -Wb,stub_export_macro=Foo_Export dynamicflags += FOO_BUILD_DLL specific { jarname = DDS_Foo_types } TypeSupport_Files { Foo.idl } }
You can leave out the
specific {...}
block if you do not need to create a jar file. In this case you can directly use the Java .class files which will be generated under the classes subdirectory of the current directory.Run MPC to generate platform-specific build files.
$ACE_ROOT/bin/mwc.pl -type gnuace
%ACE_ROOT%\bin\mwc.pl -type [CompilerType]
CompilerType can be any supported MPC type (such as “vs2019”)
Make sure this is running ActiveState Perl or Strawberry Perl.
Compile the generated C++ and Java code
make
Build the generated
.sln
(Solution) file using your preferred method. This can be either the Visual Studio IDE or one of the command-line tools. If you use the IDE, start it from a command prompt usingdevenv
so that it inherits the environment variables. Command-line tools for building include msbuild
and invoking the IDE (devenv
) with the appropriate arguments.When this completes successfully you have a native library and a Java
.jar
file. The native library names areFoo.dll
(Release) orFood.dll
(Debug) on Windows andlibFoo.so
on Linux.You can change the locations of these libraries (including the
.jar
file) by adding a line such as the following to theFoo.mpc
file:libout = $(PROJECT_ROOT)/lib
where
PROJECT_ROOT
can be any environment variable defined at build-time.You now have all of the Java and C++ code needed to compile and run a Java OpenDDS application. The generated
.jar
file needs to be added to yourclasspath
, along with the.jar
files that come from OpenDDS (in thelib
directory). The generated C++ library needs to be available for loading at run-time:Add the directory containing
libFoo.so
to theLD_LIBRARY_PATH
.Add the directory containing
Foo.dll
(orFood.dll
) to thePATH
. If you are using the debug version (Food.dll
) you will need to inform the OpenDDS middleware that it should not look forFoo.dll
. To do this, add-Dopendds.native.debug=1
to the Java VM arguments.See the publisher and subscriber directories in
java/tests/messenger/
for examples of publishing and subscribing applications using the OpenDDS Java bindings.If you make subsequent changes to
Foo.idl
, start by re-running MPC (step #5 above). This is needed because certain changes toFoo.idl
will affect which files are generated and need to be compiled.
A Simple Message Publisher¶
This section presents a simple OpenDDS Java publishing process.
The complete code for this can be found at java/tests/messenger/publisher/TestPublisher.java
.
Uninteresting segments such as imports and error handling have been omitted here.
The code has been broken down and explained in logical subsections.
Initializing the Participant¶
DDS applications are boot-strapped by obtaining an initial reference to the Participant Factory.
A call to the static method TheParticipantFactory.WithArgs()
returns a Factory reference.
This also transparently initializes the C++ Participant Factory.
We can then create Participants for specific domains.
public static void main(String[] args) {
DomainParticipantFactory dpf =
TheParticipantFactory.WithArgs(new StringSeqHolder(args));
if (dpf == null) {
System.err.println ("Domain Participant Factory not found");
return;
}
final int DOMAIN_ID = 42;
DomainParticipant dp = dpf.create_participant(DOMAIN_ID,
PARTICIPANT_QOS_DEFAULT.get(), null, DEFAULT_STATUS_MASK.value);
if (dp == null) {
System.err.println ("Domain Participant creation failed");
return;
}
Object creation failure is indicated by a null return.
The third argument to create_participant()
takes a Participant events listener.
If one is not available, a null can be passed instead as done in our example.
Registering the Data Type and Creating a Topic¶
Next we register our data type with the DomainParticipant
using the register_type()
operation.
We can specify a type name or pass an empty string.
Passing an empty string indicates that the middleware should simply use the identifier generated by the IDL compiler for the type.
MessageTypeSupportImpl servant = new MessageTypeSupportImpl();
if (servant.register_type(dp, "") != RETCODE_OK.value) {
System.err.println ("register_type failed");
return;
}
Next we create a topic using the type support servant’s registered name.
Topic top = dp.create_topic("Movie Discussion List",
servant.get_type_name(),
TOPIC_QOS_DEFAULT.get(), null,
DEFAULT_STATUS_MASK.value);
Now we have a topic named “Movie Discussion List” with the registered data type and default QoS policies.
Creating a Publisher¶
Next, we create a publisher:
Publisher pub = dp.create_publisher(
PUBLISHER_QOS_DEFAULT.get(),
null,
DEFAULT_STATUS_MASK.value);
Creating a DataWriter and Registering an Instance¶
With the publisher, we can now create a DataWriter:
DataWriter dw = pub.create_datawriter(
top, DATAWRITER_QOS_DEFAULT.get(), null, DEFAULT_STATUS_MASK.value);
The DataWriter
is for a specific topic.
For our example, we use the default DataWriter
QoS policies and a null DataWriterListener
.
Next, we narrow the generic DataWriter
to the type-specific DataWriter
and register the instance we wish to publish.
In our data definition IDL we had specified the subject_id field as the key, so it needs to be populated with the instance id (99 in our example):
MessageDataWriter mdw = MessageDataWriterHelper.narrow(dw);
Message msg = new Message();
msg.subject_id = 99;
int handle = mdw.register(msg);
Our example waits for any peers to be initialized and connected. It then publishes a few messages which are distributed to any subscribers of this topic in the same domain.
msg.from = "OpenDDS-Java";
msg.subject = "Review";
msg.text = "Worst. Movie. Ever.";
msg.count = 0;
int ret = mdw.write(msg, handle);
Setting up the Subscriber¶
Much of the initialization code for a subscriber is identical to the publisher. The subscriber needs to create a participant in the same domain, register an identical data type, and create the same named topic.
public static void main(String[] args) {
DomainParticipantFactory dpf =
TheParticipantFactory.WithArgs(new StringSeqHolder(args));
if (dpf == null) {
System.err.println ("Domain Participant Factory not found");
return;
}
DomainParticipant dp = dpf.create_participant(42,
PARTICIPANT_QOS_DEFAULT.get(), null, DEFAULT_STATUS_MASK.value);
if (dp == null) {
System.err.println("Domain Participant creation failed");
return;
}
MessageTypeSupportImpl servant = new MessageTypeSupportImpl();
if (servant.register_type(dp, "") != RETCODE_OK.value) {
System.err.println ("register_type failed");
return;
}
Topic top = dp.create_topic("Movie Discussion List",
servant.get_type_name(),
TOPIC_QOS_DEFAULT.get(), null,
DEFAULT_STATUS_MASK.value);
Creating a Subscriber¶
As with the publisher, we create a subscriber:
Subscriber sub = dp.create_subscriber(
SUBSCRIBER_QOS_DEFAULT.get(), null, DEFAULT_STATUS_MASK.value);
Creating a DataReader and Listener¶
Providing a DataReaderListener
to the middleware is the simplest way to be notified of the receipt of data and to access the data.
We therefore create an instance of a DataReaderListenerImpl
and pass it as a DataReader
creation parameter:
DataReaderListenerImpl listener = new DataReaderListenerImpl();
DataReader dr = sub.create_datareader(
top, DATAREADER_QOS_DEFAULT.get(), listener,
DEFAULT_STATUS_MASK.value);
Any incoming messages will be received by the Listener in the middleware’s thread. The application thread is free to perform other tasks at this time.
The DataReader Listener Implementation¶
The application defined DataReaderListenerImpl
needs to implement the specification’s DDS.DataReaderListener
interface.
OpenDDS provides an abstract class DDS._DataReaderListenerLocalBase
.
The application’s listener class extends this abstract class and implements the abstract methods to add application-specific functionality.
Our example DataReaderListener
stubs out most of the Listener methods.
The only method implemented is the message available callback from the middleware:
public class DataReaderListenerImpl extends DDS._DataReaderListenerLocalBase {
private int num_reads_;
public synchronized void on_data_available(DDS.DataReader reader) {
++num_reads_;
MessageDataReader mdr = MessageDataReaderHelper.narrow(reader);
if (mdr == null) {
System.err.println ("read: narrow failed.");
return;
}
The Listener callback is passed a reference to a generic DataReader
.
The application narrows it to a type-specific DataReader
:
MessageHolder mh = new MessageHolder(new Message());
SampleInfoHolder sih = new SampleInfoHolder(new SampleInfo(0, 0, 0,
new DDS.Time_t(), 0, 0, 0, 0, 0, 0, 0, false));
int status = mdr.take_next_sample(mh, sih);
It then creates holder objects for the actual message and associated SampleInfo
and takes the next sample from the DataReader
.
Once taken, that sample is removed from the DataReader
’s available sample pool.
if (status == RETCODE_OK.value) {
System.out.println ("SampleInfo.sample_rank = "+ sih.value.sample_rank);
System.out.println ("SampleInfo.instance_state = "+
sih.value.instance_state);
if (sih.value.valid_data) {
System.out.println("Message: subject = " + mh.value.subject);
System.out.println(" subject_id = " + mh.value.subject_id);
System.out.println(" from = " + mh.value.from);
System.out.println(" count = " + mh.value.count);
System.out.println(" text = " + mh.value.text);
System.out.println("SampleInfo.sample_rank = " +
sih.value.sample_rank);
}
else if (sih.value.instance_state ==
NOT_ALIVE_DISPOSED_INSTANCE_STATE.value) {
System.out.println ("instance is disposed");
}
else if (sih.value.instance_state ==
NOT_ALIVE_NO_WRITERS_INSTANCE_STATE.value) {
System.out.println ("instance is unregistered");
}
else {
System.out.println ("DataReaderListenerImpl::on_data_available: "+
"received unknown instance state "+
sih.value.instance_state);
}
} else if (status == RETCODE_NO_DATA.value) {
System.err.println ("ERROR: reader received DDS::RETCODE_NO_DATA!");
} else {
System.err.println ("ERROR: read Message: Error: "+ status);
}
}
}
The SampleInfo
contains meta-information regarding the message such as the message validity, instance state, etc.
Cleaning up OpenDDS Java Clients¶
An application should clean up its OpenDDS environment with the following steps:
dp.delete_contained_entities();
Cleans up all topics, subscribers and publishers associated with that Participant
.
dpf.delete_participant(dp);
The DomainParticipantFactory
reclaims any resources associated with the DomainParticipant
.
TheServiceParticipant.shutdown();
Shuts down the ServiceParticipant
.
This cleans up all OpenDDS associated resources.
Cleaning up these resources is necessary to prevent the DCPSInfoRepo
from forming associations between endpoints which no longer exist.
Configuring the Example¶
OpenDDS offers a file-based configuration mechanism. The syntax of the configuration file is similar to a Windows INI file. The properties are divided into named sections corresponding to common and individual transports configuration.
The Messenger example has common properties for the DCPSInfoRepo
objects location and the global transport configuration:
[common]
DCPSInfoRepo=file://repo.ior
DCPSGlobalTransportConfig=$file
and a transport instance section with a transport type property:
[transport/1]
transport_type=tcp
The [transport/1]
section contains configuration information for the transport instance named 1
.
It is defined to be of type tcp
.
The global transport configuration setting above causes this transport instance to be used by all readers and writers in the process.
See Run-time Configuration for a complete description of all OpenDDS configuration parameters.
Running the Example¶
To run the Messenger Java OpenDDS application, use the following commands:
$DDS_ROOT/bin/DCPSInfoRepo -o repo.ior
$JAVA_HOME/bin/java -ea -cp classes:$DDS_ROOT/lib/i2jrt.jar:$DDS_ROOT/lib/OpenDDS_DCPS.jar:classes TestPublisher -DCPSConfigFile pub_tcp.ini
$JAVA_HOME/bin/java -ea -cp classes:$DDS_ROOT/lib/i2jrt.jar:$DDS_ROOT/lib/OpenDDS_DCPS.jar:classes TestSubscriber -DCPSConfigFile sub_tcp.ini
The -DCPSConfigFile
command-line argument passes the location of the OpenDDS configuration file.
Java Message Service (JMS) Support¶
OpenDDS provides partial support for JMS version 1.1. Enterprise Java applications can make use of the complete OpenDDS middleware just like standard Java and C++ applications.
See the INSTALL
file in the java/jms/
directory for information on getting started with the OpenDDS JMS support, including the prerequisites and dependencies.