Sun Java Solaris Communities My SDN Account Join SDN
 
Core Java Technologies Tech Tips

Logging Localized Messages and Attach API

 
In This Issue

Welcome to the Core Java Technologies Tech Tips for August 2007. Core Java Technologies Tech Tips provides tips and hints for using core Java technologies and APIs in the Java Platform, Standard Edition 6 (Java SE 6).

You can now read the Core Java Technologies Tech Tips online as a web log.

In this issue, you'll find tips for the following:

» Logging Localized Messages
» Attach API

These tips were developed using Java SE 6. You can download the Java Platform, Standard Edition 6 Development Kit (JDK 6), to use these and future tech tips, from the Java SE Downloads page.

The author of this month's tips is John Zukowski, president and principal consultant of JZ Ventures, Inc.

Logging Localized Messages

The Logging API was last covered in the October 22, 2002 tip Filtering Logged Messages. While the API hasn't changed much since being introduced with the 1.4 release of Java SE, there are two things many people don't realize when they log a message with something like the following:

logger.log(level, message);
 

First, the message argument doesn't have to be a hard-coded string. Second, the message can take arguments. Internally, relying on MessageFormat, the logger will take any arguments passed in after the log message string and use them to fill in any blanks in the message. The first argument after the message string argument to log() will have index 0 and is represented by the string {0}. The next argument is {1}, then {2}, and so on. You can also provide additional formatting details, like {1, time} would show only the time portion of a Date argument.

To demonstrate, here's how one formatted log message call might look:

String filename = ...;
String message = "Unable to delete {0} from system.";
logger.log(level, message, filename);
 

Now, for filename GWBASIC.EXE, the message displayed would be "Unable to delete GWBASIC.EXE from system."

On its own, this isn't too fancy a deal. Where this extra bit of formatting really comes in handy is when you treat the message argument as a lookup key into a resource bundle. When you fetch a Logger, you can either pass in only the logger name, or both the name and a resource bundle name.

By combining messages fetched from a resource bundle with local arguments, you get all the benefits of localized, parameterized messages, not just in your programs, but in your log messages as well.

To treat the message argument as a lookup key to a resource bundle, the manner of fetching the Logger needs to change slightly. If you want to use resource bundles, avoid creating a Logger object like this:

private static Logger logger =
    Logger.getLogger("com.example");
 

Instead, add an optional second argument to the getLogger() call. The argument is the resource bundle that contains localized messages. Then, when you make a call to log a message, the "message" argument is the lookup key into the resource bundle, whose name is passed to the getLogger() call.

private static final String BUNDLE_NAME = "com.example.words";
private static Logger logger =
    Logger.getLogger("com.example", BUNDLE_NAME);
 

The BUNDLE_NAME resource bundle must include the appropriate message for the key provided to the logging call:

logger.log(level, "messageKey");
 

If "messageKey" is a valid key in the resource bundle, you now have the associated message text logged to the Logging API. That message text can include those {0}-like arguments to get your message arguments passed into the logger.

String filename = ...;
logger.log(level, "messageKey", filename);
 

While you don't see the {0} formatting string in "messageKey", since its value was acquired from the resource bundle, you could get your output formatted with MessageFormat again.

Let us put all the pieces together. We'll create a small application that shows localized logging.

To keep things simple, these resource bundles will be PropertyResourceBundle objects instead of ListResourceBundle objects.

Create file messages.properties in the local directory to include the messages for the default locale, assumed to be US English.

Message1=Hello, World
Message2=Hello, {0}
 

The second language will be Spanish. Place the following in the file messages_ES.properties:

Message1=Hola, mundo
Message2=Hola, {0}
 

Now, we have to create the application. Notice that the getAnonymousLogger() method also includes a second version that accepts a resource bundle name. If you want to use a named logger, feel free to pass in the name and use getLogger() instead.

import java.util.logging.*;

public class LocalLog {
  private static Logger logger = 
      Logger.getAnonymousLogger("message");
  public static void main(String argv[]) {
    logger.log(Level.SEVERE, "Message1");
    logger.log(Level.SEVERE, "Message2", "John");
  }
}
 

The LocalLog program's log messages are "Message1" and "Message2". When run with the default locale, you'll get messages similar to the following:

> java LocalLog
Aug 4, 2007 12:00:35 PM LocalLog main
SEVERE: Hello, World
Aug 4, 2007 12:00:35 PM LocalLog main
SEVERE: Hello, John
 

