Skip to content

sugar-cubes/sugar-cubes-cloner

Repository files navigation

Java deep cloning library

build maven central

Deep cloning of any objects.

  • Intuitive.
  • Fast.
  • Thread safe.
  • Supports parallel execution.
  • Java 8+ compatible.

Maven:

<dependency>
    <groupId>io.github.sugar-cubes</groupId>
    <artifactId>sugar-cubes-cloner</artifactId>
    <version>1.2.3</version>
</dependency>

Gradle:

implementation "io.github.sugar-cubes:sugar-cubes-cloner:1.2.3"

It is recommended also to include Objenesis library into your application.

Objectives

  • To get simple, convenient and configurable way of deep-cloning of objects of any types.
  • Make clean and extensible classes structure.

Existing solutions

Java serialization

Many projects use serialization for object cloning. The raw solution is:

ByteArrayOutputStream buffer = new ByteArrayOutputStream();
new ObjectOutputStream(buffer).writeObject(original);
return new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();

This code can be simplified with Apache commons-lang SerializationUtils.clone(object) or Spring Framework SerializationUtils.deserialize(SerializationUtils.serialize(object)).

Pros:

  • portable, works on any JVM
  • no external dependencies

Cons:

  • slow
  • requires all objects to be serializable
  • may be customized only by changing serialization process

Other serialization libraries

Still serialization, but with non-standard libraries, such as:

Faster than java.io serialization.

Other cloning libraries

The solution

Interface

Class Description
Cloner The cloner interface.
ClonerException Wrapper for all (checked and unchecked) exceptions, happened during cloning. Unchecked.
Cloners Factory for standard cloners.
CopyAction Copy action (skip/null/original/clone).
CopyPolicy Set of class/field rules for cloning.
ObjectCopier Object copier interface.
Predicates Predicates factory to configure policies.
ReflectionClonerBuilder Builder for creating custom cloners.
TraversalAlgorithm Depth-first (default) or breadth-first.

Usage

Clone an object

SomeObject clone = Cloners.reflection().clone(original);

Do not clone instances of a class

Cloner cloner = Cloners.builder()
    .setTypeAction(NonCloneableType.class, CopyAction.NULL)
    .build();
SomeObject clone = cloner.clone(original);

Here the instances of NonCloneableType in the original's object tree will be replaced with nulls in the clone.

Cloner cloner = Cloners.builder()
    .setTypeAction(NonCloneableType.class, CopyAction.ORIGINAL)
    .build();
SomeObject clone = cloner.clone(original);

Here the instances of NonCloneableType in the original's object tree will be copied by reference into the clone.

The same thing can also be done with annotations:

@TypePolicy(CopyAction.NULL)
public class NonCloneableType {

or

@TypePolicy(CopyAction.ORIGINAL)
public class NonCloneableType {

Skip some fields

Skip transient fields:

Cloner cloner = Cloners.builder()
    .fieldAction(field -> Modifier.isTransient(field.getModifiers()), CopyAction.SKIP)
    .build();
SomeObject clone = cloner.clone(original);

Or (Hibernate case):

Cloner cloner = Cloners.builder()
    .fieldAction(Predicates.annotatedWith(Transient.class), CopyAction.SKIP)
    .build();
SomeObject clone = cloner.clone(original);

Creating shallow copy

If you wish to create a shallow copy of an object, i.e. another object which references same children as the original, you may do:

Cloner cloner = Cloners.builder()
    .fieldPolicy(CopyPolicy.original())
    .build();
SomeObject clone = cloner.clone(original);

If you want to have deep cloning by default and shallow cloning of all instances of SomeObject, you may do:

Cloner cloner = Cloners.builder()
    .shallow(SomeObject.class)
    .build();

Customization

Cloner cloner =
    // new builder instance
    Cloners.builder()
        // custom allocator
        .objectFactoryProvider(new ObjenesisObjectFactoryProvider())
        // copy thread locals by reference
        .typeAction(Predicates.subclass(ThreadLocal.class), CopyAction.ORIGINAL)
        // copy closeables by reference
        .typeAction(Predicates.subclass(AutoCloseable.class), CopyAction.ORIGINAL)
        // skip SomeObject.cachedValue field when cloning
        .fieldAction(SomeObject.class, "cachedValue", CopyAction.SKIP)
        // set fields with @Transient annotation to null
        .fieldAction(Predicates.annotatedWith(Transient.class), CopyAction.NULL)
        // custom copier for SomeOtherObject type
        .copier(CustomObject.class, new CustomObjectCopier())
        // parallel mode
        .mode(CloningMode.PARALLEL)
        // create cloner
        .build();

// perform cloning
SomeObject clone = cloner.clone(original);

Annotations

It's possible to use annotations to configure field/type actions and custom type copiers.

Annotation Description
FieldPolicy Field copy policy.
TypeCopier Type copier.
TypePolicy Type copy policy.

Implementation

There is three modes of execution: recursive, sequential (default) and parallel.

In sequential mode does not use recursion. Uses Depth-first (by default) or Breadth-first algorithm for the object graph traversal.

In parallel mode the order of copying is unpredictable.

If the Objenesis library is available in the classpath, uses it to instantiate objects. Otherwise, uses reflection.

The priority of copy configurations is:

  1. (high) builder configuration;
  2. annotations;
  3. (low) default configuration (JDK immutable classes).

Known limitations

The library requires Java 8 or higher.

Default configuration of reflection cloner does not clone lambdas and method references. These can be cloned using UnsafeObjectFactoryProvider.

Java 9+ restricts access to objects members via Reflection API. To solve this one may

  • use --illegal-access=permit JVM argument (works on Java below 17);
  • if you add the cloner into classpath, use --add-opens JVM arguments, e.g. --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED;
  • if you have an application with the modules which are properly configured, then use cloner's module name, i.e. --add-opens java.base/java.lang=io.github.sugarcubes.cloner --add-opens java.base/java.lang.invoke=io.github.sugarcubes.cloner --add-opens java.base/java.util=io.github.sugarcubes.cloner --add-opens java.base/java.util.concurrent=io.github.sugarcubes.cloner;
  • the library also contains a Java agent which opens modules for the cloner, just run java with argument -javaagent:/path/to/sugar-cubes-cloner-1.2.3.jar.

Versioning

Semantic Versioning.

License

Apache License 2.0 © Maxim Butov