Some time ago I was inspecting code for a Java-based system. While working on it, I decided it was a good opportunity to do a small Java reflection exercise, for summing up my findings in an anonymized, generalized form and to flex my Java muscles a little beyond of what the actual task was.
The Setup
Call chain / set-up visualization. |
- 3rd party framework calls your code (as java class plugin)
- your class sets up some parameters and calls (another) 3rd party library
- in some cases, there are seemingly random problems and system does not work as expected
This issue was quite easily solved by refreshing the customer for some basics of how Java class loaders work and how static fields in classes are handled between class (loader) instances. No heap dump analysis or special tools like Plumbr (spin-off w. my employer alumni, btw) required!
A Closer look
A problem in code is best explained through code, so here we have three classes:- ClassToTest,
- DataTypeEnum and
- StaticReflection.
ClassToTest
is a class with two private fields and one public method. It's representing the customer's class causing problems:
public class ClassToTest {
private static char staticChar = '!';
private char regularChar = '?';
public ClassToTest() {}
public void publicMethod(DataTypeEnum controlOption) {
switch (controlOption) {
case OPTION1:
// do something
System.out.println("Did something with Option 1");
break;
case OPTION2:
// before doing something, set controlling characters
staticChar = '-';
regularChar = '=';
// do something
System.out.println("Did something with Option 2");
break;
case OPTION3:
// do something
System.out.println("Did something with Option 1");
break;
}
}
}
There's really nothing special here: one of the private fields in this class is static and one is not.
Public method publicMethod(DataTypeEnum controlOption)
serves as an entry point (or a interface implementation) for the first mentioned 3rd party software to this code. Method sets some parameters (if necessary), and calls the second third party lib to do something.We have also a simple enumeration for control values. These values are provided by the calling software:
public enum DataTypeEnum {
OPTION1, OPTION2, OPTION3
}
This enum is used by the (first) third party software to guide the customer code, to call the another third party library (represented by System.out.println()
) calls in previous listing.Now, in the customer headquarters, there were two distinct scenarios happening. One was obviously a happy case where their plugin adapter code worked as expected. However, sometimes the code produced erroneous results in seemingly random scenarios. Why this was happening (according to my understanding) is a combination dependent to an implementation detail of the calling software and misunderstanding of what
static
keyword actually means. To simulate the situation (and to do some polishing of my Java reflection superpowers), I also wrote a main class called
StaticReflection
. This class plays the part of the third party software calling my code. The class uses Java reflection to gain access to fields private to ClassToTest
, to imitate and simplify the plugin phase of the whole process.After some initial thoughts, I wrote my calling 3rd party simulation class as following:
public class StaticReflection {
public static void main(String[] args) {
// scenarios
classReuseScenario();
classSingleUseScenario();
}
private static void classReuseScenario() {
System.out.println("\n** class instance reuse scenario **\n");
// here we create class which is reused
// between publicMethod() calls
ClassToTest test = new ClassToTest();
// go through enum values
for (DataTypeEnum item : DataTypeEnum.values()) {
System.out.println("Called publicMethod with parameter: "+
item);
test.publicMethod(item);
// get private field values from (re-used)
// class instance via reflection
try {
Field staticCharField =
ClassToTest.class.getDeclaredField("staticChar");
staticCharField.setAccessible(true);
System.out.println("staticChar value: "+
staticCharField.getChar(test));
Field regularCharField =
ClassToTest.class.getDeclaredField("regularChar");
regularCharField.setAccessible(true);
System.out.println("regularChar value: "+
regularCharField.getChar(test));
}
catch (Exception e) {
// oops
e.printStackTrace();
}
}
}
}
This 'main' class has the obvious
main()
method plus two methods for highlighting the different scenarios. In the first scenario, the customer class is created in the scope outside of a loop and so the class instance gets reused between calls to ClassToTest.publicMethod()
. The another scenario is just a bit different:
private static void classSingleUseScenario() {
System.out.println("\n** class instance recreation scenario **\n");
// go through enum values
for (DataTypeEnum item : DataTypeEnum.values()) {
// here we create new class instance for
// each publicMethod() call, this resets
// the private fields to class default values
ClassToTest test = new ClassToTest();
System.out.println("Called publicMethod with parameter: "+
item);
test.publicMethod(item);
// get private field values from (reused)
// class instance via reflection
try {
Field staticCharField =
ClassToTest.class.getDeclaredField("staticChar");
staticCharField.setAccessible(true);
System.out.println("staticChar value: "+
staticCharField.getChar(test));
Field regularCharField =
ClassToTest.class.getDeclaredField("regularChar");
regularCharField.setAccessible(true);
System.out.println("regularChar value: "+
regularCharField.getChar(test));
}
catch (Exception e) {
// oops
e.printStackTrace();
}
}
}
I've highlighted the difference. In the first scenario, the
ClassToTest
instance was reused between the calls. In the another scenario, a new ClassToTest
instance is created inside loop and ClassToTest.publicMethod()
gets called with different class instance every time. This is done to compare the possible internal implementations of the calling software to the actual results.When run, the test program outputs following text to console:
** class instance reuse scenario **
Called publicMethod with parameter: OPTION1
Did something with Option 1
staticChar value: !
regularChar value: ?
Called publicMethod with parameter: OPTION2
Did something with Option 2
staticChar value: -
regularChar value: =
Called publicMethod with parameter: OPTION3
Did something with Option 1
staticChar value: -
regularChar value: =
** class instance recreation scenario **
Called publicMethod with parameter: OPTION1
Did something with Option 1
staticChar value: -
regularChar value: ?
Called publicMethod with parameter: OPTION2
Did something with Option 2
staticChar value: -
regularChar value: =
Called publicMethod with parameter: OPTION3
Did something with Option 1
staticChar value: -
regularChar value: ?
The main difference between scenarios is easily observed after
publicMethod()
gets called first time with OPTION3 parameter. In the first scenario, both fields have unexpected values to our unsuspecting customer (=values belonging to call with OPTION2). In the second scenario, by creating a new class instance for each distinct method call, the customer expected their code to reset the inner field values to class defaults. Obviously, this did not work as expected, but why?
See how the
staticChar
field value is not changed at all after customer method changes it (when called with OPTION2 for the first time)? Even the class instance recreation between the method calls does not help.Why different?
It's how Java class loaders roll. Declaring a field as static tells to JVM that we want this field to be accessible from all instances of given class (loaded with same classloader). If we change a value stored to static field in one instance of a class A, another instance of the same class A sees the change, too. If we omit the static keyword, the fields are visible to just one instance of the class A. Simple enough?As long as the class loader has an active link to a class (in this case via the StaticReflection class), the static fields stay in memory for all instances of that class. As we had no control to how
ClassToTest
class instances were created, the fix was to remove the static keyword.The example source with NetBeans project is available in my github, too:
https://github.com/mikko-n/StaticReflection-excercise
Original party image was found here.
This is often an indication of poor design and that static class members are being miss-used. An indication of that some refactoring is in order.
VastaaPoistaIndeed. Part of the magic is to use the right words (while the another part is actually understanding how the trick should be done).
Poista