#Problem with Mapstruct and using other mappers in a specific mapper.

49 messages · Page 1 of 1 (latest)

hearty carbon
#

I have the following mapper:

@Mapper(
        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel= "spring",
        uses = {
                KeyMapper.class,
                SubscriptionMapper.class
        }
)
public interface ConfigurationMapper {

    ConfigurationDto toDto(Configuration configuration);

    ConfigurationApiDto toApiDto(Configuration configuration);

    @BeforeMapping
    default void filterSubscriptionsAndKeys(
            Configuration configuration,
            @MappingTarget ConfigurationApiDto configurationApiDto
    ) {
        configuration.getSubscriptions().removeIf(subscription -> subscription.getUnsubscribedAt() != null);
        configuration.geKeys().removeIf(key -> key.getRevokedAt() != null);
    }
}```
But when MapStruct generates the implication and I use the toApiDto function in another mapper, I get the following error:
NullPointerException -> KeyMapper and SubscriptionMapper is null.
While when I use the toDto function everything works fine. (FYI the toDto as the toApiDto use the same Mappers)
worldly cobaltBOT
#

This post has been reserved for your question.

Hey @hearty carbon! Please use /close or the Close Post button above when you're finished. Please remember to follow the help guidelines. This post will be automatically closed after 300 minutes of inactivity.

TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.

hearty carbon
#

The Mapper that uses ConfigurationMapper to be able to set the configuration to a ApiDto is the following:

@Mapper(
        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel = "spring",
        uses = {
                DomainMapperResolver.class,
                LanguageMapper.class,
                ConfigurationMapper.class
        }
)
public interface DomainMapper {

    @Mapping(source = "domainName", target = "name")
    DomainShort toDto(DomainSQL domain);

    @Mapping(source = "domainName", target = "name")
    @Mapping(source = ".", target = "integrations", qualifiedByName = "toIntegrationDto")
    DomainApiDto toDomainApiDto(DomainSQL domain);

    @Named("toIntegrationDto")
    default List<IntegrationDto> toIntegrationDto(DomainSQL domain) {
        return domain.getConfigurations().stream()
                .map(Mappers.getMapper(ConfigurationMapper.class)::toApiDto)
                .collect(Collectors.toList());
    }
}```
Why is it going wrong in ConfigurationMapper that the KeyMapper and SubscriptionMapper are null when the toApiDto function is used?
#

Problem with Mapstruct and using other mappers in a specific mapper.

daring forge
#

Oof, this is hard.
I think the problem might be between you manually retrieving a mapper, and the spring injection of the beans, but it's a long shot.

.map(Mappers.getMapper(ConfigurationMapper.class)::toApiDto)

  1. Is there a reason for manually retrieving the mapper? Shouldn't mapstruct generate/use it?
  2. Can we see the KeyMapper and/or SubcriptionMapper class?
    (Do they have any @Autowired annotations inside them?)
hearty carbon
#
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel = "spring",
        uses = { UserMapper.class }
)
public interface KeyMapper {

    KeyDto toDto(Key key);
}``````java
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel= "spring"
)
public interface SubscriptionMapper{

    @Mapping(source = "configuration.id", target = "configurationId")
    @Mapping(source = "key.id", target = "keyId")
    SubscriptionDto toDto(Subscription subscription);

    SubscriptionApiDto toApiDto(Subscription subscription);
}```
hearty carbon
#

Plus the generated MapperImplication files do have the @AutoWired annotations. The function of toDto uses the same mapper as the toApiDto, but somehow only for the toApiDto the mappers are null?

daring forge
#

I think that when you get it manually with:

Mappers.getMapper(ConfigurationMapper.class)

it gives you back an instance of that mapper that is created outside of the spring context.
So, even if the generated ConfigurationMapper has the @Autowired annotations, Spring didn't really inject anything inside them, and they are null

So, when you get the mapper that way, and you call .toApiDto on it, it's an instance with their fields null.

When it uses the other method, it uses a different instance of the mapper with the fields correctly injected

#

(Obviously, this is only an hypotesis. I've never faced this problem)

I would try to change the 'toIntegrationDto' method in some way to avoid that Mappers.getMapper, and to let him generate the method itself, but I need to do few trials on my working PC when I can

hearty carbon
#

Funnily enough I did notice something along the lines of that, but I thought differently about it.

#

Right now I have no clue on how I can use the toApiDto without calling the mapper manually. Plus I have been working on this for the past two days and chose a really "hacky" way for now to solve it.

#

My solution after two days, was to remove the Mappers in the annotation of @Mapper and let it be generated by MapStruct itself. That seemed to work, but it's not the best solution since this can be different sometimes and can give issues in the future.

#

I have to say, you have quite the amazing insight!

daring forge
#

also:

if you remove entirely the toIntegrationDto method, and of course the qualifiedByName

@Mapping(source = ".", target = "integrations")
DomainApiDto toDomainApiDto(DomainSQL domain);

what error does it give you?
That you need a method to convert Integration to IntegrationDTO?

hearty carbon
#

Nothing happened, it's just it took me a few hours (like an hour or two) to realize what the issue could be. But I left it out, since I couldn't work without calling it manually, so I've been searching for different solutions in the ConfigurationMapper instead of the function that's being called Manually.

#

The thing with IntegrationDto is that ConfigrationApiDto is being extended to a IntegrationDto.

@Data
@JsonTypeName("configuration")
@EqualsAndHashCode(callSuper = true)
public class ConfigurationApiDto extends IntegrationDto {

    private Long id;
    private List<KeyDto> keys;
    private List<SubscriptionApiDto> subscriptions;
}``````java
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public class IntegrationDto {

    private Long id;
}```
#

That's why I am trying to do it manually, otherwise I'm not able to map it to a integrationDto, when it's supposed to be configurationApiDto.

daring forge
#

this is getting harder than I expected.

I might have a "fast"/hackish idea, just to be sure that the problem is, indeed, the Mappers.getMapper

If it works, and the problem is that one, I might proceed to try a way to generate the correct mapper for the subclass. But I'm not sure to be able to do this second part this evening 🙂

hearty carbon
#

Sadly I can't seem to figure out how I can solve this the optimised and clean way.

daring forge
#

The fast/hackish idea to debug is:

  1. changing the interface DomainMapper, into an abstract class.
  2. remove the use ConfigurationMapper.class from the uses annotation
  3. @Autowire a ConfigurationMapper variable, into the DomainMapper abstract class
  4. Use the injected instance in your method, instead of the Mappers.getMapper

(MapStruct can create implementations even from Abstract Classes, and you can Inject whatever you want from the Spring Context)

hearty carbon
#

I'm trying it out for you right now to see if it works 😛

daring forge
#

🤞

hearty carbon
#

It's building now, takes a little bit of time.

#

It works, but my filter was commented. So give me a moment and let me see if my filter is being called as well.

#

Yup if I do the following my filter works fine:

@Mapper(
        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel = "spring",
        uses = {
                DomainMapperResolver.class,
                LanguageMapper.class
        }
)
public abstract class DomainMapper {

    @Autowired
    protected ConfigurationMapper configurationMapper;

    @Mapping(source = "domainName", target = "name")
    public abstract DomainShort toDto(DomainSQL domain);

    @Mapping(source = "domainName", target = "name")
    @Mapping(source = ".", target = "integrations", qualifiedByName = "toIntegrationDto")
    public abstract DomainApiDto toDomainApiDto(DomainSQL domain);

