ObjectSnoop
 
Inspecting objects in Java  
 

Chapter 3. Using ObjectSnoop in Java programs

ObjectSnoop can be incorporated into a program to provide detailed information about the state of an object at runtime. The most likely destination of this information is a log file for examination either while the program is running, or after execution is complete.

Factory methods are provided to help hide the amount of construction needed to get snooping. Before being introduced to the factory classes, each of the core classes is briefly described. This should give you enough information to get you started on using them, and hopefully give you the incentive to use the factory methods instead.

Before you can use ObjectSnoop you'll need to make sure that it's installed on your system. The procedure for doing this is described in Chapter 2. If you're interested in a more detailed discussion of the structure of, and relationships between, the classes then you might want to look at Chapter 7.

3.1. Importing classes

All of the ObjectSnoop classes are rooted under the net.sourceforge.objectsnoop [1] package, and it is intended that you need only import one package (if at all!)

3.1.1. The standard Java way

The conventional way of importing classes is to include an import statement in your source file. The import statement for ObjectSnoop looks like this:

import net.sourceforge.objectsnoop.*;

This needs to be done before you've even added one line of debugging code. It can also be mildly annoying when you need to clean up and remove ObjectSnoop code: you've got to edit the file in at least two places.

3.1.2. The quick and dirty import

If you want to bypass the need for an import statement, you can qualify any ObjectSnoop classes with their full package path. Here's an example that accesses the FilterFactory class:

net.sourceforge.objectsnoop.FilterFactory.add("my_filter", new MyFilter());

While there's less typing when you need to use a single class, this could become cumbersome if you want to add debugging statements into multiple parts of your code.

3.2. Directing the snoop

All of the snooping is done by one class, the SnoopDirector. The director accepts objects for snooping, and the addition of custom filters for special interpretation of those objects.

Snooped output from the director needs to go somewhere, and needs to be tailored to any special formatting requirements. It would be cumbersome to update the director each time a new output format was created, so instead the director needs to be prepared with, and connected to, a formatter.

3.2.1. Creating a director

The director class, SnoopDirector, needs to be connected with a formatter during construction.

ISnoopFormatter formatter = new HTMLFormatter(new PrintWriter(System.out));
SnoopDirector snoopDirector = new SnoopDirector(formatter);

This needs two import statements to get this code running:

import net.sourceforge.objectsnoop.*;
import net.sourceforge.objectsnoop.format.*;

You might be looking forward to the factory methods already (Section 3.4).

3.2.2. Snooping with a director

Snooping involves less work than creating the director, you just need to pass the object to the snoop() method.

snoopDirector.snoop(snoopDirector);

If you want to add your own descriptive comment then try this instead:

snoopDirector.snoop(snoopDirector, "SnoopDirector self-examination");

As the snoop() method also returns a reference to the snoopDirector, we can also chain the snooping together:

snoopDirector.snoop(snoopDirector).snoop(this);

3.2.3. A small example

Here's a simple example that gathers all this together. It creates a director and connect it to an HTML formatter that writes to a file:

import java.io.FileOutputStream;
import java.io.PrintWriter;

import net.sourceforge.objectsnoop.SnoopDirector;
import net.sourceforge.objectsnoop.format.HTMLFormatter;

public TestClass {
   public static void main(String[] arg) {
      FileOutputStream fos = null;
      try {
         fos = new FileOutputStream("./TestClass.html", false);
      } catch (Exception e) {
         System.err.println("File creation error");
         return;
      }

      PrintWriter pr = new PrintWriter(fos);
      SnoopDirector sd = new SnoopDirector(new HTMLFormatter(pr));
      pr.println("<html><head><title>Snoop</title></head><body>");
      sd.snoop(sd, "SnoopDirector");
      pr.println("</body></html>");
      pr.close();
   }
}

Take a look at the factory methods (Section 3.4) for an easier way to do this.

3.3. Structure imposed by the formatter

A formatter gives structure to the information produced by the director; structure imposed by the limitations of the medium receiving and/or displaying the data. It is unneccessary, expansive, and error prone to program the director with this knowledge.

Formatters themselves need to connect to the place where they write their output. By way of example, the HTMLFormatter class accepts any class derived from java.io.Writer. This could connect the formatter to a text console, a file, or any other kind of writable data stream.

Here's an earlier example showing how the HTMLFormatter object is connected to System.out (itself a data stream object) via a PrintWriter:

ISnoopFormatter formatter = new HTMLFormatter(new PrintWriter(System.out));
SnoopDirector snoopDirector = new SnoopDirector(formatter);

Each formatter conforms to the the ISnoopFormatter interface. SnoopDirector uses this interface to describe objects in a structured and guaranteed way that any implementation of this interface can rely on.

3.4. Factory output: shrinkwrapped snooping

All the code that you've seen so far suggests that there's quite a fair bit of preparation required to do the snooping. There is. Fortunately it's possible to identify standard combinations of formatters (and filters too, more in Section 3.5), and make them available through factory methods.

A factory is, quite simply, a place where objects are manufactured for you to use. It allows the construction process to be hidden away so that you, the consumer, may use the product and then discard it. Using a factory requires only two lines of code, and even one line can be discarded with a little more typing.

3.4.1. Creating and consuming a director

The class responsible for doing all the construction is SnoopFactory. You can get it to construct a director for you by calling one of the supplied static methods. Here's the import statement you need to use the class:

import net.sourceforge.objectsnoop.SnoopFactory;

Now that you've got the factory imported, you can create a director that uses an HTMLFormatter. Not only can you create the director, but you can also use it at the same time by making use of the chaining we looked at earlier:

SnoopFactory.HTML(new PrintWriter(System.out)).snoop(this);

Notice that we've still got to supply the final destination for the output, but this is still much easier than importing the formatter class as well.

The return value of snoop() enables method chaining, and also allows you to store the SnoopDirector object in a variable for later use. Probably only needed if you're concerned about object creation, but its there if you want to take advantage of it.

3.5. Special objects, special filtering

When an object is from a family of interchangable classes then the implementation details are usually less important than how the object behaves when used. A developer is likely to be more interested in data returned by a set of getter methods than seeing the raw data contained in the internal representation.

Filters in ObjectSnoop allow this specialized interpretation to take place. Object or interface filters can be added to an existing SnoopDirector and immediately take effect: detecting when a matching object is snooped and stepping in to provide a customized description of the object to the formatter.

The list of available filters currently built into ObjectSnoop is described in some detail in Chapter 5.

3.5.1. Using your own filters

Once you've created your own filter you can give it to the instance of the SnoopDirector that you're using and let it get on with the job:

snoopDirector.filter(new MyFilter());

The filter() method actually returns the snoopDirector object, so you can do more method chaining with this too:

snoopDirector.filter(new MyFilter()).snoop(this);

3.5.2. A built in factory of filters

There's also a pre-constructed set of filters built into ObjectSnoop. They're available for use alongside your own filters, and can be identified by unique string. These filters are accessible through static methods implemented in the (appropriately named) FilterFactory class.

The use of named filters is another trick that helps reduce the overhead of importing additional classes or packages into your class. Suppose you know that a filter identified by the string my_filter existed. You could add it to your director like this:

snoopDirector.filter(FilterFactory.get("my_filter")).snoop(this);

In fact there's an even easier method: SnoopDirector is aware of the existance of the FilterFactory so you can write the above code in the following way:

snoopDirector.filter("my_filter").snoop(this);

3.5.3. Adding to the filter factory

You can initialize the FilterFactory with your own filters as well. This needs to be done (ideally) during the initialization of your program.

The following code adds your filter to the pool of those available, overriding any other filter in the pool of the same name:

FilterFactory.add("my_filter", new MyFilter());

Trying to use your named filter before you've added it to the FilterFactory isn't going to cause any crashes; any objects that would have matched just won't be filtered.

Any filter class added to the factory must meets several stringent requirements. The most obvious is the implementation of the ISnoopFilter interface. The most important, however, is "statelessness". This means that the filter doesn't store information in class instance variables. The factory serves up the same object time and time again, so thread-like issues around state may lead to unexpected behaviour.

3.5.4. Multiple filters and execution order

Once you've got the filter bug you'll find you're putting multiple filters into a single director. This won't cause a problem with the director, but you do need to know the order in which they're executed. The rule for this is a simple one:

The last filter added gets to check first.

This stack means that you can add filters that take precendence over any filters previously added to the director.

3.5.5. Filter collections

If you find yourself using several filters together all the time, then you can create a single composite filter that contains all of them. This kind of filter is usually derived from the FilterComposite base class, but you can also use method chaining to add a filter anonymously.

As you'd expect when adding filters to the composite, it is the last filter added that is given the first attempt on a snooped object.

An example of a filter derived from FilterComposite:

import net.sourceforge.objectsnoop.FilterComposite;

public class MyComposite extends FilterComposite {
   public MyComposite() {
      add(new MyFilter3());
      add(new MyFilter2());
      add(new MyFilter1());
   }
}

An example of an equivalent anonymous filter using method chaining:

FilterFactory.add("my_composite", new FilterComposite().add(new MyFilter3())
   .add(new MyFilter2()).add(new MyFilter1()));

In both examples the filters will be checked in the order 1, 2, 3.



[1] SourceForge® is a trademark of VA Software Corporation, and is used to root ObjectSnoop with permission.