使用 MapStruct 映射继承层次结构

使用 mapstruct 映射继承层次结构

简介

mapstruct 提供了一组丰富的功能来映射 java 类型。技术文档广泛描述了 mapstruct 提供的类和注释以及如何使用它们。网络上的几篇社区撰写的文章描述了更复杂的用例。为了补充可用文章库,本文将重点关注映射继承层次结构,并提供一种可能的解决方案,该解决方案具有简单性和可重用性。我假设读者有 mapstruct 的基本知识。如果您对正在运行的示例感兴趣,请随时查看此存储库并尝试一下。

例子

为了以简单的方式演示 mapstruct 的功能,我们将使用一个非常小且无用的域模型,对于该模型,mapstruct 的使用似乎过于复杂,但它允许代码片段在整篇文章中保持简单。 mapstruct 的真正好处变得显而易见,尤其是对于较大的模型。

// source classes
public class sourceproject {
  private string name;
  private localdate duedate;
  // getters + setters omitted throughout the code
}

// target classes
public class targetproject {
  private projectinformation projectinformation;
}

public class projectinformation {
  private string projectname;
  private localdate enddate;
}

如您所见,源实体和目标实体表达相同的信息,但结构略有不同。映射器可以这样定义...

@mapper
public interface projectmapper {
  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  targetproject mapproject(sourceproject source);
}

...mapstruct 将生成如下所示的代码:

public class projectmapperimpl implements projectmapper {

    @override
    public targetproject mapproject(sourceproject source) {
        if ( source == null ) {
            return null;
        }

        targetproject targetproject = new targetproject();

        targetproject.setprojectinformation( sourceprojecttoprojectinformation( source ) );

        return targetproject;
    }

    protected projectinformation sourceprojecttoprojectinformation(sourceproject sourceproject) {
        if ( sourceproject == null ) {
            return null;
        }

        projectinformation projectinformation = new projectinformation();

        projectinformation.setprojectname( sourceproject.getname() );
        projectinformation.setenddate( sourceproject.getduedate() );

        return projectinformation;
    }
}

现在让我们介绍一些使用继承的新实体:

// source classes
@data
public class sourcescrumproject extends sourceproject {
  private integer velocity;
}

// target classes
@data
public class targetscrumproject extends targetproject {
  private velocity velocity;
}

@data
public class velocity {
  private integer value;
}

如果我们想通用地使用父映射器来映射父实体和子实体,我们可以使用@subclassmapping注释,它通过instanceof检查生成对可能的子类映射的调度。

@mapper
public interface projectmapper {
  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  @subclassmapping(source = sourcescrumproject.class, target = targetscrumproject.class)
  targetproject mapproject(sourceproject source);

  @mapping(target = "velocity.value", source = "velocity")
  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  targetscrumproject mapscrumproject(sourcescrumproject source);
}

这会生成以下代码。

public class projectmapperimpl implements projectmapper {

    @override
    public targetproject mapproject(sourceproject source) {
        if ( source == null ) {
            return null;
        }

        if (source instanceof sourcescrumproject) {
            return mapscrumproject( (sourcescrumproject) source );
        }
        else {
            targetproject targetproject = new targetproject();

            targetproject.setprojectinformation( sourceprojecttoprojectinformation( source ) );

            return targetproject;
        }
    }

    @override
    public targetscrumproject mapscrumproject(sourcescrumproject source) {
        if ( source == null ) {
            return null;
        }

        targetscrumproject targetscrumproject = new targetscrumproject();

        targetscrumproject.setvelocity( sourcescrumprojecttovelocity( source ) );
        targetscrumproject.setprojectinformation( sourcescrumprojecttoprojectinformation( source ) );

        return targetscrumproject;
    }

    protected projectinformation sourceprojecttoprojectinformation(sourceproject sourceproject) {
        if ( sourceproject == null ) {
            return null;
        }

        projectinformation projectinformation = new projectinformation();

        projectinformation.setprojectname( sourceproject.getname() );
        projectinformation.setenddate( sourceproject.getduedate() );

        return projectinformation;
    }

    protected velocity sourcescrumprojecttovelocity(sourcescrumproject sourcescrumproject) {
        if ( sourcescrumproject == null ) {
            return null;
        }

        velocity velocity = new velocity();

        velocity.setvalue( sourcescrumproject.getvelocity() );

        return velocity;
    }

    protected projectinformation sourcescrumprojecttoprojectinformation(sourcescrumproject sourcescrumproject) {
        if ( sourcescrumproject == null ) {
            return null;
        }

        projectinformation projectinformation = new projectinformation();

        projectinformation.setprojectname( sourcescrumproject.getname() );
        projectinformation.setenddate( sourcescrumproject.getduedate() );

        return projectinformation;
    }
}

我们已经可以在这里看到一些问题:

  1. 我们正在从父映射复制 @mapping 注释。
  2. 部分生成的代码是重复的(sourceprojecttoprojectinformation 和 sourcescrumprojecttoprojectinformation)。
  3. 接口变得更宽,因为它包含父实体和子实体的映射方法。

只有这两个字段,这看起来并不可怕,但想象一下,如果我们有更多包含更多字段的子类,生成的代码会是什么样子。效果会更大。

让我们尝试解决问题 #1。 mapstruct 提供了注释 @inheritconfiguration,它允许我们重用同一类或所使用的映射配置类中的映射配置:

@mapper
public interface projectmapper {
  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  @subclassmapping(source = sourcescrumproject.class, target = targetscrumproject.class)
  targetproject mapproject(sourceproject source);

  @mapping(target = "velocity.value", source = "velocity")
  @inheritconfiguration(name = "mapproject")
  targetscrumproject mapscrumproject(sourcescrumproject source);
}

这至少为我们省去了很多重复配置。剧透:我们以后不想再使用它了。但让我们首先解决问题#2 和#3。

由于我们可能拥有包含大量重复代码的潜在广泛接口,因此使用、理解和调试生成的代码可能会变得更加困难。如果我们为每个子类都有一个独立的映射器,并且只分派到子映射器或执行映射,但不能同时执行两者,那么会更容易。因此,让我们将 scrum 项目的映射移至单独的界面。

@mapper(uses = scrumprojectmapper.class)
public interface projectmapper {
  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  @subclassmapping(source = sourcescrumproject.class, target = targetscrumproject.class)
  targetproject mapproject(sourceproject source);
}

@mapper
public interface scrumprojectmapper {
  @mapping(target = "velocity.value", source = "velocity")
  @inheritconfiguration(name = "mapproject") // not working
  targetscrumproject mapscrumproject(sourcescrumproject source);
}

我们告诉projectmapper通过uses子句将scrumprojects的映射分派到scrumprojectmapper。这里的问题是,mapproject 方法中的配置对 scrumprojectmapper 不再可见。我们当然可以让它扩展projectmapper,但是这样我们又会遇到宽接口和重复代码的问题,因为所有方法都合并到scrumprojectmapper中。我们可以使用 @mapperconfig 注释将 projectmapper 设为一个配置,并在 scrumprojectmapper 中引用它,但由于它还在 uses 子句中使用 scrumprojectmapper 来启用调度,因此 mapstruct 会抱怨循环依赖。此外,如果我们有一个高度为 > 1 的继承层次结构,我们很快就会注意到 mapstruct 不会将配置在映射器层次结构中向下传递超过一级,从而使级别 0 的配置在级别 2 及更高级别上不可用。

幸运的是,有一个解决方案。 @mapping注解可以应用于其他注解。通过声明一个注释 projectmappings(它基本上包装了项目的所有映射信息),我们可以在任何我们想要的地方重用它。让我们看看这会是什么样子。

@mapper(uses = scrumprojectmapper.class)
public interface projectmapper {
  @mappings
  @subclassmapping(source = sourcescrumproject.class, target = targetscrumproject.class)
  targetproject mapproject(sourceproject source);

  @mapping(target = "projectinformation.projectname", source = "name")
  @mapping(target = "projectinformation.enddate", source = "duedate")
  @interface mappings {
  }
}

@mapper
public interface scrumprojectmapper {
  @mapping(target = "velocity.value", source = "velocity")
  @projectmapper.mappings
  targetscrumproject mapscrumproject(sourcescrumproject source);
}

想象一下,我们有比 scrumproject 更多的子类。通过简单地将映射信息捆绑在共享注释中,我们可以集中信息并避免重复带来的所有陷阱。这也适用于更深的继承层次结构。我只需要使用父映射器的 @mappings-annotation 来注释我的映射方法,该父映射器使用其父映射器的注释,依此类推。

我们现在可以在生成的代码中看到映射器仅针对它们构建的类进行调度或执行映射:

public class ProjectMapperImpl implements ProjectMapper {

    private final ScrumProjectMapper scrumProjectMapper = Mappers.getMapper( ScrumProjectMapper.class );

    @Override
    public TargetProject mapProject(SourceProject source) {
        if ( source == null ) {
            return null;
        }

        if (source instanceof SourceScrumProject) {
            return scrumProjectMapper.mapScrumProject( (SourceScrumProject) source );
        }
        else {
            TargetProject targetProject = new TargetProject();

            targetProject.setProjectInformation( sourceProjectToProjectInformation( source ) );

            return targetProject;
        }
    }

    protected ProjectInformation sourceProjectToProjectInformation(SourceProject sourceProject) {
        if ( sourceProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceProject.getName() );
        projectInformation.setEndDate( sourceProject.getDueDate() );

        return projectInformation;
    }
}

public class ScrumProjectMapperImpl implements ScrumProjectMapper {

    @Override
    public TargetScrumProject mapScrumProject(SourceScrumProject source) {
        if ( source == null ) {
            return null;
        }

        TargetScrumProject targetScrumProject = new TargetScrumProject();

        targetScrumProject.setVelocity( sourceScrumProjectToVelocity( source ) );
        targetScrumProject.setProjectInformation( sourceScrumProjectToProjectInformation( source ) );

        return targetScrumProject;
    }

    protected Velocity sourceScrumProjectToVelocity(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        Velocity velocity = new Velocity();

        velocity.setValue( sourceScrumProject.getVelocity() );

        return velocity;
    }

    protected ProjectInformation sourceScrumProjectToProjectInformation(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceScrumProject.getName() );
        projectInformation.setEndDate( sourceScrumProject.getDueDate() );

        return projectInformation;
    }
}

我想说这使得理解和调试单个映射器变得更容易。由于我们已经介绍了很多内容,现在让我们总结一下。仍然有一些边缘情况可能导致进一步的问题,但这些将在下一部分中介绍。

包起来

使用 mapstruct 编写用于继承层次结构的映射器应该是一项常见任务并且很容易实现,但您很快就会陷入 mapstruct 的一些怪癖中。将整个层次结构映射到一个类中会导致大型类实现难以阅读和调试的宽接口。当将映射器拆分为每个类一个时,我们希望重用父映射器的映射信息以避免重复映射信息。扩展父映射器以使其映射配置可见以供在 @inheritconfiguration 中使用是不可取的,因为我们将再次遇到带有大量重复代码的宽接口的问题。由于循环依赖,使用父映射器作为配置也是不可能的。我们可以看到,创建一个自定义注释来捆绑用于子映射器的映射信息可以解决这个问题。通过另外使用 subclassmapping,父映射器提供有关如何映射它所知道的实体的捆绑信息,仅包含该类的映射,并沿着映射器层次结构调度任何其他子实体的映射。

以上就是使用 MapStruct 映射继承层次结构的详细内容,更多请关注其它相关文章!