springspring-bootautowiredcyclemapstruct

Springboot & mapstruct autowired cycle dependency issue


I hvae a springboot application that relies on a number of (mapstruct) mapper beans. In many instances one mapper relies on another in a bidirectional fashion (thus forming a cycle).

Spring doesn't like this and complains.


APPLICATION FAILED TO START


Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐ | associatedAccountMapperImpl (field private com.server.springboot.mapping.global.ContactLocationRelationMapper com.server.springboot.mapping.customers.AssociatedAccountMapperImpl.contactLocationRelationMapper) ↑ ↓ | contactLocationRelationMapperImpl (field private com.server.springboot.mapping.customers.AssociatedAccountMapper com.server.springboot.mapping.global.ContactLocationRelationMapperImpl.associatedAccountMapper) └─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

Now I have followed the official mapstruct guide text to resolve cyclical issues, however my particular springboot & mapstruct combination issue doesn't seem to have a documented answer anywhere on the internet.

You see in their answer they suggest you pass the CycleAvoidingMappingContext object into your methods as a parameter and they retrieve their mappers with the following call EmployeeMapper MAPPER = Mappers.getMapper( EmployeeMapper.class );

However in my case with springboot, I've autowired the CycleAvoidingMappingContext object through the '@Mapper(componentModel = "spring", uses = CycleAvoidingMappingContext.class)` annotation.

Here is the posted github soliution

public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();

    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return (T) knownInstances.get( source );
    }

    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put( source, target );
    }
}

Here is an example of an interface and its associated autocreated code.

@Mapper(
    componentModel = "spring",
    subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION,
    uses = {
        CycleAvoidingMappingContext.class,
        CategoryMapper.class,
        NoteMapper.class, 
        PaymentTermMapper.class,
        PriceLevelMapper.class,
        LateFeeMapper.class,
        ContactLocationRelationMapper.class
    }
)
public interface AssociatedAccountMapper {



 AssociatedAccountDTO toDTO(AssociatedAccount source);
}
@Component
public class AssociatedAccountMapperImpl implements AssociatedAccountMapper {

    @Autowired
    private CycleAvoidingMappingContext cycleAvoidingMappingContext;
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private NoteMapper noteMapper;
    @Autowired
    private PaymentTermMapper paymentTermMapper;
    @Autowired
    private PriceLevelMapper priceLevelMapper;
    @Autowired
    private LateFeeMapper lateFeeMapper;
    @Autowired
    private ContactLocationRelationMapper contactLocationRelationMapper;

    @Override
    public AssociatedAccountDTO toDTO(AssociatedAccount source) {
        AssociatedAccountDTO target = cycleAvoidingMappingContext.getMappedInstance( source, AssociatedAccountDTO.class );

As you can see the uses = attribute in the @Mapper annotation results in all the classes being autowired and used where necessary by mapstruct.

This is awesome however as you've seen, this class is autowired with my contactLocationRelationMapper which in turn is autowired with the AssosciatedAccountMapper thus resulting in this cyclic dependency issue despite the fact that the method are using the CycleAvoidingMappingContext object to avoid the cycle.

So it seems the me that the issue is coming more from the fact that the mappers are being autowired rather than the contexts being used in the methods themselves that are causing this cyclic dependency error.

From my research there were a few workarounds that I found that I wasn't particularly fond of.

  1. Annotate the autowired mapper instances with @Lazy which works however it requires I go and manually add @Lazy to all instances of mappers that are in cycles in their respective mapper implementations - this method sucks because after any change or reload, mapstruct reconstructs the code and thus erases my manually added @lazy annotations and leaves me doing the same unnecessary work over and over again.

  2. generative ai suggests I can use autowiring with constructors rather than autowiring with field variables. I don't like this work around because it seems uneccesary and like it wouldn't work.

  3. I can always remove the work the springboot is doing and implement my mappers and their beans manually as the github page demonstrates, however I don't like the fact that I have access to springboot and am unable to use its awesome capabilities along with mapstruct just because autowiring causes cyclical dependencies with no other documented solution.


Solution

  • You can use the setter injection available at version 1.6.0.Beta1, which is also suggested by the Spring Docs which then looks like:

    @Mapper(
        componentModel = "spring",
        subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION,
        injectionStrategy = InjectionStrategy.SETTER
        uses = {
            CycleAvoidingMappingContext.class,
            CategoryMapper.class,
            NoteMapper.class, 
            PaymentTermMapper.class,
            PriceLevelMapper.class,
            LateFeeMapper.class,
            ContactLocationRelationMapper.class
        }
    )
    public interface AssociatedAccountMapper {
    
       AssociatedAccountDTO toDTO(AssociatedAccount source);
    
    }
    

    There is also an unofficial springlazy component model extension. For more details see the MapStruct GitHub issue #3558