Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'No converter found' when injection ConversionService into Spring Service. / indirect reference to ConversionService #93

Closed
SimonFischer04 opened this issue Sep 5, 2023 · 6 comments

Comments

@SimonFischer04
Copy link

SimonFischer04 commented Sep 5, 2023

Already looked through existing issues but couldn't find anything. (Although this helped me with some other common issues like componentmodel=spring, central config, Injecting the Interface instead of the Adapter, ...)
I created an MVP to show an issue I had in one of my applications.

Given a Mapper like this:

@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
    /*
        this breaks
     */
    @Autowired
    ExampleService exampleService;

    /*
        this works
     */
//    @Autowired
//    ExampleServiceWithoutConversionService exampleServiceWithoutConversionService;

    @Override
    public abstract Example convert(@Nullable  ExampleDTO exampleDTO);
}

With Services:

// ExampleService.java
@Service
public class ExampleService {
    @Autowired
    ConversionService conversionService;

    // methods that do something ...
}
// ExampleServiceWithoutConversionService.java
@Service
public class ExampleServiceWithoutConversionService {
    // methods that do something ...
}

Central Config:

// MapperSpringConfig.java
@MapperConfig(componentModel = "spring", uses = MyAdapter.class)
@SpringMapperConfig(
        conversionServiceAdapterPackage ="com.example.springmapstructissue",
        conversionServiceAdapterClassName ="MyAdapter"
)
public interface MapperSpringConfig {
}

When running the application like this it results in the following error:

Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.example.springmapstructissue.ExampleDTO] to type [com.example.springmapstructissue.Example]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322) ~[spring-core-6.0.11.jar:6.0.11]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195) ~[spring-core-6.0.11.jar:6.0.11]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175) ~[spring-core-6.0.11.jar:6.0.11]
	at com.example.springmapstructissue.SpringMapstructIssueApplication.lambda$commandLineRunner$0(SpringMapstructIssueApplication.java:23) ~[classes/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771) ~[spring-boot-3.1.3.jar:3.1.3]
	... 5 common frames omitted

But when changing to the other Mapper (ExampleServiceWithoutConversionService.java in this example) then everything works. So something gets broken when injecting a Service into a Mapper that uses ConversionService.

(PS: In the real application this Service also injects a lot of other things like jpa repositories and does more, but not relevant for this problem as this does not result in the issue mentioned - so I also don't think its a Spring Context problem because injecting Repositories and other Services works but something seems special about ConversionService)

Full source of this example available on my Github: https://github.com/SimonFischer04/spring-mapstruct-issue

@Chessray
Copy link
Collaborator

Chessray commented Sep 5, 2023

Have you tried injecting the ConversionService with @Lazy like we do in the generated adapter? It sounds like you're experiencing an issue similar to #21.

@SimonFischer04 SimonFischer04 changed the title 'No converter found' when injection ConversionService into Spring Service. 'No converter found' when injection ConversionService into Spring Service. / indirect reference to ConversionService Sep 9, 2023
@SimonFischer04
Copy link
Author

just got some to test this a bit. Thanks for pointing me into the direction of #21. In fact I now think this is basically the same issue just that I had a Service-Layer in between that hid the real cause - still in cyclic dependency, but indirectly.

Firstly the solution, if anyone stumbles upon the same issue. One solution is to either:
Add @lazy inside the Mapper.

@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
    @Lazy
    @Autowired
    ExampleService exampleService;
}

Or inside the Service:

@Service
public class ExampleService {
    @Lazy
    @Autowired
    ConversionService conversionService;

    // methods that do something ...
}

This basically comes down to the same thing discussed in #21:

@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
    // FIX by adding @Lazy
   @Lazy
   @Autowired
    ExampleService exampleService;

Furthermore, after reading among others: #22 and https://mapstruct.org/documentation/spring-extensions/reference/html/#mapperAsConverter it seems to be recommended to inject the ConversionService interface instead to the generated Adapter - like shown in #21. #21 recommends (according to spring) to use something like instead of @lazy:

@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
//    @Autowired
    ConversionService conversionService;

    @Autowired
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }
}

This results in the same error.

But this works:

@Mapper(config = MapperSpringConfig.class)
public abstract class ExampleMapper implements Converter<ExampleDTO, Example> {
    //    @Autowired
    MyAdapter conversionServiceAdapter;

