Search code examples
javaimmutabilitycglib

Create DEEP immutable object in runtime


I need to create immutable copy of an object in runtime with Java. I made use of org.springframework.cglib.beans.ImmutableBean, which can create immutable copy of an object using CGLIB.

But the problem is that it provides "first-level" immutability: it disallows change of an input object's properties, but it allows to change inner objects (e.g. get collection and add an element to it or get inner object and modify it's parameters etc.)

So the question is: what's the correct way of creating deep (recursive) immutable copy of an object so that one can't change inner objects also (at any level of nesting)?


Solution

  • You may traverse the object tree and use CGLIB to make each object immutable by using interceptor which skips required methods. The tough part though is to determine all methods which modify the object's state - for each object in the tree.

    package ut.test;
    
    import static org.junit.Assert.assertEquals;
    
    import java.lang.reflect.Method;
    import java.util.ArrayList;
    import java.util.List;
    
    import org.junit.Test;
    
    import com.google.common.collect.Lists;
    
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    public class MyTest {
    
        public static class Inner {
            private String data = "hello";
    
            public Inner() {}
    
            public String getData() {
                return data;
            }
    
            public void setData(String data) {
                this.data = data;
            }
    
            @Override
            public String toString() {
                return data;
            }
    
        }
    
        public static class Outer {
            private List<Inner> list = Lists.newArrayList(new Inner());
    
            public Outer() {}
    
            public List<Inner> getList() {
                return list;
            }
    
            public void setList(List<Inner> list) {
                this.list = list;
            }
        }
    
        public static class GetOnlyDelegatingMethodInterceptor implements MethodInterceptor {
            private Object delegate;
    
            public GetOnlyDelegatingMethodInterceptor(Object delegate) {
                this.delegate = delegate;
            }
    
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                if (method.getName().startsWith("get")) {
                    return makeImmutable(proxy.invoke(delegate, args));
                }
    
                if (method.getName().equals("toString")) {
                    return proxy.invoke(delegate, args);
                }
    
                if (method.getDeclaringClass().equals(Object.class)) {
                    return proxy.invoke(delegate, args);
                }
    
                // you may check for other methods here
    
                // skip all others
                return null;
            }
        }
    
        private static Object makeImmutable(Object obj) {
            if (obj == null) {
                return obj;
            }
    
            Enhancer e = new Enhancer();
            e.setSuperclass(obj.getClass());
            e.setCallback(new GetOnlyDelegatingMethodInterceptor(obj));
            return e.create();
        }
    
        @Test
        public void testImmutable() {
            Outer outerImmutable = (Outer) makeImmutable(new Outer());
    
            // this is initial state
            assertEquals(outerImmutable.getList().toString(), "[hello]");
    
            // trying to set empty list
            outerImmutable.setList(new ArrayList<>());
            // but it's still the same
            assertEquals(outerImmutable.getList().toString(), "[hello]");
    
            // going deeper
            outerImmutable.getList().get(0).setData("bye!");
            // but still no changes
            assertEquals(outerImmutable.getList().toString(), "[hello]");
        }
    }