Menu Close

Leveraging Java’s Proxy Class to Dynamically Add JSON Output Properties to a REST API

Recently, while working on the next generation of the middleware for our advanced predictive analytics platform, I repeatedly found myself encountering situations in our business logic where I wanted to be able to dynamically include additional information for some of our model objects to have with debugging via our REST API.  In particular, I wanted to be tag objects with the effective permissions the caller has for them based on our security model.

Since these objects could be used in many places including lists and nested results, it would have been a major effort refactor the code to have it work with more complex result structures that could carry along this dynamic additional information up the call stack back to the API endpoints.

Similarly, I considered adding support for additional properties to our domain transfer object (DTO) implementations. But since we define the model objects via Java interfaces, it encapsulates the higher-level business logic from the actual implementations of the objects. I did not want the business logic layers to have to make encapsulation breaking assumptions about the structure of the DTO’s generated by the persistence tier code just so that any layer of logic could annotate a model object with additional information for return by the API.

Since all of our model objects are defined by interfaces, I was able to solve our design challenge by leveraging the Java Proxy class that it is part of the reflection package of the standard library. I developed a custom invocation handler that supports the setting of additional properties and their output during JSON serialization but wraps the original model object so that it can be transparently substituted for the model object at any point in the processing. Furthermore, any additional attempts to decorate the object with more properties for its JSON output will just preserve the current wrapper and not result in multiple proxied layers.

For example, here is the start of the interface defining our user model:

/**
 * Model object representing a user of the system.
 * 
 * @author pwisneskey@nuwavesolutions.com
 */
@ApiModel(value="User service (model): User")
public interface User {

	/**
	 * Unique identifier for the user.
	 */
	@ApiModelProperty(value = "Unique identifier for the user.")
	String getUserId();

	/**
	 * Unique login id for the user.
	 */
	@ApiModelProperty(value = "Login identifier for the user.")
	String getLoginId();

	/**
	 * First name.
	 */
	@ApiModelProperty(value = "User's first name.")
	String getFirstName();

 

And in the business method to retrieve a user we can now support a flag to request that the effective privileges the caller has for the user be returned in the REST API as part of the user object, all without having to know how the actual user object implementation has been done:

Set<Privilege> privileges = objectPrivileges(callerDetails, userObject);
userObject = JsonDecorator.decorateObject(userObject, "sessionPrivileges", privileges);

 

After these two calls, the userObject variable contains a user object that behaves exactly the same as the original object but when it is serialized to JSON in the REST API, it will include an additional field named “sessionPrivileges” that includes the set of effective privileges the caller has for the object (e.g. the privileges that define what the caller may do to manipulate the object.) No other code changes are necessary; there is no need for the layers above to know anything about the additional information and they are not prevented from adding their own additional JSON property decorations using the same technique.

I’ve already mentioned that I do this using the Java Proxy class which is a very cool class that generates dynamic proxy objects that back an arbitrary set of interfaces. Since our model objects are all defined by interfaces, it is simply a matter of creating a proxy invocation handler that supports additional methods for setting the dynamic properties to decorate the model object with. The trick for this is to define a special interface that I call JsonDecorated:

/**
 * Interface used to designate an object that has been decorated with additional properties for serialization to JSON by
 * creating a proxy for it to manage the decorations.
 * 
 * @author pwisneskey@nuwavesolutions.com
 */
public interface JsonDecorated {

	/**
	 * Sets the value for an additional property to be returned alongside the object's normal JSON properties.
	 * 
	 * @param propertyName Name of the addition property to set.
	 * @param value        Value for the property
	 */
	void setAdditionalJsonProperty(String propertyName, Object value);

	/**
	 * Returns the additional JSON properties to be written alongside the normal object properties. 
	 */
	@JsonAnyGetter
	public Map<String, Object> getAdditionalJsonProperties();
}

 

Now by creating a proxy for both the model object interface and the JsonDecorated interface, I have a proxy object that will expose a method for setting the additional properties. Furthermore, its getAdditionalJsonProperties() method is annotated with the Jackson JSON library’s @JsonAnyGetter annotation. This is a special annotation the tells the Jackson JSON serializer the method will return a map and the contents of the map should be included as additional JSON properties alongside the object’s normally serialized properties.

But one of my initial requirements was that I did not want the business tier code to have to know anything about the special structure of the model objects in order to set additional properties. This would not be the case if the business tier code had to create the proxy instance and then know it implements the JsonDecorated interface so that it could set additional properties. To prevent this breaking of encapsulation, I instead provide a static method in the JsonDecorator proxy class that both wraps a model object and sets it property so that the caller only ever sees the model object interface:

/**
 * Wrap a model object with a special proxy that allows for additional JSON output properties to be set.
 * 
 * @param  <T>      Type of model object being wrapped.
 * @param  object   Model object to wrap (if it is not already wrapped).
 * @param  property Name of the additional property to set.
 * @param  value    Value to set the additional property to.
 * @return          Proxy for a wrapped model to be used in downstream processing.
*/
@SuppressWarnings("unchecked")
public static <T> T decorateObject(T object, String property, Object value) {

	// If the object has not been decorated before, create the proxy for it that will support decorating it.
	if (!JsonDecorated.class.isInstance(object)) {
		Class<?>[] interfaces = object.getClass().getInterfaces();
		interfaces = Arrays.copyOf(interfaces, interfaces.length + 1);
		interfaces[interfaces.length - 1] = JsonDecorated.class;

		object = (T) Proxy.newProxyInstance(object.getClass().getClassLoader(), interfaces, new JsonDecorator(object));
	}

	// Now set the additional property to be written with the JSON.
	((JsonDecorated) object).setAdditionalJsonProperty(property, value);

	// And return the object so that any decorator proxy is used going forward.
	return object;
}

 

The final piece of the puzzle is the actual invocation handler implementation itself. A second trick I used here is to have the invocation handler itself also implement the JsonDecorated interface. I do this so that it is trivial to redirect the invocation of the additional property setter and getter methods when a proxied method invocation is handled:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

	if (method.getDeclaringClass().equals(JsonDecorated.class)) {

	        // Redirect Decorated interface methods to the invocation handler itself.
		return method.invoke(this, args);

	} else {

		// Pass through any other method invocations to the original object.
		return method.invoke(decoratedObject, args);
	}
}

 

The rest of the invocation handler implementation is just the bookkeeping necessary to track properties that are set. The whole class is as follows:

package com.nuwave.vane.middleware.util.json;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Utility class for decorating objects with arbitrary additional properties that will be output when the object is
 * serialized to JSON.
 * 
 * @author pwisneskey@nuwavesolutions.com
 */
public class JsonDecorator implements InvocationHandler, JsonDecorated {

	/**
	 * Original object that was decorated.
	 */
	private Object decoratedObject;

	/**
	 * Map of additional properties and their values to output when the object is serialized to JSON.
	 */
	private Map<String, Object> decorationMap = new HashMap<String, Object>();

	// ---------------------------------------------------------------------
	// Constructors.
	// ---------------------------------------------------------------------

	private JsonDecorator(Object decoratedObject) {
		this.decoratedObject = decoratedObject;
	}

	// ---------------------------------------------------------------------
	// Decorated
	// ---------------------------------------------------------------------

	@Override
	public void setAdditionalJsonProperty(String propertyName, Object value) {

		decorationMap.put(propertyName, value);
	}

	@Override
	public Map<String, Object> getAdditionalJsonProperties() {

		return decorationMap;
	}

	// ---------------------------------------------------------------------
	// Invocation handler methods.
	// ---------------------------------------------------------------------

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

		if (method.getDeclaringClass().equals(JsonDecorated.class)) {

			// Redirect Decorated interface methods to the invocation handler itself.
			return method.invoke(this, args);
		} else {
			// Pass through any other method invocations to the original object.
			return method.invoke(decoratedObject, args);
		}
	}

	// ---------------------------------------------------------------------
	// Static methods.
	// ---------------------------------------------------------------------

	/**
	 * Wrap a model object with a special proxy that allows for additional JSON output properties to be set.
	 * 
	 * @param  <T>      Type of model object being wrapped.
	 * @param  object   Model object to wrap (if it is not already wrapped).
	 * @param  property Name of the additional property to set.
	 * @param  value    Value to set the additional property to.
	 * @return          Proxy for a wrapped model to be used in downstream processing.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T decorateObject(T object, String property, Object value) {

		// If the object has not been decorated before, create the proxy for it that will support decorating it.
		if (!JsonDecorated.class.isInstance(object)) {
			Class<?>[] interfaces = object.getClass().getInterfaces();
			interfaces = Arrays.copyOf(interfaces, interfaces.length + 1);
			interfaces[interfaces.length - 1] = JsonDecorated.class;

			object = (T) Proxy.newProxyInstance(object.getClass().getClassLoader(), interfaces, new JsonDecorator(object));
		}

		// Now set the additional property to be written with the JSON.
		((JsonDecorated) object).setAdditionalJsonProperty(property, value);

		// And return the object so that any decorator proxy is used going forward.
		return object;
	}
}

 

And finally, here is an example of the JSON output of our user object when it has been dynamically decorated with the effective privileges:

"user": {
    "active": true,
    "organization": "NuWave Solutions",
    "loginId": "workflow",
    "ownerGroupId": "UGRP-SysAdmin",
    "email": "pwisneskey @nuwavesolutions.com",
    "userId": "USER-00003",
    "lastLogin": null,
    "passwordDate": "2020-09-02T00:00:00",
    "firstName": "Paul",
    "lastName": "Wisneskey",
    "sessionPrivileges": [
      "UserDeactivate",
      "UserDelete",
      "UserActivate",
      "UserUpdate",
      "UserCreate",
      "SysAdmin",
      "AuthChangeOtherPassword",
      "AuthChangeOwnPassword"
    ]
  }

 

Note that the sessionPrivileges property is not defined in our User interface. That is the property that was dynamically added to the JSON output by code in the business layer. The one drawback of this technique is that since the additional JSON properties are dynamic, they are not documented in the Swagger documentation for the endpoint unless they are explicitly declared in the Swagger configuration.

Java’s Proxy class is a powerful but under-appreciated class. It can be leveraged in all sorts of creative ways such as I did here. I’ve also used it in the past to record method invocations for timing and tracing and even replaying them to help with debugging. Similarly, it can be particularly useful for constructing mock objects for testing. In short, it is a class worth knowing.

Posted in Blog

Leave a Reply

Your email address will not be published. Required fields are marked *