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

Switching vom 1.2.0.Final to 1.3.0.Final leads to ignored ObjectFactory methods #1713

Open
lazydays79 opened this issue Feb 12, 2019 · 21 comments

Comments

@lazydays79
Copy link

Yesterday I tried to switch from Mapstruct 1.2.0.Final to 1.3.0.Final. But this lead to some NullPointerExceptions in our unit tests.

After some investigation, I found out, that mapstruct now seems to ignore the ObjectFactory annotated method.

Here is some quick overview.

The target class, in which mapstruct will map:

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class TargetClass {

    private String name;
    private Boolean oneFlag;
    private Boolean anotherFlag;

}

The source class, from which mapstruct will map:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "sourceClass_Type", propOrder = {
        "anXmlFlag",
        "anotherXmlFlag"
})
public class SourceClass {

    private Boolean anXmlFlag;
    private Boolean anotherXmlFlag;

    // jaxb generates is...() although it is a Boolean and not a boolean
    public Boolean isAnXmlFlag() {
        return anXmlFlag;
    }

    public void setAnXmlFlag(Boolean anXmlFlag) {
        this.anXmlFlag = anXmlFlag;
    }

    // jaxb generates is...() although it is a Boolean and not a boolean
    public Boolean isAnotherXmlFlag() {
        return anotherXmlFlag;
    }

    public void setAnotherXmlFlag(Boolean anotherXmlFlag) {
        this.anotherXmlFlag = anotherXmlFlag;
    }
}

The mapper:

@Mapper
public abstract class GreatMapper {

    public static final GreatMapper MAPPER = Mappers.getMapper(GreatMapper.class);

    @Mappings({
            @Mapping(source = "str", target = "name"),
            @Mapping(source = "sourceClass.anXmlFlag", target = "oneFlag", defaultValue = "false"),
            @Mapping(source = "sourceClass.anotherXmlFlag", target = "anotherFlag", defaultValue = "false")
    })
    @BeanMapping(nullValueMappingStrategy = RETURN_DEFAULT)
    public abstract TargetClass mapSourceClass(String str, SourceClass sourceClass);

    @ObjectFactory
    protected TargetClass createDefaultTargetClass() {
        return new TargetClass(null, FALSE, FALSE);
    }
}

Let's have a look into the mapper implementation generated by 1.2.0.Final:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.2.0.Final, compiler: javac, environment: Java 1.8.0_60 (Oracle Corporation)"
)
@Component
public class GreatMapperImpl extends GreatMapper {

    @Override
    public TargetClass mapSourceClass(String str, SourceClass sourceClass) {

        TargetClass targetClass = createDefaultTargetClass();

        if ( str != null ) {
            targetClass.setName( str );
        }
        if ( sourceClass != null ) {
            if ( sourceClass.isAnXmlFlag() != null ) {
                targetClass.setOneFlag( sourceClass.isAnXmlFlag() );
            }
            else {
                targetClass.setOneFlag( Boolean.parseBoolean( "false" ) );
            }
            if ( sourceClass.isAnotherXmlFlag() != null ) {
                targetClass.setAnotherFlag( sourceClass.isAnotherXmlFlag() );
            }
            else {
                targetClass.setAnotherFlag( Boolean.parseBoolean( "false" ) );
            }
        }

        return targetClass;
    }
}

And now the one generated by mapstruct 1.3.0.Final:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_60 (Oracle Corporation)"
)
@Component
public class GreatMapperImpl extends GreatMapper {

    @Override
    public TargetClass mapSourceClass(String str, SourceClass sourceClass) {

        TargetClassBuilder targetClass = TargetClass.builder();

        if ( str != null ) {
            targetClass.name( str );
        }
        if ( sourceClass != null ) {
            if ( sourceClass.isAnXmlFlag() != null ) {
                targetClass.oneFlag( sourceClass.isAnXmlFlag() );
            }
            else {
                targetClass.oneFlag( false );
            }
            if ( sourceClass.isAnotherXmlFlag() != null ) {
                targetClass.anotherFlag( sourceClass.isAnotherXmlFlag() );
            }
            else {
                targetClass.anotherFlag( false );
            }
        }

        return targetClass.build();
    }
}

Can you spot the difference? :)

The propblem here is, that this leads to uninitialized properties in the TargetClass object, in this case the two Boolean ones. And thanks to Autoboxing (which they should have never introduced in java IMHO), NullPointerExceptions occur in our unit tests.

I will provide more details in further comments to this issue...

Regards,
Michael

@lazydays79
Copy link
Author

lazydays79 commented Feb 12, 2019

One interesting fact is that it took me hours to create a maven project from scratch, which reproduces the problem.

As long as I put all together in one maven artifact (including the model, the mapper, and both dependencies to lombok and mapstruct), everything worked quite well. The problem does not occur then.

So I splitted the project into two artifacts: one containing just the model and the dependency to lombok, and one just containing the mapper and the dependency to mapstruct. And then, the error occurs.

Or in other words: The error does not occur as long as mapstruct seems to find lombok (or the lombok processor) on its classpath.

I tried to have a look into the source code differences between mapstruct 1.2.0 and 1.3.0 to identify the possible root cause... but that's too much code to review for me right now.

@lazydays79
Copy link
Author

I'm providing now the example project, which proves the error. Please have a look into the following two branches:

Example using 1.2.0.Final
Example using 1.3.0.Final

They only differ in the used mapstruct version.

Output by maven when building the 1.2.0.Final version of the project:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.wigball.mapstruct.issue.mapper.GreatMapperTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.064 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ mapstruct1.3.0.issue.impl ---
[INFO] Building jar: C:\Users\A163814\dev\mapstruct1.3.0issue\impl\target\mapstruct1.3.0.issue.impl-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-install-plugin:2.4:install (default-install) @ mapstruct1.3.0.issue.impl ---
[INFO] Installing C:\Users\A163814\dev\mapstruct1.3.0issue\impl\target\mapstruct1.3.0.issue.impl-0.0.1-SNAPSHOT.jar to C:\Users\A163814\.m2\repository\com\wigball\mapstruct\mapstruct1.3.0.issue.impl\0.0.1-SNAPSHOT\mapstruct1.3.0.iss
ue.impl-0.0.1-SNAPSHOT.jar
[INFO] Installing C:\Users\A163814\dev\mapstruct1.3.0issue\impl\pom.xml to C:\Users\A163814\.m2\repository\com\wigball\mapstruct\mapstruct1.3.0.issue.impl\0.0.1-SNAPSHOT\mapstruct1.3.0.issue.impl-0.0.1-SNAPSHOT.pom
[INFO]
[INFO] ---------< com.wigball.mapstruct:mapstruct1.3.0.issue.parent >----------
[INFO] Building mapstruct1.3.0.issue.parent 0.0.1-SNAPSHOT                [3/3]
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ mapstruct1.3.0.issue.parent ---
[INFO]
[INFO] --- maven-install-plugin:2.4:install (default-install) @ mapstruct1.3.0.issue.parent ---
[INFO] Installing C:\Users\A163814\dev\mapstruct1.3.0issue\pom.xml to C:\Users\A163814\.m2\repository\com\wigball\mapstruct\mapstruct1.3.0.issue.parent\0.0.1-SNAPSHOT\mapstruct1.3.0.issue.parent-0.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] mapstruct1.3.0.issue.model ......................... SUCCESS [  3.067 s]
[INFO] mapstruct1.3.0.issue.impl .......................... SUCCESS [  2.509 s]
[INFO] mapstruct1.3.0.issue.parent 0.0.1-SNAPSHOT ......... SUCCESS [  0.027 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.772 s
[INFO] Finished at: 2019-02-12T09:32:45+01:00
[INFO] ------------------------------------------------------------------------

Output by the 1.3.0.Final version of the project:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.wigball.mapstruct.issue.mapper.GreatMapperTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.1 sec <<< FAILURE!
testMapping(com.wigball.mapstruct.issue.mapper.GreatMapperTest)  Time elapsed: 0.023 sec  <<< ERROR!
java.lang.NullPointerException
        at com.wigball.mapstruct.issue.mapper.GreatMapperTest.testMapping(GreatMapperTest.java:22)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
        at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
        at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)


