Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A Mapstruct SPI extension to map protobuf-java classes to POJOs, Immutables, Records and other protobufs

License

NotificationsYou must be signed in to change notification settings

S1artie/mapstruct-spi-protobuf

Repository files navigation

Maven Central VersionGitHub Actions Workflow Status

This project provides a SPI implementation forMapstruct to generate mapping code from protocolbuffer messages (in the form of protobuf-java objects) to the following targets:

  • Plain Old Java Objects (POJOs)
  • Immutables value objects (presence of org.immutables:value dependency at generationtime enables Immutables compatibility!)
  • Java records
  • other protobuf messages in the form of other protobuf-java objects (see caveat below!)

Unit tests exist to validate all of these mappings. The SPI implementation requiresMapstruct 1.6.0+andJava 1.8+ (of course if you want to map to records, Java 14+ is required).

This SPI implementation is released under the MIT license, built on GitHub and availableonMaven Central.Note that it's a different implementationthanentur/mapstruct-spi-protobuf; the protobuf mapping logic wasreimplemented from scratch here with the goal of explicitly supporting more mapping targets, particularly org.immutablesand other protobufs, both of which pose some unique challenges not properly handled by the entur plugin.

The enum mapping strategy assumes that Google's enum value naming scheme is used, as describedhere:https://developers.google.com/protocol-buffers/docs/style#enum

This SPI implementation also includes apull request from theMapstruct repository that was not merged yet, but fixes adeficiency with Mapstructs' own org.immutables support when using inner classes and @Value.Enclosing.

Usage

Your protobuf mapping interfaces must be annotated with@MapperandcollectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERREDbecause the protobuf classes use a builder pattern.

@Mapper(collectionMappingStrategy =CollectionMappingStrategy.ADDER_PREFERRED)publicinterfaceXXX {

Include the mapstruct dependency and the annotation processor in your Maven project:

<dependencies>    <dependency>        <groupId>org.mapstruct</groupId>        <artifactId>mapstruct</artifactId>        <version>1.6.2</version>    </dependency></dependencies><build><plugins>    <plugin>        <artifactId>maven-compiler-plugin</artifactId>        <configuration>            <annotationProcessorPaths>                <path>                    <groupId>de.firehead</groupId>                    <artifactId>mapstruct-spi-protobuf</artifactId>                    <version>1.1.0</version>                </path>            </annotationProcessorPaths>        </configuration>    </plugin></plugins></build>

Or for Gradle:

implementation"org.mapstruct:mapstruct:1.6.2"annotationProcessor"org.mapstruct:mapstruct-processor:1.6.0"annotationProcessor"de.firehead:mapstruct-spi-protobuf:1.1.0"

Protobuf-to-Protobuf mapping

There is an important caveat with regard to Protobuf-to-Protobuf mapping when it comes to enumerations!

Protobuf enums have two "hard-coded" values:UNRECOGNIZED and_UNSPECIFIED, with the latter typically prefixed bythe enum name in snake-case (the latter is not strictly a hard-coded value; the only thing that's hard-coded isthat the first enum value is the "default" value or "null equivalent", and it's common convention to name this specificvalue_UNSPECIFIED).

By default, this SPI implementation will map both of these tonull in the target enum, which is usuallyfine if you have a "normal" Java enum on the other side, as that will not have a value likeUNRECOGNIZED. However, ifyou are mapping between two Protobuf enums, then there is anUNRECOGNIZED value on the other side that the sourcevalue can be mapped to, and due to the SPI implementation mapping both of these values tonull, the mapping losesinformation: Mapstruct generates mapping code that happily maps_UNSPECIFIED toUNRECOGNIZED in that case - eventhough of course it could map oneUNRECOGNIZED to the otherUNRECOGNIZED (and the_UNSPECIFIED to the other_UNSPECIFIED as well). Here's an example from our unit tests:

@OverridepublicTestProtos.TestEnummapOtherEnumToEnum(Proto2ProtoTestProtos.OtherTestEnumotherTestEnum) {if (otherTestEnum ==null ) {returnTestProtos.TestEnum.TEST_ENUM_UNSPECIFIED;    }TestProtos.TestEnumtestEnum;switch (otherTestEnum ) {caseOTHER_TEST_ENUM_UNSPECIFIED:testEnum =TestProtos.TestEnum.UNRECOGNIZED;break;caseOTHER_TEST_ENUM_VALUE:testEnum =TestProtos.TestEnum.TEST_ENUM_VALUE;break;caseUNRECOGNIZED:testEnum =TestProtos.TestEnum.UNRECOGNIZED;break;default:thrownewIllegalArgumentException("Unexpected enum constant: " +otherTestEnum );    }returntestEnum;}

The mappingcase OTHER_TEST_ENUM_UNSPECIFIED: testEnum = TestProtos.TestEnum.UNRECOGNIZED; is not what we had inmind!

Due to limitations of the SPI interface however, the Protobuf enum mapping rule implementation cannot discern whetherthe mapping target is a Protobuf enum or a Java enum, so it cannot automatically take this special case into account.But there is an SPI-specific configuration option available to disable the automatic mapping ofUNRECOGNIZED tonull:protobuf.enum.mapUnrecognizedToNull. If you are mapping to another Protobuf enum, you should set this optiontofalse, which works just like any of Mapstructs' own configuration options by adding a compiler parameter. In Maven,this would look for example like this:

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-compiler-plugin</artifactId>    <configuration>        <annotationProcessorPaths>            <path>                <groupId>de.firehead</groupId>                <artifactId>mapstruct-spi-protobuf</artifactId>                <version>1.1.0</version>            </path>        </annotationProcessorPaths>        <compilerArgs>            <arg>-Aprotobuf.enum.mapUnrecognizedToNull=false</arg>        </compilerArgs>    </configuration></plugin>

With that option set tofalse, the generated mapping code will look like this:

@OverridepublicTestProtos.TestEnummapOtherEnumToEnum(Proto2ProtoTestProtos.OtherTestEnumotherTestEnum) {if (otherTestEnum ==null ) {returnTestProtos.TestEnum.TEST_ENUM_UNSPECIFIED;    }TestProtos.TestEnumtestEnum;switch (otherTestEnum ) {caseOTHER_TEST_ENUM_UNSPECIFIED:testEnum =TestProtos.TestEnum.TEST_ENUM_UNSPECIFIED;break;caseOTHER_TEST_ENUM_VALUE:testEnum =TestProtos.TestEnum.TEST_ENUM_VALUE;break;caseUNRECOGNIZED:testEnum =TestProtos.TestEnum.UNRECOGNIZED;break;default:thrownewIllegalArgumentException("Unexpected enum constant: " +otherTestEnum );    }returntestEnum;}

Beware though: Performing a correct mapping is just one part of the equation - there's still anIllegalArgumentException looming in the darkness!

Protobuf considers theUNRECOGNIZED value to be special. Enum values are transmitted over the wire in the form ofintegers, and if a value is received that does not correspond to any known enum value, theUNRECOGNIZED value isbasically a catch-all target for these unknown numeric values. This also means that theUNRECOGNIZED enum valuedoesn't have a single static numeric value associated to it (the -1 value found in the Java enum is just a placeholder,it's not what's typically received over the wire), so consequently, Protobuf enums contain the following code thatthrows an exception whenever the number of anUNRECOGNIZED value is accessed:

publicfinalintgetNumber() {if (this ==UNRECOGNIZED) {thrownewjava.lang.IllegalArgumentException("Can't get the number of an unknown enum value.");    }returnvalue;}

This makes theUNRECOGNIZED value effectively un-settable into a Protobuf messages' enum field, because the fields'setter will callgetNumber() on the enum value, which will throw the exception. Not what we'd like to get when usingour Mapstruct-generated mapper to map a Protobuf message to another Protobuf message!

So instead of setting theUNRECOGNIZED value, we'll unfortunately have to do the next-best thing: NOT setting anyvalue at all, which effectively means that the field will have the_UNSPECIFIED value in the target message. Thisboils down to the same logic as with Protobuf-to-Java mappings, where the_UNSPECIFIED value is mapped tonull. But:it's a bit more difficult to achieve this in Protobuf-to-Protobuf mapping. The SPI interface does not allow to generatecode that would conditionally skip setting a field based on the value to be set, so it's necessary to resort to a@Condition method in the mapper interface. This method can fortunately be specified in a generic way, able to dealwith all Protobuf enums, so you'll have to worry about this once and not for every single enum added in the future.Here's a suggestion:

@org.mapstruct.ConditionprotectedbooleanisNotUnrecognized(com.google.protobuf.ProtocolMessageEnumaProtoEnum) {return !"UNRECOGNIZED".equals(aProtoEnum.toString());}

The resulting enum value assignments generated by Mapstruct in a Protobuf message mapping will then look something likethis:

if (isNotUnrecognized(testObject.getEnumField() ) ) {testProtoMessage.setEnumField(mapOtherEnumToEnum(testObject.getEnumField() ) );}

Et voilá! TheUNRECOGNIZED value is not set into the target message, and the_UNSPECIFIED value is retained. If youneed an example, see the mapstruct-spi-protobuf-test-proto2proto test module where this particular situation isunit-tested!

Note that this generic @Condition solution does NOT work for Protobuf enums in Maps! In order to not run into thedreadedIllegalArgumentException when mapping Maps that contain Protobuf enums, you'll probably not get around writingcustom mapping code specifically for each mapping case.

About

A Mapstruct SPI extension to map protobuf-java classes to POJOs, Immutables, Records and other protobufs

Topics

Resources

License

Stars

Watchers

Forks

Contributors2

  •  
  •  

Languages


[8]ページ先頭

©2009-2025 Movatter.jp