【笔记】Java - mapstruct 转换VO、DTO、PO


在这里插入图片描述
这篇文章中提到: Java项目为了可读性、可维护性考虑,根据不同场景,搞了一堆类的概念。

不同场景之间使用这些类,必然需要频繁的类转换。

  • 如果这些对象的属性名相同还好,可以用如下工具类赋值

    • 【⚠️避免使用】Apache BeanUtils,性能较差
    • Spring BeanUtils 简单易用,但不能对属性进行特定处理
    • Cglib BeanCopier
  • 如果属性名不同呢?如果是将多个PO对象合并成一个VO对象呢?

    • 一般方法: 需要频繁的编写映射规则、组织重用代码
    • MapStruct: 通过注解声明,在编译阶段生成映射规则
    • orika 能够精细控制,解耦

下面介绍mapstruct

原理

MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

MapStruct属于在编译期,生成调用get/set方法进行赋值的代码,生成对应的java文件。在编译期间消耗少许的时间,换取运行时的高性能。

优点分析

  1. 性能高
    这是相对反射来说的,反射需要去读取字节码的内容,花销会比较大。而通过 MapStruct 来生成的代码,其类似于人手写。速度上可以得到保证。
  2. 使用简单
    如果是完全映射的,使用起来肯定没有反射简单。用类似 BeanUtils 这些工具一条语句就搞定了。但是,如果需要进行特殊的匹配(特殊类型转换,多对一转换等),其相对来说也是比较简单的。
    基本上,使用的时候,我们只需要声明一个接口,接口下写对应的方法,就可以使用了。当然,如果有特殊情况,是需要额外处理的。
  3. 代码独立、非入侵性
    不需要在原有的类(PO、DTO)中增加任何注解,只需要在转换时调用既可实现转换功能
    (💡与lombok相比)
  4. 易于 debug
    代码编译后,我们可以直接进入生成impl类,进行查看、debug

案例代码

引入依赖

pom.xml(⚠️最新的配置 ── https://mapstruct.org/documentation/installation/

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.3.1.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.3.1.Final</version>
    <scope>provided</scope>
</dependency>

🔥 映射配置相关
⚙️ 扩展配置相关

# 🔥默认规则、隐式转换、@Mappings、@Mapping

官方文档(隐式转换): https://mapstruct.org/documentation/stable/reference/html/#implicit-type-conversions

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/8c4a0ca528696b2c356f685cb966b7e707e6a752

默认规则

  1. 同名、同类型 = 自动映射
  2. 同名、不同类型 = 自动进行类型转换、自动映射
    自动转换类型:
    1. 8种基本类型和它们对应的包装类型
    2. 8种基本类型(包括它们的包装类型)和string之间
    3. 日期类型和string之间
  • 多余的属性,不会报错

通过@Mapping的规则补充

  • 指定属性之间的映射关系
    • 日期格式化: dateFormat = "yyyy-MM-dd HH:mm:ss"
    • 数字格式化: numberFormat = "#.00"
# 🔥子类映射 ── @SubclassMapping

当源和目标对象都具有继承关系时,需要指定其映射对应关系。

比如Apple和 Banana都继承自Fruit类,可以写一个父类的转换方法,然后使用@SubclassMapping注解标识子类的对应关系。这样就可以传递子类参数进行映射。

@Mapper
public interface FruitMapper {

    @SubclassMapping( source = AppleDto.class, target = Apple.class )
    @SubclassMapping( source = BananaDto.class, target = Banana.class )
    Fruit map( FruitDto source );
}

⚠️问题:

  1. @SubclassMapping不支持与更新方法结合使用。如果您尝试使用子类映射,则会出现编译错误。@Context和@TargetType参数也存在同样的问题。
  2. 如果Fruit是抽象类或接口,则会出现编译错误。
    要允许抽象类或接口的映射,您需要将 subclassExhaustiveStrategy设置为RUNTIME_EXCEPTION。
# 🔥默认值 ── @Mapping.constant、@Mapping.expression、@Mapping.defaultValue、@Mapping.defaultExpression

官方文档: https://mapstruct.org/documentation/stable/reference/html/#default-values-and-constants

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/7a8fcc77aa007e175f2f2b0c4a6322bfa53ba0e0

  • constant: 仅需要指定target。设置target值固定为constant。(💡只能指定String,然后如果需要类型转换,再进行类型转换)
  • expression: 作用同constant,但以expression形式赋值。(💡可以直接指定非String的值)
  • defaultValue: 需要有对应 target、source。当source值为null时,使用默认值。
  • defaultExpression: 作用同defaultValue,但以expression形式赋值
# 🔥忽略默认规则 ── @BeanMapping.ignoreByDefault = false

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/7a04786980958f23d8d5dd66552d5fcb2bc1a1a1

  • IgnoreByDefault: 忽略mapstruct的默认映射行为。避免不需要的赋值、避免属性覆盖
# 🔥未匹配目标策略 ── @Mapper.unmappedTargetPolicy
  • IGNORE:未映射的target属性将被忽略
  • ERROR:任何未映射的target属性都将导致映射代码生成失败
  • WARN:任何未映射的target属性都会在构建时发出警告
# 🔥集合映射
List、Set集合

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/527ce480a21afaf1ccdaf07ed30f36256824b5d1

@Mapper
public interface CarMapper {

    Set<String> integerSetToStringSet(Set<Integer> integers);

    List<CarDto> carsToCarDtos(List<Car> cars);

    CarDto carToCarDto(Car car);
}

生成代码

Override
public Set<String> integerSetToStringSet(Set<Integer> integers) {
    if ( integers == null ) {
        return null;
    }

    Set<String> set = new LinkedHashSet<String>();

    for ( Integer integer : integers ) {
        set.add( String.valueOf( integer ) );
    }

    return set;
}

@Override
public List<CarDto> carsToCarDtos(List<Car> cars) {
    if ( cars == null ) {
        return null;
    }

    List<CarDto> list = new ArrayList<CarDto>();

    for ( Car car : cars ) {
        list.add( carToCarDto( car ) );
    }

    return list;
}
Map集合
public interface SourceTargetMapper {

    @MapMapping(valueDateFormat = "dd.MM.yyyy")
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

生成代码

@Override
public Map<Long, Date> stringStringMapToLongDateMap(Map<String, String> source) {
    if ( source == null ) {
        return null;
    }

    Map<Long, Date> map = new LinkedHashMap<Long, Date>();

    for ( Map.Entry<String, String> entry : source.entrySet() ) {

        Long key = Long.parseLong( entry.getKey() );
        Date value;
        try {
            value = new SimpleDateFormat( "dd.MM.yyyy" ).parse( entry.getValue() );
        }
        catch( ParseException e ) {
            throw new RuntimeException( e );
        }

        map.put( key, value );
    }

    return map;
}
Stream流

映射java.util.Stream与集合类型映射类似,即通过在映射器接口中定义具有所需源和目标类型的映射方法j即可。

@Mapper
public interface CarMapper {

    Set<String> integerStreamToStringSet(Stream<Integer> integers);

    List<CarDto> carsToCarDtos(Stream<Car> cars);

    CarDto carToCarDto(Car car);
}

生成代码

//GENERATED CODE
@Override
public Set<String> integerStreamToStringSet(Stream<Integer> integers) {
    if ( integers == null ) {
        return null;
    }

    return integers.map( integer -> String.valueOf( integer ) )
        .collect( Collectors.toCollection( LinkedHashSet<String>::new ) );
}

@Override
public List<CarDto> carsToCarDtos(Stream<Car> cars) {
    if ( cars == null ) {
        return null;
    }

    return cars.map( car -> carToCarDto( car ) )
        .collect( Collectors.toCollection( ArrayList<CarDto>::new ) );
}
集合映射策略

通过 @Mapper#collectionMappingStrategy 设置集合的映射策略:

  • CollectionMappingStrategy.ACCESSOR_ONLY (默认)
  • CollectionMappingStrategy.SETTER_PREFERRED
  • CollectionMappingStrategy.ADDER_PREFERRED
  • CollectionMappingStrategy.TARGET_IMMUTABLE

在这里插入图片描述

用于集合映射的实现类型
接口类型实现类型
IterableArrayList
CollectionArrayList
ListArrayList
SetHashSet
SortedSetTreeSet
NavigableSetTreeSet
MapHashMap
SortedMapTreeMap
NavigableMapTreeMap
ConcurrentMapConcurrentHashMap
ConcurrentNavigableMapConcurrentSkipListMap
# 🔥枚举映射 ── @ValueMappings、@ValueMapping

默认情况下,源枚举中的每个常量都映射到目标枚举类型中同名的常量。

如果需要,可以在@ValueMapping注解的帮助下将源枚举中的常量映射到具有另一个名称的常量。源枚举中的几个常量可以映射到目标类型中的同一个常量。

@Mapper
public interface OrderMapper {

    OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );

    @ValueMappings({
        @ValueMapping(source = "EXTRA", target = "SPECIAL"),
        @ValueMapping(source = "STANDARD", target = "DEFAULT"),
        @ValueMapping(source = "NORMAL", target = "DEFAULT")
    })
    ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
}

生成代码

// GENERATED CODE
public class OrderMapperImpl implements OrderMapper {

    @Override
    public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
        if ( orderType == null ) {
            return null;
        }

        ExternalOrderType externalOrderType_;

        switch ( orderType ) {
            case EXTRA: externalOrderType_ = ExternalOrderType.SPECIAL;
            break;
            case STANDARD: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case NORMAL: externalOrderType_ = ExternalOrderType.DEFAULT;
            break;
            case RETAIL: externalOrderType_ = ExternalOrderType.RETAIL;
            break;
            case B2B: externalOrderType_ = ExternalOrderType.B2B;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );
        }

        return externalOrderType_;
    }
}
名称前缀、后缀 ── @EnumMapping

如果没有定义@ValueMapping,则源枚举中的每个常量都会映射到目标枚举类型中具有相同名称的常量。但是,有些情况下需要在进行映射之前转换源枚举。例如,需要应用后缀以从源映射到目标枚举。

public enum CheeseType {

    BRIE,
    ROQUEFORT
}

public enum CheeseTypeSuffixed {

    BRIE_TYPE,
    ROQUEFORT_TYPE
}
@Mapper
public interface CheeseMapper {

    CheeseMapper INSTANCE = Mappers.getMapper( CheeseMapper.class );

    @EnumMapping(nameTransformationStrategy = "suffix", configuration = "_TYPE")
    CheeseTypeSuffixed map(CheeseType cheese);

    @InheritInverseConfiguration
    CheeseType map(CheeseTypeSuffix cheese);
}

生成的代码

// GENERATED CODE
public class CheeseSuffixMapperImpl implements CheeseSuffixMapper {

    @Override
    public CheeseTypeSuffixed map(CheeseType cheese) {
        if ( cheese == null ) {
            return null;
        }

        CheeseTypeSuffixed cheeseTypeSuffixed;

        switch ( cheese ) {
            case BRIE: cheeseTypeSuffixed = CheeseTypeSuffixed.BRIE_TYPE;
            break;
            case ROQUEFORT: cheeseTypeSuffixed = CheeseTypeSuffixed.ROQUEFORT_TYPE;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
        }

        return cheeseTypeSuffixed;
    }

    @Override
    public CheeseType map(CheeseTypeSuffixed cheese) {
        if ( cheese == null ) {
            return null;
        }

        CheeseType cheeseType;

        switch ( cheese ) {
            case BRIE_TYPE: cheeseType = CheeseType.BRIE;
            break;
            case ROQUEFORT_TYPE: cheeseType = CheeseType.ROQUEFORT;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + cheese );
        }

        return cheeseType;
    }
}

MapStruct 提供以下开箱即用的枚举名称转换策略:

  • suffix - 在源枚举上应用后缀
  • stripSuffix - 从源枚举中去除后缀
  • prefix - 在源枚举上应用前缀
  • stripPrefix - 从源枚举中去除前缀
  • 也可以注册自定义策略。
# 🔥判断条件设置 ── @Condition

允许用户编写自定义条件方法,这些方法将被调用以检查是否需要映射属性。

@Mapper
public interface CarMapper {

    CarDto carToCarDto(Car car);

    @Condition
    default boolean isNotEmpty(String value) {
        return value != null && !value.isEmpty();
    }
}

生成的代码

// GENERATED CODE
public class CarMapperImpl implements CarMapper {

    @Override
    public CarDto carToCarDto(Car car) {
        if ( car == null ) {
            return null;
        }

        CarDto carDto = new CarDto();

        if ( isNotEmpty( car.getOwner() ) ) {
            carDto.setOwner( car.getOwner() );
        }

        // Mapping of other properties

        return carDto;
    }
}
# 🔥继承配置 ── @InheritConiguration

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/7a8fcc77aa007e175f2f2b0c4a6322bfa53ba0e0

  • name 指定继承的配置所在的方法名
# 🔥继承配置(反向) ── @Inheritin verseConfiguration
# 🔥共享配置 ── @MapperConfig、@Mapper.config

MapStruct 提供了@MapperConfig注解定义公有配置,使用@Mapper#config引入配置就可以让映射器使用共享配置。

@MapperConfig注解具有与@Mapper注解相同的属性。任何未声明的属性@Mapper都将从共享配置中继承。@Mapper中配置的优先级高于@MapperConfig。

e.g.

首先定义一个公共配置:

@MapperConfig(
        unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface CentralConfig {

    @Mapping(target = "primaryKey", source = "technicalKey")
    BaseEntity anyDtoToEntity(BaseDto dto);
}

在其他映射器中,就可以引入共享配置:

@Mapper(config = CentralConfig.class, uses = { CustomMapperViaMapper.class } )
public interface SourceTargetMapper {
  ...
}

共享配置中可以设置原型映射,也可以是父类映射,其使用@MapperConfig中的mappingInheritanceStrategy属性配置策略(@Mapper注解也有):

  • EXPLICIT(默认):默认,要想继承原型映射必须使用@InheritConfiguration或@InheritInverseConfiguration注解方法,且此方法的源类型和目标类型要能赋予原型映射类型。
  • AUTO_INHERIT_FROM_CONFIG:不需要@InheritConfiguration注解方法,只需要满足类型条件就能继承,但只能是正映射。
  • AUTO_INHERIT_REVERSE_FROM_CONFIG:不需要@InheritInverseConfiguration注解方法,只需要满足类型条件就能继承,但只能是逆映射。
  • AUTO_INHERIT_ALL_FROM_CONFIG:不需要@InheritInverseConfiguration注解方法,只需要满足类型条件就能继承,正/逆映射都可以。
# 🔥异常处理

调用映射方法时调用应用程序可能需要处理异常。这些异常可能由手写逻辑和生成的内置映射方法或 MapStruct 的类型转换引发。当调用应用程序需要处理异常时,可以在映射方法中定义 throws 子句:

@Mapper(uses = HandWritten.class)
public interface CarMapper {

    CarDto carToCarDto(Car car) throws GearException;
}

声明检查异常的自定义映射方法:

public class HandWritten {

    private static final String[] GEAR = {"ONE", "TWO", "THREE", "OVERDRIVE", "REVERSE"};

    public String toGear(Integer gear) throws GearException, FatalException {
        if ( gear == null ) {
            throw new FatalException("null is not a valid gear");
        }

        if ( gear < 0 && gear > GEAR.length ) {
            throw new GearException("invalid gear");
        }
        return GEAR[gear];
    }
}

MapStruct 将 FatalException包装在一个 try-catch 块中并重新抛出RuntimeException。

// GENERATED CODE
@Override
public CarDto carToCarDto(Car car) throws GearException {
    if ( car == null ) {
        return null;
    }

    CarDto carDto = new CarDto();
    try {
        carDto.setGear( handWritten.toGear( car.getGear() ) );
    }
    catch ( FatalException e ) {
        throw new RuntimeException( e );
    }

    return carDto;
}
# ⚙️指定映射规则 ── @Named、@Qualifier、@Mapping.qualifiedBy

官方文档: https://mapstruct.org/documentation/stable/reference/html/#selection-based-on-qualifiers

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/3d1f5af487e48c91feec45c1afd17cbc89f2a514

  • @Mapping.qualifiedBy: 指定具体字段的映射规则
  • @Named: 通过String指定规则
  • @Qualifier: 通过注解(annotation)指定规则

在使用一个映射的时候,如果有两个参数类型、返回类型都一样的方法,那么mapstruct编译报错。因为mapstruct默认不知道使用哪一个方法。这时候需要进一步声明。

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title")
     GermanRelease toGerman( OriginalRelease movies );

}
public class Titles {

    public String translateTitleEG(String title) {
        // some mapping logic
    }

    public String translateTitleGE(String title) {
        // some mapping logic
    }
}
@Named

接口

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
     GermanRelease toGerman( OriginalRelease movies );

}

实现

@Named("TitleTranslator")
public class Titles {

    @Named("EnglishToGerman")
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @Named("GermanToEnglish")
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}
@Qualifier

接口

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
     GermanRelease toGerman( OriginalRelease movies );

}

实现

@TitleTranslator
public class Titles {

    @EnglishToGerman
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @GermanToEnglish
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

注解(annotation)定义

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}
import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}
import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface GermanToEnglish {
}
默认值

如果设定了默认值 @Mapper.defaultValue 则当source的属性值确实为空时,指定的映射规则收到的传入参数就是默认值。

# 🎄类AOP ── @MappingTarget、@AfterMapping、@BeforeMapping

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/7a8fcc77aa007e175f2f2b0c4a6322bfa53ba0e0

在映射前、映射后做自定义处理