Results :

Tests in error:
  testMapping(com.wigball.mapstruct.issue.mapper.GreatMapperTest)

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] mapstruct1.3.0.issue.model ......................... SUCCESS [  3.000 s]
[INFO] mapstruct1.3.0.issue.impl .......................... FAILURE [  2.625 s]
[INFO] mapstruct1.3.0.issue.parent 0.0.1-SNAPSHOT ......... SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.824 s
[INFO] Finished at: 2019-02-12T09:31:53+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project mapstruct1.3.0.issue.impl: There are test failures.

@lazydays79
Copy link
Author

Please let me know if I you need any further information.

Thanks for looking into this!

Regards,
Michael

@sjaakd
Copy link
Contributor

sjaakd commented Feb 12, 2019

Hey Michael.. I'll have a look asap. Thanks for your info. Passing default values as primitive values has been implemented by me as an optimization request. You could try defaultExpression..

@sjaakd
Copy link
Contributor

sjaakd commented Feb 12, 2019

@lazydays79 , I'm trying to read through your comments above.. And I'm a bit puzzled. Lets take them one by one:

After some investigation, I found out, that mapstruct now seems to ignore the ObjectFactory annotated method.

Where is this shown in your example? Or was this an initial though?

Can you spot the difference? :)

The propblem here is, that this leads to uninitialized properties in the TargetClass object, in this case the two Boolean ones. And thanks to Autoboxing (which they should have never introduced in java IMHO), NullPointerExceptions occur in our unit tests.

The problem is not as such in Autoboxing. Thats fine. Its the Auto-unboxing which forms the root of the problem. So, the case were you have a wrapper-type that is potentially null and try to map it to a primitive. Vice versa there should be no problem.

I'm providing now the example project, which proves the error. Please have a look into the following two branches:

Example using 1.2.0.Final
Example using 1.3.0.Final

I'll focus on that part.. Right?

@sjaakd
Copy link
Contributor

sjaakd commented Feb 12, 2019

        TargetClassBuilder targetClass = TargetClass.builder();

Your problem is above.. You are using Lombok with the builder. However, You are also requesting Mapstruct to create the object for your in an ObjectFactory method.. You should disable the builders if you don't want that.. Have a look in the documentation. on howto.


EDIT

You could also remove the @Builder annotation from your Target object (if you are not using that). That solves it immediately

@sjaakd
Copy link
Contributor

sjaakd commented Feb 12, 2019

@filiphr .. I tried to extend @lazydays79's example.. @lazydays79 : (your 1.3.0.Final example is actually 1.2.0.Final) with the noopbuilder. However it still creates the builder iso calling the object factory annotated method.. Think that that's an error.. Can you have a look?

@lazydays79
Copy link
Author

So, now you feel as confused as me the last days. 😁

Removing @builder is not an option, since this would lead to many other source code changes.

The problem is indeed that mapstruct 1.3.0 ignores the object factory method, but 1.2.0 uses it.

And this only occurs as long lombok is not a dependency in the same artifact where I'm using mapstruct.

So I would say that this is definetly an issue here.

Regards,
Michael

@lazydays79
Copy link
Author

@sjaakd maybe I missed up the links, I will check that tomorrow. But you should find two branches in my repository. One using 1.3.0 and one using 1.2.0.

Just do a mvwn clean install in both, and you should see the difference.

@lazydays79
Copy link
Author

And I would say that object factories must be used by mapstruct if defined. Regardless whether the target class uses constructors, builders or whatsoever.

Object factories should beat them all.

@sjaakd
Copy link
Contributor

sjaakd commented Feb 12, 2019

I possibly see two problems.. yes.. you are right about objectfactories.. and.. it is strange that a builder is generated when with the noopbuilder option..

@sjaakd
Copy link
Contributor

sjaakd commented Feb 13, 2019

Hi Michael: its also a bit strange that you use both @Data and @Builder. The first one implies (at least in my mind 😄 ) mutable beans, the second one immutable. Note: the '@DaTaalso implies@Setter.. Cant you limit the @DaTato only@Getter`?

