Using reflection in Java

Maybe not so much a bnd tip … However, I am a great fan of Java reflection and much of the code I write uses reflection to make my code more declarative. Yesterday I had a great example and just want to share it.

Domain Specific Languages (DSL) are a well known technique to reduce boiler plate code. Where boiler plate code is very low density code that requires not a lot of brainpower to write but feels offensive that one has to write it. True, AI can nowadays do a lot of it but then we’re still left to have to read it now and then. Although it is low density code, the quantity then tends to obscure potential errors. And changing this kind of code is, at least for me, very error prone. So I am very much in favor of DSLs.

Creating a DSL parser is unfortunately a lot of work. Before you have the editor, the completion support, the quick fixes, the parser, and the runtime you’ve spent quite a lot of hours. Sometimes the DSL requires so many special tricks that this is worth it. Most of the times it isn’t, so we wrestle through the mud of boiler plate code.

One technique I use a lot is to create an interface that models a DSL. Since interfaces are already first class Java objects, the IDE tends to already have a lot of help built in. One of my most favorite methods in Java is therefore Proxy.newProxyInstance(). It creates a proxy object that implements all the given interfaces. It is highly popular among general framework developers like Spring but I have observed that it tends to be overlooked by many application developers. (Or tell me if I am wrong?)

Yesterday I was working on an API for the Assistant OpenAI API. Since I am also a relatively late convert to final fields and immutable objects, I noticed myself writing a lot of builders. Where a builder is a class that gathers the information to create an immutable object. The builder pattern is where each method returns the builder itself so you can configure an object in a few short lines.

	interface AssistantBuilder {
		AssistantBuilder metadata(String key, String value);
		AssistantBuilder name(String name);
		AssistantBuilder description(String description);
		AssistantBuilder instructions(String instructions);
		Assistant get();
	}

Such an interface can then be used to construct an Assistant:

OpenAI provider = ...;
Assistant assistant = provider.assistant()
   .name("myassistant")
   .description("""
        A bndtools magic helper to reduce coding time, 
        review code, and assist you in finding the meaning 
        of life
   """)
   .instructions("""
         ....
   """)
   .metadata("aQute.search", "test.1")
   .get();

Implementing 10 of such builders is just no fun, even using Chat GPT. And maintaining low density code was not what I was built for. So while writing the second builder my mind started to wonder and soon I saw my fingers type Proxy.newProxyInstance(

The basic idea of the Proxy is that you provide the interfaces the returned proxy object should implement and then an Invocation Handler, which is functional interface with one method:

   Object invoke(Object proxy, Method method, Object ... args) throws Throwable

When a method on the proxy gets called, it is delegated to the Invocation Handler. The Invocation Handler gets the Method object that was called and the actual arguments. In this case we need to gather the information so that in the get() method we have all the possible information in place for a construction method.
In the case of the Assistant the builder’s actual data was quite simple. Either a scalar like String or a Number and the metadata parameters map. For the scalars, model them as a single argument method and put the argument in a map under the name of the method. For the metadata it is slightly harder. The object is a Map itself. The metadata takes two String arguments. In that case, ensure the parameters map has a Map object under the name of the method, and put the second argument with the method name as key.
To finally create the object that is being build, we use a factoryFunction<Map<String,Object>,T>. The caller can then use it for any type and factory method it likes.
So without further ado, the code:

 static <B, T> B builder(Class<B> builderInterface, Function<Map<String, Object>, T> build) {
	Map<String, Object> parameters = new LinkedHashMap<>();
	return (B) Proxy.newProxyInstance(
            builderInterface.getClassLoader(), 
            new Class[] { builderInterface },
			(p, m, args) -> {
				if (m.getName().equals("get") 
                && m.getParameterCount() == 0) {
				return build.apply(parameters);
			}
			if (m.getParameterCount() == 2) {
				Map<String, Object> map = 
                  (Map<String, Object>) parameters.computeIfAbsent(m.getName(),
					k -> new LinkedHashMap<>());
				map.put((String) args[0], args[1]);
			} else if (m.getParameterCount() == 1) {
				parameters.put(m.getName(), args[0]);
			} else
				throw new IllegalArgumentException(
						"""
                           A builder interface must have a get() method, 2 arg 
                           methods for map parameters, and 1 arg methods for parameters
                        """);
				return p;
			});
}

In an OSGi world it is quite important to know where the classes are loaded from. In this case, we load the classes from the same class loader as the interface. This is usually the best solution in OSGi as far as I know.

1 Like