    @Autowired
    public void setConversionServiceAdapter(MyAdapter conversionServiceAdapter) {
        this.conversionServiceAdapter = conversionServiceAdapter;
    }
}

So how is the proper way to handle this (somehow injecting a ConversionService into a mapper) now? Inject the ConversionService using @Lazy or directly injecting the Adapter using setter-injection or another way?

PS: after now knowing the actual issue now, I can see a couple of related issues, maybe it would be nice to add a docs entry about using ConversionService inside a mapper.

@Chessray
Copy link
Collaborator

Chessray commented Sep 9, 2023

Why would you want to inject the ConversionService directly into a Mapper? The project's whole idea is that you assign the generated adapter to the Mapper's uses property. and thus decouple the Mappers from each other.

As for the various forms of injection: Ideally I'd want to use constructor injection, but MapStruct has no way of specifying this yet. I typically rely on field injection for things I can't put into the uses property.

The underlying issue is that the ConversionService's initialization seems to be a little "weird", i.e. not standard Spring behavior. There's not much we can do about this inside MapStruct (or this extension) which is why we chose to go down the @Lazy route - and wash our hands.

@SimonFischer04
Copy link
Author

Why would you want to inject the ConversionService directly into a Mapper? The project's whole idea is that you assign the generated adapter to the Mapper's uses property. and thus decouple the Mappers from each other.

I agree that direct injection does not make sense. Like the initial issue explains, I'm actually using a service in between. This was just to show the issue because it produces the same result.

Maybe a little bit more explanation of what I'm trying to accomplish would help. Maybe there's another way to solve this (still trying to make it as short as possible):
So I typically have 2/3 Classes for each type: XEntity, X (typical "domain" / "business" object) and XDTO. With 'XEntity' and 'X' having references to the actual objects and the DTO sometimes only containing ids to reference stuff, so I need to fetch the actual objects back from the database.

 // ImageEntity.java
@Entity
public class ImageEntity {
  // some fields ...
}

// ButtonEntity.class
@Entity
public class ButtonEntity {
   ImageEntity background;
   // ...
}
// Image.class
public record Image(/* some fields */){}

// Button.class
public record Button(
  Image background
   // ...
){}
// ButtonDTO
public record ButtonDTO(
  int backgroundID
   // ...
){}

So when receiving the dto on a post it needs firstly needs to be converted back to a domain object. For this I have a Mapper:

// ButtonDTOToButtonMapper.java
@Mapper(config = MapperSpringConfig.class)
public abstract class ButtonDTOToButtonMapper implements Converter<ButtonDTO Button> {
    @Mapping(target = "background", source = "backgroundId", qualifiedByName = "mapBackground"),
    public abstract Field convert(@Nullable ViewController.FieldDTO fieldDTO);

    @Autowired
    @Lazy // here the issue started, without using @Lazy
    private ImageService imageService;

    @Named("mapBackground")
    public Image mapBackground(Integer backgroundId) {
        // service required to get the actual object by id
        return imageService.findById(backgroundId);
    }
}

The service encapsulates all the Entity/DB specific logic. XEntity objects never leave/enter the service. The X objects are what the main application logic is using. But this Service also requires ConversionService access because it ultimately (after doing some other stuff) maps from the Entity to the domain Object.

// ImageService.class
@Service
@RequiredArgsConstructor
public class ImageService {
    private final ImageRepository repository;
    private final ConversionService conversionService; // ConversionService is required here

    @Override
    public Image findById(int id) {
        ImageEntity entity = repository.findById(id).orElse(null);
       // do some stuff
        return conversionService.convert(entity, Image.class);
    }
}

The ImageEntity -> Image mapper is just a simple 1 to 1 mapping:

// ImageEntityToImageMapper.java
@Mapper(config = MapperSpringConfig.class)
public interface ImageEntityToImageMapper extends Converter<ImageEntity, Image> {
    Image convert(@Nullable ImageEntity imageEntity);
}

So one way to deal with this / work more with uses would of course be to inject the ImageRepository directly into the ButtonDTOToButtonMapper but then I'd need to duplicate the service-logic in there...

Hope this helps to understand my reasoning.

@Chessray
Copy link
Collaborator

Thanks, that makes some sense. Some of the Mappers function as enrichers with DB information. I still think the best option in this scenario is using @Lazy in one of the injection points.

@SimonFischer04
Copy link
Author

OK, thanks.
(I ended up injecting the Service using @lazy into the mapper.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants