Blog-Archiv

Freitag, 27. Januar 2017

A Proxy-Based DTO in Java

A proxy is something that stands for something else. It is the deputy for another object which is not present because of different reasons. Maybe it is loading lazily, or it is not implemented at all. In the Java runtime library and virtual machine we have built-in support for proxying Java interfaces (not classes).

This Blog presents a generic data-transfer-object (DTO) implementation, based on that Java Proxy utility. Read about the resulting overhead for maintaining DTOs. You will have a parallel hierarchy of interfaces beside your entity hierarchy. There are a lot of voices that doubt the advantage of using DTOs as layer between persistence and presentation.

Base Interface

We assume that a data-transfer-object is a bean that consists just of getters and setters, all public. To be able to implement such a DTO generically, we need interfaces for all objects to transfer. In other words, you must have an interface for any POJO (entity, domain-object, however you call your database-record) that you want to transfer over the network.

Here is a base interface all these interfaces to be proxied would have to derive (at least to be marked as DTO):

import java.io.Serializable;

/**
 * The base interface for any proxied DTO that provides
 * reading and writing of properties. Supports a "dirty" flag.
 */
public interface TransferableData extends Serializable
{
    /** @return true when this object has been changed by calling some setter. */
    boolean dirty();
}

Example Application

Before going into details, here is an example application of what follows:

import java.lang.reflect.Proxy;
import java.util.Date;

public class Demo
{
    public interface DemoEntity extends TransferableData
    {
        int getNumber();
        void setNumber(int number);
        
        String getName();
        void setName(String name);
        
        Date getDate();
        void setDate(Date date);
        
        boolean getBoolean();
        void setBoolean(boolean flag);
    }
    
    public static void main(String[] args) {
        final DemoEntity dto = (DemoEntity) Proxy.newProxyInstance(
                DemoEntity.class.getClassLoader(), 
                new Class<?> [] { DemoEntity.class },
                new GenericDtoInvocationHandler(DemoEntity.class));

        assert dto.dirty() == false : "DTO is dirty after construction!";
    }
}

The DemoEntity interface outlines the functionality the example-DTO should provide. There are properties with primitive (int) and complex (Date) data-type. All have getter and setter, no read-only properties (with just a getter) are generically possible here. The interface derives TransferableData and thus already has the dirty() flag.

The main() method builds a real DTO for that interface by calling the static Java Proxy.newProxyInstance() method. You must pass a (1) class-loader, (2) an array of interfaces to implement, and (3) the invocation-handler that will receive all method calls on that DTO, except wait() and notify(), done by the Java virtual machine. That last GenericDtoInvocationHandler parameter will be introduced in the following.

We can call dto.dirty() to make sure that it is not already dirty after construction.

Generic DTO Invocation Handler

The invocation-handler will be called for any property the interface describes. So it must be able to store any bean-property generically. For that purpose it holds a property-value map. Further it holds a property-type map, so that it can generate defaults for primitive properties in case their value is null (never has been set). These defaults should conform with Java defaults like 0 (zero) for numbers, and false for booleans.

Here is the outline of the invocation-handler with its constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.io.Serializable;
import java.util.*;
import java.lang.reflect.*;

/**
 * The invocation-handler for a generic data-transfer-object
 * supporting getters and setters, and a dirty() flag.
 */
public class GenericDtoInvocationHandler implements InvocationHandler, Serializable
{
    private final Class<? extends TransferableData> implementedInterface;
    
    private final Map<String,Object> fieldValueMap = new HashMap<>();
    private final Map<String,Class<?>> fieldTypeMap = new Hashtable<>();
    private final Set<String> predefinedMethods = new HashSet<>();
 
    private boolean dirty;
 
    /**
     * Construct an InvocationHandler for given interface extending TransferableData.
     * @param implementedInterface must not be null, the interface that the proxy receiving
     *      this invocation-handler will implement.
     */
    public GenericDtoInvocationHandler(Class<? extends TransferableData> implementedInterface) {
        assert implementedInterface != null;
     
        this.implementedInterface = implementedInterface;
        
        // handle internal methods defined in java.lang.Object
        predefinedMethods.add("equals");
        predefinedMethods.add("hashCode");
        predefinedMethods.add("toString");
        // getClass() and thread-methods like wait() and notify() are handled by JVM
        
        // provide a custom method to find out if a setter has been called
        predefinedMethods.add("dirty");
        
        for (Method interfaceMethod : implementedInterface.getMethods())  {
            if (isGetter(interfaceMethod))  {
                final Class<?> interfaceGetterReturnType = interfaceMethod.getReturnType();
                final String fieldName = makeFieldName(interfaceMethod);
                fieldTypeMap.put(fieldName, interfaceGetterReturnType);
            }
        }
    }

    ....

}

The fields of this class represent the "object's state". This class holds fields that are evaluated in the constructor, and are not touched after any more. Thus any object of this class should have a "stable state".

In the constructor, the implemented interface is remembered to make sure that any invoke() call delivers the same interface. Then the names of some predefined methods are stored. Finally the interface-methods are iterated, and the data-type of each getter is stored into a map.