To run the program with a different locale, set the user.language system property on the command line:

>java -Duser.language=ES LocalLog
ago 4, 2007 12:01:18 p.m. LocalLog main
GRAVE: Hola, mundo
ago 4, 2007 12:01:18 p.m. LocalLog main
GRAVE: Hola, John
 

Notice that your log message contains the localized resource bundle message, and the logger also uses localized date strings and localized severity level text.

Keep this feature in mind to create localized log messages. You can use resource bundles to provide both localized log messages and user interface text. Those reading log files should be able to see translated text, too.

For additional information on resource bundles, see the Resource Bundle Loading tip and the Isolating Locale-Specific Data lesson in The Java Tutorial.

Attach API

When working with the Java platform, you typically program with the standard java.* and javax.* libraries. However, those aren't the only things that are provided for you with Sun's Java Development Kit (JDK). Several additional APIs are provided in a tools.jar file, found in the lib directory under your JDK installation directory. You'll find support for extending the javadoc tool and an API called the Attach API.

As the name may imply, the Attach API allows you to attach to a target virtual machine (VM). By attaching to another VM, you can monitor what's going on and potentially detect problems before they happen. The Attach API classes are found in the com.sun.tools.attach and com.sun.tools.attach.spi packages, though you'll typically never directly use the com.sun.tools.attach.spi classes.

Even including the one class in the .spi package that you won't use, the whole API includes a total of seven classes. Of that, three are exception classes and one a permission. That doesn't leave much to learn about, only VirtualMachine and its associated VirtualMachineDescriptor class.

The VirtualMachine class represents a specific Java virtual machine (JVM) instance. You connect to a JVM by providing the VirtualMachine class with the process id, and then you load a management agent to do your customized behavior:

VirtualMachine vm = VirtualMachine.attach (processid);
String agent = ...
vm.loadAgent(agent);
 

The other manner of acquiring a VirtualMachine is to ask for the list of virtual machines known to the system, and then pick the specific one you are interested in, typically by name:

String name = ...
List vms = VirtualMachine.list();
for (VirtualMachineDescriptor vmd: vms) {
    if (vmd.displayName().equals(name)) {
        VirtualMachine vm = VirtualMachine.attach(vmd.id());
        String agent = ...
        vm.loadAgent(agent);
        // ...
    }
}
 

Before looking into what you can do with the agent, there are two other things you'll need to consider. First, the loadAgent() method has an optional second argument to pass settings into the agent. As there is only a single argument here to potentially pass multiple options, multiple arguments get passed in as a comma-separated list:

vm.loadAgent (agent, "a=1,b=2,c=3");
 

The agent would then split them out with code similar to the following, assuming the arguments are passed into the agent's args variable.

String options[] = args.split(",");
for (String option: options)
    System.out.println(option);
}
 

The second thing to mention is how to detach the current virtual machine from the target virtual machine. That's done via the detach() method. After you load the agent with loadAgent(), you should detach().

A JMX agent exists in the management-agent.jar file that comes with the JDK. Found in the same directory as tools.jar, the JMX management agent allows you to start the remote JMX agent's MBean Server and get an MBeanServerConnection to that server. And, with that, you can list things like threads in the remote virtual machine.

The following program does just that. First, it attaches to the identified virtual machine. It then looks for a running remote JMX server and starts one if not already started. The ,code>management-agent.jar file is specified by finding the java.home of the remote virtual machine, not necessarily the local one. Once connected, the MBeanServerConnection is acquired, from which you query the ManagementFactory for things like threads or ThreadMXBean as the case may be. Lastly, a list of thread names and their states are displayed.

import java.lang.management.*;
import java.io.*;
import java.util.*;
import javax.management.*;
import javax.management.remote.*;
import com.sun.tools.attach.*;

public class Threads {