    @Named("toIntegrationDto")
    public List<IntegrationDto> toIntegrationDto(DomainSQL domain) {
        return domain.getConfigurations().stream()
                .map(configurationMapper::toApiDto)
                .collect(Collectors.toList());
    }
}```
#

Everything works as it should, but why would it not work as an interface?

daring forge
#

wait, it's working like this?

You didn't change
.map(Mappers.getMapper(ConfigurationMapper.class)::toApiDto)
into
.map(configurationMapper::toApiDto)
?

hearty carbon
#

My bad forgot to change it.

#

I'm not sending the actual code, since that is monstrous.

daring forge
#

Oh, ok, ok, no problem

hearty carbon
#

Changing from above and adding values with it.

daring forge
#

it's now working, because the ConfigurationMapper has been injected through spring.
The ConfigurationMapper instance that has been injected, it's been correctly handled by the Spring Context, and has the KeyMapper and the other one, correctly injected.

Evidently, when you use Mappers.getMapper(), as we hypotetized before, it gives you back an instance outside of the spring context.

(it probably uses something like new ConfigurationMapper() instead of doing its magic of dependency injections, and that class has nulls in its class variables

#

I'm pretty sure I have some piece of code that might be useful, to use the mapper as a regular interface, and to copy the entity into the correct DTO subclass, but I have it on the work-pc, and I'll check it out tomorrow

hearty carbon
#

That would be awesome! I'm in no rush and I'll leave this ticket open for you to answer me with the code that you have in mind 🙂

#

You can tag me anytime!

#

BTW, I have to say you have been great help in explaining and looking into the problem with me. One thing I noticed is that you have way better insight in MapStruct as well in Spring then I have and this gives me motivation to keep striving to become better!

daring forge
#

Thanks, I'm glad I managed to help you a bit 🙂

And don't worry...I only managed to understand that, because I cried A LOT over exceptions and similiar problems 😂

worldly cobaltBOT
#

💤 Post marked as dormant

This post has been inactive for over 300 minutes, thus, it has been archived.
If your question was not answered yet, feel free to re-open this post or create a new one.

hearty carbon
#

I hope this post stays open.

hearty carbon
#

I found a fix for it, @daring forge .
I don't know if it's the same way you have on your work, but my solution was as follows:

@Component
public class SpringContext implements ApplicationContextAware {

    private static ApplicationContext context;

    public static <T extends Object> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        SpringContext.context = context;
    }
}``````java
@Mapper(
        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel = "spring",
        uses = {
                DomainMapperResolver.class,
                LanguageMapper.class
        }
)
public interface DomainMapper {

    @Mapping(source = "domainName", target = "name")
    DomainShort toDto(DomainSQL domain);

    @Mapping(source = "domainName", target = "name")
    @Mapping(source = ".", target = "integrations", qualifiedByName = "toIntegrationDto")
    DomainApiDto toDomainApiDto(DomainSQL domain);

    @Named("toIntegrationDto")
    default List<IntegrationDto> toIntegrationDto(DomainSQL domain) {
        return domain.getConfigurations().stream()
                .map(SpringContext.getBean(ConfigurationMapper.class)::toApiDto)
                .collect(Collectors.toList());
    }
}
``` Now Spring is getting the right Bean and Mapper instance 😄
daring forge
#

Hi there!
That's actually interesting. You're basically doing a manual injection 🙂

It wasn't my solution (I'm looking for it, but I can't find the project where I used it), but I'm glad it's working!

hearty carbon
#

If you don't mind, please do keep looking. I want to see what your solution would be. That way I can learn more!

daring forge
#

Sorry man, I can't really find it anywhere

The idea, was to simply declare the mapping between your integrations and configurations

@Mapping(source = "configurations", target = "integrations")

this way, mapStruct should generate its own implementation, with correct injections.

Now, your remaining problem, is the fact that ConfigurationDto is a subclass of the source Integration

to solve this last problem, I remember having used either the @SubclassMapping annotation or the @ObjectFactory

#

(I deleted a couple of messages, because the examples didn't match your specific case)

somber bluff
# hearty carbon I found a fix for it, <@529305055791415297> . I don't know if it's the same way ...

Swap to componentModel=MappingConstants.ComponentModel.SPRING if you can, just easier to work with since you aren't hardcoding strings.

Likewise because we are dealing with Spring, you could change the DomainMapper like so.

@Mapper(
        collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
        unmappedTargetPolicy = ReportingPolicy.IGNORE,
        componentModel = MappingConstants.ComponentModel.SPRING,
        uses = {
                DomainMapperResolver.class,
                LanguageMapper.class
        }
)
public abstract class DomainMapper {
    @Autowired
    protected ConfigurationMapper configMapper; // Since Configuration Mapper is Spring Managed.

    @Mapping(source = "domainName", target = "name")
    DomainShort toDto(DomainSQL domain);

    @Mapping(source = "domainName", target = "name")
    @Mapping(source = ".", target = "integrations", qualifiedByName = "toIntegrationDto")
    DomainApiDto toDomainApiDto(DomainSQL domain);

    @Named("toIntegrationDto")
    protected List<IntegrationDto> toIntegrationDto(DomainSQL domain) {
        return domain.getConfigurations().stream()
                .map(configMapper::toApiDto)
                .collect(Collectors.toList());
    }
}

Since both the wanted mapper and the main mapper are Spring Managed, it's better to just @Autowire instead of using the Static Spring Context. Especially for Performance.