Hello! I would like to introduce you to a new java library for mapping / merging objects, which I “modestly” position as a possible alternative to
dozer . If you are developing an enterprise application in java, you are not indifferent to the effectiveness of your work, and want to write less boring code, then I invite you to read more!
UPD. Posted in the central repository of maven
<dependency> <groupId>net.sf.brunneng.jom</groupId> <artifactId>java-object-merger</artifactId> <version>0.8.5.1</version> </dependency>
UPD2 .
Version 0.8.4')

What are object mappers for?
The simple answer is: to copy data automatically from one object to another. But then you may ask: why is this copying necessary? One can doubt that this is needed very often. So you should give a more detailed answer.
In the world of enterprise applications, it is customary to beat the internal structure into layers: a base access layer, a business, and a presentation / web service. In the database access layer, as a rule, there are objects mapping on tables in the database. We agree to call them DTO (from Data transfer object). For good, they only transfer data from the tables and do not contain business logic. On the presentation / web services layer, there are objects that deliver data to the client (browser / clients of web services). Let's call them VO (from View object). VOs may require only a fraction of the data that is in the DTO, or aggregate data from several DTOs. They can additionally engage in localization or transformation of data into a convenient form for presentation. So transmitting the DTO immediately to the presentation is not entirely correct. Also, business objects (business object) are sometimes distinguished in the business layer. They are wrappers over DTO and contain the business logic of working with them: saving, modifying, business operations. Against this background, the problem arises of transferring data between objects from different layers. Say, for example, part of the data from the DTO to VO. Or from VO to BO and then save what happened.
If you solve the problem in the forehead, you get something like this “stupid” code:
… employeeVO.setPositionName(employee.getPositionName()); employeeVO.setPerson(new PersonVO()); PersionVO personVO = employeeVO.getPerson(); PersonDTD person = employee.getPerson(); personVO.setFirstName(person.getFirstName()); personVO.setMiddleName(person.getMiddleName()); personVO.setLastName(person.getLastName()); ...
Familiar? :) If yes, then I can please you. For this problem has come up with a solution.
Object Mappers
Invented, of course, not by me. There are a lot of java implementations. You can read, for example
here .
In short, the mapper's task is to copy all the properties of one object into another, and also do the same recursively for all child objects, doing the necessary type conversion, if necessary.
The mappers from the list above are all different, more or less primitive. Perhaps the most powerful
dozer , I worked with him for about 2 years, and some things in him ceased to work. And the slow pace of further development of the doser prompted me to write my own “bicycle” (yes, I got to know other mappers — they are even worse for our customers).
What is bad dozer
- Poor annotation configuration support (there is only
@Mapping
). - It is impossible to draw from several fields into one (for example, to collect the full name from the name, surname and patronymic).
- Problems with mapping generic properties. If the parent abstract class has a getter that returns a generic type T, where
, T , T. IEntity, , ..
Property classes are stored as strings in the internal cache of the doser, and a special loader class is used to get the class. Problems with this arise in the osgi environment, when the dozer is in one bundle, and the desired bin class in another, not accessible from the first. We overcame the problem, even in a standard way - by slipping the required loader class, but the implementation itself: storing the class as a string - looks strange. Perhaps this is in order not to create perm gen space of memory of faces. But still not very clear.
If something suddenly does not map, then it is very difficult to understand this. If you debug a dozer, you will understand why. There is some kind of ... just a crazy jumble of OOP patterns - everything is confusing and not explicit. However, this is just for my taste.
What qualities should a mapper have?
- Broad configuration support via annotations.
- Full support for generics.
- Clean, understandable code that anyone can play without risking breaking the brain.
- By default, without any additional settings, it should map as the developer most likely would expect.
- It should be possible to fine tune (no worse than the dosage).
Why merger and not mapper?
java-object-merger distinguishes from other mappers one feature. The basic idea was to give an opportunity to create snapshots of objects ( Snapshot ) for a certain moment in time, and then, comparing them, find differences ( Diff ) just as we find a diff between two texts. Moreover, it should be possible to view snapshots and diffs in a text that can be understood by humans. So that once you look at the diff, all the differences are immediately clear, as well as the target object will be changed after applying the diff. Thus we achieve full transparency of the process. No magic and black boxes! Creating snapshots opens another interesting scenario. You can make a snapshot of the object, then somehow changing it, make a new snapshot - check what has changed, and, if desired, roll back the changes. By the way, diff can be bypassed with a special visitor, and you can mark only those changes that you want to apply, and ignore the rest.
So it can be said that merger is more than just a mapper.
Using
The “Hello world” program looks like this:
import net.sf.brunneng.jom.IMergingContext; import net.sf.brunneng.jom.MergingContext; public class Main { public static class A1 { private String field1; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } } public static class A2 { private String field1; public A2(String field1) { this.field1 = field1; } public String getField1() { return field1; } } public static void main(String[] args) { IMergingContext context = new MergingContext(); A2 a2 = new A2("Hello world!"); A1 a1 = context.map(a2, A1.class); System.out.println(a1.getField1()); } }
First, we see that for mapping it is necessary that the property has a getter on both objects. This is needed to compare values. And the target setter has to record a new value. The properties themselves must be named the same.
Let's see how the map method is implemented. This will help to understand many things about the library.
@Override public <T> T map(Object source, Class<T> destinationClass) { Snapshot sourceSnapshot = createSnapshot(source); Snapshot destSnapshot = null; if (sourceSnapshot.getRoot().getType().equals(DataNodeType.BEAN)) { Object identifier = ((BeanDataNode)sourceSnapshot.getRoot()).getIdentifier(); if (identifier != null) { destSnapshot = createSnapshot(destinationClass, identifier); } } if (destSnapshot == null) { destSnapshot = createSnapshot(destinationClass); } Diff diff = destSnapshot.findDiffFrom(sourceSnapshot); diff.apply(); return (T)destSnapshot.getRoot().getObject(); }
If the source snapshot is a bin, and if it has an identifier, then we try to find the target bin for the destinationClass class using IBeanFinder [here createSnapshot(destinationClass, identifier);
]. We have not registered these, and identifier, no, it means we go further. Otherwise, the bean is created using the appropriate IObjectCreator [here createSnapshot(destinationClass)
]. We also did not register these, but in the standard package there is a creator of objects by the default constructor - it is used. Next, the target snapshot is taken diff from the source snapshot and is applied to the target object. Everything.
By the way, diff, for this simple case, will look like this:
MODIFY { dest object : Main$A1@28a38b58 src object : Main$A2@76f8d6a6 ADD { dest property : String field1 = null src property : String field1 = "Hello world!" } }
Key annotations
Located in the net.sf.brunneng.jom.annotations
package.
@Mapping
- sets the path to the field for mapping at the other end of the association (for example, “employee.person.firstName”
). May be specified on the class of the target object or source object.@Skip
- the field does not fall into snapshot, is not compared and is not mapped.@Identifier
- marks a field that is considered a bean identifier. Thus, when comparing collections, we will know which object should be compared with which one. Namely, objects with matching identifiers will be compared. Also, if in the process of applying defa there is a need to create a bin, and the identifier is known, then there will be an attempt to first find this bin using registered IBeanFinder
. Thus, an IBeanFInder
implementation can search for IBeanFInder
for example in the database.@MapFromMany
- the same as @Mapping is only indicated on the class of the target object and allows you to specify an array of properties on the source object that will be mapped to the field in the target object.@Converter
- allows you to set a class on the property that is the successor of PropertyConverter
. - it will perform the conversion between properties. A property converter is required when mapping several fields into one, since it will just have to collect all the values from the source together and form one of them.@OnPropertyChange, @OnBeanMappingStarted, @OnBeanMappingFinished
- allow you to mark the methods that listen for the corresponding events in the mapping life cycle that occur in this bin.- Other.
Type conversion
In IMergingContext, you can register custom type converters, from one type to another ( TypeConverter
interface). A standard set of converters includes conversions:
- primitive types in wrappers, and vice versa
- date conversions
- objects in a row
- enums to enums, and strings to enums named enums constant
Object categories
Mapper divides all objects into categories such as:
- Value objects: primitive types, objects in the
java.lang
, dates, arrays of value objects. The list of classes considered as values can be extended via IMergingConext
. - Collections are arrays that all derive from
java.util.Collection
. - Mapy - all inherited from
java.util.Map
. - Beans - all the rest.
Performance
Honestly, while I was writing the library, I didn’t think much about performance. Yes, and initially for high performance was not. However, I decided to measure the mapping time N times per test object. The source code of the test . The object is quite complex, with fields of values, child bins, collections and maps. For comparison, I took the latest dozer for the current version 5.4.0. I expected that the doser would not leave any chances. But it turned out quite the opposite! dozer zampil 5000 test objects in 32 seconds, and java-object-merger 50,000 objects in 8 seconds. Some kind of wild difference - 40 times ...
Application
The java-object-merger was tested on the current project from my main job (osgi, spring, hibernate, hundreds of mapping classes). To replace them, the dose completely took less than 1 day. Along the way there were some obvious jambs, but after the correction, all the main scenarios worked fine.
Lazy Snapshots
One of the obvious problems found while screwing a mapper to a real project was that if you do snapshots on a DTO that has lazy lists of other entities, and those others refer to third others, then you can create one snapshot inadvertently, deflate the floor of the base. Therefore, it was decided to make all the properties in the snapshot lazy by default. This means that they will not be pulled out of the objects until they are compared with the corresponding property when taking the diff. Or until we explicitly call the loadLazyProperties()
method on snapshot. And when the property is pulled out, the automatic completion of the snapshot occurs - again with lazy properties that are waiting for them to be loaded.
Conclusion
If interested - the project, with the source code and documentation is here . All the main functionality of the library is covered by unit tests, so you can be sure that you will not see any stupid trivial errors in it. Practically all classes and methods are documented by javadoc.
Download, try, write your feedback :). I promise to respond quickly and listen to your wishes.