@lazydays79
Copy link
Author

Yes, I know. But please don't blame me for that code. 😅

I just wanted to stick as near as possible to our commercial code, which in this case isn't made by me. And I don't want to change the code in one of our basic libraries, because it would be a breaking change (although it makes sence, I agree on that with you).

Never the less, we still have the fact that mapstruct 1.3.0 ignores object factories under certain circumstances.

@sjaakd
Copy link
Contributor

sjaakd commented Feb 13, 2019

There are 2 options, I think..

  1. I tried the opt out for the builder. It did not work on my side. That's what I wrote in the previous comment. We should fix that and it should be simpler to do an opt-out (not via SPI). That will, most likely end up in a bugfix release. So: wait on bugfix release.

  2. You might want to return a builder from your factory. Did not try it yet in your example. MapStruct will use the builder to construct the target object. I don't know how big the impact is on your side. (How many cases you need to fix in practice).. But you got give it a shot.

@sjaakd
Copy link
Contributor

sjaakd commented Feb 13, 2019

Hi @lazydays79 .. The NoOpBuilderProvider works I could not directly push to your branch. But here's the diff in the form of a PR.

@filiphr .. our documentation is not right. It suggests that the SPI can be in the project-at-hand itself. Thats not possible because you will not get the SPI file on your annotation processor class path.. Hence you need to define a small module that does so.

@lazydays79
Copy link
Author

lazydays79 commented Feb 13, 2019 via email

@filiphr
Copy link
Member

filiphr commented Feb 17, 2019

@sjaakd the reason why the NoOpBuilderProvider is not picked up is because the maven-compiler annotationProcessorPaths is used. In order to solve this it either needs to be done like you did in the PR or add the mapstruct-processor as a direct dependency.

@lazydays79 builders have highest priority. Perhaps we need to iterate that and if there is an @ObjectFactory for the type to use that. The reason why we want with this approach is due to the fact that we thought that if you use builders then your objects are immutable.

@sjaakd what do you think about #1661? I think that could be a solution for this, I would say that perhaps we even need to add a compiler option so that you can turn off the builders, this way it would be easier for people to do it.

@sjaakd
Copy link
Contributor

sjaakd commented Feb 17, 2019

@sjaakd the reason why the NoOpBuilderProvider is not picked up is because the maven-compiler annotationProcessorPaths is used. In order to solve this it either needs to be done like you did in the PR or add the mapstruct-processor as a direct dependency.

I guess we have to update our documentation for that and provide an example in our repo...

@sjaakd
Copy link
Contributor

sjaakd commented Feb 17, 2019

@sjaakd what do you think about #1661? I think that could be a solution for this, I would say that perhaps we even need to add a compiler option so that you can turn off the builders, this way it would be easier for people to do it.

I think we should have more fine grained control on when to use builders. This issue nicely demonstrates that.. Here setters are used (falsely IHMO) after the object has been build by means of an object factory in a default way: in my mind the builder is inextricably connected to immutable objects.

But having said it's quite reasonable to assume that a bean consists out of properties that are immutable (builder) and mutable (setter). I don't think we can handle that out of the box. It would demand even mixed scenarios within one mapping method:

  1. first handling immutable properties via a builder
  2. next handling mutable properties via setters

Having an object factory to initialise a bean using a builder, continuing with setters is also quite reasonable.

@thunderhook
Copy link
Contributor

But having said it's quite reasonable to assume that a bean consists out of properties that are immutable (builder) and mutable (setter). I don't think we can handle that out of the box. It would demand even mixed scenarios within one mapping method:

1. first handling immutable properties via a builder

2. next handling mutable properties via setters

Having an object factory to initialise a bean using a builder, continuing with setters is also quite reasonable.

Just for reference, the mix of immutability with (some) mutable fieldshas been also been asked for in #2155.

@filiphr
Copy link
Member

filiphr commented Sep 30, 2023

This seems to have slipped through the cracks. Reading it now again we should perhaps give @ObjectFactory higher priority. So if @ObjectFactory exists for the target class we should be using that instead of a builder. In the end an @ObjectFactory is an explicit user choice.

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

4 participants