How to use constructor injection in Mapstruct's mapper? - java

In some mapper classes, I need to use an autowired ObjectMapper to transform String to JsonNode or verse-vera. I can acheive my goal by using the field injection with #autowired. But it's not suitable for unit test so I'd like to try using the constructor injection.
My current working code with the field injection:
#Mapper(componentModel = "spring")
public class CustomMapper {
#autowired
ObjectMapper mapper;
}
I try to convert it to the constructor injection so I can provide constructor argument in my unit test:
#Mapper(componentModel = "spring")
public class CustomMapper {
ObjectMapper mapper;
public CustomMapper(ObjectMapper mapper) {
this.mapper = mapper;
}
}
But I get a Constructor in CustomMapper cannot be applied to the given type error during compilation.
How do I fix it? Or is there other better way to map String to JsonNode in Mapstruct?

Constructor injection cannot be used in the mapper definition. Only in the mapper implementation.
However, for unit testing I'd suggest that you use setter injection.
Your mapper will then look like:
#Mapper( componentModel = "spring")
public class CustomMapper {
protected ObjectMapper mapper;
#Autowired
public void setMapper(ObjectMapper mapper) {
this.mapper = mapper;
}
}

1)The MapStruct has good feature:
#Mapper(componentModel = "spring", uses ={ObjectMapper.class}, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
2)You could do it this way:
#Mapper(componentModel = "spring")
#RequiredArgsConstructor //lombok annotation, which authowire your field via constructor
public class CustomMapper {
private final ObjectMapper mapper;
}
But still you could do it via field.You stiil should mock it in tests in both cases. Just remember to use #InjectMocks
public CustomMapperTest {
#InjectMocks
private CustomMapper customMapper;
#Mock
private ObjectMapper objectMapper
#BeforeEach
void setUp() {
customMapper= new CustomMapperImpl();
MockitoAnnotations.initMocks(this);
when(objectMapper.map()).thenReturn(object);
}
#Test
void shouldMap() {
Object toObject = customerMapper.map(fromObject);
assertThat(toObject)
.hasFieldWithValue("fieldName", fromObject.getField());
}
}

Related

Mapstruct: how to autowire abstract class mapper

my problem seems easy, but may be I'm doing something wrong. this is mapper class with #Named("toLocationListCommon") bean which suppose to be used in other mappers
#Mapper(componentModel = "spring")
public abstract class CommonLocationMapper {
#Setter(onMethod_ = #Autowired)
protected TestService testService;
#Named("toLocationListCommon")
List<Location> toLocationListCommon(List<? extends ILocation> loc) {
//do mapping logic
}
}
here I'm trying to use it:
#Mapper(implementationName = "V1Impl", config = CommonMapper.CommonMapperConfig.class, uses = CommonLocationMapper.class)
interface TestMapper {
//other mapping methods
}
I expect to have autowired bean CommonLocationMapper in implementation for TestMapper, but I haven't.
What I'm doing wrong? thanks in advance!
uses clause in #Mapper annotation allow your mapper to use the mappers you defined if they are required. If you want to just get a reference to mapper you can use.
MyMapper INSTANCE = Mappers.getMapper( MyMapper.class );

Could not inject different beans with named qualifier in Micronaut

