Previous Next |
Applying Reflection to MutableObjectNow that you have a thorough understanding of the tools used in reflection, it's time to put them to work to solve a practical problem. In , we discussed how to implement a data model to encapsulate the data in a hypothetical bank. Most of the data model objects in this model are structurally the same. They differ only in the number, name, and types of properties they contain. Reflection can be used on this data model to implement several key features. Reflecting on toString( )In your Online Only Bank data model, there are many classes that encapsulate data, and each has several properties. Although you can compare them and obtain their hashCode( ), you cannot print them to the console. This is the job of the toString( ) method. However, the default toString( ) method defined on class Object will print only the hash code and type of object. This is not enough information to do any serious debugging with these classes. What you really need is for the toString( ) method to print the values of the properties in the mutable object. One tactic you could implement is to reabstract the toString( ) method and make each class declare its own toString( ) method. This would force developers to implement the method on each class, which would solve the problem. However, the implementations of the toString( ) method on each class would be the same, except for the names of the properties and classes; also, the method would have to be modified manually if new properties are added or removed. It would be better if you could save the users of MutableObject all of this work by implementing a toString( ) method in the MutableObject class that would print the properties of the derived classes. You can do this with reflection (see Example 9-4). Example 9-4. The toString( ) method of MutableObjectpackage oracle.hcj.datamodeling; public abstract class MutableObject implements Serializable { public String toString( ) { try { final BeanInfo info = Introspector.getBeanInfo(this.getClass( ), Object.class); final PropertyDescriptor[] props = info.getPropertyDescriptors( ); final StringBuffer buf = new StringBuffer(500); Object value = null; buf.append(getClass( ).getName( )); buf.append("@"); buf.append(hashCode( )); buf.append("={"); for (int idx = 0; idx < props.length; idx++) { if (idx != 0) { buf.append(", "); } buf.append(props[idx].getName( )); buf.append("="); if (props[idx].getReadMethod( ) != null) { value = props[idx].getReadMethod( ) .invoke(this, null); if (value instanceof MutableObject) { buf.append("@"); buf.append(value.hashCode( )); } else if (value instanceof Collection) { buf.append("{"); for (Iterator iter = ((Collection)value).iterator( ); iter.hasNext( );) { Object element = iter.next( ); if (element instanceof MutableObject) { buf.append("@"); buf.append(element.hashCode( )); } else { buf.append(element.toString( )); } } buf.append("}"); } else if (value instanceof Map) { buf.append("{"); Map map = (Map)value; for (Iterator iter = map.keySet( ) .iterator( ); iter.hasNext( );) { Object key = iter.next( ); Object element = map.get(key); buf.append(key.toString( )); buf.append("="); if (element instanceof MutableObject) { buf.append("@"); buf.append(element.hashCode( )); } else { buf.append(element.toString( )); } } buf.append("}"); } else { buf.append(value); } } } buf.append("}"); return buf.toString( ); } catch (Exception ex) { throw new RuntimeException(ex); } } } Although this method may seem long for such a simple task, it is a fraction of the cut-and-paste code that would be required to implement toString( ) methods on all of the individual MutableObject types in a large data model. Furthermore, the reflexive toString( ) method doesn't require maintenance if you decide to add a new property to your data model object. It automatically updates itself. Most of the code in Example 9-4 is fairly mundane string construction. However, the emphasized portions show various uses of the reflection toolkit. First, ask the Introspector for the BeanInfo on the object and its superclasses up to, but not including, Object. Then get the PropertyDescriptor objects for that class, which will give you access to the properties. At this point, you can loop through the properties, but make sure you check for a null read method, which could happen if the property is write-only. While looping through the properties, you also have to watch out for recursive reflection. If you used the toString( ) method to print MutableObjects that are members of a MutableObject, then you could get caught in an endless loop. For example, if you used toString( ) on Account, the toString( ) method would be accessed on the Customer class; however, since Customer has a member holding its Accounts, you would recurse back into the Account object and end up in a loop. For this reason, you print only the hashCode( ) of members that are MutableObjects. Additionally, since the collection and map properties may also contain MutableObject descendants, you have to iterate through them to make sure you don't get caught in a loop. Once the toString( ) method is compiled, it will work for any MutableObject descendant that you can dream up. Whether you have 4 data model objects or 40,000, you won't have to lift another finger to give your objects the ability to print their contents. Let's try it on some of your objects: package oracle.hcj.reflection; public class DemoToStringUsage { public static void main(String[] args) { // Create some objects. Person p = new Person( ); p.setFirstName("Robert"); p.setLastName("Simmons"); p.setGender(Gender.MALE); p.setTaxID("123abc456"); Customer c = new Customer( ); c.setPerson(p); c.setCustomerID(new Integer(414122)); c.setEmail("foo@bugmenot.com"); SavingsAccount a = new SavingsAccount( ); a.setCustomer(c); a.setBalance(new Float(2212.5f)); a.setID(new Integer(412413789)); a.setInterestRate(new Float(0.062f)); Set accounts = new HashSet( ); accounts.add(a); c.setAccounts(accounts); // Now print them. System.out.println(p); System.out.println(c); System.out.println(a); } } Using this code, let's examine the new toString( ) method. The resulting output of this code is shown here: >ant -Dexample=oracle.hcj.reflection.DemoToStringUsage run_example run_example: [java] oracle.hcj.bankdata.Person@-1989116069={birthDate=Sun Dec 07 18:04:11 CET 2003, firstName=Robert, gender=oracle.hcj.bankdata.Gender.MALE, lastName=Simmons, taxID=123abc456} [java] oracle.hcj.bankdata.Customer@414122={accounts={@412413789}, apps={}, cardColor=java.awt.Color[r=0,g=0,b=255], customerID=414122, email=foo@bugmenot.com, passPhrase=null, payments={}, person=@-1989116069} [java] oracle.hcj.bankdata.SavingsAccount@412413789={ID=412413789, balance=2212.5, customer=@414122, interestRate=0.062, transactionList={}} Each of the objects is printed correctly with each of their properties without any additional input from the developer of the data model. Although this is a good demonstration of how reflection is used, it just scratches the surface of its capabilities. Fetching ConstraintsSince the constraints that you put on your data model objects are designed to be accessible to panels, JSP pages, and other classes using these objects, it would be convenient if there was a way to fetch the constraints from the model. Rather than having the developer specify the constraints at compile time in a data structure, you can use reflection to fetch the constraints. Example 9-5 shows how you can build a map of the constraints on a MutableObject descendant. The emphasized lines use reflection. Example 9-5. Fetching constraints on a MutableObject descendantpackage oracle.hcj.datamodeling; public abstract class MutableObject implements Serializable { public static final Map buildConstraintMap(final Class dataType) throws IllegalAccessException { final int modifiers = Modifier.PUBLIC | Modifier.FINAL | Modifier.STATIC; // -- Map constraintMap = new HashMap( ); final Field[] fields = dataType.getFields( ); Object value = null; for (int idx = 0; idx < fields.length; idx++) { if ((fields[idx].getModifiers( ) & modifiers) == modifiers) { value = fields[idx].get(null); if (value instanceof ObjectConstraint) { constraintMap.put(((ObjectConstraint)value).getName( ), value); } } } return Collections.unmodifiableMap(constraintMap); } } Get the fields in the target class and then check each of the public static final fields to see whether they contain instances of the class ObjectConstraint. If they do, put the constraint in the map and key it with the name of the property it constrains (which you can get from the ObjectConstraint class). The buildConstraintMap( ) method will give you references to the constraints on your objects, which you can use in your GUI or web page form validation. However, there is one problem with the constraint mechanism. Although reflection is a powerful tool, the process of introspection (analyzing a class to determine its contents) takes up a lot of CPU time. If the users of MutableObject have to fetch the constraints in this manner every time they need them, your programs would run slowly. You need to cache your constraints in MutableObject. You can create a simple catalog to do this: package oracle.hcj.datamodeling; public abstract class MutableObject implements Serializable { private static final Map CONSTRAINT_CACHE = new HashMap( ); public static ObjectConstraint getConstraint(final Class dataType, final String name) { Map constraintMap = getConstraintMap(dataType); return (ObjectConstraint)constraintMap.get(name); } public static final Map getConstraintMap(final Class dataType) throws RuntimeException { try { Map constraintMap = (Map)CONSTRAINT_CACHE.get(dataType); if (constraintMap == null) { constraintMap = buildConstraintMap(dataType); CONSTRAINT_CACHE.put(dataType, constraintMap); return constraintMap; } Collections.unmodifiableMap(constraintMap) } catch (final IllegalAccessException ex) { throw new RuntimeException(ex); } } } This code will cache each of the constraint maps the first time they are fetched. Now all you have to do is change the visibility of buildConstraintMap( ) to protected to force the users to call the caching methods: package oracle.hcj.reflection; public class ConstraintMapDemo { public static final void main(final String[] args) { Map constraints = MutableObject.getConstraintMap(Customer.class); Iterator iter = constraints.values( ) .iterator( ); ObjectConstraint constraint = null; while (iter.hasNext( )) { constraint = (ObjectConstraint)iter.next( ); System.out.println("Property=" + constraint.getName( ) + " Type=" + constraint.getClass( ).getName( )); } constraint = MutableObject.getConstraint(SavingsAccount.class, "interestRate"); System.out.println("\nSavingsAccount interestRate property"); System.out.println("dataType = " + ((NumericConstraint)constraint).getDataType( ) .getName( )); System.out.println("minValue = " + ((NumericConstraint)constraint).getMinValue( )); System.out.println("maxValue = " + ((NumericConstraint)constraint).getMaxValue( )); } } This results in the following output: >ant -Dexample=oracle.hcj.reflection.ConstraintMapDemo run_example run_example: [java] Property=accounts Type=oracle.hcj.datamodeling.constraints.CollectionConstraint [java] Property=cardColor Type=oracle.hcj.datamodeling.constraints.ObjectConstraint [java] Property=person Type=oracle.hcj.datamodeling.constraints.ObjectConstraint [java] Property=passPhrase Type=oracle.hcj.datamodeling.constraints.StringConstraint [java] Property=email Type=oracle.hcj.datamodeling.constraints.StringConstraint [java] Property=apps Type=oracle.hcj.datamodeling.constraints.CollectionConstraint [java] Property=payments Type=oracle.hcj.datamodeling.constraints.CollectionConstraint [java] Property=customerID Type=oracle.hcj.datamodeling.constraints.NumericConstraint [java] SavingsAccount interestRate property [java] dataType = java.lang.Float [java] minValue = 0.0 [java] maxValue = 1.0 As with toString( ), adding more data objects, properties, and constraints to your model will not require any more work. The MutableObject class adapts to your code. |
Previous Next |