Dmitriy Setrakyan manages daily operations of GridGain Systems and brings over 12 years of experience to GridGain Systems which spans all areas of application software development from design and architecture to team management and quality assurance. His experience includes architecture and leadership in development of distributed middleware platforms, financial trading systems, CRM applications, and more. Dmitriy is a DZone MVB and is not an employee of DZone and has posted 54 posts at DZone. You can read more from them at their website. View Full User Profile

Refactor-Safe ToStringBuilder

04.10.2008
| 8578 views |
  • submit to reddit

We at GridGain recently were faced with a problem - how to make our toString() methods refactor-safe? Up until recently we were using simple toString() plugins for IDEA and Eclipse (Jutils) which generated toString() method automatically based on class fields. Then developer would have to tweak the generated code to remove fields that should not be included.

However, faced with numerous support questions, we noticed that sometimes during refactoring a developer would forget to add a new field to existing toString() method or print out too much and clutter up the log. Surprisingly, there is no open source library that supports this basic functionality (ToStringBuilder from Apache is not even close), so we had to implement our own. So, to summarize, the functionality we needed is this:

  • Make sure that new fields are automatically included.
  • Make sure that certain classes, like Object, Collection, Array are automatically excluded.
  • Provide class-level overrides of default rules, which will include auto-excluded fields and vice versa.
  • Provide support for custom ordering of fields in toString() output.

Here is the design we came out with:

@GridToStringInclude Annotation This annotation can be attached to any field in the class to make sure that it is automatically included even if it is excluded by default.

@ToStringExclude Annotation This annotation can be attached to any field in the class to make sure that it is automatically excluded even if it is included by default.

@ToStringOrder(int) Annotation This annotation provides custom ordering of class fields. Fields with smaller order value will come before in toString() output. By default the order is the same as the order of field declarations in the class.

ToStringBuilder Class This class is responsible for reflectively parsing all fields in class hierarchy, caching all annotations for performance reasons, and properly outputting toString() content.

So, here is an example of a class that uses this simple framework:

public class MySimpleClass {
/**
* This field would be included by
* default, but is excluded due to
* @ToStringExclude annotation.
*/
@ToStringExclude
private int intField = 1;

/**
* This field will be included
* first for toString() purposes.
*/
private String strField = "TestString";

/**
* This array field would be excluded
* by default, but is included due to
* @ToStringInclude annotation.
*/
@ToStringInclude
private int[] intArr = new int[] { 1, 2, 3 };

/**
* This field is excluded by default.
*/
private Object obj = new Object();

/**
* Generic toString() implementation.
*/
@Override
public String toString() {
return ToStringBuilder.toString(MySimpleClass.class, this);
}
}

The toString() output of the class above will look as follows:

MySimpleClass [strField=TestString, intArr={1,2,3}]

The complete source code is available in our public WebSVN . When clicking on this link you will be prompted with login popup. Just enter "guest" for username and leave the password blank. The source code is in org.gridgain.grid.utils.toString package.

Enjoy!

0
Your rating: None
Published at DZone with permission of Dmitriy Setrakyan, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Gil Collins replied on Thu, 2008/04/10 - 6:29am

Great idea, thank you for the example however,

When compiling the example above ToStringBuilder is not found is this the Apache implementation or something else? After commenting out that method then @ToStringInclude and Exclude are not found is your example missing something else?

Thank you again.

Dmitriy Setrakyan replied on Thu, 2008/04/10 - 11:38am in response to: Gil Collins

This ToStringBuilder is implemented and used internally by GridGain. We are an LGPL Open Source grid computing product, so you are free to look at the source and copy it for your own projects.

For full source code, click on the WebSVN link at the bottom of the initial post and follow the login directions specified. However, the source code has "Grid" prefix in front of every class, so ToStringBuilder will be called GridToStringBuilder (just the naming convention we have at GridGain).

Best,
Dmitriy Setrakyan
GridGain - Grid Computing Made Simple

Dmitriy Setrakyan replied on Thu, 2008/04/10 - 12:05pm in response to: Gil Collins

I should also mention that you can get the source code by downloading GridGain which comes with full source code for the project and tests.

Best,
Dmitry Setrakyan
GridGain - Grid Computing Made Simple

Marc Stock replied on Thu, 2008/04/10 - 3:31pm

[quote]how to make our toString() methods refactor-safe?[/quote]

 

Ever heard of ReflectionToStringBuilder?  It allows you to exclude specific fields.  I think I'd much rather use that than clutter my code with a lot of annotations.

Dmitriy Setrakyan replied on Thu, 2008/04/10 - 3:44pm in response to: Marc Stock

