Using MapStruct with Project Lombok

Using MapStruct with Project Lombok

3 Comments

MapStruct and Project Lombok are two tools which can make your life as a developer much easier.

MapStruct is a slick project which generates type mappers via simple interfaces. You define an interface method to accept a source type with a return type. And MapStruct generates the implementation of the mapper.

Project Lombok is a tool which helps eliminate a lot of ceremonial / boilerplate code from your Java code. For example, you can define a POJO with several properties, then use Lombok Annotations to enhance the class with Getters, Setters, Constructors, and Builders. Project Lombok is a time saver and helps to de-clutter your code.

MapStruct will use getters and setters for its mappers. MapStruct will even utilize Lombok generated builders. This is a recent addition to MapStruct, and a real nice touch.

Both of these projects utilize annotation processing at compile time to work their magic. This gives them a hook into the Java compile process to enhance the source code being compiled.

While great for performance, it does cause us a delicate dance at compile time.

If MapStruct is going to use a Lombok generated Builder or Lombok generated Setter, what if Lombok has not run yet? Obviously, the compile would fail.

A workaround to this used to be placing all your Lombok enhanced POJOs into a separate module. This will force Lombok to process before MapStruct, solving our compile time problem.

But, this is a kludgey. With more recent versions of MapStruct and Project Lombok this work around is no longer needed.

In this post, I will show you how to configure Maven to support the annotation processing needs to using MapStruct with Project Lombok.

Project Overview

For the context of this project, let’s say we are developing a Spring Boot Microservice to make ACH Payments.

There is nothing new about ACH Payments. This standard has been around for over 40 years.

However, a number of banks are now exposing REST style APIs to make ACH Payments. One implementation is by Silicone Valley Bank. You can read their ACH API documentation here.

So, our hypothetical Spring Boot Microservice will be receiving an instruction to make a ACH Payment. We wish to accept the instruction, persist it to the database, call the ACH API, update the database with the result.

Project Lombok POJOs

Our example will have 3 POJOs:

  • An inbound ‘make payment message’
  • A Hibernate Entity for persistence
  • An ACH API Entity for calling the REST style API

Three different POJOs which are ideal candidates for Project Lombok.

Possibly a 4th POJO for handing the API response. However, the SVB Bank documentation (above) that we are following, uses the same payload for the response.

MapStruct Mappers

Our example project has 3 different POJOs, each containing simular data.

New developers often complain about this. And ask, can’t we just use one type?

Short answer, is no. In our use case, we are writing the microservice. We don’t necessarily have control of the inbound message. We do have control of the Hibernate POJO. However, we definitely do not have control of the 3rd party ACH API.

We will need the following mappers:

  • Inbound make payment message to Hibernate Entity (POJO)
  • Hibernate POJO to ACH API type
  • Update Hibernate POJO from ACH API type

MapStruct and Project Lombok Spring Boot Project

In this section of the post, we will implement the data models discussed in the previous section, which entails setting up Maven dependencies, configuring Maven’s annotation processing, creating POJOs with Project Lombok annotations, and implementing MapStruct mappers.

Complete source code for this post is available on GitHub.

Maven Configuration

For the purposes of this post, we will setup a very simple Spring Boot project. If you’re creating a project using Spring Initializr you will need the following dependencies:

  • Webflux (or Spring MVC)
  • Spring Data JPA
  • Validation
  • Project Lombok

Initial Maven Dependencies

You should have the following dependencies in your Maven POM.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

MapStruct Dependency

You’ll need to add the MapStruct dependency to the Maven POM. (At the time of writing, MapStruct is not an option in Spring Initializr.)

I recommend defining the version in a Maven property.

    <properties>
        <java.version>11</java.version>
        <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
    </properties>

. . . (code omitted for brevity)

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

You can view the complete Maven POM on GitHub here.

Maven Annotation Processing Configuration

The Maven Compiler plugin needs to be configured to support the annotation processors of MapStruct and Project Lombok. The versions should match the project dependencies. Hence putting the MapStruct version in a Maven Property. We will utilize the Project Lombok version inherited from the Spring Boot Parent POM.

You need to add the following to the build / pluginssection of the POM.

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>3.8.0</version>
     <configuration>
         <annotationProcessorPaths>
             <path>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct-processor</artifactId>
                 <version>${org.mapstruct.version}</version>
             </path>
             <path>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
                 <version>${lombok.version}</version>
             </path>
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok-mapstruct-binding</artifactId>
                 <version>0.2.0</version>
             </dependency>
         </annotationProcessorPaths>
         <compilerArgs>
             <compilerArg>
                 -Amapstruct.defaultComponentModel=spring
             </compilerArg>
         </compilerArgs>
     </configuration>
 </plugin>

MapStruct Spring Configuration

A nice feature of MapStruct is the ability to optionally annotate the mappers with the Spring @Componentstereotype. With this option enabled, the generated mappers will be available for dependency injection as Spring Beans.

The following snippet enables the Spring annotation. You can omit this from your configuration if you are not using Spring.

This is also shown in the Maven Compiler configuration above.

<compilerArgs>
    <compilerArg>
        -Amapstruct.defaultComponentModel=spring
    </compilerArg>
</compilerArgs>

Java POJOs

For our example, we need to define 3 Java POJOs.

IDE Configuration for Project Lombok

When working with Project Lombok you’ll need to be sure to enable annotation processing in your IDE compiler settings.

Also, you should install a Project Lombok plugin. Details for IntelliJ are here. Instructions for other IDEs are available under the ‘install’ menu option.

Send Payment POJO

The Send Payment POJO represents a send payment message.

In our use case example, we are developing a microservice which listens for a message to send a payment. This POJO represents the message payload we are expecting.

SendPayment

Below is a Java POJO annotated with Project Lombok annotations.

In this example I’m using the @Dataannotation, which generates Getters, Setters, toString, equals, and hash code.

Two additional annotations are present to generate constructors for no arguments, and all arguments.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SendPayment {
    private UUID paymentId;
    private UUID payeeId;
    private String payoutMemo;
    private Long amount;
    private String payeeFirstName;
    private String payeeLastName;
    private String payeeAddressLine1;
    private String payeeAddressCity;
    private String payeeAddressStateOrProv;
    private String payeeAddressZipOrPostal;
    private String payeeAddressCountryCode;
    private String routingNumber;
    private String accountNumber;
    private String accountName;
}

You can see the Java POJO is free of a lot of code that you normally would need to write.

If you wish to see the actual POJO produced, run the Maven compile goal and inspect the target/classes/<pacakge>folder.

Here is the POJO that is generated via Project Lombok.

Notice all the code you did not write!

public class SendPayment {
    private UUID paymentId;
    private UUID payeeId;
    private String payoutMemo;
    private Long amount;
    private String payeeFirstName;
    private String payeeLastName;
    private String payeeAddressLine1;
    private String payeeAddressCity;
    private String payeeAddressStateOrProv;
    private String payeeAddressZipOrPostal;
    private String payeeAddressCountryCode;
    private String routingNumber;
    private String accountNumber;
    private String accountName;

    public UUID getPaymentId() {
        return this.paymentId;
    }

    public UUID getPayeeId() {
        return this.payeeId;
    }

    public String getPayoutMemo() {
        return this.payoutMemo;
    }

    public Long getAmount() {
        return this.amount;
    }

    public String getPayeeFirstName() {
        return this.payeeFirstName;
    }

    public String getPayeeLastName() {
        return this.payeeLastName;
    }

    public String getPayeeAddressLine1() {
        return this.payeeAddressLine1;
    }

    public String getPayeeAddressCity() {
        return this.payeeAddressCity;
    }

    public String getPayeeAddressStateOrProv() {
        return this.payeeAddressStateOrProv;
    }

    public String getPayeeAddressZipOrPostal() {
        return this.payeeAddressZipOrPostal;
    }

    public String getPayeeAddressCountryCode() {
        return this.payeeAddressCountryCode;
    }

    public String getRoutingNumber() {
        return this.routingNumber;
    }

    public String getAccountNumber() {
        return this.accountNumber;
    }

    public String getAccountName() {
        return this.accountName;
    }

    public void setPaymentId(final UUID paymentId) {
        this.paymentId = paymentId;
    }

    public void setPayeeId(final UUID payeeId) {
        this.payeeId = payeeId;
    }

    public void setPayoutMemo(final String payoutMemo) {
        this.payoutMemo = payoutMemo;
    }

    public void setAmount(final Long amount) {
        this.amount = amount;
    }

    public void setPayeeFirstName(final String payeeFirstName) {
        this.payeeFirstName = payeeFirstName;
    }

    public void setPayeeLastName(final String payeeLastName) {
        this.payeeLastName = payeeLastName;
    }

    public void setPayeeAddressLine1(final String payeeAddressLine1) {
        this.payeeAddressLine1 = payeeAddressLine1;
    }

    public void setPayeeAddressCity(final String payeeAddressCity) {
        this.payeeAddressCity = payeeAddressCity;
    }

    public void setPayeeAddressStateOrProv(final String payeeAddressStateOrProv) {
        this.payeeAddressStateOrProv = payeeAddressStateOrProv;
    }

    public void setPayeeAddressZipOrPostal(final String payeeAddressZipOrPostal) {
        this.payeeAddressZipOrPostal = payeeAddressZipOrPostal;
    }

    public void setPayeeAddressCountryCode(final String payeeAddressCountryCode) {
        this.payeeAddressCountryCode = payeeAddressCountryCode;
    }

    public void setRoutingNumber(final String routingNumber) {
        this.routingNumber = routingNumber;
    }

    public void setAccountNumber(final String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public void setAccountName(final String accountName) {
        this.accountName = accountName;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof SendPayment)) {
            return false;
        } else {
            SendPayment other = (SendPayment)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$amount = this.getAmount();
                Object other$amount = other.getAmount();
                if (this$amount == null) {
                    if (other$amount != null) {
                        return false;
                    }
                } else if (!this$amount.equals(other$amount)) {
                    return false;
                }

                Object this$paymentId = this.getPaymentId();
                Object other$paymentId = other.getPaymentId();
                if (this$paymentId == null) {
                    if (other$paymentId != null) {
                        return false;
                    }
                } else if (!this$paymentId.equals(other$paymentId)) {
                    return false;
                }

                Object this$payeeId = this.getPayeeId();
                Object other$payeeId = other.getPayeeId();
                if (this$payeeId == null) {
                    if (other$payeeId != null) {
                        return false;
                    }
                } else if (!this$payeeId.equals(other$payeeId)) {
                    return false;
                }

                label158: {
                    Object this$payoutMemo = this.getPayoutMemo();
                    Object other$payoutMemo = other.getPayoutMemo();
                    if (this$payoutMemo == null) {
                        if (other$payoutMemo == null) {
                            break label158;
                        }
                    } else if (this$payoutMemo.equals(other$payoutMemo)) {
                        break label158;
                    }

                    return false;
                }

                label151: {
                    Object this$payeeFirstName = this.getPayeeFirstName();
                    Object other$payeeFirstName = other.getPayeeFirstName();
                    if (this$payeeFirstName == null) {
                        if (other$payeeFirstName == null) {
                            break label151;
                        }
                    } else if (this$payeeFirstName.equals(other$payeeFirstName)) {
                        break label151;
                    }

                    return false;
                }

                Object this$payeeLastName = this.getPayeeLastName();
                Object other$payeeLastName = other.getPayeeLastName();
                if (this$payeeLastName == null) {
                    if (other$payeeLastName != null) {
                        return false;
                    }
                } else if (!this$payeeLastName.equals(other$payeeLastName)) {
                    return false;
                }

                label137: {
                    Object this$payeeAddressLine1 = this.getPayeeAddressLine1();
                    Object other$payeeAddressLine1 = other.getPayeeAddressLine1();
                    if (this$payeeAddressLine1 == null) {
                        if (other$payeeAddressLine1 == null) {
                            break label137;
                        }
                    } else if (this$payeeAddressLine1.equals(other$payeeAddressLine1)) {
                        break label137;
                    }

                    return false;
                }

                label130: {
                    Object this$payeeAddressCity = this.getPayeeAddressCity();
                    Object other$payeeAddressCity = other.getPayeeAddressCity();
                    if (this$payeeAddressCity == null) {
                        if (other$payeeAddressCity == null) {
                            break label130;
                        }
                    } else if (this$payeeAddressCity.equals(other$payeeAddressCity)) {
                        break label130;
                    }

                    return false;
                }

                Object this$payeeAddressStateOrProv = this.getPayeeAddressStateOrProv();
                Object other$payeeAddressStateOrProv = other.getPayeeAddressStateOrProv();
                if (this$payeeAddressStateOrProv == null) {
                    if (other$payeeAddressStateOrProv != null) {
                        return false;
                    }
                } else if (!this$payeeAddressStateOrProv.equals(other$payeeAddressStateOrProv)) {
                    return false;
                }

                Object this$payeeAddressZipOrPostal = this.getPayeeAddressZipOrPostal();
                Object other$payeeAddressZipOrPostal = other.getPayeeAddressZipOrPostal();
                if (this$payeeAddressZipOrPostal == null) {
                    if (other$payeeAddressZipOrPostal != null) {
                        return false;
                    }
                } else if (!this$payeeAddressZipOrPostal.equals(other$payeeAddressZipOrPostal)) {
                    return false;
                }

                label109: {
                    Object this$payeeAddressCountryCode = this.getPayeeAddressCountryCode();
                    Object other$payeeAddressCountryCode = other.getPayeeAddressCountryCode();
                    if (this$payeeAddressCountryCode == null) {
                        if (other$payeeAddressCountryCode == null) {
                            break label109;
                        }
                    } else if (this$payeeAddressCountryCode.equals(other$payeeAddressCountryCode)) {
                        break label109;
                    }

                    return false;
                }

                label102: {
                    Object this$routingNumber = this.getRoutingNumber();
                    Object other$routingNumber = other.getRoutingNumber();
                    if (this$routingNumber == null) {
                        if (other$routingNumber == null) {
                            break label102;
                        }
                    } else if (this$routingNumber.equals(other$routingNumber)) {
                        break label102;
                    }

                    return false;
                }

                Object this$accountNumber = this.getAccountNumber();
                Object other$accountNumber = other.getAccountNumber();
                if (this$accountNumber == null) {
                    if (other$accountNumber != null) {
                        return false;
                    }
                } else if (!this$accountNumber.equals(other$accountNumber)) {
                    return false;
                }

                Object this$accountName = this.getAccountName();
                Object other$accountName = other.getAccountName();
                if (this$accountName == null) {
                    if (other$accountName != null) {
                        return false;
                    }
                } else if (!this$accountName.equals(other$accountName)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof SendPayment;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $amount = this.getAmount();
        int result = result * 59 + ($amount == null ? 43 : $amount.hashCode());
        Object $paymentId = this.getPaymentId();
        result = result * 59 + ($paymentId == null ? 43 : $paymentId.hashCode());
        Object $payeeId = this.getPayeeId();
        result = result * 59 + ($payeeId == null ? 43 : $payeeId.hashCode());
        Object $payoutMemo = this.getPayoutMemo();
        result = result * 59 + ($payoutMemo == null ? 43 : $payoutMemo.hashCode());
        Object $payeeFirstName = this.getPayeeFirstName();
        result = result * 59 + ($payeeFirstName == null ? 43 : $payeeFirstName.hashCode());
        Object $payeeLastName = this.getPayeeLastName();
        result = result * 59 + ($payeeLastName == null ? 43 : $payeeLastName.hashCode());
        Object $payeeAddressLine1 = this.getPayeeAddressLine1();
        result = result * 59 + ($payeeAddressLine1 == null ? 43 : $payeeAddressLine1.hashCode());
        Object $payeeAddressCity = this.getPayeeAddressCity();
        result = result * 59 + ($payeeAddressCity == null ? 43 : $payeeAddressCity.hashCode());
        Object $payeeAddressStateOrProv = this.getPayeeAddressStateOrProv();
        result = result * 59 + ($payeeAddressStateOrProv == null ? 43 : $payeeAddressStateOrProv.hashCode());
        Object $payeeAddressZipOrPostal = this.getPayeeAddressZipOrPostal();
        result = result * 59 + ($payeeAddressZipOrPostal == null ? 43 : $payeeAddressZipOrPostal.hashCode());
        Object $payeeAddressCountryCode = this.getPayeeAddressCountryCode();
        result = result * 59 + ($payeeAddressCountryCode == null ? 43 : $payeeAddressCountryCode.hashCode());
        Object $routingNumber = this.getRoutingNumber();
        result = result * 59 + ($routingNumber == null ? 43 : $routingNumber.hashCode());
        Object $accountNumber = this.getAccountNumber();
        result = result * 59 + ($accountNumber == null ? 43 : $accountNumber.hashCode());
        Object $accountName = this.getAccountName();
        result = result * 59 + ($accountName == null ? 43 : $accountName.hashCode());
        return result;
    }

    public String toString() {
        UUID var10000 = this.getPaymentId();
        return "SendPayment(paymentId=" + var10000 + ", payeeId=" + this.getPayeeId() + ", payoutMemo=" + this.getPayoutMemo() + ", amount=" + this.getAmount() + ", payeeFirstName=" + this.getPayeeFirstName() + ", payeeLastName=" + this.getPayeeLastName() + ", payeeAddressLine1=" + this.getPayeeAddressLine1() + ", payeeAddressCity=" + this.getPayeeAddressCity() + ", payeeAddressStateOrProv=" + this.getPayeeAddressStateOrProv() + ", payeeAddressZipOrPostal=" + this.getPayeeAddressZipOrPostal() + ", payeeAddressCountryCode=" + this.getPayeeAddressCountryCode() + ", routingNumber=" + this.getRoutingNumber() + ", accountNumber=" + this.getAccountNumber() + ", accountName=" + this.getAccountName() + ")";
    }

    public SendPayment() {
    }

    public SendPayment(final UUID paymentId, final UUID payeeId, final String payoutMemo, final Long amount, final String payeeFirstName, final String payeeLastName, final String payeeAddressLine1, final String payeeAddressCity, final String payeeAddressStateOrProv, final String payeeAddressZipOrPostal, final String payeeAddressCountryCode, final String routingNumber, final String accountNumber, final String accountName) {
        this.paymentId = paymentId;
        this.payeeId = payeeId;
        this.payoutMemo = payoutMemo;
        this.amount = amount;
        this.payeeFirstName = payeeFirstName;
        this.payeeLastName = payeeLastName;
        this.payeeAddressLine1 = payeeAddressLine1;
        this.payeeAddressCity = payeeAddressCity;
        this.payeeAddressStateOrProv = payeeAddressStateOrProv;
        this.payeeAddressZipOrPostal = payeeAddressZipOrPostal;
        this.payeeAddressCountryCode = payeeAddressCountryCode;
        this.routingNumber = routingNumber;
        this.accountNumber = accountNumber;
        this.accountName = accountName;
    }
}

Payment Entity

Our project also needs a basic Hibernate Entity. This will be used to persist payments to the database.

Hibernate configuration is beyond the scope of this post.

Payment

Following is our implementation of the Payment Entity.

PaymentEntity

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {
    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    private UUID id;

    @Version
    private Integer version;

    private UUID paymentId;
    private Long amount;
    private String routingNumber;
    private String accountNumber;
    /**
     * SVB ACH Id - set with response from SVB
     */
    private String SvbId;
    private String svbBatchId;
    private String svbUrl;

    @CreationTimestamp
    @Column(updatable = false)
    private Timestamp createdDate;

    @UpdateTimestamp
    private Timestamp lastModifiedDate;
}

SVB Model

The GitHub repository has the complete model I wrote for the ACH API.

For brevity, I’m omitting the code for several Enums. The complete project is in GitHub here.

AchTransferObject

On this example, I’m also using the Project Lombok @Builderannotation. We’ll use this later to inspect the mappers generated with and with builders.

@Getter
@Setter
@Builder
public class AchTransferObject {

    private String accountNumber;
    private Integer amount;
    private String batchId;
    private Integer counterpartyId;

    @Builder.Default
    private SvbCurrency currency = SvbCurrency.USD;

    @Builder.Default
    private AchDirection direction = AchDirection.CREDIT;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private LocalDate effectiveDate; 

    private String id;
    private String memo;
    private Object metadata; 

    @NotNull //todo - not required if counter party id is provided, assuming we are not using this
    private String receiverAccountNumber;

    @Builder.Default 
    private AchAccountType receiverAccountType = AchAccountType.CHECKING;

    private String receiverName;
    private String receiverRoutingNumber;

    @JsonProperty("return") //return is Java key word
    private String returnValue; 


    @Builder.Default //todo - review this value
    @NotNull
    private SecCode secCode = SecCode.PPD;

    private AchService service;

    private AchStatus status ;
    private String type;
    private String url;
}

MapStruct Mappers

In this section, we will implement the MapStruct Mappers.

By default, MapStruct will automatically map properties where the property name and types match. It will also map automatically if it can safely do an implicit type conversation. (Like an Integer to Long)

Mapping Interface

Below is the mapping interface. The interface itself is annotated with @Mapperwhich instructs MapStruct to generate mappers from it.

Two methods are defined. One to accept a SendPaymentobject and return a Paymentobject.

A second to accept a Paymentobject and return a AchTransferObjectobject.

I’m also excluding one property from the mapper. The annotation @Mapping(target = "id", ignore = true)excludes mapping to theidproperty.

Without this exclusion, compiling will fail due to incompatible types. (UUID to String)

PaymentMapper

@Mapper
public interface PaymentMapper {

    Payment sendPaymentToPayment(SendPayment sendPayment);

    @Mapping(target = "id", ignore = true)
    AchTransferObject paymentToAchTransferObject(Payment payment);
}

NOTE: MapStruct has some very robust mapping capabilities. I will NOT be exploring those in this post. This is easily a post worthy topic! You can learn more here.

PaymentMapperImpl

Below is the mapping implementation generated by MapStruct. After running the Maven compile goal, you will find this class under /target/generated-sources/annotations/<package>.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-02-05T09:46:19-0500",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 11 (Oracle Corporation)"
)
@Component
public class PaymentMapperImpl implements PaymentMapper {

    @Override
    public Payment sendPaymentToPayment(SendPayment sendPayment) {
        if ( sendPayment == null ) {
            return null;
        }

        Payment payment = new Payment();

        payment.setPaymentId( sendPayment.getPaymentId() );
        payment.setAmount( sendPayment.getAmount() );
        payment.setRoutingNumber( sendPayment.getRoutingNumber() );
        payment.setAccountNumber( sendPayment.getAccountNumber() );

        return payment;
    }

    @Override
    public AchTransferObject paymentToAchTransferObject(Payment payment) {
        if ( payment == null ) {
            return null;
        }

        AchTransferObjectBuilder achTransferObject = AchTransferObject.builder();

        achTransferObject.accountNumber( payment.getAccountNumber() );
        if ( payment.getAmount() != null ) {
            achTransferObject.amount( payment.getAmount().intValue() );
        }

        return achTransferObject.build();
    }
}

Several things I’d like to point out in the generated code.

You can see this is annotated with a Spring Stereotype, marking it as a Spring Component. This is very handy in Spring projects. It allows you to easily autowire the mapper into other Spring managed components.

On two of the POJOs, I did not use the Project Lombok @Builderannotation. Normally, I would have.

But, I wanted to demonstrate the differences in the generated code. You can see in the first method uses setters.

While the second method uses the builder created by Project Lombok.

You’ll also notice that a number of properties did not get mapped.

Easy enough to fix with additional MapStruct configuration.

Conclusion

In this post you can clearly see how much coding MapStruct and Project Lombok can save you.

Personally, I’m a fan of the builder pattern. It’s nice to use. BUT – before Project Lombok, it was tedious to implement!

I’ve written a lot of code using Lombok builders. They are very convenient to use.

One risk is violating the DRY Principle.  A.K.A – Don’t Repeat Yourself.

In a larger project, you run the risk of doing the same type conversion using builders in multiple locations.

With each implementation, you run the risk of being inconsistent and possibly introducing a bug by forgetting a property.

Once you become accustomed to using MapStruct mappers, the type conversion is defined in one place.

If a new property is added or removed, you have one thing to change. Not every instance where the builder is used.

Here you can see the combination leads you to cleaner code, higher quality code, and saves you time.

It’s a win win win!

About jt

    You May Also Like

    3 comments on “Using MapStruct with Project Lombok

    1. July 21, 2021 at 8:38 am

      Thank you very much! This article solved my bug in maven project.

      Reply
    2. November 29, 2021 at 2:58 am

      Seems like it’s no longer working with spring 2.5.*.

      Reply
    3. November 29, 2021 at 3:22 am

      May fault. lombok 1.18.22 fixes it.

      Reply

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.