乖乖,咱不用BeanUtil.copy了,咱试试这款神级工具(超详细)

引言

在现代Java应用程序开发中,处理对象之间的映射是一个常见而且必不可少的任务。随着项目规模的增长,手动编写繁琐的映射代码不仅耗时且容易出错,因此开发者们一直在寻找更高效的解决方案。比如基于Dozer封装的或者Spring自带的BeanUtil.copyProperties对应对象之间的属性拷贝。但是Dozer采用运行时映射的方式,通过反射在运行时动态生成映射代码。这意味着在每次映射时都需要进行反射操作,Dozer在处理复杂映射时可能需要额外的配置和自定义转换器,可能导致一定的性能开销,尤其在大型项目中可能表现不佳。另外在处理处理复杂映射(例如字段名称不一致,某些字段不需要映射)时可能需要额外的配置和自定义转换器,使用起来并不是那么的便捷。那么此时MapStruct变应用而生,成为简化Java Bean映射的利器。

MapStruct是一款基于注解和编译时代码生成的工具,旨在简化Java Bean之间的映射过程。通过在编译时生成高效的映射代码,避免了运行时的性能开销,使得映射过程更加高效。MapStruct不仅消除了手写映射代码的痛苦,还提供了性能优势。它支持在Java Bean之间进行映射,并通过使用注解标记映射方法和类,提供了一种声明性的方式定义映射规则,简化了映射代码的编写。使得开发者能够专注于业务逻辑而不必过多关注对象之间的转换。并且它还支持自定义转换器和表达式,适用于处理各种复杂的映射场景。

下面我们就开始介绍如何使用MapStruct来高效的完成对象之间的映射。

如何MapStruct使用

使用MapStruct进行Java Bean映射通常包括几个基本步骤,包括项目配置、注解标记、自定义转换器等。以下是详细的使用步骤:

1、依赖
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.5.5.Final</version>
</dependency>

同时在 pom.xml 需要正确配置MapStruct的依赖和注解处理器插件。例如:

<build>  <plugins>        <plugin>            <groupId>org.apache.maven.plugins</groupId>  <artifactId>maven-compiler-plugin</artifactId>  <configuration>                 <annotationProcessorPaths>                   <path>                        <groupId>org.mapstruct</groupId>  <artifactId>mapstruct-processor</artifactId>  <version>1.5.5.Final</version>  </path>                    <path>                        <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId>  <version>1.18.22</version>  </path>                    <path>                        <groupId>org.projectlombok</groupId>  <artifactId>lombok-mapstruct-binding</artifactId>  <version>0.2.0</version>  </path>                </annotationProcessorPaths>            </configuration>        </plugin>    </plugins>
</build>

当然如果你同时使用了lombok,也需要同时配置lombok编译生成代码的插件。

2、创建映射接口

创建一个Java接口,并使用@Mapper注解标记它。例如:

@Mapper
public interface MyMapper {MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);TargetObject sourceToTarget(SourceObject source);// 定义其他映射方法
}

上述代码定义了一个映射接口MyMapper,其中有一个映射方法sourceToTarget用于将SourceObject映射为TargetObjectINSTANCE字段用于获取映射器的实例。

此时我们编译项目之后,可以看见生成的MyMapper实现类中的代码:

@Override  
public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  targetObject.setSex( source.getSex() );  return targetObject;  
}

这样就省去了我们自己手写两个对象之间的字段映射,避免了大量的重复工作,大大增加了开发效率,其次也是最重要的一点就是我们可以很直观的看见两个对象之间的字段映射关系,不像Dozer那样每次基于反射区实现映射,我们无法看见两边的字段的映射,出现问题后不方便排查,功能上不可控。

很重要的一点提示:我们要养成在写完一个映射方法后,要养成一定一定提前编译看一下生成的实现类方法是否正确,同时也看看是否存在字段映射关系设置错误导致编译不通过。

3、映射接口使用

在业务代码或者其他代码方法中,我们可以直接使用MyConverter.INSTANCE.sourceToTarget(source)进行sourcetarget之间的转换。

TargetObject handleObject(SourceObject source){  return MyConverter.INSTANCE.sourceToTarget(source);  
}

怎么样?是不是很简单。接下来让我们继续介绍MapStruct的详细功能,揭开它神秘的面纱。。。。。

MapStruct常用注解

了解MapStruct的注解及其属性是非常重要的,因为它们定义了映射规则和行为。以下是MapStruct中常用的注解及其属性:

1.@Mapper

用于标记一个接口或抽象类,用于定义对象之间的映射规则。它有多个属性可以配置映射器的功能。以下是 @Mapper 注解的一些常用属性:

1.1 componentModel

指定生成的映射器实例的组件模型,以便与应用框架集成。他有"default"(默认值)、“cdi”、"spring"等可选值(具体参考MappingConstants.ComponentModel)。我们着重介绍一下default以及spring:

  • default:MapStruct的默认组件模型
    在默认模式下,MapStruct 会生成一个无参数的构造函数的映射器实例。映射器实例的创建和管理由 MapStruct自动处理。实例通常通过 Mappers.getMapper(Class)获取。适用于简单的映射场景,无需额外的依赖注入或容器管理。

  • spring:使用Spring Framework的组件模型
    在 Spring 模式下,MapStruct 会生成一个使用 @Component 注解标记的映射器实例,从而允许通过 Spring 的 IoC 容器进行管理和依赖注入。适用于 Spring 框架中的应用,可以利用 Spring 的依赖注入功能。稍后我们会介绍这种模型的使用,也是我们日常使用SpringBoot开发时用的比较多的模型。比如上例中,我们使用spring的模型,则生成的代码:

@Component  
public class MySpringMapperImpl implements MySpringMapper {  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  targetObject.setSex( source.getSex() );  return targetObject;  }  
}

可以看见实现类中自动加上了@Component,注入到Spring的容器中管理。

  • cdi:使用 Contexts and Dependency Injection (CDI) 的组件模型。
    在 CDI 模式下,MapStruct 会生成一个使用 @Dependent 注解标记的映射器实例,允许通过 CDI 容器进行管理和依赖注入。适用于Java EEJakarta EE中使用 CDI 的应用,可以利用 CDI 容器进行管理。

其余的大家感兴趣的可以去阅读源码,平时使用不多,这里就不过多介绍了。

1.2 uses

指定映射器使用的自定义转换器。自定义转换器是在映射过程中调用的方法,用于处理特定类型之间的自定义映射逻辑。如果我们两个对象之间有一个字段的属性值需要特殊处理之后在进行映射,即需要加上一些转换逻辑,我们就可以自定义一个转换器,然后在映射器中使用转换器中的方法。例如:SoureObject中的有一个枚举值,但是转换到TargetObject中时需要转换为具体的说明,那么此时我们就可以使用自定义转换器。

我们自定义一个转换器,并且定义一个转换方法:

public class MyConverter {  @Named("convertSexDesc")  public String convertSexDesc(Integer sex){  return SexEnum.descOfCode(sex);  }  
}

然后再映射器MyMapper中使用uses指定转换器,同时使用@Mapping注解指定两个字段的映射规则:

@Mapper(uses = {MyConverter.class})  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  TargetObject sourceToTarget(SourceObject source);  
}

编译后可以看见实现类中生成的代码:

public class MyMapperImpl implements MyMapper {  private final MyConverter myConverter = new MyConverter();  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  return targetObject;  }  
}

当然假如你的转换器或者转换方法,是你这个映射器独有,其他映射器不会使用这个转换方法,那么你可以直接在MyMapper中定义一个default的转换方法,就不必使用uses引入转换器:

@Mapper  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  TargetObject sourceToTarget(SourceObject source);  @Named("convertSexDesc")  default String convertSexDesc(Integer sex){  return SexEnum.descOfCode(sex);  }  
}

编译后生成的实现类中,直接可以调用到这个方法:

public class MyMapperImpl implements MyMapper {  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setSex( convertSexDesc( source.getSex() ) );  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  return targetObject;  }  
}

在Java中,接口可以包含默认方法(Default Methods)。默认方法是在接口中提供一个默认的实现,这样在接口的实现类中就不需要强制性地实现该方法了。默认方法使用关键字 default 进行声明。

1.3 imports

导入其他类的全限定名,使其在生成的映射器接口中可见。比如我们可以导入其他的工具类去处理我们的字段,例如:StringUtils, CollectionUtilsMapUtils,或者一些枚举类等。同常运用@Mapping中的expression上。

@Mapper(imports = {StringUtils.class, SexEnum.class})  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex",  expression = "java(SexEnum.descOfCode(source.getSex()))")  TargetObject sourceToTarget(SourceObject source);
}    

编译后生成的实现类中直接importimports中定义的类:

import com.springboot.code.mapstruct.SexEnum;
import org.springframework.util.StringUtils;public class MyMapperImpl implements MyMapper {  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  return targetObject;  }  
}

当然我们也可以不使用imports去导入其他的类,那我们在使用这些类的方法时,必须写上他们的全路径:

@Mapper  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex",  expression = "java(com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()))")  TargetObject sourceToTarget(SourceObject source);
}    

编译后生成的实现类中,就不会import类了:

public class MyMapperImpl implements MyMapper {  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  targetObject.setSex( com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()) );  return targetObject;  }  
}
1.4 config

config 属性允许你指定一个映射器配置类,该配置类用于提供全局的配置选项。通过配置类,你可以定义一些全局行为,例如处理 null 值的策略、映射器名称、映射器组件模型等。

我们使用@MapperConfig定义一个映射器配置类 MyMapperConfig

@MapperConfig(  nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,  componentModel = "default",  uses = MyConverter.class,  unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然后再MyMapper中指定config:

@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  TargetObject sourceToTarget(SourceObject source);}   

我们可以集中管理映射器的一些全局行为,而不需要在每个映射器中重复配置。
在实际应用中,你可以根据项目需求定义不同的映射器配置类,用于管理不同的全局配置选项。这有助于提高代码的组织性和可维护性。

1.5 nullValueCheckStrategy

用于指定映射器对源对象字段的null值进行检查的策略。检查策略枚举类NullValueCheckStrategy值如下:

  • ALWAYS:始终对源值进行NULL检查。
    生成的实现类中,都是源值进行判NULL
@Override  
public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  if ( source.getSex() != null ) {  targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  }  if ( source.getUserName() != null ) {  targetObject.setUserName( source.getUserName() );  }  if ( source.getUserId() != null ) {  targetObject.setUserId( source.getUserId() );  }  return targetObject;  
}
  • ON_IMPLICIT_CONVERSION:不检查NULL值,直接将源值赋值给目标值

除了上述的属性值之外,还有一些其他的属性值,例如:

  • unmappedSourcePolicy: 未映射源对象字段的处理策略。
  • unmappedTargetPolicy: 未映射目标对象字段的处理策略。
    可选值:ReportingPolicy.IGNORE(忽略未映射字段,默认)、ReportingPolicy.WARN(警告)、ReportingPolicy.ERROR(抛出错误)。

以及其他的一些属性值,如果需要用到的同学,可以看一下源码中的介绍,这里就不过多叙述了。

2.@MapperConfig

注解用于定义映射器配置类,它允许在一个单独的配置类中集中管理映射器的全局配置选项。可以将一些全局的配置选项集中在一个配置类中,使得映射器的配置更为清晰和可维护。在实际应用中,可以根据需要定义不同的映射器配置类,以便在不同的场景中使用。配置类可以在映射器中通过@Mapperconfig属性引入。它大部分的属性值跟@Mapper一致。

@MapperConfig(  nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION,  componentModel = "default",  uses = MyConverter.class,  unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然后再MyMapper中指定config:

@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  TargetObject sourceToTarget(SourceObject source);} 
3.@Mapping

用于自定义映射器方法中的映射规则。它允许你指定源对象和目标对象之间字段的映射关系。

3.1 sourcetarget:
  • source 含义: 源对象字段的名称或表达式。
  • target 含义: 目标对象字段的名称。
@Mapping(target = "sourceField", source = "sourceField")  
TargetObject sourceToTarget(SourceObject source);

或者使用表达式的方式:

@Mapping(expression = "java(source.getSourceField())", target = "targetField")
TargetObject sourceToTarget(SourceObject source);
3.2 qualifiedByNamequalifiedBy:
  • qualifiedByName: 指定使用自定义转换器方法进行映射。

定义一个转换器MyNameConverter:

public class MyNameConverter {  @Named("convertUserName")  public String convertUserName(String userName){  return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  }  
}

使用自定义转换器的方法:

@Mapper( uses = {MyNameConverter.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {  MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);  @Mapping(target = "userName", source = "userName", qualifiedByName = "convertUserName")  TargetObject sourceToTarget(SourceObject source);
  • qualifiedBy: 指定使用基于@qualifier注解的转换方法

先定义一个基于@qualifier(mapstruct包下)的作用于转换器类上的注解@StrConverter:

@Qualifier  
@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.CLASS)  
public @interface StrConverter {  
}

再定义一个基于@qualifier(mapstruct包下)的作用于转换器方法上的注解@NameUpper:

@Qualifier  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.CLASS)  
public @interface NameUpper {  
}

最后定义一个自定义转换器MyNameConverter:

@StrConverter  
public class MyNameConverter {  @NameUpper  public String convertUserName(String userName){  return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  }  
}

然后我们在@Mappinbg中通过使用:

@Mapper(uses = {MyNameConverter.class} ,nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {@Mapping(target = "userName", source = "userName", qualifiedBy = NameUpper.class) TargetObject sourceToTarget(SourceObject source);
}

最终两种方式编译后的结果是一致的:

public class MyMapperImpl implements MyMapper {  private final MyNameConverter myNameConverter = new MyNameConverter();  @Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  if ( source.getUserName() != null ) {  targetObject.setUserName( myNameConverter.convertUserName( source.getUserName() ) );  }  if ( source.getUserId() != null ) {  targetObject.setUserId( source.getUserId() );  }  return targetObject;  }  
}

以上基于qualifiedBy的使用示例参考自@Qualifier源码文档。

3.3 ignore

是否忽略某字段的映射。为true时忽略。

@Mapping(target = "sex", source = "sex", ignore = true)
TargetObject sourceToTarget(SourceObject source);

编译后实现类中不会对这个字段进行赋值:

@Override  
public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  if ( source.getUserName() != null ) {  targetObject.setUserName( source.getUserName() );  }  if ( source.getUserId() != null ) {  targetObject.setUserId( source.getUserId() );  }  return targetObject;  
}
3.4 defaultExpression

指定默认表达式,当源对象字段为 null 时使用。

@Mapping(target = "sex", source = "sex", defaultExpression = "java(SexEnum.MAN.desc)")
TargetObject sourceToTarget(SourceObject source);

编译后实现类:

 if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  else {  targetObject.setSex( SexEnum.MAN.desc );  } 

defaultExpression不能与expression,defaultValue,constant一起使用。

3.5 defaultValue

指定默认值,当源对象字段为 null 时使用。

@Mapping(target = "sex", source = "sex", defaultValue = "男人")  
TargetObject sourceToTarget(SourceObject source);

编译后:

if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  
}  
else {  targetObject.setSex( "男人" );  
}

defaultValue不能与expression,defaultExpression,constant一起使用。

3.6 constant

将目标对象的字段设置为该常量。不从源对象中映射值。

@Mapping(target = "source", constant = "API")  
TargetObject sourceToTarget(SourceObject source);

编译后:

targetObject.setSource( "API" );

constant不能与defaultExpression,expression,defaultValue,constant, source一起使用。

3.7 expression

通过表达式完成映射。要基于该字符串设置指定的目标属性。目前,Java 是唯一受支持的“表达式语言”,表达式必须使用以下格式以 Java 表达式的形式给出:java(<EXPRESSION>)

@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
TargetObject sourceToTarget(SourceObject source);

编译后:

targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

expression不能与source, defaultValue, defaultExpression, qualifiedBy, qualifiedByName 以及constant 一起使用

3.8 dateFormat

指定日期格式化模式,仅适用于日期类型的字段。可以实现String类型时间和Date相互转换,基于SimpleDateFormat实现。

@Data  
public class TargetObject {private String createTime;  private Date loginDate;
}@Data  
public class SourceObject {private Date createTime;  private String loginDate;
}@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")  
@Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd")  
TargetObject sourceToTarget(SourceObject source);

编译后:

if ( source.getCreateTime() != null ) {  targetObject.setCreateTime( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( source.getCreateTime() ) );  
}  
try {  if ( source.getLoginDate() != null ) {  targetObject.setLoginDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( source.getLoginDate() ) );  }  
}  
catch ( ParseException e ) {  throw new RuntimeException( e );  
}
3.9 numberFormat

指定数值格式化格式,仅适用Number类型的字段。可以实现String类型数值与Number相互转换,基于DecimalFormat实现。

@Data  
public class TargetObject {private double amountDouble;  private String amountStr;
}@Data  
public class SourceObject {private String amountStr;  private double amountDouble;
}@Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00")  
@Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
TargetObject sourceToTarget(SourceObject source);

编译后:

try {  if ( source.getAmountStr() != null ) {  targetObject.setAmountDouble( new DecimalFormat( "#,###.00" ).parse( source.getAmountStr() ).doubleValue() );  }  
}  
catch ( ParseException e ) {  throw new RuntimeException( e );  
}  
targetObject.setAmountStr( new DecimalFormat( "#,###.00" ).format( source.getAmountDouble() ) );

还有其他的属性,这里就不过多叙述了,有兴趣或者需要的可以阅读源码。

4.@Mappings

包含多个@Mapping注解,将多个字段映射规则组合在一起,使代码更清晰。

@Mappings({  @Mapping(target = "source", constant = "API"),  @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),  @Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd"),  @Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00"),  @Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
})  
TargetObject sourceToTarget(SourceObject source);
5.@Named:

用于标记自定义转换器或者映射器中的某个方法的名称。一般配合qualifiedByName 使用:

/**
* 标记映射方法名称
*/
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  /**
* 标记转换器方法名称
*/
@Named("convertSexDesc")  
default String convertSexDesc(Integer sex){  return SexEnum.descOfCode(sex);  
}

我们在定义自己的转换器方法时,最好把方法都加上@Named的注解标记你的方法名称,否则如果后续代码中再写一个同类型的不同方法名的转换方法时编译报错:不明确的映射方法。

image.png

6. @IterableMapping
用于集合映射,定义集合元素的映射规则。其中一些属性例如:`qualifiedByName`,`qualifiedBy`以及`dateFormat`,`numberFormat`参考`@Mapping`中的用法。
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  @IterableMapping(qualifiedByName = "sourceToTarget")  
List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后的实现类代码:

@Override  
public List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList) {  if ( sourceObjectList == null ) {  return null;  }  List<TargetObject> list = new ArrayList<TargetObject>( sourceObjectList.size() );  for ( SourceObject sourceObject : sourceObjectList ) {  list.add( sourceToTarget( sourceObject ) );  }  return list;  
}

可看出它内部循环调用sourceToTarget的方法完成list的转换。

需要特别注意,在写集合类型的转换时一定要配合IterableMappingqualifiedByNameNamed使用,如果不使用@IterableMapping中显示声明循环使用的方法时,它的内部会重新生成一个映射方法去使用。这样会在开发过程中出现一些莫名其妙的忽然就不好使的错误。。。。。

    @Named("sourceToTarget")  TargetObject sourceToTarget(SourceObject source);  @Named("sourceToTarget2")  TargetObject sourceToTarget2(SourceObject source);  //    @IterableMapping(qualifiedByName = "sourceToTarget")  List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后,实现类中代码可以看出并没有使用以上两个方法,而是重新生成的:

image.png
image.png

7.@MappingTarget

标记在映射方法的目标对象参数上,允许在映射方法中修改目标对象的属性。当目标对象已经创建了,此时可以将目标对象也当做参数传递到映射器方法中。

@Mapping(target = "source", constant = "API")  
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
@Named("sourceToTarget3")
void sourceToTarget3(@MappingTarget TargetObject targetObject, SourceObject source);

编译后实现类代码:

@Override  
public void sourceToTarget3(TargetObject targetObject, SourceObject source) {  if ( source == null ) {  return;  }  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  targetObject.setSource( "API" );  targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  
}
8.@InheritConfiguration

它用于在映射接口中引用另一个映射方法的配置。主要用于减少代码重复,提高映射方法的可维护性。

	@Mappings({  @Mapping(target = "source", constant = "API"),  @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")})  @Named("sourceToTarget")  TargetObject sourceToTarget(SourceObject source);@InheritConfiguration(name = "sourceToTarget")  @Named("sourceToTarget2")  TargetObject sourceToTarget2(SourceObject source);@InheritConfiguration(name = "sourceToTarget")  void sourceToTarget4(@MappingTarget TargetObject targetObject, SourceObject source);

sourceToTarget2sourceToTarget4就可以直接继承使用sourceToTarget的规则了。避免了再次定义一份相同的规则。

9. @BeanMapping

用于配置映射方法级别的注解,它允许在单个映射方法上指定一些特定的配置。例如忽略某些属性、配置映射条件等(开始我们在@Mapper中定义)。它提供了一种在方法级别自定义映射行为的方式。

@BeanMapping(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
@Named("sourceToTarget2")  
TargetObject sourceToTarget2(SourceObject source);

编译后实现的代码:

@Override  
public TargetObject sourceToTarget2(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  if ( source.getUserName() != null ) {  targetObject.setUserName( source.getUserName() );  }  if ( source.getUserId() != null ) {  targetObject.setUserId( source.getUserId() );  }  if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  return targetObject;  
}

校验了源对象值的null

10.@ValueMapping

用于自定义枚举类型或其他可映射类型的值映射。该注解允许在枚举类型映射时,定义自定义的值映射规则,使得在映射中可以转换不同的枚举值。他只有两个属性值:

  • source:只能取值:枚举值名称,MappingConstants.NULLMappingConstants.ANY_REMAININGMappingConstants.ANY_UNMAPPED
  • target: 只能取值:枚举值名称MappingConstants.NULLMappingConstants.ANY_UNMAPPED
  public enum OrderType { RETAIL, B2B, C2C, EXTRA, STANDARD, NORMAL }public enum ExternalOrderType { RETAIL, B2B, SPECIAL, DEFAULT }@ValueMappings({  @ValueMapping(target = "SPECIAL", source = "EXTRA"),  @ValueMapping(target = "DEFAULT", source = "STANDARD"),  @ValueMapping(target = "DEFAULT", source = "NORMAL"),  @ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = "C2C" )  
})  
ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType);	

编译后实现类代码:

@Override  
public ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType) {  if ( orderType == null ) {  return null;  }  ExternalOrderTypeEnum externalOrderTypeEnum;  switch ( orderType ) {  case EXTRA: externalOrderTypeEnum = ExternalOrderTypeEnum.SPECIAL;  break;  case STANDARD: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  break;  case NORMAL: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  break;  case C2C: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  case RETAIL: externalOrderTypeEnum = ExternalOrderTypeEnum.RETAIL;  break;  case B2B: externalOrderTypeEnum = ExternalOrderTypeEnum.B2B;  break;  default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  }  return externalOrderTypeEnum;  
}
11.@Context

@Context注解在MapStruct框架中用于标记映射方法的参数,使得这些参数作为映射上下文来处理。被标注为@Context的参数会在适用的情况下传递给其他映射方法、@ObjectFactory方法或者@BeforeMapping@AfterMapping方法,从而可以在自定义代码中使用它们。

具体作用如下:

  • 传递上下文信息: 当MapStruct执行映射操作时,它会将带有@Context注解的参数值向下传递到关联的方法中。这意味着你可以在不同的映射阶段(包括属性映射、对象工厂方法调用以及映射前后的处理方法)共享和利用这些上下文数据。

  • 调用相关方法: MapStruct还会检查带有@Context注解的参数类型上是否声明了@BeforeMapping@AfterMapping方法,并在适用时对提供的上下文参数值调用这些方法。

  • 空值处理: 注意,MapStruct不会在调用与@Context注解参数相关的映射前后方法或对象工厂方法之前进行空值检查。调用者需要确保在这种情况下不传递null值。

  • 生成代码的要求: 为了使生成的代码能够正确调用带有@Context参数的方法,正在生成的映射方法声明必须至少包含那些相同类型(或可赋值类型)的@Context参数。MapStruct不会为缺失的@Context参数创建新实例,也不会以null代替它们传递。

因此,@Context注解提供了一种机制,允许开发者在映射过程中携带并传播额外的状态或配置信息,增强了映射逻辑的灵活性和定制能力。

一个简单的用法示例:

	@Named("sourceToTarget5")  @Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  @Named("formatDate")  default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  return dateTimeFormatter.format(createTime);  
}

生成的实现类代码:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  return targetObject;  
}
12.@BeforeMapping

这个注解可以标注在一个没有返回值的方法上,该方法会在执行实际映射操作前被调用。在此方法中可以通过@Context注入上下文对象,并根据需要对源对象或上下文进行修改或预处理。

	@Named("sourceToTarget5")  @Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  @Named("formatDate")  default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  return dateTimeFormatter.format(createTime);  }  @BeforeMapping  default void beforeFormatDate(@Context ContextObject context) {  // 在映射之前初始化或更新上下文中的信息  context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}

编译后生成的实现类代码中,会发现在sourceToTarget5的方法第一行会调用beforeFormatDate这个方法:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  // 第一行调用@BeforeMapping的方法beforeFormatDate( contextObject );  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  return targetObject;  
}
13.@AfterMapping

这个注解同样可以标注在一个没有返回值的方法上,但它会在完成所有属性映射后被调用。你可以在这里执行一些额外的转换逻辑或者基于映射结果和上下文进行后期处理。

@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  @Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  return dateTimeFormatter.format(createTime);  
}  @BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  
// 在映射之前初始化或更新上下文中的信息  context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  @AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  targetObject.setContext(contextObject.getContext());  
}

编译后,可以发现在sourceTarget5的实现方法中的最后会调用afterHandler方法:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  beforeFormatDate( contextObject );  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  afterHandler( source, targetObject, contextObject );  return targetObject;  
}

@BeforeMapping@AfterMapping 注解的方法默认会作用于在同一接口内使用了相同参数类型的映射方法上。如果想要在一个地方定义一个通用的前置或后置处理逻辑,并让它应用于多个映射方法,可以编写一个不带具体映射源和目标参数的方法,并在需要应用这些逻辑的所有映射方法上保持相同的@Context参数类型。

14.@ObjectFactory

此注解用于声明一个工厂方法,该方法在目标对象实例化阶段被调用。这里也可以通过@Context获取到上下文信息,以便在创建目标对象时就考虑到某些上下文依赖。

@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  @Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  return dateTimeFormatter.format(createTime);  
}  @BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  // 在映射之前初始化或更新上下文中的信息  context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  @AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  targetObject.setContext(contextObject.getContext());  
}  @ObjectFactory  
default TargetObject createTargetObject(@Context ContextObject contextObject){  TargetObject targetObject = new TargetObject();  // 根据上下文初始化dto的一些属性  targetObject.setContext(contextObject.getContext());  return targetObject;  
}

编译后生成的实现类中,会看见TargetObject会通过createTargetObject方法创建:

@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  beforeFormatDate( contextObject );  if ( source == null ) {  return null;  }  TargetObject targetObject = createTargetObject( contextObject );  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  if ( source.getSex() != null ) {  targetObject.setSex( String.valueOf( source.getSex() ) );  }  targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  afterHandler( source, targetObject, contextObject );  return targetObject;  
}

@ObjectFactory 标记的方法则更具有针对性,它通常用于为特定的目标对象创建实例。如果你定义了一个@ObjectFactory方法且没有指定具体映射方法,则这个工厂方法会作为默认的实例化方式,在所有未明确提供实例化方法的映射目标对象时被调用。

SpringBoot集成

上面我们说到了@Mapper注解以及他的属性componentModel,将该值设置为Spring也就是MappingConstants.ComponentModel.SPRING值时,这个映射器生成的实现类就可以被Spring容器管理,这样就可以在使用时就可以注入到其他组件中了。

@Mapper(uses = {MyNameConverter.class}, imports = {SexEnum.class}, componentModel = MappingConstants.ComponentModel.SPRING)  
public interface MyMapper {@Mappings({  @Mapping(target = "source", constant = "API"),  @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  })  @Named("sourceToTarget")  TargetObject sourceToTarget(SourceObject source);
}	

生成的实现类自动加上@Component注解,并将其注册为Spring Bean,:

@Component  
public class MyMapperImpl implements MyMapper {@Override  public TargetObject sourceToTarget(SourceObject source) {  if ( source == null ) {  return null;  }  TargetObject targetObject = new TargetObject();  targetObject.setUserName( source.getUserName() );  targetObject.setUserId( source.getUserId() );  if ( source.getCreateTime() != null ) {  targetObject.setCreateTime( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( source.getCreateTime() ) );  }  targetObject.setSource( "API" );  targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  return targetObject;  }
}

这样就可以在其他组件中注入MyMapper

@SpringBootTest  
public class SpringbootCodeApplicationTests {private MyMapper mapper;@Test  void testMapper(){  TargetObject targetObject = mapper.sourceToTarget(new SourceObject());  System.out.println(targetObject.getSex());  }@Autowired  public void setMapper(MyMapper mapper) {  this.mapper = mapper;  
}

总结

MapStruct是一个利用注解和编译时代码生成技术的Java Bean映射工具,通过在接口上定义映射规则并自动创建实现类,极大地简化了对象转换过程。相比于手动编写映射代码及运行时反射工具如Dozer,MapStruct提供了更高的性能、更好的可读性和易于维护性。它支持灵活的字段映射配置、自定义转换逻辑,并可通过组件模型适应不同框架,是提升开发效率与降低维护成本的理想对象映射解决方案。

写在最后:可能大家觉得要防御性编程,但是咱可以把编译后实现类的代码CV到你的代码里面就可以了,这样免去了自己手写getset方法映射,这样不出错,还可以节省时间摸鱼。。。。

本文章已收录于我的个人博客:码农Academy,专注分享Java技术干货,包括Java基础、SpringBoot、SpringCloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/648963.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI Infra论文阅读之通过打表得到训练大模型的最佳并行配置

目录 0x0. 前言0x1. 摘要0x2. 介绍0x3. 背景0x4. 实验设置0x5. 高效的LLM训练分析0x5.1 Fused Kernels 和 Flash Attention0x5.1.1 Attention0x5.1.2 RMSNorm Kernel 0x5.2 Activation Checkpointing0x5.3 Micro-Batch 大小0x5.4 Tensor Parallelism和Pipline Parallelism0x5.5…

幻兽帕鲁服务器一键搭建脚本

前言 幻兽帕鲁刚上线就百万在线人数。官方服务器的又经常不稳定。所以这里给大家带来最快捷的搭建教程。废话不多说直接开始。 服务器配置要求 这里推荐腾讯云的轻量云服务器 测试环境&#xff1a; CPU &#xff1a; 2核 内存&#xff1a;4GB 系统&#xff1a;Debian 12 64…

leetcode:二叉树的中序遍历(外加先序,后序遍历)

题外&#xff1a;另外三种遍历可以看这&#xff1a; 层序遍历&#xff1a; Leetcode:二分搜索树层次遍历-CSDN博客 先序遍历&#xff1a; 二叉树的先序&#xff0c;中序&#xff0c;后序遍历-CSDN博客 后序遍历&#xff1a; 二叉树的先序&#xff0c;中序&#xff0c;后序…

鸿蒙开发(Harmonyos兼容与Harmonyos适配)

布局的实现 Layout_ability_main.xml布局&#xff1a; <?xml version"1.0" encoding"utf-8"?> <DirectionalLayoutxmlns:ohos"http://schemas.huawei.com/res/ohos"ohos:height"match_parent"ohos:width"match_pare…

测试人员为什么要编写测试用例?好的测试用例应该具备那些特点?

设计测试用例可以说是测试人员的一项最基本技能 。很多时候当我们接到设计测试用例的任务时 &#xff0c;往往都是想的该如何更快的完成这项任务 &#xff1f;而很少去想为什么要完成这项任务? 对于测试用例也是如此&#xff0c;为什么要设计测试用例呢(目的)&#xff1f;其实…

Jellyfin影音服务本地部署并结合内网穿透实现公网访问本地资源

文章目录 1. 前言2. Jellyfin服务网站搭建2.1. Jellyfin下载和安装2.2. Jellyfin网页测试 3.本地网页发布3.1 cpolar的安装和注册3.2 Cpolar云端设置3.3 Cpolar本地设置 4.公网访问测试5. 结语 1. 前言 随着移动智能设备的普及&#xff0c;各种各样的使用需求也被开发出来&…

拼图小游戏的界面和菜单的搭建

package Puzzlegame.com.wxj.ui;import javax.swing.*;public class GameJframe extends JFrame { //游戏主界面 public GameJframe(){//初始化界面initJFrame();//初始化菜单initJmenuBar();//让界面显示出来this.setVisible(true); }private void initJmenuBar() {//创建整个…

需求变化频繁的情况下,如何实施自动化测试

一.通常来说&#xff0c;具备以下3个主要条件才能开展自动化测试工作: 1.需求变动不频繁 自动化测试脚本变化的频率决定了自动化测试的维护成本。如果需求变动过于频繁&#xff0c;那么测试人员就需要根据变动的需求来不断地更新自动化测试用例&#xff0c;从而适应新的功能。…

qiankun子应用静态资源404问题有效解决(涉及 css文件引用图片、svg图片无法转换成 base64等问题)

在&#x1f449;&#x1f3fb; qiankun微前端部署&#x1f448;&#x1f3fb;这个部署方式的前提下&#xff0c;遇到的问题并解决问题的过程 最开始的问题现象 通过http请求本地的静态json文件404css中部分引入的图片无法显示 最开始的解决方式 在&#x1f449;&#x1f3…

行测-言语:2.语句表达

行测-言语&#xff1a;2.语句表达 1. 语句排序题 捆绑就是看两句话是不是讲的同一个内容&#xff0c;相同内容的句子应该相连。 1.1 确定首句 1.1.1 下定义&#xff08;……就是 / 是指&#xff09; A 1.1.2 背景引入&#xff08;随着、近年来、在……大背景 / 环境下&#…

Android 渲染机制

1 Android 渲染流程 一般情况下&#xff0c;一个布局写好以后&#xff0c;使用 Activity#setContentView 调用该布局&#xff0c;这个 View tree 就创建好了。Activity#setContentView 其实是通过 LayoutInflate 来把布局文件转化为 View tree 的&#xff08;反射&#xff09;…

使用Go语言编写自定义的HTTP代理:探险网络奇幻之旅

你是否曾经想过自己也能编写一个代理服务器&#xff0c;掌握网络冲浪的主动权&#xff1f;现在&#xff0c;有了Go语言&#xff0c;这个梦想不再遥不可及&#xff01;让我们一起踏上这段探险之旅&#xff0c;用Go语言编写一个自定义的HTTP代理&#xff0c;开启网络奇幻之旅&…

音频格式之AAC:(3)AAC编解码原理详解

系列文章目录 音频格式的介绍文章系列&#xff1a; 音频编解码格式介绍(1) ADPCM&#xff1a;adpcm编解码原理及其代码实现 音频编解码格式介绍(2) MP3 &#xff1a;音频格式之MP3&#xff1a;(1)MP3封装格式简介 音频编解码格式介绍(2) MP3 &#xff1a;音频格式之MP3&#x…

HNU-编译原理-甘晴void学习感悟

前言 熬过煎熬的考试周、复习以及更加煎熬的等成绩&#xff0c;查到成绩的那一刻&#xff0c;心里还是挺开心的。 虽然我没有完全学懂这门课程&#xff0c;但我还是兢兢业业地通过了课程的考试&#xff0c;拿到了这门课程的认可。 记录一下自己对编译原理的学习感悟&#xf…

优化用户体验测试应用领域:提升产品质量与用户满意度

在当今数字化时代&#xff0c;用户体验测试应用已经成为确保产品质量、提升用户满意度的关键工具。随着技术的不断发展&#xff0c;用户的期望也在不断演变&#xff0c;因此&#xff0c;为了保持竞争力&#xff0c;企业必须将用户体验置于产品开发的核心位置。本文将探讨用户体…

知识圣殿,智慧熔炉

知识圣殿&#xff0c;智慧熔炉 知识殿堂&#xff0c;巍然屹立 一座灵魂熔炉&#xff0c;号称图书馆 万卷书香盈架&#xff0c;智慧如星河汇聚 每一册书页&#xff0c;流淌着人类文明的血脉 钢笔与墨水交织诗篇 思想发芽&#xff0c;真理绽放光焰 浩瀚知识海洋&#xff0c;波涛…

Dlearning

Deep Learning Basic 神经网络&#xff1a; #mermaid-svg-rR22a8Udy5SxGOoP {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-rR22a8Udy5SxGOoP .error-icon{fill:#552222;}#mermaid-svg-rR22a8Udy5SxGOoP .error-t…

【MATLAB源码-第122期】基于matlab斑马优化算法(ZOA)无人机三维路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 斑马优化算法&#xff08;Zebra Optimization Algorithm&#xff0c;简称ZOA&#xff09;是一种模仿斑马群体行为的优化算法。在自然界中&#xff0c;斑马是一种社会性很强的动物&#xff0c;它们具有独特的群体行为模式&…

【STM32】STM32学习笔记-硬件SPI读写W25Q64(40)

00. 目录 文章目录 00. 目录01. SPI简介02. W25Q64简介03. SPI相关API3.1 SPI_Init3.2 SPI_Cmd3.3 SPI_I2S_SendData3.4 SPI_I2S_ReceiveData3.5 SPI_I2S_GetFlagStatus3.6 SPI_I2S_ClearFlag3.7 SPI_InitTypeDef 04. 硬件SPI读写W25Q64接线图05. 硬件SPI读写W25Q64示例06. 程序…

1块9毛钱,修复拓牛TC1D智能垃圾桶盖子不能正常开合的故障

前言 21年9月份买了拓牛的智能垃圾桶&#xff0c;一直用的很流畅&#xff0c;再加上屋里没啥有机垃圾&#xff0c;也没有宠物&#xff0c;用上之后每次投入垃圾&#xff0c;之后都会盖上盖子&#xff0c;没有很多的异味散发&#xff0c;屋里也没有蟑螂等害虫。 再加上门口有帘…