001/* 002 * ============================================================================ 003 * Copyright © 2002-2026 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.mgmt; 019 020import static java.lang.String.format; 021import static java.rmi.registry.LocateRegistry.createRegistry; 022import static java.rmi.registry.LocateRegistry.getRegistry; 023import static javax.management.remote.JMXConnectorServerFactory.newJMXConnectorServer; 024import static org.apiguardian.api.API.Status.INTERNAL; 025import static org.apiguardian.api.API.Status.STABLE; 026import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_CHARSEQUENCE; 027import static org.tquadrat.foundation.lang.DebugOutput.ifDebug; 028import static org.tquadrat.foundation.lang.Objects.isNull; 029import static org.tquadrat.foundation.lang.Objects.nonNull; 030import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 031import static org.tquadrat.foundation.lang.Objects.requireNotBlankArgument; 032import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument; 033 034import javax.management.InstanceNotFoundException; 035import javax.management.MBeanRegistrationException; 036import javax.management.MBeanServer; 037import javax.management.MalformedObjectNameException; 038import javax.management.ObjectName; 039import javax.management.remote.JMXConnectorServer; 040import javax.management.remote.JMXServiceURL; 041import java.io.IOException; 042import java.net.MalformedURLException; 043import java.rmi.RemoteException; 044import java.rmi.registry.Registry; 045import java.util.ArrayList; 046import java.util.HashMap; 047import java.util.List; 048import java.util.Map; 049import java.util.StringJoiner; 050 051import org.apiguardian.api.API; 052import org.tquadrat.foundation.annotation.ClassVersion; 053import org.tquadrat.foundation.annotation.UtilityClass; 054import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError; 055import org.tquadrat.foundation.exception.UnexpectedExceptionError; 056import org.tquadrat.foundation.lang.NameValuePair; 057 058/** 059 * This class provides some utilities that are useful in the context of JMX. 060 * 061 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 062 * @version $Id: JMXUtils.java 1258 2026-06-04 18:33:06Z tquadrat $ 063 * @since 0.0.1 064 * 065 * @UMLGraph.link 066 */ 067@UtilityClass 068@ClassVersion( sourceVersion = "$Id: JMXUtils.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 069@API( status = STABLE, since = "0.0.1" ) 070public final class JMXUtils 071{ 072 /*-----------*\ 073 ====** Constants **======================================================== 074 \*-----------*/ 075 /** 076 * <p>{@summary The JNDI name for the exposed 077 * {@link MBeanServer}: {@value}}</p> 078 * 079 * @see #enableRemoteAccess(MBeanServer,JMXServiceURL,int,Map) 080 * @see #enableRemoteAccess(MBeanServer,int,Map) 081 * @see #enableRemoteAccess(MBeanServer,String,int,int,Map) 082 * @see #disableRemoteAccess(JMXServiceURL) 083 */ 084 public static final String BIND_NAME = "jmxrmi"; 085 086 /** 087 * The property name for the connector address: {@value}. 088 */ 089 public static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress"; 090 091 /** 092 * The name of the JMX domain that is used by all JMX enabled components 093 * of the library. 094 */ 095 public static final String JMX_DOMAIN = "org.tquadrat"; 096 097 /** 098 * The property name for the class of an MBean: {@value}. 099 */ 100 public static final String MBEAN_CLASS = "class"; 101 102 /** 103 * The property name for the function of an MBean: {@value}. 104 */ 105 public static final String MBEAN_FUNCTION = "function"; 106 107 /** 108 * The property name for the loader of an MBean: {@value}. 109 */ 110 public static final String MBEAN_LOADER = "loader"; 111 112 /** 113 * The property name for the name of an MBean: {@value}. 114 */ 115 public static final String MBEAN_NAME = "name"; 116 117 /** 118 * The property name for the MBean type: {@value}. 119 */ 120 public static final String MBEAN_TYPE = "type"; 121 122 /*------------*\ 123 ====** Attributes **======================================================= 124 \*------------*/ 125 /** 126 * The 127 * {@link javax.management.remote.JMXConnectorServer} 128 * instance that expose an 129 * {@link MBeanServer}. 130 * 131 * @see #enableRemoteAccess(MBeanServer,int,Map) 132 */ 133 private static final Map<JMXServiceURL,JMXConnectorServer> m_ConnectorServers = new HashMap<>(); 134 135 /*--------------*\ 136 ====** Constructors **===================================================== 137 \*--------------*/ 138 /** 139 * No instance allowed for this class. 140 */ 141 private JMXUtils() { throw new PrivateConstructorForStaticClassCalledError( JMXUtils.class ); } 142 143 /*---------*\ 144 ====** Methods **========================================================== 145 \*---------*/ 146 /** 147 * <p>{@summary Composes an object name from the given domain name and the 148 * given properties.}</p> 149 * <p>The object name has the form</p> 150 * <pre><code> <Domain>:type=<Type>,function=<Function><b>[</b>, class=<Class><b>]</b><b>[</b>,…<b>]</b></code></pre> 151 * <p>The type is something like a category.</p> 152 * <p>The function is a description for what the MBean does.</p> 153 * <p>The class can be provided, if multiple MBean implementations with 154 * the same type and function will be loaded.</p> 155 * <p>Additional properties in the form 156 * <code><name>=<value></code> can be added as required.</p> 157 * 158 * @param domainName The domain name. 159 * @param type The type of the MBean that will be named with the new 160 * object name. 161 * @param function The function of the MBean. 162 * @param mbeanClass The MBean's class; can be {@null}. 163 * @param properties Additional properties as name-value-pairs; can be 164 * {@null}. 165 * @return The object name. 166 * @throws MalformedObjectNameException It is not possible to create a 167 * valid object name from the given domain name and properties. 168 */ 169 @SafeVarargs 170 @API( status = STABLE, since = "0.0.1" ) 171 public static ObjectName composeObjectName( final String domainName, final String type, final String function, final Class<?> mbeanClass, final NameValuePair<String>... properties ) throws MalformedObjectNameException 172 { 173 final var propertyList = new ArrayList<NameValuePair<String>> (); 174 if( nonNull( properties ) ) propertyList.addAll( List.of( properties ) ); 175 if( nonNull( mbeanClass ) ) propertyList.add( new NameValuePair<>( MBEAN_CLASS, mbeanClass.getName() ) ); 176 propertyList.add( new NameValuePair<>( MBEAN_FUNCTION, requireNotEmptyArgument( function, "function" ) ) ); 177 propertyList.add( new NameValuePair<>( MBEAN_TYPE, requireNotEmptyArgument( type, "type" ) ) ); 178 179 @SuppressWarnings( "unchecked" ) 180 final var retValue = composeObjectName( domainName, propertyList.toArray( NameValuePair []::new ) ); 181 182 //---* Done *---------------------------------------------------------- 183 return retValue; 184 } // composeObjectName() 185 186 /** 187 * Composes an object name from the given domain name and the given 188 * properties. 189 * 190 * @param domainName The domain name. 191 * @param properties The properties as name-value-pairs; at least one 192 * property has to be provided. 193 * @return The object name. 194 * @throws MalformedObjectNameException It is not possible to create a 195 * valid object name from the given domain name and properties. 196 */ 197 @SafeVarargs 198 @API( status = STABLE, since = "0.0.1" ) 199 public static ObjectName composeObjectName( final String domainName, final NameValuePair<String>... properties ) throws MalformedObjectNameException 200 { 201 final var name = new StringJoiner( ",", format( "%s:", requireNotEmptyArgument( domainName, "domainName" ) ), EMPTY_CHARSEQUENCE ); 202 for( final var property : requireNotEmptyArgument( properties, "properties" ) ) 203 { 204 name.add( toString( property ) ); 205 } 206 final var retValue = new ObjectName( name.toString() ); 207 208 //---* Done *---------------------------------------------------------- 209 return retValue; 210 } // composeObjectName() 211 212 /** 213 * <p>{@summary Composes an instance of 214 * {@link JMXServiceURL} 215 * as ist is needed to expose an MBean server or to connect to an 216 * exposed MBean server.}</p> 217 * <p>This version creates a URL that can be used for local connections 218 * (both processes are running on the same machine).</p> 219 * 220 * @param registryPort The number of the registry port. 221 * @return The service URL. 222 * 223 * @see #BIND_NAME 224 * 225 * @since 0.25.3 226 */ 227 @API( status = STABLE, since = "0.25.3" ) 228 public static final JMXServiceURL composeServiceURL( final int registryPort ) 229 { 230 final JMXServiceURL retValue; 231 try 232 { 233 retValue = new JMXServiceURL( "service:jmx:rmi:///jndi/rmi://localhost:%2$d/%1$s".formatted( BIND_NAME, registryPort ) ); 234 } 235 catch( MalformedURLException e ) 236 { 237 throw new UnexpectedExceptionError( e ); 238 } 239 240 //---* Done *---------------------------------------------------------- 241 return retValue; 242 } // composeServiceURL() 243 244 /** 245 * <p>{@summary Composes an instance of 246 * {@link JMXServiceURL} 247 * as ist is needed to expose an MBean server or to connect to an 248 * exposed MBean server.}</p> 249 * <p>This version creates a URL that can be used for remote connections 250 * (the processes are running on different machines).</p> 251 * 252 * @param hostName The host name. 253 * @param registryPort The number of the registry port. 254 * @param dataPort The number of the data port; can be the same as the 255 * registry port. 256 * @return The service URL. 257 * @throws MalformedURLException It is not possible to compose a valid 258 * {@link JMXServiceURL} 259 * with the given {@code hostName}. 260 * 261 * @see #BIND_NAME 262 * 263 * @since 0.25.3 264 */ 265 @API( status = STABLE, since = "0.25.3" ) 266 public static final JMXServiceURL composeServiceURL( final String hostName, final int registryPort, final int dataPort ) throws MalformedURLException 267 { 268 final var retValue = new JMXServiceURL( "service:jmx:rmi://%2$s:%4$d/jndi/rmi://%2$s:%3$d/%1$s".formatted( BIND_NAME, requireNotBlankArgument( hostName, "hostName" ), registryPort, dataPort ) ); 269 270 //---* Done *---------------------------------------------------------- 271 return retValue; 272 } // composeServiceURL() 273 274 /** 275 * <p>{@summary Disables the external access to the 276 * {@link MBeanServer} 277 * for the given service URL.} Nothing happens if there was no NBean 278 * server exposed that URL.</p> 279 * <p>Internally, this method deactivates the 280 * {@linkplain JMXConnectorServer connector server}, 281 * that is, stops listening for client connections. Calling this method 282 * will also close all client connections that were made by this server. 283 * After this method returns, whether normally or with an exception, the 284 * connector server will not create any new client connections.</p> 285 * <p>Once a connector server has been stopped, it cannot be started 286 * again.</p> 287 * <p>Calling this method when the connector server has already been 288 * stopped has also no effect.</p> 289 * <p>If closing a client connection produces an exception, that 290 * exception is not thrown from this method. A 291 * {@link javax.management.remote.JMXConnectionNotification JMXConnectionNotification} 292 * with type 293 * {@link javax.management.remote.JMXConnectionNotification#FAILED JMXConnectionNotification.FAILED} 294 * is emitted from this MBean with the connection ID of the connection 295 * that could not be closed.</p> 296 * <p>Closing a connector server is a potentially slow operation. For 297 * example, if a client machine with an open connection has crashed, the 298 * close operation might have to wait for a network protocol timeout. 299 * Callers that do not want to block in a close operation should do it in 300 * a separate thread.</p> 301 * <p>This method works for both locally and remotely exposed connector 302 * servers.</p> 303 * 304 * @param serviceURL The URL that is used to connect to the MBean 305 * server. 306 * @throws IOException The connection server cannot be closed cleanly. 307 * When this exception is thrown, the connection server has already 308 * attempted to close all client connections. All client connections 309 * are closed except possibly those that generated exceptions when the 310 * server attempted to close them. 311 * @since 0.25.3 312 */ 313 @API( status = STABLE, since = "0.25.3" ) 314 public static final void disableRemoteAccess( final JMXServiceURL serviceURL ) throws IOException 315 { 316 final JMXConnectorServer connectorServer; 317 synchronized( m_ConnectorServers ) 318 { 319 connectorServer = m_ConnectorServers.remove( requireNonNullArgument( serviceURL, "serviceURL" ) ); 320 } 321 if( nonNull( connectorServer ) ) 322 { 323 /* 324 * Stopping the connector server will also unbind it from the 325 * registry automatically. We do not need to do explicitly here. 326 */ 327 connectorServer.stop(); 328 } 329 } // disableRemoteAccess() 330 331 /** 332 * <p>{@summary Enables the external access to the 333 * {@link MBeanServer}.}</p> 334 * <p>{@link #enableRemoteAccess(MBeanServer,String,int,int,Map)} 335 * and 336 * {@link #enableRemoteAccess(MBeanServer,int,Map)} 337 * are delegating to this method. See there for details.</p> 338 * 339 * @param mbeanServer The MBean server that should be exposed. 340 * @param serviceURL The service URL. 341 * @param registryPortNumber The port number that is used for the 342 * registry connection. 343 * @param environment The configuration settings for the 344 * {@link JMXConnectorServer}. 345 * @throws RemoteException The RMI registry cannot be created/exported. 346 * @throws IOException Failed to create the connection server. 347 * @throws IllegalStateException The connection server was previously 348 * stopped and the attempt to restart it failed. 349 * 350 * @since 0.25.3 351 */ 352 @API( status = INTERNAL, since = "0.25.3" ) 353 private static final void enableRemoteAccess( final MBeanServer mbeanServer, final JMXServiceURL serviceURL, final int registryPortNumber, final Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException 354 { 355 requireNonNullArgument( mbeanServer, "mBeanServer" ); 356 357 synchronized( m_ConnectorServers ) 358 { 359 var connectorServer = m_ConnectorServers.get( requireNonNullArgument( serviceURL, "serviceURL" ) ); 360 if( isNull( connectorServer ) ) 361 { 362 //---* Start the RMI registry *------------------------------------ 363 startRMIRegistry( registryPortNumber ); 364 365 connectorServer = newJMXConnectorServer( serviceURL, environment, mbeanServer ); 366 connectorServer.start(); 367 m_ConnectorServers.put( serviceURL, connectorServer ); 368 } 369 else 370 { 371 if( !connectorServer.isActive() ) 372 { 373 try 374 { 375 connectorServer.start(); 376 } 377 catch( final IOException e ) 378 { 379 throw new IllegalStateException( "Cannot (re)start ConnectorServer on port %d (URL: %s)".formatted( registryPortNumber, serviceURL ), e ); 380 } 381 } 382 } 383 } 384 } // enableRemoteAccess() 385 386 /** 387 * <p>{@summary Enables the external access to the 388 * {@link MBeanServer} 389 * from a process running on the same machine.}</p> 390 * <p>Basically, this method creates a new instance of 391 * {@link JMXConnectorServer}, 392 * registers it to a 393 * {@linkplain Registry JNDI registry} 394 * associated with the given port number, and finally 395 * {@linkplain JMXConnectorServer#start() starts} 396 * it.</p> 397 * <p>If the registry does not exist yet, it will be created first.</p> 398 * <p>The same MBean server can be multiple times, using this method or 399 * {@link #enableRemoteAccess(MBeanServer,String,int,int,Map)}, 400 * but the given port numbers must be different.</p> 401 * 402 * @param mbeanServer The MBean server that should be exposed. 403 * @param registryPortNumber The port number that is used for the connection. 404 * @param environment The configuration settings for the 405 * {@link JMXConnectorServer} 406 * that is used to expose the MBean server. This parameter can be 407 * {@null}, although it is recommended to use 408 * {@link Map#of()} 409 * in case no attributes should be provided. The keys in this map must 410 * be Strings. The appropriate type of each associated value depends 411 * on the attribute. The contents of {@code environment} are not 412 * changed by this call. 413 * @return The 414 * {@link JMXServiceURL} 415 * that was used to register the MBean server. It has the format 416 * {@code service:jmx:rmi:///jndi/rmi://localhost:<port>/jmxrmi}. 417 * @throws RemoteException The RMI registry cannot be created/exported. 418 * @throws IOException Failed to create the connection server. 419 * @throws IllegalStateException The connection server was previously 420 * stopped and the attempt to restart it failed. 421 * 422 * @see #BIND_NAME 423 424 * @since 0.25.3 425 */ 426 @API( status = STABLE, since = "0.25.3" ) 427 public static final JMXServiceURL enableRemoteAccess( final MBeanServer mbeanServer, final int registryPortNumber, final Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException 428 { 429 final var retValue = composeServiceURL( registryPortNumber ); 430 enableRemoteAccess( requireNonNullArgument( mbeanServer, "mBeanServer" ), retValue, registryPortNumber, environment ); 431 432 //---* Done *---------------------------------------------------------- 433 return retValue; 434 } // enableRemoteAccess() 435 436 /** 437 * <p>{@summary Makes the 438 * {@link MBeanServer} 439 * accessible for remote machines.}</p> 440 * <p>Basically, this method creates a new instance of 441 * {@link JMXConnectorServer}, 442 * registers it to a 443 * {@linkplain Registry JNDI registry} 444 * associated with the given port number, and finally 445 * {@linkplain JMXConnectorServer#start() starts} 446 * it.</p> 447 * <p>If the registry does not exist yet, it will be created first.</p> 448 * <p>The same MBean server can be multiple times, using this method or 449 * {@link #enableRemoteAccess(MBeanServer, int, Map)}, 450 * but the given port numbers must be different.</p> 451 * 452 * @param mbeanServer The MBean server that should be exposed. 453 * @param hostName The host name that is used for the connection. 454 * @param registryPortNumber The port number that is used for the 455 * registry connection. 456 * @param dataPortNumber The port number that is used for the 457 * data transport. 458 * @param environment The configuration settings for the 459 * {@link JMXConnectorServer} 460 * that is used to expose the MBean server. This parameter can be 461 * {@null}, although it is recommended to use 462 * {@link Map#of()} 463 * in case no attributes should be provided. The keys in this map must 464 * be Strings. The appropriate type of each associated value depends 465 * on the attribute. The contents of {@code environment} are not 466 * changed by this call. 467 * @return The 468 * {@link JMXServiceURL} 469 * that was used to register the MBean server. It has the format 470 * {@code service:jmx:rmi://<host>:<dataPort>/jndi/rmi://<host>:<registryPort>/jmxrmi} 471 * @throws RemoteException The RMI registry cannot be created/exported. 472 * @throws IOException Failed to create the connection server. 473 * @throws IllegalStateException The connection server was previously 474 * stopped and the attempt to restart it failed. 475 * @throws MalformedURLException It is not possible to compose a valid 476 * {@link JMXServiceURL} 477 * with the given {@code hostName}. 478 * 479 * @see #BIND_NAME 480 * 481 * @since 0.25.3 482 */ 483 @API( status = STABLE, since = "0.25.3" ) 484 public static final JMXServiceURL enableRemoteAccess( final MBeanServer mbeanServer, final String hostName, final int registryPortNumber, final int dataPortNumber, Map<String,?> environment ) throws IllegalStateException, IOException, RemoteException 485 { 486 final var retValue = composeServiceURL( hostName, registryPortNumber, dataPortNumber ); 487 enableRemoteAccess( requireNonNullArgument( mbeanServer, "mBeanServer" ), retValue, registryPortNumber, environment ); 488 489 //---* Done *---------------------------------------------------------- 490 return retValue; 491 } // enableRemoteAccess() 492 493 /** 494 * <p>{@summary Ensures that an 495 * {@linkplain Registry RMI registry} 496 * is running for given the port.} If there is no registry, a new one will 497 * be started.</p> 498 * 499 * @param port The registry port. 500 * @throws RemoteException The registry cannot be created. 501 */ 502 private static final void startRMIRegistry( final int port ) throws RemoteException 503 { 504 try 505 { 506 //---* Check if RMI registry already exists *------------------ 507 /* 508 * LocateRegistry.getRegistry() returns only a stub or proxy, 509 * but does not verify whether the registry really exists. 510 * The following call Registry::list() enforces a connection 511 * with the registry – and fails with a RemoteException if the 512 * registry does not exist. 513 */ 514 final var registry = getRegistry( port ); 515 registry.list(); 516 } 517 catch( final RemoteException _ ) 518 { 519 //---* Create the RMI registry if it does not exist *---------- 520 createRegistry( port ); 521 } 522 } // startRMIRegistry() 523 524 /** 525 * Converts an instance of 526 * {@link NameValuePair} 527 * to a String. 528 * 529 * @param pair The name-value-pair. 530 * @return The String representation of the name-value-pair. 531 */ 532 private static final String toString( final NameValuePair<String> pair ) 533 { 534 if( isNull( pair.value() ) ) throw new IllegalArgumentException( "value is null" ); 535 final var retValue = "%1$s=%2$s".formatted( pair.name(), pair.value() ); 536 537 //---* Done *---------------------------------------------------------- 538 return retValue; 539 } // toString() 540 541 /** 542 * Unregisters the given MBean from the MBeanServer. All exceptions – if 543 * any – will be swallowed silently. 544 * 545 * @param mbean The mbean to unregister; may be {@null}. 546 */ 547 @API( status = STABLE, since = "0.0.1" ) 548 public static void unregisterQuietly( final JMXSupport<?> mbean ) 549 { 550 if( nonNull( mbean ) ) 551 { 552 try 553 { 554 mbean.unregister(); 555 } 556 catch( final InstanceNotFoundException | MBeanRegistrationException e ) 557 { 558 ifDebug( e ); 559 /* Deliberately ignored */ 560 } 561 } 562 } // unregisterQuietly() 563} 564// class ManagementUtils 565 566/* 567 * End of File 568 */