当前位置: 首页 > news >正文

Spring依赖注入最佳实践:应对接口多实现的挑战

文章目录

  • 前言
  • 一、基础方案:@Qualifier、@Resource 的精准匹配
    • @Qualifier 示例
    • @Resource 示例
  • 二、默认策略:@Primary 的优雅降级
    • @Primary 标记主实现
    • 直接注入默认实现
  • 三、 动态管理:Map 注入的灵活扩展
  • 四、高阶实践:自定义注解的语义增强
    • 自定义注解
    • 普通类使用自定义注解标识
      • 被@RegisterEntity 注解标识的几个普通实体类
      • 初始化扫描注入工具ExportEntityRegistryInitializer
      • 注册列表及工厂类ExportEntityRegistryFactory
      • 接口工厂类OrderUploadServiceRegistryFactory
      • 初始化工具类OrderUploadServiceRegistryInitializer
  • 五、总结


前言

在 Spring 框架中,依赖注入(DI)的优雅性往往决定了代码的可维护性。当我们为一个接口编写多个实现类(如支付接口支持微信 / 支付宝 / 银联),如何准确获取所需的 Bean 实例?直接注入接口类型时,Spring 默认会抛出NoUniqueBeanDefinitionException,提示存在多个候选实现。这种情况下,硬编码 Bean 名称或暴力使用@Lazy注解并非长久之计,我们需要一套系统化的解决方案。

本文将通过以下四个维度,带你从基础到进阶掌握 Spring 多实现类注入的核心技巧:

  • 基础方案:@Qualifier、@Resource 的精准匹配
  • 默认策略:@Primary 的优雅降级
  • 动态管理:Map 注入的灵活扩展
  • 高阶实践:自定义注解的语义增强

笔者后续介绍的着重点在高阶实践这部分,也是笔者工作中用的比较多的一种。

一、基础方案:@Qualifier、@Resource 的精准匹配

@Qualifier 示例

@Service
public class ZooService {
    // 通过Bean名称精准注入Dog
    @Autowired
    @Qualifier("dog")  // 对应@Component("dog") 或默认类名首字母小写
    private Animal animal;

    public void makeSound() {
        animal.shout(); // 输出:汪汪汪
    }
}

@Resource 示例

@Service
public class ZooService {
    // 优先按名称匹配,名称未指定时默认使用字段名
    @Resource(name = "cat")
    private Animal animal;

    public void makeSound() {
        animal.shout(); // 输出:喵喵喵
    }
}

适用场景:

明确知道需要注入的 Bean 名称时使用,适合固定依赖关系的场景。


二、默认策略:@Primary 的优雅降级

@Primary 标记主实现

@Component
@Primary  // 标记为主实现
public class Dog implements Animal {
    @Override public void shout() { System.out.println("汪汪汪"); }
}

@Component
public class Cat implements Animal {
    @Override public void shout() { System.out.println("喵喵喵"); }
}

直接注入默认实现

@Service
public class ZooService {
    // 自动注入@Primary标记的Dog
    @Autowired
    private Animal animal;

    public void makeSound() {
        animal.shout(); // 输出:汪汪汪
    }
}

这里默认注入的就直接时被@Primary标记主实现类了,即为Dog类,如果想注入非主实现的类,则需要配合@Autowired和@Qulifier(“cat”)或者@Resource(name=“cat”)

// 服务类,注入非主要实现类
@Service
class ZooService {
    @Autowired
    @Qualifier("cat")
    private Animal animal;

    public void makeSound() {
        animal.shout();
    }
}

//或者如下方式

@Service
class ZooService {
    @Resource("cat")
    private Animal animal;

    public void makeSound() {
        animal.shout();
    }
}


三、 动态管理:Map 注入的灵活扩展

假如现在我有如下接口

public interface DataProvider {
}

这个接口呢有几个实现类,分别如下

@Service("bydSupplier")
public class BydDataProvider implements DataProvider {
}

@Service("qrSupplier")
public class QrDataProvider implements DataProvider {
}

@Service("cxSupplier")
public class CxDataProvider implements DataProvider {
}

那我现在要使用其中一个实现类,该怎么获取呢?
我们可以定义一个工厂类工具

@Component
public class DataProviderFactory {
    @Resource
    private Map<String, DataProvider> providers;
    
    public DataProvider getDataProvider(String key) {
        return providers.get(key);
    }
}

这个providers就注入了DataProvider接口的所有实现类,其中key就是各个实现类上@Service注解中标识的name名称,但这种有个问题,就是每个实现类的名称绑死固定了,如果我@Service(“bydSupplier”)、@Service(“qrSupplier”)、@Service(“cxSupplier”)中的name名称是作为唯一标识,并且有其他类需要使用怎么办?而且我需要根据外部传入的唯一标识来获取对应的实现类名称,不能都写上@Service(“bydSupplier”)、@Service(“qrSupplier”)、@Service(“cxSupplier”),这时候就需要其他方法了,接着往下看


四、高阶实践:自定义注解的语义增强

自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RegisterEntity {
    String value();
}

这是一个自定义注解,专门用来注解标识名称的,笔者下面分两种情况来使用这个注解

普通类使用自定义注解标识

被@RegisterEntity 注解标识的几个普通实体类

@RegisterEntity("bydSupplier")
public class BydExportEntity extends BaseExportEntity{
}

@RegisterEntity("qrSupplier")
public class QrExportEntity extends BaseExportEntity{
}

@RegisterEntity("cxSupplier")
public class CxExportEntity extends BaseExportEntity {
}

初始化扫描注入工具ExportEntityRegistryInitializer

/**
 * 启动初始化扫描所有供应商导出实体类对应注解,并注册到EntityRegistryFactory工厂中
 */
@Configuration
public class ExportEntityRegistryInitializer {

    // 指定扫描的包路径
    private static final String BASE_PACKAGE = "com.usteu.exportEntity";

    /**
     * 启动时初始化注册所有供应商导出实体类
     */
    @Bean
    public CommandLineRunner initExportEntityRegistry() {
        return args -> {
            ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false);
            // 添加自定义注解过滤器
            scanner.addIncludeFilter(new AnnotationTypeFilter(RegisterEntity.class));
            // 扫描包路径
            scanner.findCandidateComponents(BASE_PACKAGE).forEach(beanDefinition -> {
                try {
                    Class<?> clazz = ClassUtils.forName(Objects.requireNonNull(beanDefinition.getBeanClassName()), getClass().getClassLoader());
                    RegisterEntity annotation = clazz.getAnnotation(RegisterEntity.class);
                    String identifier = annotation.value();

                    // 确保实现 Entity 接口
                    if (!BaseExportEntity.class.isAssignableFrom(clazz)) {
                        throw new RuntimeException(clazz.getName() + " 必须继承基础类BaseExportEntity");
                    }

                    // 注册构造逻辑
                    ExportEntityRegistryFactory.register(identifier, () -> {
                        try {
                            return (BaseExportEntity) clazz.getDeclaredConstructor().newInstance();
                        } catch (Exception e) {
                            throw new RuntimeException("创建实例失败 " + clazz.getName(), e);
                        }
                    });

                } catch (ClassNotFoundException e) {
                    throw new RuntimeException("类未发现: " + beanDefinition.getBeanClassName(), e);
                }
            });
        };
    }
}

上面的类中有一行代码是配置注解扫描路径的

 // 指定扫描的包路径
    private static final String BASE_PACKAGE = "com.usteu.exportEntity";

这个就是上面三个实体类所在包路径

在这里插入图片描述

注册列表及工厂类ExportEntityRegistryFactory

public class ExportEntityRegistryFactory {

    private static final Map<String, Supplier<? extends BaseExportEntity>> registry = new ConcurrentHashMap<>();

    public static void register(String identifier, Supplier<? extends BaseExportEntity> supplier) {
        registry.put(identifier, supplier);
        System.out.println("注册了"+supplier.get().getClass().getName());
    }

    public static BaseExportEntity getExportEntity(String identifier) {
        Supplier<? extends BaseExportEntity> supplier = registry.get(identifier);
        if (supplier == null) {
            throw new IllegalArgumentException("未知的实体类: " + identifier);
        }
        return supplier.get();
    }

    public static void clear() {
        registry.clear();
    }
}

这样的话在启动的时候,三个有注解了@RegisterEntity的普通的实体类会被获取注册到ExportEntityRegistryFactory工厂类中的Map对象registry中,使用时我们只需要传入标识即可获取对应的实体类

ExportEntityRegistryFactory.getExportEntity("bydSupplier")

上述代码获取的就是BydExportEntity实体类,一般情况下我们的普通实体类需要统一继承一个类,这样好封装,遵循里氏替换原则,用父类类型隐藏子类实例。


上面说的是普通实体类,那如果是接口呢,其实还是有点区别的,比如我有三个接口,每个接口又分别有自己的实现类,大概如下:

//公共接口
public interface IOrderUploadService {
  void uploadDataHandler(List<? extends BaseOrderModel> dataList);
}

@RegisterEntity("bydSupplier")
public interface IBydOrderUploadService extends IOrderUploadService{
}

@RegisterEntity("qrSupplier")
public interface IQrOrderUploadService extends IOrderUploadService{
}

@RegisterEntity("cxSupplier")
public interface ICxOrderUploadService extends IOrderUploadService{
}

//每个子接口的具体实现类
@Slf4j
@Service
public class BydOrderUploadServiceImpl implements IBydOrderUploadService {

    @Resource
    private BydOrderDataService bydOrderDataService;

    @Override
    @SuppressWarnings("unchecked")
    @Transactional(rollbackFor = Exception.class)
    public void uploadDataHandler(List<? extends BaseOrderModel> dataList) {
        bydOrderDataService.saveBatchData((List<BydOrderModel>) dataList);
    }
}

@Service
@Slf4j
public class QrOrderUploadServiceImpl implements IQrOrderUploadService {

    @Resource
    private QrOrderDataService qrOrderDataService;

    @Override
    @SuppressWarnings("unchecked")
    @Transactional(rollbackFor = Exception.class)
    public void uploadDataHandler(List<? extends BaseOrderModel> dataList) {
        qrOrderDataService.saveBatchData((List<QrOrderModel>) dataList);
    }
}

@Slf4j
@Service
public class CxOrderUploadServiceImpl implements ICxOrderUploadService {

    @Resource
    private CxOrderDataService cxOrderDataService;

    @Override
    @SuppressWarnings("unchecked")
    @Transactional(rollbackFor = Exception.class)
    public void uploadDataHandler(List<? extends BaseOrderModel> dataList) {
        cxOrderDataService.saveBatchData((List<CxOrderModel>) dataList);
    }
}

针对以上代码,我最终想要获取的是每个具体实现类,并调用其中的方法。对应的注册工厂类和初始化注入spring容器类的代码如下:

接口工厂类OrderUploadServiceRegistryFactory

public class OrderUploadServiceRegistryFactory {

    private static final Map<String, Supplier<? extends IOrderUploadService>> registry = new ConcurrentHashMap<>();

    public static void register(String identifier, Supplier<? extends IOrderUploadService> supplier) {
        registry.put(identifier, supplier);
        System.out.println("注册了"+supplier.get().getClass().getName());
    }

    public static IOrderUploadService getOrderUploadService(String identifier) {
        Supplier<? extends IOrderUploadService> supplier = registry.get(identifier);
        if (supplier == null) {
            throw new IllegalArgumentException("未知的实体类: " + identifier);
        }
        return supplier.get();
    }

    public static void clear() {
        registry.clear();
    }
}

初始化工具类OrderUploadServiceRegistryInitializer


@Configuration
public class OrderUploadServiceRegistryInitializer {

    private static final String BASE_PACKAGE = "com.usteu.service.orderUpload";

    @Resource
    private ApplicationContext applicationContext;

    @Bean
    public CommandLineRunner initOrderUploadServiceRegistry() {
        return args -> {
            // 创建自定义扫描器以支持接口扫描
            ClassPathScanningCandidateComponentProvider scanner =
                    new ClassPathScanningCandidateComponentProvider(false) {
                        @Override
                        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                            AnnotationMetadata metadata = beanDefinition.getMetadata();
                            // 允许接口和具体类作为候选组件
                            return metadata.isIndependent() &&
                                    (metadata.isConcrete() || metadata.isInterface());
                        }
                    };
            scanner.addIncludeFilter(new AnnotationTypeFilter(RegisterEntity.class));
            scanner.findCandidateComponents(BASE_PACKAGE).forEach(beanDefinition -> {
                try {
                    Class<?> clazz = ClassUtils.forName(Objects.requireNonNull(beanDefinition.getBeanClassName()), getClass().getClassLoader());
                    RegisterEntity annotation = clazz.getAnnotation(RegisterEntity.class);
                    if (annotation == null) return;

                    String identifier = annotation.value();

                    // 增强类型检查
                    if (!IOrderUploadService.class.isAssignableFrom(clazz)) {
                        throw new IllegalStateException(clazz.getName() + " 必须实现 IOrderUploadService 接口");
                    }
                    OrderUploadServiceRegistryFactory.register(identifier, () -> {
                        try {
                            return (IOrderUploadService) applicationContext.getBean(clazz);
                        } catch (Exception e) {
                            throw new RuntimeException("实例化失败: " + clazz.getName(), e);
                        }
                    });

                } catch (ClassNotFoundException e) {
                    throw new RuntimeException("类加载失败: " + beanDefinition.getBeanClassName(), e);
                }
            });
        };
    }
}

这个初始化接口注入工具类实际注入的是每个子接口的实现类了,是从spring容器中获取的,笔者使用了ApplicationContext 这个应用程序上下文接口获取每个子接口的实现类,这个很关键,另外一个就是创建自定义扫描器以支持接口注解扫描这部分代码,和扫描普通实体类的代码还是有区别的,注意甄别。

 // 创建自定义扫描器以支持接口扫描
ClassPathScanningCandidateComponentProvider scanner =
  new ClassPathScanningCandidateComponentProvider(false) {
       @Override
       protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
            AnnotationMetadata metadata = beanDefinition.getMetadata();
            // 允许接口和具体类作为候选组件
            return metadata.isIndependent() &&
                   (metadata.isConcrete() || metadata.isInterface());
  }
};

五、总结

以上就是笔者在面对接口多实现时的一些实际处理方法,这里介绍给大家,方便各位码友参考借鉴,笔者技术能力有限,不足之处还请多多指教!

http://www.dtcms.com/a/112140.html

相关文章:

  • Centos7.9怎样安装Mysql 5.7
  • MySQL数据库如何在线修改表结构及字段类型?
  • FreeRTOS/任务创建和删除的API函数
  • HTML表单属性1
  • 线程同步与互斥(上)
  • 计算机通识
  • NB-IoT单灯控制器:智慧照明的“神经末梢”
  • 蓝桥杯嵌入式第15届真题-个人理解+解析
  • 【系统】换硬盘不换系统,使用WIN PE Ghost镜像给电脑无损扩容换硬盘
  • Python3.13安装教程-2025最新版超级详细图文安装教程(附所需安装包环境)
  • PhotoShop学习04
  • 详解大模型四类漏洞
  • Vue2+Vue3 45-90集学习笔记
  • P12013 [Ynoi April Fool‘s Round 2025] 牢夸 Solution
  • CMAKE中使用外部动态库
  • C++中,应尽可能将引用形参声明为const
  • Smart Link 技术全面解析
  • 使用人工智能大模型腾讯元宝和ttsmp3工具,免费使用文字进行配音
  • Python入门(6):Python序列结构-元组
  • FastAPI-Cache2: 高效Python缓存库
  • Linux系统调用编程
  • 嵌入式开发中栈溢出的处理方法
  • MySQL学习笔记(一)——MySQL下载安装配置
  • 一文全面了解GEO中的知识图谱
  • leetcode数组-长度最小的子数组
  • 【Git】“warning: LF will be replaced by CRLF”的解决办法
  • 【MySQL】DML:添加-修改-删除数据 (数据操作语言) 学习笔记
  • 投影向量的计算公式推导
  • RTOS任务句柄的作用
  • Unity的插件TouchScripts插件的新手入门指南和常用的API使用方法