Here comes the invoke() responsibility, implementing InvocationHandler:

    /**
     * Implements InvocationHandler.
     * @param proxy the JVM-generated object this invocation-handler acts for.
     * @param method the interface-method currently called on proxy.
     * @param arguments the parameters of the interface-method call.
     * @return the return of called method, as expected in the interface implemented by proxy.
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] arguments) throws Exception {
        assert proxy.getClass().getInterfaces()[0].equals(implementedInterface) :
            "The proxy's interface "+proxy.getClass().getInterfaces()[0]+" does not match the handler's one: "+implementedInterface;

        final String methodName = method.getName();
        if (predefinedMethods.contains(methodName))
            return invokePredefinedMethod(proxy, methodName, arguments);
  
        final String fieldName = makeFieldName(method);
        
        if (isGetter(method)) {
            final Object value = fieldValueMap.get(fieldName);
            final Class<?> type = fieldTypeMap.get(fieldName);
            assert type != null : "Unknown getter: "+method.getName();

            if (value == null && type.isPrimitive())
                return convertPrimitiveNullValue(type);
   
            return value;
        }
        else {    // must be setter
            putFieldValue(fieldName, arguments[0]);
            return null;
        }
    }

First we assert that the received proxy conforms to the interface we support. Then we look at the Method we have to respond to. When it is predefined, we answer it specially (see invokePredefinedMethod() below). When not, we build a property-name from the method, e.g. "Number" from "setNumber". Then we distinguish between getter ad setter.

In case a getter is called, we return the value from the map. In case that is null, and the data-type of the property is primitive, we have to provide a Java-compliant default-value for it (see convertPrimitiveNullValue() below).

In case a setter is called, we put the value into the map.

Here is the remaining private part of GenericDtoInvocationHandler:

    private Object invokePredefinedMethod(Object proxy, String methodName, Object [] arguments) throws Exception  {
        if (methodName.equals("equals"))
            return Objects.equals(proxy, arguments[0]);

        if (methodName.equals("hashCode"))
            return proxy.hashCode();

        if (methodName.equals("toString"))
            return toString();

        if (methodName.equals("dirty"))
            return dirty;
        
        throw new IllegalStateException("Not an internal method: "+methodName);
    }
    
    private final String makeFieldName(Method method) {
        final String methodName = method.getName();
        
        if (methodName.startsWith("is"))
            return method.getName().substring("is".length());
        
        if (methodName.startsWith("get"))
            return method.getName().substring("get".length());
        
        if (methodName.startsWith("set"))
            return method.getName().substring("set".length());
        
        throw new IllegalArgumentException("Invoked method is neither getter nor setter: "+methodName);
    }

    private boolean isGetter(Method method) {
        return
            method.getReturnType().equals(void.class) == false &&
            (method.getName().startsWith("get") || method.getName().startsWith("is"));
    }
    
    private Object convertPrimitiveNullValue(final Class<?> clazz) {
        if (clazz.equals(boolean.class))
            return Boolean.FALSE;
        if (clazz.equals(byte.class))
            return Byte.valueOf((byte) 0);
        if (clazz.equals(char.class))
            return Character.valueOf((char) 0);
        if (clazz.equals(int.class))
            return Integer.valueOf(0);
        if (clazz.equals(short.class))
            return Short.valueOf((short) 0);
        if (clazz.equals(long.class))
            return Long.valueOf((long) 0);
        if (clazz.equals(float.class))
            return Float.valueOf((float) 0);
        if (clazz.equals(double.class))
            return Double.valueOf((double) 0);
                    
        throw new IllegalArgumentException("Unknown primitive type: "+clazz);
    }
    
    private void putFieldValue(String fieldName, Object value)    {
        assert fieldName != null;
        
        final Object oldValue = fieldValueMap.get(fieldName);
        if (Objects.equals(oldValue, value) == false)
            dirty = true;
        
        fieldValueMap.put(fieldName, value);
    }

I think this is neither hard to read nor hard to understand. We could add a toString() by returning the fieldValueMap.toString(). Important is to respond to any illegal situation by throwing an exception. That way you will discover bugs early.

Mind that we set the "dirty" state in putValue(). Thanks to the new Objects.equals(), calling the setter with the same value as it had before will not make the object dirty.

Test Code

Finally we need some code to test this thing. Here it is:

    public static void main(String[] args) {
        // test data definition
        
        final int number = 3;
        final String name = "Hello";
        final Date now = new Date();
        
        // test execution
        
        final DemoEntity dto = (DemoEntity) Proxy.newProxyInstance(
                DemoEntity.class.getClassLoader(), 
                new Class<?> [] { DemoEntity.class },
                new GenericDtoInvocationHandler(DemoEntity.class));
        
        assert dto.dirty() == false : "DTO is dirty after construction!";
        
        dto.setNumber(number);
        dto.setName(name);
        dto.setDate(now);
        // leave boolean on default
        
        // test data assertions
        
        assert dto.dirty() == true : "DTO is not dirty!";
        assert dto.getNumber() == number : "Number is "+dto.getNumber();
        assert dto.getName().equals(name) : "Name is "+dto.getName();
        assert dto.getDate().equals(now) : "Date is "+dto.getDate();
        assert dto.getBoolean() == false : "Boolean is "+dto.getBoolean();
        
        System.out.println("Test succeeded!");
    }

Like in every test there is

  1. a test-data definition,
  2. a test execution,
  3. a test-data assertion

Put all this code into Java files, compile it, run it (don't forget to enable asserts by -ea), and you will see:

Test succeeded!



Keine Kommentare: