< Day Day Up > |
4.4 ReflectionThe most accessible way to build a service that depends on model data is runtime reflection. I don't know why Java developers never have embraced runtime reflection the way that other languages have. It tends to be used by tool developers and framework providers but not general application programmers. You don't have to use reflection everywhere, nor should you try. Instead, apply a little reflection where it can have a tremendous impact. Here are some things to keep in mind:
Bear in mind that although the performance of reflection has improved in recent years, postponing binding decisions until runtime has a performance cost. Jumping to a specifically named method or property is much faster than going through the reflection API by a factor of two or more. Reflection offers great power, but be judicious. 4.4.1 The Reflection APIThe Java reflection API lets you access all of the elements that make up a class at runtime without requiring source code or any other advanced knowledge of the classes. Using reflection, you can:
In short, you can learn anything that you need to know about a class and directly manipulate an instance of that class. If you want to build a service that's independent of the structure of an object, that's a powerful combination of tools. You can inspect any method or field. You can load a class, call a method, or get the data from a field without knowing anything about the class in advance. Further, reflection works well with a passive model because through reflection, the model already has all of the information that a potential service might need. That service can accept the whole model (or one class from the model) as input and use reflection to extract the data to do something useful, such as serialize the data to XML (like Castor), save it to a database (like Hibernate), or even wire together behaviors and properties (like Spring). When you use the reflection framework, you must import the reflection libraries: import java.lang.reflect.*; The java.lang.reflection package contains everything you need, including four major classes: Class, Constructor, Field, and Method. These classes let you deal with each major element of a Java class. Java's runtime architecture makes it possible. If you're like most Java developers, you probably deal much more with an instance of an object rather than the class object, but the class object is an integral part of Java's runtime architecture. It contains the basic DNA for a class. It's used to create classes and serves as an attachment point for static methods and members. It keeps track of its parent, to manage inheritance. You've probably used classes in these ways, but it's also the tool that enables reflection. Figure 4-5 shows a class instance at runtime. Each object has an associated class, which is the DNA for a class that determines its type. The class is the central entry point for the Java reflection API. Using it, you can access the fields, methods, and constructors for any class. You can also invoke methods and access the data for an individual instance of the class. The rectangles in grey represent the Java reflection framework. Figure 4-5. All Java objects have an associated class4.4.2 Accessing a ClassThe class is the entry point for reflection. Once you have the class, you can call specific methods to get the associated methods, constructors, and fields. Using the class, you can get a single method or field by name, or get an array of all of the supported fields or methods. You can get the class in several ways. Sometimes, you don't have an instance. For example, if you're working on a factory, you might have access to only the name of a class. In that case, load the class like so: Class c = Class.forName(aString); Other times, you might have nothing more than an object. In that case, you'd get the class from the instance, like this: Class cls = obj.getClass( ); It's actually not quite that simple. But for now, you just need to understand that Java supports more than one class loader (though the architecture for doing so changed for Version 1.2 and beyond). Chapter 6 fills in the details about class loading. 4.4.3 Accessing FieldsYou're probably ready to see a method that's a little more concrete. Let's use reflection to build a transparent service. Assume that you need a service to emit XML for a given object. Further, you want the model to remain transparent to the service, so you won't need to change any model code to support new objects. Given a target object, you're going to attack the problem using reflection:
I'll show you the code all together, and then we'll go through it in pieces. Here's the entire method to process an object: public static void doObject(Object obj) throws Exception { [1] Class cls = obj.getClass( ); emitXMLHeader(cls); [2] Field[] fields = cls.getDeclaredFields( ); for (int i=0; i < fields.length; i++) { Field field = fields[i]; [3] field.setAccessible(true); [4] Object subObj = field.get(obj); [5] if (!Modifier.isStatic(field.getModifiers( ))) { if ((field.getType( ).isPrimitive( )) || ((field.getType( ).getName( ) == "java.lang.String"))) { [6] emitXML(field.getName( ), subObj); } else { [7] doObject(subObj); } } } emitXMLFooter(cls); } That's it. Let's see how the individual pieces work.
The bulk of the work is done within the doObject method, as it should be. The code to emit the XML is surprisingly simple. Here are the methods to emit XML for the overall class, and for a field: public static void emitXML(String name, Object value) { System.out.println("<" + name + ">"); System.out.println(value.toString( )); System.out.println("</" + name + ">"); } public static void emitXMLHeader(Class cls) { System.out.println("<"+cls.getName( )+">"); } public static void emitXMLFooter(Class cls) { System.out.println("</"+cls.getName( )+">"); } The nuts and bolts are all there for an XML emitter. You've probably noticed that I cheated and handled only the simplest case. In fact, you'll need to handle at least four types of fields for a general-purpose emitter:
We've only handled the first two types of fields, plus strings, but you can see how reflection works. You've supported a surprising number of classes without needing to alter model code at all. I must note that the emitter we've constructed here, though generic and useful, is not a full implementation. For a truly generalized emitter, our class would have to be able to handle circular references between classes, optional omission of referenced classes, logically transient fields, and some kind of optional name-substitution mapping pattern. Regardless, the point is no less salient: reflection can provide an enormous amount of power without any tight coupling. You've seen how many transparent services use reflection: they simply access a list of properties, recursively if needed, and do the appropriate service. The types of services are unlimited:
So far, I've only told you how to deal with data. Fortunately, the reflection API also makes it easy to deal with behavior. 4.4.4 Accessing Methods and ConstructorsYou can use reflection to examine and execute methods. You can access methods through java.lang.reflection.Method and constructors through java.lang.reflection.Constructor. I'll describe the way that methods work; you'll find constructors work the same way. As with fields, you can use Class to access methods in two ways: getMethods( ) returns an array with all supported methods, and getDeclaredMethods( ) returns only the declared methods for a class. You t hen can access the parameter types, modifiers, and return value from the Method class. Here's an example that prints all of the declared methods in a class. It also prints out the types of each parameter, and the return value: public static void printMethods(Object obj) throws Exception { Class cls = obj.getClass( ); [1] Method[] methods = cls.getDeclaredMethods( ); for (int i=0; i < methods.length; i++) { Method method = methods[i]; [2] System.out.println("Method name:" + method.getName( )); [3] Class parmTypes[] = method.getParameterTypes( ); for (int j = 0; j < parmTypes.length; j++) { System.out.print(" Parameter " + (j+1) + " type:"); System.out.println(parmTypes[j]); } System.out.println(" Returns: "+method.getReturnType( )+"\n"); } } Here's what the annotations indicate:
As you can see, inspecting methods works a whole lot like inspecting fields. All that remains is to invoke a method. 4.4.4.1 Invoking a methodOften, you'll want to invoke a method or constructor without knowing all of the details until runtime, such as configuring an object from a file. To do so, you'll need several things:
Here's the code to invoke a method called sum on class Adder that takes two int parameters and returns an Integer: // target object is called "target" Class c = Class.forName("Adder"); Class parameterTypes[] = new Class[2]; parameterTypes[0] = Integer.TYPE; parameterTypes[1] = Integer.TYPE; Method m = c.getMethod("sum", parameterTypes); Object parms[] = new Object[2]; parms[0] = new Integer(1); parms[1] = new Integer(1); Integer returnValue = (Integer)m.invoke(target, parms); That's really the bulk of working with reflection. Compared to a simple method invocation or a simple field access, it does not look simple. When you consider the overall impact, though, the effort makes a huge difference. |
< Day Day Up > |