  public static void main(String args[]) throws Exception {
    if (args.length != 1) {
      System.err.println("Please provide process id");
      System.exit(-1);
    }
    VirtualMachine vm = VirtualMachine.attach(args[0]);
    String connectorAddr = vm.getAgentProperties().getProperty(
      "com.sun.management.jmxremote.localConnectorAddress");
    if (connectorAddr == null) {
      String agent = vm.getSystemProperties().getProperty(
        "java.home")+File.separator+&qut;lib&qut;+File.separator+
        "management-agent.jar";
      vm.loadAgent(agent);
      connectorAddr = vm.getAgentProperties().getProperty(
        "com.sun.management.jmxremote.localConnectorAddress");
    }
    JMXServiceURL serviceURL = new JMXServiceURL(connectorAddr);
    JMXConnector connector = JMXConnectorFactory.connect(serviceURL); 
    MBeanServerConnection mbsc = connector.getMBeanServerConnection(); 
    ObjectName objName = new ObjectName(
      ManagementFactory.THREAD_MXBEAN_NAME);
    Set<ObjectName> mbeans = mbsc.queryNames(objName, null);
    for (ObjectName name: mbeans) {
      ThreadMXBean threadBean;
      threadBean = ManagementFactory.newPlatformMXBeanProxy(
        mbsc, name.toString(), ThreadMXBean.class);
      long threadIds[] = threadBean.getAllThreadIds();
      for (long threadId: threadIds) {
        ThreadInfo threadInfo = threadBean.getThreadInfo(threadId);
        System.out.println (threadInfo.getThreadName() + &qut; / &qut; +
            threadInfo.getThreadState());
      }
    }
  }
}
 

To compile this program, you need to make sure you include tools.jar in your CLASSPATH. Assuming JAVA_HOME is set to the Java SE 6 installation directory, of which the Attach API is a part, the following line will compile your program:

> javac -cp %JAVA_HOME%/lib/tools.jar Threads.java
 

From here, you could run the program, but there is nothing to attach to. So, here's a simple Swing program that displays a frame. Nothing fancy, just something to list some thread names you might recognize.

import java.awt.*
import javax.swing.*

public class MyFrame {
  public static void main(String args[]) {
    Runnable runner = new Runnable() {
      public void run() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 300);
        frame.setVisible(true);
      }
    };
    EventQueue.invokeLater(runner);
  }
}
 

Run the MyFrame program in one window and be prepared to run the Threads program in another. Make sure they share the same runtime location so the system can connect to the remote virtual machine.

Launch MyFrame with the typical startup command:

> java MyFrame
 

Then, you need to find out the process id of the running application. That's where the jps command comes in handy. It will list the process ids for all virtual machines started for the Java runtime directory you are using. Your output, specifically the process ids, will probably be different:

> jps
5156 Jps
4276 MyFrame
 

Since the jps command is itself a Java program, it shows up in the list, too. Here, 4276 is what is needed to pass into the Threads program. Your id will most likely be different. Running Threads then dumps the list of running threads:

> java -cp %JAVA_HOME%/lib/tools.jar;. Threads 4276

JMX server connection timeout 18 / TIMED_WAITING
RMI Scheduler(0) / TIMED_WAITING
RMI TCP Connection(1)-192.168.0.101 / RUNNABLE
RMI TCP Accept-0 / RUNNABLE
DestroyJavaVM / RUNNABLE
AWT-EventQueue-0 / WAITING
AWT-Windows / RUNNABLE
AWT-Shutdown / WAITING
Java2D Disposer / WAITING
Attach Listener / RUNNABLE
Signal Dispatcher / RUNNABLE
Finalizer / WAITING
Reference Handler / WAITING
 

You can use the JMX management agent to do much more than just list threads. For instance, you can call the findDeadlockedThreads() method of ThreadMXBean to find deadlocked threads.

Creating your own agent is actually rather simple. Similar to how applications require a main() method, agents have an agentMain() method. This isn't a part of any interface. The system just knows to look for one with the right argument set as parameters.

import java.lang.instrument.*

public class SecretAgent {
   public static void agentmain(String agentArgs, 
       Instrumentation instrumentation) {
     // ...
   }
}
 

To use the agent, you must then package the compiled class file into a JAR file and specify the Agent-Class in the manifest:

  Agent-Class: SecretAgent
 

The main() method of your program then becomes a little shorter than the original Threads program since you don't have to connect to the remote JMX connector. Just be sure to change the JAR file reference there to use your newly packaged agent. Then, when you run the SecretAgent program, it will run the agentmain() method right at startup, even before the application's main() method is called. Like Applet, there are other magically named methods for doing things that are not part of any interface.

Use the Threads program to monitor more virtual machines and try to get some threads to deadlock to show how you can still communicate with the blocked virtual machine.

For a look at the API documentation for the Attach API. Consider also reading up on the Java Virtual Machine Tool Interface (JVM TI). It requires the use of the Attach API.

Developer Assistance

Need programming advice on Java SE? Try Developer Expert Assistance.
 

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.

Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.