Monday, March 9, 2009

Friends for java

I read a blog entry of Roman Kennke. He's talking about implementing something akin to the friend class concept in java. The problem goes like this:
package api;

public class WantedCode {
void wantedMethod() {
System.out.println("Wanted method in com.example");
}
}

// --- in another package ---

package com.example

import api.WantedCode;

public class NeedingCode {
void needingMethod() {
// .. we have an instance of this class
WantedCode instance = new WantedCode();
// And need to access some package private method in that class
instance.wantedMethod() // <- error, since it is package private
}
}
The solution is to make an interface within NeedingCode's package, which declares a method that will invoke the package private method in WantedCode, taking as argument the instance of WantedCode on which the method shall be invoked. This interface will be implemented somewhere in WantedCode's package, and then statically set somewhere in NeedingCode's package. The corresponding getter is package private, and one has thereby established a "friend link" for this method which only can be used by NeedingCode's package.

I had some problems following the post since the code snippets was so fragmented, so here I've dumped the full code, and also hopefully answered my own question put forward in the post: Anyone could set the proxy instance, thereby redirecting NeedingCode's invocations to some evil code. The idea is to use a two-way "handshake" to make sure that only the friend sets the proxy, by use of a "secret" (a private Object instance).

Update: Roman Kennke pointed out the obvious: This pretty much ruins the original intent, as one then have ended up with a public facing "magic method" that is shown in the API (and one could then basically just have let the package-private method be public instead). On the other hand, this method can only be used for the sole purpose of establishing the specific friend aspect, and only to the selected other package - none other can make any use of it. Furthermore, if one can accept one such public facing static magic method in the API-package, one could use this method to bridge this package (and any package-private method in that package) to any other (internal) packages.

I've made a second attempt! That one uses introspection to verify the origin of the supplied proxy instance.

(If you use Eclipse, you can mark the entire code below (all classes in one go), Ctrl+C, then activate the project node of a project and hit Ctrl+V. You will probably have to organize imports afterwards).
package com.example;

import api.WantedCode;

public class NeedingCode {
// Driver
public static void main(String[] args) {
// .. we have an instance of this class
WantedCode code = new WantedCode();

// And access its package private method through a "friend-proxy"
FriendProxy.getProxy().invokeWantedMethodOf(code);
}
}

// ------------

package com.example;

import api.WantedCode;

public abstract class FriendProxy {

// :: The proxy interface aspect

public abstract void invokeWantedMethodOf(WantedCode instance);

// :: "Handshake" that establishes friendship

private final static Object _secret = new Object();

static {
WantedCode.makeFriends(_secret);
}

private static FriendProxy _proxy;

public static void setProxy(FriendProxy proxy, Object secret) {
if (secret != _secret) {
throw new IllegalAccessError("Cannot set proxy without correct secret.");
}
_proxy = proxy;
}

// :: Package-private accessor to get to friend's package private method.

static FriendProxy getProxy() {
return _proxy;
}
}

// ------------

package api;

import com.example.FriendProxy;

public class WantedCode {

void wantedMethod() {
System.out.println("Hi from the package-private method in the API!");
}

// :: 2nd part of "handshake" that establishes friendship

public static void makeFriends(Object secret) {
FriendProxy.setProxy(new GivingAccessToNeedingCode(), secret);
}

private static class GivingAccessToNeedingCode extends FriendProxy {
@Override
public void invokeWantedMethodOf(WantedCode instance) {
instance.wantedMethod();
}
}
}

0 comments:

Post a Comment