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());
}
};
五、总结
以上就是笔者在面对接口多实现时的一些实际处理方法,这里介绍给大家,方便各位码友参考借鉴,笔者技术能力有限,不足之处还请多多指教!