# 🎄对象工厂 ── @ObjectFactory、@TargetType

我的代码示例: https://gitcode.net/LawssssCat/learn-mapstruct/-/commit/2fa0001dd9e24edaa54a61c1bfa4384a99b2a83c

默认情况下,用于将类型映射到另一种类型时生成代码将调用默认构造函数来实例化目标类型。

在这里插入图片描述

对象工厂可以自定义对象的构造过程

todo https://yunyanchengyu.blog.csdn.net/article/details/124171504

# 🌟与spring结合使用 ── @Mapper.componentModel = “spring”

实际上就是加上了 spring @Component 注解

  • default: 默认的情况,mapstruct不使用任何组件类型, 可以通过Mappers.getMapper(Class)方式获取自动生成的实例对象。
  • cdi: the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
  • spring(经常使用): 生成的实现类上面会自动添加一个@Component注解,可以通过Spring的 @Autowired方式进行注入
  • jsr330: 生成的实现类上会添加@javax.inject.Named 和@Singleton注解,可以通过 @Inject注解获取。
# 🌟@TargetType、@Context

问题

# ❓IDE提示impl报错
  • 如果代码映射有问题,修改代码
  • 如果代码没问题,maven clean
# ❓MapStruct与lombok加载顺序问题与annotationProcessorPaths的关系?

https://blog.csdn.net/hanqing456/article/details/128178701

# ❓循环依赖问题

当转换的bean具有双向关系时,就会出现StackOverflowError异常。

解决:

以官方提出的解决方法为例。

官方方法思路: 每次映射(💡mapper),设置一个map作为缓存(💡这里名为“context”)。取值时如果缓存有,就取缓存的;如果缓存没有,才创造。

mapper

package com.lawsssscat.msblog.mapper.common;

import com.lawsssscat.msblog.mapper.annotation.DoNotSelectForMapping;
import com.lawsssscat.msblog.mapper.util.CycleAvoidingMappingContext;
import org.mapstruct.Context;

import java.util.Collection;
import java.util.List;

/**
 * @author lawsssscat
 */
public interface BaseMapper<PO, DTO> {

    @DoNotSelectForMapping
    default DTO mapPO2DTO(PO po) {
        return mapPO2DTO(po, new CycleAvoidingMappingContext());
    }

    DTO mapPO2DTO(PO po, @Context CycleAvoidingMappingContext context);

    @DoNotSelectForMapping
    default List<DTO> mapPO2DTO(Collection<PO> pos) {
        return mapPO2DTO(pos, new CycleAvoidingMappingContext());
    }

    List<DTO> mapPO2DTO(Collection<PO> pos, @Context CycleAvoidingMappingContext context);
}

context

package com.lawsssscat.msblog.mapper.util;

import org.mapstruct.BeforeMapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.TargetType;

import java.util.IdentityHashMap;
import java.util.Map;

/**
 * 避免循环依赖
 *
 * @author lawsssscat
 */
public class CycleAvoidingMappingContext {

    /**
     * 成品、半成品缓存
     */
    private Map<Object, Object> knownInstances = new IdentityHashMap<>();

    /**
     * 在Mapper转换中,在创建targetType实例前,先看该类型是否在缓存knownInstances中,如果有,则直接取缓存中的实例。
     *
     * @param source 转换前的实例
     * @param targetType 转换后的目标类型Class
     * @return 转换后的目标
     * @param <T> 转换后的目标类型
     */
    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return (T) knownInstances.get( source );
    }

    /**
     * 在Mapper转换器中,当转换后的对象创建后,马上存入缓存knownInstances中。
     *
     * @param source 转换前的实例
     * @param target 转换后的实例(半成品:未添加属性)
     */
    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put( source, target );
    }
}

⚠️annotation: 除了上述两个类,还用到一个自定义注解。
这个自定义注解是处理衍生问题的: 因为添加了带context的方法,当List map(List, context)这个方法找映射规则的时候会有两个选择(这时就会报错ambiguous methods)。
解决: 被这个自定义注解修饰的方法默认不被选择。

package com.lawsssscat.msblog.mapper.annotation;

import org.mapstruct.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 在方法上添加该注解后,只要不显式使用(qualifiedBy),就不会被自动使用。
 * (💡默认所有方法都被考虑为可以自动使用)
 *
 * @author lawsssscat
 */
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface DoNotSelectForMapping {
}

具体参考: https://gitcode.net/LawssssCat/msblog/-/commit/2a9bd0d5e998698e006801d2327dbd0cad2b7435

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骆言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值