I have defined an ObjectMapper factory class like this:
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
#Factory
public class MyObjectMapper {
#Singleton
#Named("jsonObjectMapper")
public ObjectMapper getJsonObjectMapper() {
return new ObjectMapper(new JsonFactory());
}
#Singleton
#Named("yamlObjectMapper")
public ObjectMapper getYamlObjectMapper() {
return new ObjectMapper(new YAMLFactory());
}
}
Then, on client class, I tried to inject them like this:
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
#Singleton
public class MyServiceImpl implements MyService {
private ObjectMapper jsonMapper;
private ObjectMapper yamlMapper;
#Inject
#Named("jsonObjectMapper")
public void setJsonMapper(ObjectMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
#Inject
#Named("yamlObjectMapper")
public void setYamlMapper(ObjectMapper yamlMapper) {
this.yamlMapper = yamlMapper;
}
...
My goal is to have jsonMapper to be injected by the bean with #Named("jsonObjectMapper") on MyObjectMapper class, and yamlMapper with #Named("yamlObjectMapper"). But, when I tried to debug, jsonMapper and yamlMapper had the same reference, which means they are injected by the same ObjectMapper. My question is how to inject 2 different beans for json and yaml mapper on Micronaut?
Thank you!
The injection qualified by name can be done with the #Named annotation used on the method argument, and not the method itself. It means that in your case you would have to move the #Named annotation to the setJsonMapper and setYamlMapper methods arguments.
#Singleton
public class MyServiceImpl {
private ObjectMapper jsonMapper;
private ObjectMapper yamlMapper;
#Inject
public void setJsonMapper(#Named("jsonObjectMapper") ObjectMapper jsonMapper) {
this.jsonMapper = jsonMapper;
}
#Inject
public void setYamlMapper(#Named("yamlObjectMapper") ObjectMapper yamlMapper) {
this.yamlMapper = yamlMapper;
}
// ...
}
Alternatively, you could use construction injection combined with the #Named annotation for each parameter. It allows you to mark both fields as private, just to make sure these objects are not re-assigned at the runtime.
#Singleton
public class MyServiceImpl {
private final ObjectMapper jsonMapper;
private final ObjectMapper yamlMapper;
public MyServiceImpl(
#Named("jsonObjectMapper") ObjectMapper jsonMapper,
#Named("yamlObjectMapper") ObjectMapper yamlMapper) {
this.jsonMapper = jsonMapper;
this.yamlMapper = yamlMapper;
}
// ...
}

Use Jackson Objectmapper configured by Spring boot in Hibernate

I want to configure Hibernate to use Jackson's Objectmapper created by Spring to map between json and entities. In the project I'm working on I already configured Jooq to use the Spring's ObjectMapper but I'm having trouble how to configure Hibernate to use it. The goal in the end is that both Jooq and Hibernate would use the same ObjectMapper.
I checked this article by Vlad. Unfortunately all the tips given in the article don't work for the project I'm working on.
Here's an example configuration I tried
#Configuration
public class HibernateConfiguration implements HibernatePropertiesCustomizer {
//Autowire Objectmapper created by Spring
#Autowired
ObjectMapper objectMapper;
#Override
public void customize(Map<String, Object> hibernateProperties) {
ObjectMapperSupplier objectMapperSupplier = () -> objectMapper;
// Below config doesn't work since Hibernate types creates it's own mapper
hibernateProperties.put("hibernate.types.jackson.object.mapper", objectMapperSupplier);
}
Also tried the same approach by adding the Objectmapper to hibernate-types.properties.
#Used by Hibernate but cannot get reference of Spring managed ObjectMapper since this is class is called outside of Spring's context.
hibernate.types.jackson.object.mapper=path.to.ObjectMapperSupplier
Another approach I used but it fails with a NullpointerException when converting from JSON to an entity in JsonTypeDescriptor class.
#Configuration
public class HibernateConfiguration implements HibernatePropertiesCustomizer{
#Autowired
ObjectMapper objectMapper;
#Override
public void customize(Map<String, Object> hibernateProperties) {
// Underlying implementation needs some JavaType or propertyClass, otherwise when converting
// from JSON we get a nullpointer.
var jsonBinaryType = new JsonBinaryType(objectMapper);
hibernateProperties.put("hibernate.type_contributors", (TypeContributorList) () ->
Collections.singletonList((typeContributions, serviceRegistry) ->
typeContributions.contributeType(jsonBinaryType)));
}
Below is the type declaration for entity super class.
// This makes Hibernate types create it's own mapper.
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
#MappedSuperclass
public abstract class Entity{
}
So, are there any possible solutions how I can hook up Spring managed ObjectMapper to Hibernate?
I finally figured this out, but it is kind of an creative solution...
TLDR: I have a bean that stores the Spring-configured objectMapper in a static field. A BeanFactoryPostProcessor ensures that this bean is initialized before Hibernate (types) tries to load / get the ObjectMapper.
hibernate.properties
hibernate.types.jackson.object.mapper=com.github.lion7.example.HibernateObjectMapperSupplier
HibernateObjectMapperSupplier.kt
package com.github.lion7.example
import com.fasterxml.jackson.databind.ObjectMapper
import com.vladmihalcea.hibernate.type.util.ObjectMapperSupplier
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.stereotype.Component
class HibernateObjectMapperSupplier : ObjectMapperSupplier {
override fun get(): ObjectMapper =
ObjectMapperHolder.objectMapper
}
#Component
class ObjectMapperHolder(objectMapper: ObjectMapper) {
companion object {
lateinit var objectMapper: ObjectMapper
}
init {
Companion.objectMapper = objectMapper
}
}
#Component
class ObjectMapperDependencyFixer : BeanFactoryPostProcessor {
override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
val beanDefinition = beanFactory.getBeanDefinition("entityManagerFactory")
val oldDependsOn = beanDefinition.dependsOn ?: emptyArray()
val newDependsOn = oldDependsOn + "objectMapperHolder"
beanDefinition.setDependsOn(*newDependsOn)
}
}
Same code as gist: https://gist.github.com/lion7/c8006b69a309e38183deb69124b888b5
A Java implementation.
#Component
public class HibernateObjectMapper implements Supplier<ObjectMapper> {
private static ObjectMapper objectMapper;
#Autowired
public void setObjectMapper(ObjectMapper objectMapper) {
HibernateObjectMapper.objectMapper = objectMapper;
}
#Override
public ObjectMapper get() {
return objectMapper;
}
}
If you define your own JPA beans, simply add #DependsOn("hibernateObjectMapper") to their config. Otherwise you need a BeanPostProcessor to add the dependency to the autoconfigured bean:
#Component
class HibernateBeanDependencyProcessor implements BeanFactoryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) {
BeanDefinition beanDefinition = factory.getBeanDefinition("entityManagerFactory");
String[] dependsOn = beanDefinition.getDependsOn();
dependsOn = dependsOn == null ? new String[]{} : dependsOn;
String[] newDependsOn = new String[dependsOn.length + 1];
System.arraycopy(dependsOn, 0, newDependsOn, 1, dependsOn.length);
newDependsOn[0] = "hibernateObjectMapper";
beanDefinition.setDependsOn(newDependsOn);
}
}
As for the property, hibernate.types.* don't work when set programmatically. The library looks directly in the hibernate.properties, hibernate-types.properties, and application.properties files.
I think that I've found solution to do it programmatically (without magic with fixing dependency graph).
HibernateConfiguration.kt
#Configuration(proxyBeanMethods = false)
class HibernateConfiguration {
#Bean
fun hibernatePropertiesCustomizer(
objectMapper: ObjectMapper // Thanks to that Spring can create correct dependency graph
): HibernatePropertiesCustomizer =
HibernatePropertiesCustomizer { hibernateProperties ->
HibernateObjectMapperSupplier.objectMapper = objectMapper
hibernateProperties["hibernate.types.jackson.object.mapper"] = HibernateObjectMapperSupplier::class.qualifiedName
}
}
HibernateObjectMapperSupplier.kt
class HibernateObjectMapperSupplier : Supplier<ObjectMapper> {
override fun get(): ObjectMapper {
return objectMapper
}
companion object {
lateinit var objectMapper: ObjectMapper
}
}
System.getProperties().put(
Configuration.PropertyKey.JACKSON_OBJECT_MAPPER.getKey(),
MyObjectMapperSupplier.class.getName()
);

How to mock ObjectMapper with mokito?

I'm having trouble with mocking an ObjectMapper bean with mokito.
My class containing the ObjectMapper :
public class ServiceParent{
#Autowired
protected ObjectMapper objectMapper;
public void someMethod(){
...
Map<String, String> mapResponse = objectMapper.readValue("some json", new TypeReference<Map<String, String>>(){});
}
Other class extending previous one
public class ServiceChild extends ServiceParent{
...
}
My test class :
#SpringBootTest
public class TestService{
#Mock
ObjectMapper objectMapper;
#InjectMocks
ServiceChild serviceChild;
#Test
public void test(){
Map<String, String> mapResponse=new HashMap<String, String>();
mapResponse.put("access_token", "token_bouhon");
Mockito.when(objectMapper.readValue(Mockito.anyString(), Mockito.any(Class.class))).thenReturn(mapResponse);
}
So when I debug this, in ServiceParent, objectMapper isn't null but readValue(...) return null.
Do you have an idea on how to return the correct mocked object?
Thanks
I faced the same NPE with the objectmapper. This issue is resolved after adding ,
my test class has class level annotation with
#RunWith(MockitoJUnitRunner.class)
and the fix
#Before
public void setupBefore() {
MockitoAnnotations.initMocks(this);
}
#Mock creates a new mock. It's equivalent to calling mock(SomeClass.class)
#MockBean creates a mock, like #Mock, but also replaces any bean already in the application context with the same type with that mock. So any code which Autowires that bean will get the mock.
You need to use #MockBean

MapStruct : mocking nested mapper

I using MapStruct to map my entities, and I'm mocking my objects using Mockito.
I want to test a method that contains a mapping with mapStruct.
The problem is the nested mapper is always null in my unit tests (works well in the application)
this is my mapper declaration :
#Mapper(componentModel = "spring", uses = MappingUtils.class)
public interface MappingDef {
UserDto userToUserDto(User user)
}
this is my nested mapper
#Mapper(componentModel = "spring")
public interface MappingUtils {
//.... other mapping methods used by userToUserDto
this is the method that I want to test :
#Service
public class SomeClass{
#Autowired
private MappingDef mappingDef;
public UserDto myMethodToTest(){
// doing some business logic here returning a user
// User user = Some Business Logic
return mappingDef.userToUserDto(user)
}
and this is my unit test :
#RunWith(MockitoJUnitRunner.class)
public class NoteServiceTest {
#InjectMocks
private SomeClass someClass;
#Spy
MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
#Spy
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);
//initMocks is omitted for brevity
#test
public void someTest(){
UserDto userDto = someClass.myMethodToTest();
//and here some asserts
}
mappingDef is injected correctly, but mappingUtils is always null
Disclamer : this is not a duplicate of this question. He is using #Autowire so he is loading the spring context so he is doing integration tests. I'm doing unit tests, so I dont to use #Autowired
I dont want to make mappingDef and mappingUtils #Mock so I don't need to do when(mappingDef.userToUserDto(user)).thenReturn(userDto) in each use case
If you are willing to use Spring test util, it's fairly easy with org.springframework.test.util.ReflectionTestUtils.
MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);
...
// Somewhere appropriate
#Before
void before() {
ReflectionTestUtils.setField(
mappingDef,
"mappingUtils",
mappingUtils
)
}
force MapStruct to generate implementations with constructor injection
#Mapper(componentModel = "spring", uses = MappingUtils.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MappingDef {
UserDto userToUserDto(User user)
}
#Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MappingUtils {
//.... other mapping methods used by userToUserDto
use constructor injection, so that you can construct the class under test with a mapper.
#Service
public class SomeClass{
private final MappingDef mappingDef;
#Autowired
public SomeClass(MappingDef mappingDef) {
this.mappingDef = mappingDef;
}
public UserDto myMethodToTest(){
// doing some business logic here returning a user
// User user = Some Business Logic
return mappingDef.userToUserDto(user)
}
Test SomeClass. Note: its not the mapper that you test here, so the mapper can be mocked.
#RunWith(MockitoJUnitRunner.class)
public class SomeClassTest {
private SomeClass classUnderTest;
#Mock
private MappingDef mappingDef;
#Before init() {
classUnderTest = new SomeClass(mappingDef);
// defaultMockBehaviour:
when(mappingDef.userToUserDto(anyObject(User.class).thenReturn(new UserDto());
}
#test
public void someTest(){
UserDto userDto = someClass.myMethodToTest();
//and here some asserts
}
And in a true unit test, test the mapper as well.
#RunWith(MockitoJUnitRunner.class)
public class MappingDefTest {
MappingDef classUnderTest;
#Before
void before() {
// use some reflection to get an implementation
Class aClass = Class.forName( MappingDefImpl.class.getCanonicalName() );
Constructor constructor =
aClass.getConstructor(new Class[]{MappingUtils.class});
classUnderTest = (MappingDef)constructor.newInstance( Mappers.getMapper( MappingUtils.class ));
}
#Test
void test() {
// test all your mappings (null's in source, etc)..
}
As a variant on Sjaak’s answer, it is now possible to rely on MapStruct itself to retrieve the implementation class while also avoiding casts by properly using generics:
Class<? extends MappingDef> mapperClass = Mappers.getMapperClass(MappingDef.class);
Constructor<? extends MappingDef> constructor = mapperClass.getConstructor(MappingUtils.class);
MappingDef mappingDef = constructor.newInstance(Mappers.getMapper(MappingUtils.class));
This could probably even be made completely generic by inspecting the constructor, finding all mappers it requires as arguments and recursively resolving those mappers.
So, try this:
Maven:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>test</scope>
</dependency>
#ComponentScan(basePackageClasses = NoteServiceTest.class)
#Configuration
public class NoteServiceTest {
#Autowired
private SomeClass someClass;
private ConfigurableApplicationContext context;
#Before
public void springUp() {
context = new AnnotationConfigApplicationContext( getClass() );
context.getAutowireCapableBeanFactory().autowireBean( this );
}
#After
public void springDown() {
if ( context != null ) {
context.close();
}
}
#test
public void someTest(){
UserDto userDto = someClass.myMethodToTest();
//and here some asserts
}
Even better would be use constructor injection all the way... Also in SomeClass and by using #Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR).. Then you don't need spring / spring mocking in your test cases.
As mentioned you can use the injectionStrategy = InjectionStrategy.CONSTRUCTOR for mappers using other mappers (in this case for the MappingDef).
And than in the test simply:
#Spy
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);
#Spy
MappingDef mappingDef = new MappingDefImpl(mappingUtils);
Maybe not the most elegant but it works.
There is no need to use reflections. The simplest way for me was the following:
#RunWith(MockitoJUnitRunner.class)
public class NoteServiceTest {
#InjectMocks
private SomeClass someClass;
#Spy
#InjectMocks
MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
#Spy
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);
this however only works on the first level of nested Mappers. If you have a Mapper that uses a Mapper which uses a thrid Mapper than you need to use ReflectionTestUtils to Inject the third Mapper into the second Mapper.

Categories