You automatically assume that I have not heard of ReflectionToStringBuilder while I have a link to it in the initial post (I assume you are talking about Apache commons). From what I have seen, the only fields you can exclude with that are transient fields.

Our ToStringBuilder does not require you to put any annotations into your code - it just works and 99% of the time the default toString() output it produces is exactly what you want. Annotations are provided for the cases when you would like to override the default inclusion/exclusion policy.

Best,
Dmitriy Setrakyan
GridGain - Grid Computing Made Simple

 

Marc Stock replied on Thu, 2008/04/10 - 4:16pm

You are not restricted to transients.   All you have to do is call:

ReflectionToStringBuilder.toStringExclude(Object object, String[] excludeFieldNames)

 This I believe accomplishes your original goal of making a toString that will work reliably with refactoring.    Optionally, you can just extend that class and set some defaults like: don't output transients, don't output statics, always exclude fields named X, etc.).  Style rules can be defaulted as well.  The only thing I'm not sure about is whether you can specify an arbitrary order for the fields.

Dmitriy Setrakyan replied on Thu, 2008/04/10 - 4:46pm in response to: Marc Stock

It only solves it to an extent. For example, what if a field gets renamed? I know you can also automatically rename all text occurencies in most IDEs, but I usually use that feature with extreme caution. Annotations just seem like the easiest and most reliable way to go here.

On top of that, apache approach is a lot harder to use compared to one-liner toString() implementation in my example.

Best,
Dmitriy Setrakyan
GridGain - Grid Computing Made Simpe

Marc Stock replied on Thu, 2008/04/10 - 5:10pm

Your point about the renaming is valid.  You have to be careful.  Depending on the name and scope, it may or may not be trivial.  I don't worry about it in IntelliJ because it's easy and safe to undo massive refactorings if you need to.  It's not so reliable in Eclipse.

 Have you done any performance comparisons between your framework and Apache's?  Apache's is slow but I know you're going to pay something for using all that reflection.

Dmitriy Setrakyan replied on Thu, 2008/04/10 - 5:25pm in response to: Marc Stock

We have not compared performance to Apache, but we compared it to standard toString() implementation using java.lang.StringBuilder. The price is that our ToStringBuilder is about 20% slower, but it's good enough for us since toString() methods are mostly used in debug mode where performance does not really matter.

Our ToStringBuilder does as much caching as possible, stopping short of caching the actual java.lang.Class objects which would not be safe due to hot-redeployment issues (we don't want to keep undeployed classes in the cache).

Best,
GridGain - Grid Computing Made Simple

Soylent Green replied on Sun, 2008/04/13 - 4:15am in response to: Dmitriy Setrakyan

Dmitriy, thanks for your post. I think we have here a good approach but I think there are some areas of improvement.

1.) For sure we use Logging mostly in Debug-Mode. But sometimes we use Logging also to be able to figure out problems in a productive environment. And then we would come across a lot of 20% more costly toString Methods, which is in my opinion a bad idea. By the way you only mentioned time-cost. I'm sure that your ToStringBuilder will not clutter the VM with lots of garbage, right? 

2.) Let's face it: If your fellows tend to forget fields in toString they will forget it in equals and hashCode, too. Hopefully you don't suggest to apply your approach to hashCode/equals, do you?

3.) Last but not least - My suggestion: Let your API spend effort elsewhere. Move from Runtime to...

3.1) Compile-Time: Use your annotations to automatically create code for the toString methods, code that follows broadly accepted coding schemes (like the eclipse toString-Helper). Adding a field means compiling anyway, so just add an extra step into your ant build script or whatever tool you use to build. Nice additional effect - that would spare you the liability to fiddle around with securitymanager like fore ReflectiveToStringBuilder.

3.2) Classloading-Time: Use Bytecode Instrumentation (ASM) to inject a toString method on the fly using your annotations.

Maybe these two approaches would also work for equals and hashCode? Just an idea, not well thought out... 

4.) Don't call it framework. Sorry this is my personal battle against all those tiny little APIs that claim to be a framework. Let's just call it what it is: A utillity. JEE or OSGi are frameworks.

Regrads, 

Peter 

 

Artur Biesiadowski replied on Sun, 2008/04/13 - 12:07pm

20% slower???? When I read that I knew something is fishy. I have done some tests myself and here is the result:

Manual 821ms
GridToString 3529ms

(this is server jdk 6, after warmup phase).

With client jvm, it is

Manual 1457ms
GridToString 8109ms


It is 4-6 times slower for a simple test I have performed. I'm quite sure that there are even worse cases. I'm also sure that you are able to come up with some border case where it will be only 20% slower (like putting one field with array of 10000 integers), but let's talk about middle ground cases.

I have nothing against you or your utility - it is very nice thing for debugging/fast prototyping. I'm just more and more angry with people claiming some unreasonable benchmarks to promote their stuff. It was happening with javolution, it is happening with groovy, now you join the crowd.

 





import org.gridgain.grid.util.tostring.*;


public class Test {

@GridToStringInclude
private int x = 1;
@GridToStringInclude
private int y = 2;
@GridToStringInclude
private int z = 3;
@GridToStringInclude
private double a = 5.4343;
@GridToStringInclude
private String txt = "Testme";


public String manualString() {
return "Test [x="+x + ", y="+ y + ", z="+z + ", a="+a+", txt="+txt+"]";
}

public String autoString() {
return GridToStringBuilder.toString(Test.class, this);
}

static final int INTERATIONS = 1000000;

public static void main(String[] args) {

Test t = new Test();

long start;
for ( int i =0; i < 10; i++ ) {
start = System.currentTimeMillis();
if (testManual(t) == 0 ) {
throw new Error("Wrong");
}
System.out.println("Manual " + (System.currentTimeMillis()-start));

start = System.currentTimeMillis();
if ( testAuto(t) == 0 ) {
throw new Error("Wrong");
}
System.out.println("Auto " + (System.currentTimeMillis()-start));
}

}

private static long testAuto(Test t) {
long length = 0;
for (int i =0; i < INTERATIONS; i++ ) {
length += t.autoString().length();
}
return length;
}

private static long testManual(Test t) {
long length = 0;
for (int i =0; i < INTERATIONS; i++ ) {
length += t.manualString().length();
}
return length;
}


}

Dmitriy Setrakyan replied on Sun, 2008/04/13 - 2:28pm in response to: Artur Biesiadowski

First of all I want to note that I didn't post this ToStringBuilder here to get into any performance wars. It's a utility we use internally at GridGain and I just thought it would be useful to share it with DZone readers.

Secondly, people who perform benchmark comparisons should keep in mind that most likely their assumptions are wrong (just like in this case). I believe that the problem with test above is that manualToString() method generates shorter strings than auto ToStringBuilder (there may be other issues that I didn't notice).

Below is the code we have tested in our project. The TestClass is just an ordinary class that uses some of our annotations. As you see, the manual test takes about 773ms and the auto test takes about 1076ms, which shows that my initial 20% overhead claim was correct.

Here is the correct test that actually does compare apples to apples:

class TestClass {
@GridToStringOrder(0)
private String id = "1234567890";

private int intVar;

private long longVar;

@GridToStringOrder(1)
private final UUID uuidVar = UUID.randomUUID();

private boolean boolVar;

private byte byteVar;

private String name = "qwertyuiopasdfghjklzxcvbnm";

private final Integer finalInt = 2;

private List<String> strList = null;

@GridToStringInclude
private Map<String, String> strMap = null;

private final Object obj = new Object();

private ReadWriteLock lock = null;

/**
* Manual toString() implementation.
*/
String toStringManual() {
StringBuilder buf = new StringBuilder();

buf.append(getClass().getSimpleName()).append(" [");

buf.append("id=").append(id).append(", ");
buf.append("uuidVar=").append(uuidVar).append(", ");
buf.append("intVar=").append(intVar).append(", ");
buf.append("longVar=").append(longVar).append(", ");
buf.append("boolVar=").append(boolVar).append(", ");
buf.append("byteVar=").append(byteVar).append(", ");
buf.append("name=").append(name).append(", ");
buf.append("finalInt=").append(finalInt).append(", ");
buf.append("strMap=").append(strMap);

buf.append("]");

return buf.toString();
}

/**
* Automatic toString() implementation.
*/
String toStringAutomatic() {
return GridToStringBuilder.toString(TestClass1.class, this);
}
}
Here is the test method:
public void testToStringPerformance() {
TestClass obj = new TestClass();

// Logger we use for testing.
GridLogger log = getLogger();

// Warm up.
obj.toStringAutomatic();

long start = System.currentTimeMillis();

for (int i = 0; i < 100000; i++) {
obj.toStringManual();
}

log.info("Manual toString() took: " + (System.currentTimeMillis() - start) + "ms.");

start = System.currentTimeMillis();

for (int i = 0; i < 100000; i++) {
obj.toStringAutomatic();
}

log.info("Automaic toString() took: " + (System.currentTimeMillis() - start) + "ms.");
}

 

The output from the above test:

[GridToStringBuilderSelfTest] >>> Starting test: testToStringPerformance <<<
[GridToStringBuilderSelfTest] Manual toString() took: 773ms.
[GridToStringBuilderSelfTest] Automaic toString() took: 1076ms.
[GridToStringBuilderSelfTest] >>> Stopping test: testToStringPerformance <<<

 

Best,
GridGain - Grid Computing Made Simple

Artur Biesiadowski replied on Sun, 2008/04/13 - 2:54pm in response to: Dmitriy Setrakyan

First point - if you add

System.out.println(t.autoString());
System.out.println(t.manualString());

at start of my main method, can you spot any differences in string producted? I have used gridgain.jar 2.0.2 and the strings produced look exactly the same to me.

Second, never do a single run of test method.  If you have testToStringPerformance method, call it from outside few times and disregard the first run. In other case, you will see the costs of the interpreted method and compilation in the results.

 

When running your benchmark, I'm getting

Manual toString() took: 670ms.
Automaic toString() took: 909ms.

on first run and

Manual toString() took: 277ms.
Automaic toString() took: 625ms.

on following runs with server compiler.

With client on first run:

Manual toString() took: 406ms.
Automaic toString() took: 1364ms.

And on following runs:

Manual toString() took: 377ms.
Automaic toString() took: 1327ms.

So, with exception of non-optimized first run on server jvm, I'm getting 3-4 times slowdown compared to manual toString.

I can now see from where your confusion about 20% came - if you run test loop once on server compiler, it really looks like it is the case. But it is clear that it is an artifact of microbenchmark of single method call - in reality your toString is few times slower (3-6 times in our 2 examples), depending on the complexity of the class handled and server/client jvm.

Which doesn't mean that your class is not useful - I'm disputing only 20% slowdown claim, nothing else.

Dmitriy Setrakyan replied on Sun, 2008/04/13 - 3:26pm in response to: Artur Biesiadowski

Absolutely last post on this matter.

  1. I tried doing what you suggested, and on the worst runs I never got a performance slower than 2 times.
  2. As far as string length in your test, simply compare the length returned by your methods and you will see that string output in your manual methods has less characters than in automatic methods.

Remember, the purpose of this utility is not performance (although performance does matter to an extent), but to provide consistent and refactor-safe output in Debug mode. I am sure that performance of our utility is not slower (and most likely much faster) than other comparable implementations on the market.

If you like, try implementing your own. I doubt you will manage to beat our performance numbers.

Best,
GridGain - Grid Computing Made Simple

 

 

 

 

Artur Biesiadowski replied on Sun, 2008/04/13 - 5:19pm in response to: Dmitriy Setrakyan

[quote=dsetrakyan]

As far as string length in your test, simply compare the length returned by your methods and you will see that string output in your manual methods has less characters than in automatic methods.

[/quote]

I suppose that we are testing against different versions of gridgain. For me it is a string

Test [x=1, y=2, z=3, a=5.4343, txt=Testme]

and total length of

42000000

in both cases.

[quote] 

Remember, the purpose of this utility is not performance (although performance does matter to an extent), but to provide consistent and refactor-safe output in Debug mode. I am sure that performance of our utility is not slower (and most likely much faster) than other comparable implementations on the market.

If you like, try implementing your own. I doubt you will manage to beat our performance numbers.

[/quote]

I have never said you are slower than other implementations of same idea and I have stressed in each of my posts that your utility is still useful for debugging. I don't think that even few times slowdown in synthetic test makes it not performant enough for real world usage. I have managed to convince you of 100% slowdown and let's leave it at that number - over that it is too dependent on CPU/JVM version used.

As far as implementing something faster, I'll pass, thank you ;)  I think that your code is very fast for something not using dynamic code generation.

Ricky Clarkson replied on Mon, 2008/04/14 - 6:23am

If you start from somewhere better than Java you don't even have the problem.

scala> case class Foo(bar: String)
defined class Foo

scala> println(Foo("hello"))
Foo(hello)

The toString() override is generated by the compiler.

Marc Stock replied on Mon, 2008/04/14 - 9:24am

@Ricky:

 1) The library in question has more features than the default functionality of Scala.

2) Nobody is going to re-write their code to another language to get better toString() support.

3) Did you seriously suggest people use another language to solve this problem?  Wow, that's brilliant.   

Ricky Clarkson replied on Mon, 2008/04/14 - 11:51am in response to: Marc Stock

Marc,

1. I didn't say otherwise.  I was commenting on one feature.

2. Sure, but small reasons add up.

3. No, you appear to have misread what I wrote.  I described a feature, I didn't preach that you should use Scala. 

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.