import java.util.*; import java.lang.reflect.*; public class Reflector { public static void main(String[] args) throws Exception { Set<String> s = new HashSet<String>(); s.add("foo"); Iterator it = s.iterator(); Method m = it.getClass().getMethod("hasNext"); System.out.println(m.invoke(it)); } }
Solution 78: Reflection Infection
The program creates a set with a single
element in it, gets an iterator over the set, invokes the iterator's
hasNext method reflectively, and prints the result of the method
invocation. As the iterator hasn't yet returned the set's sole element,
hasNext should return TRue. Running the program, however,
tells a different story:
Exception in thread "main" IllegalAccessException: Class Reflector can not access a member of class HashMap $HashIterator with modifiers "public" at Reflection.ensureMemberAccess(Reflection.java:65) at Method.invoke(Method.java:578) at Reflector.main(Reflector.java:11)
How can this be? Of course the hasNext method is
public, just as the exception tells us, and so can be accessed from anywhere. So
why should the reflective method invocation be illegal?
The problem isn't the access level of the method; it's the
access level of the type from which the method is selected. This type plays the
same role as the qualifying type in an ordinary
method invocation [JLS 13.1]. In this program, the method is selected from the
class represented by the Class object that is returned by
it.getClass. This is the dynamic type of the iterator, which happens to
be the private nested class java.util.HashMap.KeyIterator. The reason
for the IllegalAccessException is that this class is not public and
comes from another package: You cannot legally access
a member of a nonpublic type from another package [JLS 6.6.1].
This prohibition applies whether the access is normal or
reflective. Here is a program that runs afoul of this rule without resorting to
reflection:
package library; public class Api { static class PackagePrivate {} public static PackagePrivate member = new PackagePrivate(); } package client; import library.Api; class Client { public static void main(String[] args) { System.out.println(Api.member.hashCode()); } }
Client.java:5: Object.hashCode() isn't defined in a public class or interface; can't be accessed from outside package System.out.println(Api.member.hashCode()); ^
This diagnostic makes about as much sense as the runtime error
generated by the original reflective program. The class Object and the
method hashCode are both public. The problem is that the
hashCode method is invoked with a qualifying type that is inaccessible
to the client. The qualifying type of the method invocation is
library.Api.PackagePrivate, which is a nonpublic class in a different
package.
This does not imply that Client can't invoke
hashCode on Api.member. To do this, it has merely to use an
accessible qualifying type, which it can do by casting Api.member to
Object. With this change, Client compiles and runs
successfully:
System.out.println(((Object)Api.member).hashCode());
As a practical matter, this problem doesn't arise in ordinary
nonreflective access, because API writers use only public types in their public
APIs. Even if the problem were to occur, it would manifest itself as a
compile-time error, so it would be fixed quickly and easily. Reflective access
is another matter. Although common, the idiom
object.getClass().getMethod("methodName") is broken and should not be
used. It can easily result in an IllegalAccessException at run
time, as we saw in the original program.
When accessing a type reflectively,
use a Class object that represents an accessible type. Going
back to our original program, the hasNext method is declared in the
public type java.util.Iterator, so its class object should be used for
reflective access. With this change, the Reflector program prints
TRue as expected:
Method m = Iterator.class.getMethod("hasNext");
You can avoid this whole category of problem if you use
reflection only for instantiation and use interfaces to invoke methods [EJ Item
35]. This use of reflection isolates the class that invokes methods from the
class that implements them and provides a high degree of type-safety. It is
commonly used in Service Provider Frameworks. This pattern does not solve every
problem that demands reflective access, but if it solves your problem, by all
means use it.
In summary, it is illegal to access a member of a nonpublic
type in a different package, even if the member is also declared public
in a public type. This is true whether the member is accessed normally or
reflectively. The problem is likely to manifest itself only in reflective
access. For platform designers, the lesson, as in Puzzle 67, is to make diagnostics as
clear as possible. Both the runtime exception and the compiler diagnostic leave
something to be desired.
No comments:
Post a Comment
Your comments are welcome!