The JMX scandir example is an application that scans parts of a filesystem - e.g. a set of directories used by a number of lab machines when running tests - in order to clean up and optimize disk space by removing obsolete files - e.g. files that are leaked by the test suites running on those machines, like coredump files, or temporary files that might remain after a test crash. It could also serve as a basis for an application that would monitor disk usage and suggest removal of old big long-unaccessed files.
The JMX scandir example does not however implement the full fledged logic that such an application might have. It implements a subset of this logic which is sufficient to demonstrate common patterns and solutions used when implementing a monitoring and management interface for an application with JMX Technology.
This example is an advanced JMX example, which presents advanced JMX concepts. It is assumed that the reader is already familiar with the JMX API. Newcomers to JMX Technology are invited to have a look at the JMX API Overview, Tutorial and Examples before going any further.
Note: This example was developed using NetBeans 5.0 IDE. The instructions given in this document to build, run, and test the example assume that you have at your disposal:
- either NetBeans 5.0 IDE,
- or Apache Ant 1.6.5 and JUnit 3.8.1 or 3.8.2
(JUnit is only needed to run the example's unit tests).In order to build the example, you may need to copy the jmx-scandir directory to somewhere where you have write permissions.
In that case, you will need to update the nbjdk.home variable in the copied build.properties file located at the root of the copied project directory. Please make sure that this variable points to the JDK 6 home directory.If you wish to run the testsuite from within the NetBeans IDE you will also have to set the libs.junit.classpath variable in build.properties. The libs.junit.classpath variable should point to your junit.jar, version 3.8.1 or 3.8.2.
Table Of Contents:
Before reading further, you will need to generate the Java Documentation for the example's sources.
In the example root directory (where the build.xml
file is located) run the following command:
ant javadoc
Alternatively you can open the jmx-scandir project with the
NetBeans IDE and generate the Javadoc from its Build
menu.
If building the documentation fails, please make sure to read the note at the beginning of this document.
The JMX scandir example is built around the following MBeans:
DirectoryScannerMXBean
is an MBean that scans a
file system starting at a given root directory, and then looks
for files that match the given criteria. When such a file is
found, the DirectoryScannerMXBean
takes the
action for which it was configured: emit a notification,
and/or log a record
for this file,
and/or delete that file. The code that would actually
delete the file is commented out - so that nothing valuable is
lost if the example is run by mistake on the wrong set of
directories.DirectoryScannerMXBeans
are
created by the ScanManagerMXBean - see next item on the list, from its
configuration.
ScanManagerMXBean
lets you start, stop, and
schedule directory scans. The
ScanManagerMXBean
is a singleton
MBean: there can be at most one instance of such
an MBean registered in a given MBeanServer.
ScanManagerMXBean
to apply the
configuration again.ScanDirConfigMXBean
is created by the
ScanManagerMXBean
, when the
ScanManagerMXBean
is registered.
It is also possible to create an alternate
ScanDirConfigMXBean
, and to switch the
ScanDirConfigMXBean
to use one or the other
configuration.
ScanDirConfigMXBean
interface.
ResultLogManagerMXBean
is
responsible for logging these result records.
The ResultLogManagerMXBean
can be configured to log
such records to a flat file, or into a log held in memory, or
both. Both logs (file and memory) can be configured with a
maximum capacity.
ResultLogManagerMXBean
will let you interactively clear these result logs, change their
capacity, and decide where (memory or file) to log.
The memory log is useful in that its content can be interactively
returned by the ResultLogManagerMXBean
, while
the file log doesn't have this facility.ResultLogManagerMXBean
is a singleton
MBean created by the ScanManagerMXBean
which reads and writes its configuration from the
ScanDirConfigMXBean
.
An application main()
method is
provided in the ScanDirAgent class. The main()
simply registers
a ScanManagerMXBean
in the platform MBeanServer, and
then waits for someone to call close()
on the
ScanManagerMXBean
.
When the ScanManagerMXBean
is registered, it
will create a default ScanDirConfigMXBean
bound
to a default XML config file.
The application's default XML config file is determined as follows:
scandir.config.file
is
defined, the default application file will be the
file pointed to by this property. If that file
doesn't exist, it will be created when
ScanDirConfigMXBean.save()
is
invoked.
jmx-scandir.xml
,
located in the user's directory (as defined by
the System property user.home
).
If that file doesn't exists, it will be created when
ScanDirConfigMXBean.save()
is
invoked.
It is worth noting that this project is defined to run with the following properties:
-Djava.util.logging.config.file=logging.properties
-Dscandir.config.file=src/etc/testconfig.xmlWith
ScanDirAgent
defined as the project's
main class. Hence when you invoke from the NetBeans IDE
Run Project on the jmx-scandir project,
or Run file on the ScanDirAgent
, the
application starts with the test configuration provided in
src/etc/testconfig.xml
Once generated, the Javadoc of example classes can
be found starting from dist/javadoc/index.html
.
You can view the sources in the src
subdirectory.
This section discusses some common patterns and design choices that this example demonstrates, and some pitfalls that it avoids.
What is an MXBean? MXBeans made their appearance in J2SE 5.0 (Tiger), with the Management and Monitoring API of the JVM. However, Java SE 6 is the first Java SE release that contains a standard framework which makes it possible to create and register your own MXBeans.
MXBeans are a special kind of MBean, which once registered
in the MBeanServer, get automatically transformed into
OpenMBeans. From a developer point of view, nothing changes:
A Wombat MBean can become an MXBean simply by renaming
its WombatMBean
interface into WombatMXBean
.
Using MXBeans rather than plain Standard MBean brings its own advantages:
DirectoryScannerMXBean
and points
towards a ScanDirConfigMXBean
.
In short, MXBeans are so much easier to use that this example doesn't even have a single regular Standard MBean.
See also What is an MXBean? and Inter-MXBean References.
Hint: In order to simplify the task of coding a JMX programmatic client, we recommend that getters, setters, and operations defined in MBean and MXBean interfaces throwIOException
. Proxy objects will then be able to rethrow directly anyIOException
received from their underlying MBean Server connection, without wrapping them intoUndeclaredThrowableExceptions
.
Since the life cycle of the proxy object is not directly tied to the life cycle of the MBean it proxies, you may also want to have all methods in the MBean or MXBean interface throwInstanceNotFoundException
or more generallyJMException
.
As you must know if you've been studying JMX, MBeans are
named objects. The names of MBeans are represented by
instances of ObjectName
. An ObjectName is
composed of a domain, followed by a colon ':',
followed by a comma-separated list of key=value
pairs.
The ordering of the key=value pairs is not
important, but ObjectNames
are case sensitive
(both keys and values are case sensitive) and white space
is not ignored.
A common pitfall for JMX beginners is to inadvertently
insert white space after commas into an ObjectName,
and expect that two ObjectNames which differ only by such white
space will be considered identical. This is not the
case.
As an example, the ObjectName 'D:k1=v1, k2=v2, k3=v3
' has
three keys, which are 'k1
', ' k2
',
and ' k3
': beware
of the space in the name of the second and third
keys!
It is therefore a different ObjectName from
'D:k1=v1,k2=v2,k3=v3
' (the keys are now
'k1
', 'k2
', and
'k3
'), but the same ObjectName as
'D: k2=v2, k3=v3,k1=v1
', and yet different
from 'D:k2=v2, k3=v3, k1=v1
'!
In this example, we are following the rules for ObjectName suggested in the JMX Best Practices:
type=
key property. This property is different for every
object type in our domain.
name=
key.
type=
.
The ObjectNames of the ScanManagerMXBean and ResultLogManagerMXBean, which are both singleton MBeans, are
composed in this way.
type=
: the name=
key.
In this example, a key property list of the form
type=X,name=Y
is always enough to uniquely name
an MBean. Tools like jconsole are usually aware
of the semantics of the type=
key and
name=
key, and are therefore able to
display this form of name in a way that
is easier to read than other name forms.
The rules listed above are implemented by a couple
of static helper functions in the ScanManager class. See the code of the
makeSingletonName
and
makeMBeanName
methods.
One of the most common problems that needs to be solved
when designing a management interface with JMX is to
choose a representation for inter-MBean relationships.
Prior to Java 6, there were basically three possible
choices:
ObjectName
or
ObjectName[]
values. The ObjectNames
point to the MBeans which are related to that
object. For instance , GlassFish
defines MBeans which also use this pattern.
In Java 6, these three possibilities still remain, but
the new MXBean framework brings up an interesting
alternative. Instead of returning an ObjectName or
an ObjectName array, an MXBean can return a proxy
to its related MXBeans. This is how we have chosen to
implement our inter MBean relationships in this
example:
For instance the
ScanManagerMXBean
/DirectoryScannerMXBean
relationship and the
ScanManagerMXBean
/ScanDirConfigMXBean
relationships are implemented in this way.
The additional benefit, as compared to returning ObjectNames or using the RelationService is that interface type of the MBeans which are pointed to by the relationship becomes directly apparent. The method:
public Map<String,DirectoryScannerMXBean> getDirectoryScanners();makes it immediately obvious that the MBeans to which we point are DirectoryScannerMXBeans. It would have been much less obvious in prior versions of Java SE, were the returned type would have had to be
Map<String,ObjectName>
, or
even worse just Map
.
However, it must be clear that the behaviour will be quite different when an MXBean is returned as compared to when a simple bean is returned.
When an MXBean is returned, the remote client sees either
an ObjectName, if it is a generic client like jconsole, or
a proxy to a remote MXBean, if the client is working with the
MXBean interface. Invoking an operation on one of the
proxy returned by a method such as
getDirectoryScanners
will cause the
MBean to be invoked on the remote server side.
If getDirectoryScanners
were
defined as:
public Map<String,DirectoryScannerConfig> getDirectoryScanners();then invoking a method on one of the returned objects would have absolutely no effect on the remote server side - because the returned objects in this case would simply be a bunch of serialized data objects.
It is worth noting that although an MXBean interface can have getters and operations which return an MXBean interface, a regular standard MBean shouldn't have any getters or methods which return MBean interfaces or MXBean interfaces.
For more information see also Inter-MXBean References.
Sometimes, an MBean needs to have a reference to the MBeanServer in which it is registered, or needs to know with which ObjectName it has been registered.
Sometimes also, an MBean may need to perform some checks before being registered, or will need to carry out some actions right after it has been successfully registered in the MBeanServer.
Sometimes again, an MBean may need to perform some checks, or some cleaning actions, just before, or just after, it is unregistered.
When an MBean has such needs, the easiest solution
for it is to implement the MBeanRegistration
interface.
The MBeanRegistration
interface is a callback
interface which defines pre and post registration and
unregistration callbacks.
When an MBean implementing this interface is created
(with createMBean
) or registered
(with registerMBean
) in an MBeanServer,
the MBeanServer will call the preRegister
and postRegister
method implemented by
the MBean. The preRegister
method
has an MBeanServer
and ObjectName
parameter, which are passed by the MBeanServer to the
MBean. The MBean can store the reference it is being passed
in a private instance variable for later use.
Most of the MXBeans we have defined in this example
implement the MBeanRegistration
interface. The table
below show how our MBeans use this interface to control
their own names, make sanity checks, perform
initialization steps or cleanup actions.
MBean Requirement | callback | use case example |
---|---|---|
get a reference to the MBeanServer | preRegister |
The ScanManagerMXBean needs a reference to the MBeanServer in order to create and register other MBeans, such as the ResultLogManagerMXBean, and the DirectoryScannerMXBeans. |
reject registration if conditions are not met. | preRegister |
The ScanManagerMXBean will throw
an IllegalArgumentException in preRegister
if the ObjectName it is being passed is
illegal. Throwing an exception in
preRegister makes the registration fail.
|
get my client-assigned MBean name | preRegister |
The ScanDirConfigMXBean propagates the
value of the name= property of
the ObjectName it is given into its
ScanManagerConfig bean.
|
provide my own default ObjectName if none was given to the MBeanServer | preRegister |
The name that is returned by preRegister
is the ObjectName with which the MBean will be
eventually registered.
The ScanDirConfigMXBean is able to suggest
a value for its own ObjectName if none was
provided. Similarly, the ScanManagerMXBean
always returns its singleton ObjectName
defined by ScanManagerMXBean.SCAN_MANAGER_NAME.
|
perform initialization steps | preRegister |
The ScanDirConfigMXBean uses preRegister
to initialize its internal ScanManagerConfig bean.
|
perform initialization steps, once it is known that the registration was successful. | postRegister |
The postRegister method
can be used to implement
initialization steps that need to be done once it
is known that the registration was successful, or to
undo any action performed by preRegister once it
is known that registration was not successful.
The postRegister method has a Boolean parameter
which tells the MBean whether it was or wasn't
successfully registered in the MBeanServer.
The ScanManagerMXBean uses postRegister to create
and register other MBeans, such as the
ResultLogManagerMXBean and the default
ScanDirConfigMXBean.
Note that postRegister is not expected to throw any
exception. If an exception needs to be thrown, it should
be thrown in preRegister .
|
check whether the MBean can be deregistered | preDeregister |
The ScanManagerMXBean uses this method to verify
that its state allows it to be deregistered.
In particular, it will refuse to be deregistered
if it is in the RUNNING or SCHEDULED state.
If preDeregister throws an exception, the unregisterMBean
call will fail and the MBean will remain registered in
the MBeanServer.
Take particular care when implementing business logic
in this method: if the logic you implement has an
unfortunate bug which makes it always throw an
exception, you will never be able to unregister
that MBean.
|
clean up resources, refusing to be deregistered if it fails | preDeregister |
The ScanManagerMXBean uses this method to unregister all the other MBeans it has created and registered in the MBeanServer. This includes the ResultLogManagerMXBean, the ScanDirConfigMXBeans it has created, and the DirectoryScannerMXBeans it has created when applying its configuration. |
clean up resources which need to be released in a best-effort way, when it is known that the MBean is no longer registered. | postDeregister |
postDeregister is only called if the MBean was succesfully
unregistered.
The ScanManagerMXBean uses this method to cancel
its internal java.util.Timer.
|
A singleton MBean is an MBean which can only have one
instance registered in a given MBeanServer.
A singleton MBean usually has a well-known name,
which can be defined as a constant. In that case,
clients no longer need to call new ObjectName(...)
and catch the declared MalformedObjectNameException
.
There are already quite a few examples of singleton MBeans in the java.lang.management API. The ThreadingMXBean, ClassLoadingMXBean, RuntimeMXBean, etc. are all singleton MBeans.
In this example, we have two singleton MBeans:
The ScanManagerMXBean
and the
ResultLogManagerMXBean
. But in fact,
the only real singleton MBean is the
ScanManagerMXBean
. The
ResultLogManagerMXBean
just happens to
be a singleton MBean because it has a 1-1 relationship
with the ScanManagerMXBean
.
The ScanManagerMXBean
implements the
singleton MBean pattern in this way:
ScanManagerMXBean
name has a single
key property: type=ScanManagerMXBean
.SCAN_MANAGER_NAME
in the ScanManager
classScanManagerMXBean
enforces its status of
singleton MBean. It will refuse to be registered
with a name other than
the SCAN_MANAGER_NAME
. You can therefore depend on
the fact that the ScanManagerMXBean
will always
be registered with its singleton SCAN_MANAGER_NAME
(see preRegister
)
ScanManagerMXBean
: if you pass null,
then the ScanManager
will be registered with
its singleton SCAN_MANAGER_NAME
(see preRegister
).
ScanManager
class has a no-arg static
register
method that will register
the singleton instance in the Platform MBeanServer.
This static register
method returns
a proxy to the registered singleton.
ScanManager
class has also a static
register
method that will create
a singleton instance in a (possibly remote)
MBeanServerConnection - using
createMBean
.
This static register
method
also returns a proxy to the registered singleton.
On the other hand, the ResultLogManagerMXBean
has a much more relaxed implementation of the pattern:
It simply provides its own singleton name if it is
registered with a null ObjectName, but will not enforce
the use of that name.
Note that all singleton MBean names in this example
are created using the ScanManager.makeSingletonName
method, which implements the pattern for ObjectNames suggested
in the JMX Best Practices.
A common task that many JMX applications have is to manage the life cycle of MBeans registered in the MBeanServer.
In this example, we have decided to follow a simple pattern:
ScanManagerMXBean
will then
in turn register any other MBean that the
application might need:
ResultLogManagerMXBean
ScanDirConfigMXBean
which loads the initial configuration
DirectoryScannerMXBeans
as
needed when the configuration is applied
ScanDirConfigMXBean
, to
which you can later switch in order
to apply a new alternate configuration.
ScanManagerMXBean
will unregister
any DirectoryScannerMXBeans
it has
previously registered, and will re-create
brand new DirectoryScannerMXBeans
from the applied configuration.
ScanManagerMXBean
,
it does all the cleanup for you, by unregistering
all the MBeans that it has created during the
course of the application.
The ScanManagerMXBean
makes use of its
MBeanRegistration
interface in order
to register the other MBeans it needs (see the
ScanManager.postRegister
method) and to unregister
every MBean it has created (see the ScanManager.preDeregister
method).
You will note that the ScanManagerMXBean
will only allow itself to be deregistered if it can be
closed - that is if there's no other action in
progress.
This is to make sure that the deregistration of
dependent MBeans will work smoothly.
The deregistration of related MBeans will happen
in the ScanManager.preDeregister
method.
If one of these MBeans could not be deregistered,
then the ScanManagerMXBean
will throw
an exception, refusing to be deregistered.
This leaves you a chance to try to deregister it
again later. Since the ScanManagerMXBean
has switched its state to CLOSED before starting
to unregister its dependent MBeans, it will refuse
any further actions, ensuring that e.g. nobody
can try to start it or schedule it while it
is in that partially-deregistered state.
Handling the LifeCycle of all the application's MBeans in a single MBean is usually a good design pattern, especially if the application is a module which is intended to share a JVM - or an MBeanServer - with other modules.
This is specially useful if the application needs to
be loaded and unloaded on demand: in that
case, simply registering or unregistering the top level
MBean (in our example the ScanManagerMXBean
) does
the trick.
In order to emit notifications, an MBean must be
an instance of NotificationEmitter
.
The NotificationEmitter
interface defines methods
that the MBeanServer will call on the MBean in order
to register NotificationListeners
with the MBean.
It is worth noting that the MBean may not be
invoked each time a JMX client wants to register
a listener. For instance, the RMIConnectorServer
registers only once a single listener with each MBean
which is a NotificationEmitter
.
In that specific case, the listener may even be registered
with the MBean before any client has actually subscribed
for notifications from that particular MBean.
An MBean can therefore make no assumption about which client or how many clients have registered for notifications.
It is also worth noting that the logic of the
methods defined in NotificationEmitter
would not
be trivial to implement from scratch. Fortunately
the JMX API defines a helper class, called
NotificationBroadcasterSupport
, which
provides an implementation for these methods.
There are actually three ways for an MBean to
implement NotificationEmitter
, of which only two
are recommended.
This is the simplest way of coding an MBean which
is a NotificationEmitter
:
Simply extend NotificationBroadcasterSupport
,
then override its getNotificationInfo
method
which returns the MBeanNotificationInfo[]
array
that should be included in your MBean's MBeanInfo
and that's it.
You just need to call the sendNotification
method
inherited from NotificationBroadcasterSupport
whenever
your MBean needs to send a notification.
In our example, both the ScanDirConfigMXBean and ResultLogManagerMXBean extend
NotificationBroadcasterSupport
in order
to send notifications.
There may be cases however where delegating to a
wrapped NotificationBroadcasterSupport
object may be preferred to extending
NotificationBroadcasterSupport
.
For instance, if your MBeans already derive from
some base class, extending NotificationBroadcasterSupport
might not be an option.
Similarly, if you do not want to have the inherited
public void sendNotification(Notification notification)
method appear in the Javadoc of the concrete class of your
MBean, you may want to consider using the delegation
pattern instead of extending
NotificationBroadcasterSupport
In our example both the ScanManagerMXBean and the DirectoryScannerMXBean use the delegation
pattern rather than extending
NotificationBroadcasterSupport
.
In the end, choosing between one or the other method
is more a question of taste, although the delegation
pattern could be considered more flexible since it
doesn't require extending any given superclass.
It may be also worth noting that some tools like
the JMX Module of NetBeans IDE, will be able to
generate for you all the code that delegates to a
wrapped NotificationBroadcasterSupport
.
This is the last possibility for an MBean that
needs to send notifications: simply implement
NotificationEmitter
from scratch. This is highly
discouraged since that logic is not trivial, and
already provided by
NotificationBroadcasterSupport
anyway.
One thing you must keep in mind when sending notifications is not to send them from within a synchronized block, or while holding a lock on some resource.
Indeed, what happens when you send a notification
may vary greatly depending on whether the client
which has registered for notifications has done
so through a JMXConnector
(like the
JMXRMIConnector
)
or through a direct reference to the MBeanServer
(by calling
MBeanServer.addNotificationListener
).
In this latter case, the listener will be invoked
synchronously in the same thread that your MBean is
using to send its notification. If by misfortune, the
code of that listener now re-enters your MBean through a
call that flows through a JMXConnector, a deadlock
could occur. It is therefore very important to release
any lock you may have before calling
sendNotification
.
An easy way to do that is demonstrated in the
ScanManager
class. The ScanManager
has an internal private queue of pending notifications.
When a notification needs to be sent (e.g. because the
ScanManager state is being switched), the notification
is simply prepared and put into the pending notification
queue.
The notification queue is then processed later on,
at the end of the method, when the processing is finally
completed and all the locks have been released.
At this point the notification queue might already
have been emptied by another thread - in which case
the pending notifications will have already been
removed from the queue. Which thread actually gets
to send the notifications is of no importance. The
important point is that all the locks detained by
your MBean code in that thread were released before
the notification was sent.
In our example the ScanManager
class
ensures this by:
sendNotification
in its private sendQueuedNotifications
method.
sendQueuedNotifications
when all locks have been released.
sendQueuedNotifications
from within
a synchronized block.Another common best practice when you want to improve interoperability is to use directly the Notification base classes provided in the JMXTM API. Do not create your own subclasses of these standard classes.
Indeed, if you code your own subclass, a generic client, like jconsole, will not be able to receive that notification unless it has that custom subclass in its classpath.
If you want your application to be interoperable, it is therefore preferable not to subclass any of the standard Notification classes. You can define your own Notification type string, and if you need to send additional data, you can put a CompositeData, or a HashMap of serializable standard types in the Notification's user data fields.
In this example, we are using directly the standard notification classes:
AttributeChangeNotification
to notify
changes in their State
attribute.
Notification
class directly in order to notify whenever
it finds a matching file.
Notification
class with a new
com.sun.jmx.examples.scandir.filematch
type.
Notification
class.
Careful readers will have noted that the ScanManagerMXBean and the DirectoryScannerMXBean both use the
AttributeChangeNotification
class
to notify about their state change, whereas the
ScanDirConfigMXBean uses the base
Notification
class.
In fact, this is because the semantics of these notifications is not exactly the same - although both denote a state change:
In the case of ScanManagerMXBean
and DirectoryScannerMXBean
, the
notification which is emitted is more about a
state transition, from one state to another.
For instance, going from RUNNING
to STOPPED
, or from
SCHEDULED
to STOPPED
.
In that case, the
AttributeChangeNotification
was
more appropriate because it made it possible
to send the previous and the new value of the
state attribute, thus reflecting the whole
state transition.
In the case of the ScanDirConfigMXBean
however, what is of interest is the state in
which the MBean has arrived. Using the base
Notification
class with three different
notification type strings -
com.sun.jmx.examples.scandir.config.loaded
,
com.sun.jmx.examples.scandir.config.modified
,
and
com.sun.jmx.examples.scandir.config.saved
-
was therefore closer to what we wanted to model.
A common practice when designing a management application is to have an MBean, or a set of MBeans, dedicated to configuration. Separating configuration from control and monitoring allows more appropriate logic, and often simplifies the design and implementation of the management interface.
In our example, the ScanDirConfigMXBean is dedicated to the application configuration.
The ScanDirConfigMXBean
will let you interactively
modify, save, or load the application configuration. The modifications
will not be taken into account until it is applied, by invoking
applyConfiguration
on the ScanManagerMXBean.
It is also possible to create many configurations, by creating as
many ScanDirConfigMXBean
s, and then to choose and apply
one of these configurations by calling
ScanManagerMXBean.setConfigurationMBean
and then
ScanManagerMXBean.applyConfiguration
.
In this way, all configurations aspects are gathered and concentrated
inside the ScanDirConfigMXBean
instead of being scattered
throughout all the MBeans that compose the application.
In order to save and store the application configuration data, the
ScanDirConfigMXBean
uses a set of XML serializable Java beans
defined in the com.sun.jmx.examples.scandir.config package. These beans are very
simple Java beans which have been lightly annotated for XML binding.
It is worth noting that these same beans can also be handled by the MXBean framework (our beans don't contain recursive data structures) and can therefore be used directly as attributes and parameters of MXBeans, without needing to be Java-serializable (the MXBean framework transform them in CompositeData objects - which are serializable).
The same ScanManagerConfig bean that we use to read from and write to the
XML configuration file is thus also used as attribute of the ScanDirConfigMXBean. It is transformed into a CompositeData
by the MXBean framework, and can be easily introspected with
jconsole.
A question often asked by newcomers to JMX technology is whether the MBeanServer is thread-safe. Well, the MBeanServer is thread safe, but it doesn't put any locks on the MBeans it contains. The MBeans can be concurrently accessed by multiple threads, and must therefore take care of their own thread safety.
In this example, we have been using two methods to ensure thread safety for our MBeans: synchronized blocks, and semaphores.
Using synchronized blocks is probably the most common and easiest way to implement thread safety in Java. When dealing with MBeans though, here are a couple of rules to keep in mind:
Another means of implementing thread-safe code is to use semaphores.
The ScanManagerMXBean uses a semaphore called
sequencer
to ensure
that critical code sections are not executed concurrently. In this
MBean, we use Semaphore.tryAcquire
to lock the sequencer
semaphore before entering the critical section. If the
Semaphore.tryAcquire
returns true then we enter the critical
section. If it returns false, we throw an IllegalStateException, stating
that we couldn't acquire the lock. The code looks like this:
if (!sequencer.tryAcquire()) throw new IllegalStateException("resource locked"); try { // critical code here ... } finally { // Always use try/finally to ensure that the semaphore // will be released, even if exceptions or errors are raised! sequencer.release(); }
Using Semaphore.tryAcquire
and throwing an exception if
the semaphore is already locked makes it safer to call other MBeans
from within the critical section: in potential deadlock situations
the calling code will get the IllegalStateException
instead of being blocked on the deadlocked lock.
It is worth noting that each of these techniques has its own advantages and disadvantages - which can make one of them more or less appropriate depending on the inner logic of the MBean you're implementing.
Careful readers will also have noted that we used
IllegalStateException
directly, instead of defining
our own subclass of RuntimeException, which could have had a more
precise semantics. If you define a new exception for your JMX application,
you must keep in mind that your client will need to have the class
of your exception in its classpath to get that exception.
Otherwise your client will get a completely different exception, indicating a
deserialization issue.
Implementing code that needs to wait for notifications is sometimes difficult. Because notifications are asynchronous, doing something like:
// register a notification listener ... // start a management action ... // wait for a notification ... // do something based on whether the expected notification // is received ...is not always trivial. However, there's a very easy way to do that: use a blocking queue of notifications.
final BlockingQueue<Notification> notifQueue = new LinkedBlockingQueue<Notification>(); final NotificationListener listener = new NotificationListener() { public void handleNotification(Notification notification, Object handback) { try { // Just put the received notification in the queue. // It will be consumed later on. // notifQueue.put(notification); } catch (InterruptedException ex) { // OK } } }; // register the listener - possibly also as a JMXConnectionNotification // listener to get Notification Lost notification ... // start management action ... // wait for notification while (expected notif not received and delay not expired) { Notification n = notifQueue.poll(3,TimeUnit.SECONDS); // if expected notif, do something ... } // if expected notification not received do something else. ....
You will note that this is a technique we've been using in the ScanDirAgent class and in the example unit tests.
We have seen that MXBeans will let you return proxy references to other MXBeans. But should that MXBean hold a direct reference to the MXBeans it relates to, or would it be better for it to hold only a proxy?
As a general rule it is better when an MBean reference is only held by the MBeanServer. It is a better design to hold a reference to a proxy, rather than to hold a hard reference to an MBean. However there are two cases when holding a hard reference might be preferred:
In our example, the ScanManagerMXBean holds only proxy references to the ScanDirConfigMXBean and the DirectoryScannerMXBeans.
However it holds a direct reference to the ResultLogManager. This makes it possible to pass a direct
reference to the DirectoryScannerMXBeans
,
which can then log their results
more efficiently, and would also make it possible to remove
the log
method from the ResultLogManagerMXBean interface - leaving it in the
ResultLogManager
class (possibly as a package method)
should we wish to do so.
The ScanDirAgent is the Agent class for the scandir application.
This class contains the main
method to start a standalone
scandir application.
The main
method simply registers a ScanManagerMXBean in the platform MBeanServer, and then waits
for someone to call ScanManagerMXBean.close
.
When the ScanManagerMXBean
state is switched to
ScanManagerMXBean.ScanState.CLOSED
, the
ScanManagerMXBean
is unregistered, and the application
terminates (i.e. the main thread completes).
Standalone JMX applications usually have an Agent class that contain
their main
method, which performs all the MBean
registration steps.
However, it is usually not a bad idea if that class can
be easily turned into an MBean. Indeed, this will make your
application easier to integrate in an environment where it would
no longer be standalone and would no longer control the implementation
of main
. In our example the Agent
class could be easily turned into an MBean, exposing its three
init
, waitForClose
and cleanup
method. However we didn't go as far as turning it into an MBean since
the application can be already easily started by registering an instance
of ScanManagerMXBean.
The ScanDirClient is an example class that shows how a
programmatic client can connect to a secured scandir application.
This class contains a main
method which creates and
configures a JMXConnector
client to connect with
a secured scandir daemon. This class will not work with
the default unsecured agent since it requires mutual authentication.
How to secure a JMX scandir application and run
the secure ScanDirClient
is discussed later in this document.
The ScanDirClient
is not really part of the
application - and is given here only for the sake of
the example.
Make sure that you have access to junit.jar (either 3.8.1 or 3.8.2).
Make sure also that you have junit.jar in your
CLASSPATH
.
Then in the example root directory (where the build.xml
file is located) run the following command:
ant test -Dlibs.junit.classpath=path to junit jar (either 3.8.1 or 3.8.2)
Alternatively you can open the jmx-scandir project with the
NetBeans IDE and test the jmx-scandir project from the
Run
menu.
In the example root directory (where the build.xml
file is located) run the following commands:
ant jar ant run-single -Drun.class=com.sun.jmx.examples.scandir.ScanDirAgent -Djavac.includes=srcor simply
ant run
This will run the example using the configuration file provided in the src/etc directory.
Alternatively you can open the jmx-scandir project with the
NetBeans IDE. You can run the example by
selecting the ScanDirAgent
file
and run it with Run File
in the
Run
menu or simply
set the jmx-scandir project as main project and
select Run Main Project
from the
main menu. Both targets will use the configuration
file provided in the src/etc directory.
When the application is started, you can connect to it with jconsole.
Note: You can also run the scandir application directly from thejava
command line. Make sure to build the project jar first.
On Unix systems:ant jar java -Djava.util.logging.config.file=logging.properties \ -Dscandir.config.file=src/etc/testconfig.xml \ -jar dist/jmx-scandir.jar
On Windows systems:
ant jar
java -Djava.util.logging.config.file=logging.properties -Dscandir.config.file=src\etc\testconfig.xml -jar dist\jmx-scandir.jar
Run the example as explained in the previous section, so
that it uses the provided src/etc/testconfig.xml
configuration file. Then start
jconsole. In the connection window choose the process that runs
com.sun.jmx.examples.scandir.ScanDirAgent
or
jmx-scandir.jar
.
Open the MBeans tab, and look for the
ScanDirConfigMXBean
.
Click on its Attributes
node and double click on its
Configuration
attribute, to look at
the loaded configuration - values in bold can
be expanded by a double-click.
Now go to the ScanManagerMXBean
, click on
its Notifications
node, and subscribe
for notifications. Then click on the
Operations
node and invoke the
start()
operation:
You can see that the notifications counter was incremented by three: you have just scheduled, run, and completed a batch of directory scans.
Now go to the ResultLogManagerMXBean
,
click on its Attributes
node, and
expand its MemoryLog
attribute:
You can see that the directory scan results have been logged.
To make the application terminate go back to the
ScanManagerMXBean
and invoke
close()
. The ScanDirAgent
will receive the notification, step out of
the application main thread, and the application
will terminate.
This is of course a very limited scenario. Feel free
to improvise with all the features of the example, creating
a new configuration -
ScanManagerMXBean.createOtherConfigurationMBean
-
adding multiple directory scanners to that configuration -
ScanDirConfigMXBean.addDirectoryScanner
-
then switching the ScanManagerMXBean
current
configuration by changing the value of the ConfigurationMBean
attribute - ScanManagerMXBean.setConfigurationMBean
- then applying the new configuration -
ScanManagerMXBean.applyConfiguration(true)
-
then scheduling repeated directory scans every 10 seconds -
ScanManagerMXBean.schedule(0,10000)
-
subscribing for notifications, etc...
In this section, we will see how to configure and
start the scandir example so that the JVM agent
is bootstrapped with a secure JMXConnectorServer. Indeed, until
now we have only used the insecure local connection,
which can only be used as long as both the client and
the server run on the same machine. This section will
explain how to start the ScanDirAgent
so
that a real secure RMIConnectorServer is started at bootstrap.
To achieve this we will: provide our own management.properties, create our own password and access files, provide a keystore and truststore, start the ScanDirAgent with the appropriate system properties.
The easiest way to configure the
JVM Agent for Secure Remote
Connection is to use your own management.properties file.
In this example, we have copied the default
$JRE/lib/management/management.properties
file to the example's src/etc
directory and
modified it in this way:
# For setting the JMX RMI agent port use the following line com.sun.management.jmxremote.port=4545
# For RMI monitoring with SSL client authentication use the following line com.sun.management.jmxremote.ssl.need.client.auth=true
# For using an SSL/TLS protected RMI Registry use the following line com.sun.management.jmxremote.registry.ssl=true
# For a non-default password file location use the following line com.sun.management.jmxremote.password.file=src/etc/password.properties
# For a non-default password file location use the following line com.sun.management.jmxremote.access.file=src/etc/access.properties
You will note that we haven't provided any value
for the other security properties, like
com.sun.management.jmxremote.authenticate=true
,
because these properties already default to a value
which enables security by default.
Note however that protecting the RMI Registry with SSL
improves the application security, but only as long as
mutual authentication is also switched on. Otherwise, just
anybody would be able to connect to the registry and
get the RMIServer stub.
We do recommend that you use the most secure configuration when you deploy a JMX agent - which means switching on SSL protection for the RMI registry and requiring mutual authentication, as we show in this example.
We will use the com.sun.management.config.file
system property to pass our management.properties
file to the ScanDirAgent
.
As explained above, we have created our own password file and access file for access control and authorization.
In the password file, we have defined two logins: guest and admin. The password for guest is guestpasswd and the password for admin is adminpasswd.
In the access file, we have mapped these two logins to access rights: the admin login has read-write access, while the guest login only has read-only.
Before starting the ScanDirAgent
, you will
need to restrict access permission to the password file,
in such a way that nobody but you can read it. Otherwise, the
JVM Agent will refuse to start the JMXConnectorServer, as it will
fear that security can be compromised if other parties can
have read access to the password file. How to restrict
read access to the password file is explained in detail
here.
As we have seen above, the location of our access and password files is configured in our own management.properties file.
Using SSL with mutual authentication means that both client and server will need a keystore and a truststore to store their own certificates, and the certificates of the parties they trust. Usually, client and server will have their own keystore and truststore.
For the sake of simplicity - and to get you started without the tedious necessity of creating your own keystore and truststore, we are providing a dummy keystore and truststore, containing a certificate self-signed by duke. The password for our keystore is password, and the password for our truststore is trustword. We suggest that you first get the example running with the keystore and truststore we are providing before attempting to use your own keystore and truststore.
A secure application will obviously need to use its own keystore and truststore, and should not rely on the keystore and truststore we are providing here!
How to create your own keystore and truststore, is explained
in here.
As shown later,
we will need to use system properties to pass our truststore
and keystore to the ScanDirAgent
.
To start a secure scandir agent, go to the scandir example root directory and type the following command:
On Unix Systems:
ant jar java \ -Djava.util.logging.config.file=logging.properties \ -Djavax.net.ssl.keyStore=keystore \ -Djavax.net.ssl.keyStorePassword=password \ -Djavax.net.ssl.trustStore=truststore \ -Djavax.net.ssl.trustStorePassword=trustword \ -Dcom.sun.management.config.file=src/etc/management.properties \ -Dscandir.config.file=src/etc/testconfig.xml \ -jar dist/jmx-scandir.jar
On Windows Systems:
ant jar
java
-Djava.util.logging.config.file=logging.properties
-Djavax.net.ssl.keyStore=keystore
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore=truststore
-Djavax.net.ssl.trustStorePassword=trustword
-Dcom.sun.management.config.file=src\etc\management.properties
-Dscandir.config.file=src\etc\testconfig.xml
-jar dist\jmx-scandir.jar
If you start jconsole now, you will see that you are still able to connect to the agent using the local connection. However, if you try to connect through the remote connector, using localhost:4545, the connection will fail, even if you provide a correct login/password pair. Indeed, since the JMXConnectorServer is now protected with SSL, jconsole must also be configured with the appropriate SSL parameters so that it can authenticate the server and get authenticated by the server too as the SSL configuration of the server requires mutual authentication.
The next section will discuss how to connect to the secure agent.
We will now see how to connect to the secure agent, using jconsole, and using a programmatic client.
The only special thing you need to do in order to be able to connect to your secure agent with jconsole, is to give it a keystore (containing its client certificate) and a truststore (containing the certificates of the servers it can trust). In our example, we use the same keystore/truststore pair on the client and server side - but this is not what a real application would do. Indeed a real application would have different certificates for the client and the server, and thus use different keystores (and probably truststores). More information on SSL authentication can be obtained from the JavaTM Secure Socket Extension (JSSE) Reference Guide.
To start jconsole with our provided keystore and truststore, go to the scandir example root directory and type in the following command:
jconsole
-J-Djava.util.logging.config.file=logging.properties
-J-Djavax.net.ssl.keyStore=keystore
-J-Djavax.net.ssl.keyStorePassword=password
-J-Djavax.net.ssl.trustStore=truststore
-J-Djavax.net.ssl.trustStorePassword=trustword
The -J-Djava.util.logging.config.file=logging.properties
flag is not mandatory, but passing a logging.properties
may help you debug connection problems if anything goes wrong.
In jconsole connection window, choose to connect to a remote process, using the address localhost:4545 and the guest login:
You will see that the agent will let view all the MBeans and their attributes, but will reject any attribute modification or remote method invocation.
Note: if jconsole fails to connect and show
you this screen
you have probably misspelled some of the properties on jconsole
command line, or you didn't start jconsole from the
scandir example root directory where our truststore
and keystore
files are located. This article - Troubleshooting connection problems in JConsole - may help
you figure out what is going wrong.
In this section we will show the steps involved in writing a programmatic client that will connect to our secure agent.
The ScanDirClient is an example class that shows how a
programmatic client can connect to a secured scandir application.
This class contains a main
method which creates and
configures a JMXConnector
client to connect with
the secured scandir agent.
The secure client differs only from a non secure client in so far as it needs to use SSL RMI Factories and credentials to connect to the secure agent. The steps required mainly involve:
// Create an environment map to hold connection properties // like credentials etc... We will later pass this map // to the JMX Connector. // System.out.println("\nInitialize the environment map"); final Map<String,Object> env = new HashMap<String,Object>();
// Provide the credentials required by the server // to successfully perform user authentication // final String[] credentials = new String[] { "guest" , "guestpasswd" }; env.put("jmx.remote.credentials", credentials);
SslRMIClientSocketFactory
to interact
with the secure RMI Registry:
// Provide the SSL/TLS-based RMI Client Socket Factory required // by the JNDI/RMI Registry Service Provider to communicate with // the SSL/TLS-protected RMI Registry // env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory());
// Create the RMI connector client and // connect it to the secure RMI connector server. // args[0] is the server's host - localhost // args[1] is the secure server port - 4545 // System.out.println("\nCreate the RMI connector client and " + "connect it to the RMI connector server"); final JMXServiceURL url = new JMXServiceURL( "service:jmx:rmi:///jndi/rmi://"+args[0]+":"+args[1]+ "/jmxrmi"); final JMXConnector jmxc = JMXConnectorFactory.connect(url, env);
For this to work, we also need to start the ScanDirClient
with the appropriate system properties that will point to our
keystore
and truststore
. To start the secure
client, go to the scandir example root directory and type
the following command:
ant jar
java
-Djava.util.logging.config.file=logging.properties
-Djavax.net.ssl.keyStore=keystore
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore=truststore
-Djavax.net.ssl.trustStorePassword=trustword
-classpath dist/jmx-scandir.jar
com.sun.jmx.examples.scandir.ScanDirClient localhost 4545
You should be seeing this trace:
Initialize the environment map Create the RMI connector client and connect it to the RMI connector server Connecting to: service:jmx:rmi:///jndi/rmi://localhost:4545/jmxrmi Get the MBeanServerConnection Get ScanDirConfigMXBean from ScanManagerMXBean Get 'Configuration' attribute on ScanDirConfigMXBean Configuration: <ScanManager xmlns="jmx:com.sun.jmx.examples.scandir.config" name="testconfig"> <InitialResultLogConfig> <LogFileMaxRecords>2048</LogFileMaxRecords> <LogFileName>build/scandir.log</LogFileName> <MemoryMaxRecords>128</MemoryMaxRecords> </InitialResultLogConfig> <DirectoryScannerList> <DirectoryScanner name="scan-build"> <Actions>NOTIFY LOGRESULT</Actions> <ExcludeFiles/> <IncludeFiles> <FileFilter> <FilePattern>.*\.class</FilePattern> <SizeExceedsMaxBytes>4096</SizeExceedsMaxBytes> </FileFilter> </IncludeFiles> <RootDirectory>build</RootDirectory> </DirectoryScanner> </DirectoryScannerList> </ScanManager> Invoke 'close' on ScanManagerMXBean Got expected security exception: java.lang.SecurityException: Access denied! Invalid access level for requested MBeanServer operation. Close the connection to the server Bye! Bye! |
If the ScanDirClient
fails to connect with
the secure agent, then this article - Troubleshooting connection problems in JConsole - may help
you figure out what is going wrong. Indeed the connection steps
performed by the ScanDirClient
are very similar to
those performed by jconsole
, and the problems you
could encounter are identical. Just remember that
jconsole
needs the extra -J
flag to pass
system properties to the VM, which is not needed with regular
java
launcher invocations.
In this document, we have presented an advanced JMX example, and shown how to run a secure JMX agent in a production environment. We have also shown how to connect to such a secure agent with both jconsole and a programmatic client. We have also discuss various JMX design-patterns and best practices. Readers who would wish to learn more about JMX, and Monitoring and Management of the JVM, are invited to follow the links given in reference below.