Spring原理分析--@Primary注解

1.@Primary在实际项目中的应用

在支付的场景下,通常面临需要支持多个渠道的情况,常常将这些渠道具体的实现放入第三方代理模块中处理,接口请求参数中通常包含渠道字段,去掉其他业务字段及方法,示例如下:

public interface ThirdProxyService {
    default void doSomething(RequestData data) {
        System.out.println("default do something");
    }
}

@Getter
@Setter
public class RequestData {
    // 参数中包含渠道字段
    private String payChnl;

    // 其他字段...
}

@Service
public class GoogleThirdProxyServiceImpl implements ThirdProxyService {
    @Override
    public void doSomething(RequestData requestData) {
        System.out.println("google do something");
    }
}

@Service
public class HuaweiThirdProxyServiceImpl implements ThirdProxyService {
    @Override
    public void doSomething(RequestData requestData) {
        System.out.println("huawei do something");
    }
}

在账单或订单服务中通常需要依赖三方代理类获取具体的渠道数据

@Service
public class BillService {
    @Autowired
    private ThirdProxyService thirdProxyService;

    // do something...
    public void doBill(RequestData request) {
        thirdProxyService.doSomething(request);
    }
}

这种情况下启动会报错,因为容器中有多个ThirdProxyService的实现类,错误信息如下:

Field thirdProxyService in com.limin.study.spring.primary.BillService required a single bean, but 2 were found:
        - googleThirdProxyServiceImpl: defined in file [D:\gitee\SpringStudy\spring-study\target\classes\com\limin\study\spring\primary\GoogleThirdProxyServiceImpl.class]
        - huaweiThirdProxyServiceImpl: defined in file [D:\gitee\SpringStudy\spring-study\target\classes\com\limin\study\spring\primary\HuaweiThirdProxyServiceImpl.class]

但是我们在注入ThirdProxyService并不知道具体要调用哪个ThirdProxyService,我们希望根据渠道字段动态调用对应渠道的实现类,该如何做呢?

1)我们可以使用@Primary定义一个ThirdProxyService的动态代理类,这样BillService依赖注入时会注册此代理类

@Configuration
public class ThirdProxyAutoConfig {
    @Primary
    @Bean
    public ThirdProxyService thirdProxyInvocationHandler(ApplicationContext context){
        return (ThirdProxyService) Proxy.newProxyInstance(ThirdProxyService.class.getClassLoader(),
                                                          ThirdProxyInvocationHandler.class.getInterfaces(), new ThirdProxyInvocationHandler(context));
    }
}

2)ThirdProxyInvocationHandler中定义具体的代理逻辑,这里是根据请求参数中的渠道名称匹配容器中的实现类的渠道名称,找到具体的实现类后,调用具体实现类的方法,例如:

public class ThirdProxyInvocationHandler implements ThirdProxyService, InvocationHandler {
    private ApplicationContext context;
    private Class delegateClass;

    private Map<String, IChnlSupport> serviceCache = new ConcurrentHashMap<>();

    public ThirdProxyInvocationHandler(ApplicationContext context){
        this.context = context;
        delegateClass = ThirdProxyService.class;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (args == null || args.length == 0) {
            return null;
        }
        // 这里从参数中获取渠道名
        String payChnl = getChnl(args);
        if (StringUtils.isEmpty(payChnl)) {
            return null;
        }
        // 获取具体的实现类
        Object service = getService(payChnl);
        // 调用具体实现类的方法
        return method.invoke(service, args);
    }

    protected String getChnl(Object[] args) {
        // 反射获取参数中payChnl字段值
        return (String) ReflectUtil.getFieldValue(args[0], "payChnl");
    }

    protected IChnlSupport getService(String payChnl) {
        // 先从缓存中获取
        IChnlSupport service = serviceCache.get(payChnl);
        if (service == null) {
            // 缓存中没有,从spring容器中获取
            Map<String, IChnlSupport> beanMap = context.getBeansOfType(IChnlSupport.class);
            if (!CollectionUtils.isEmpty(beanMap)) {
                // 从容器中匹配ThirdProxyService的子类,且渠道名称payChnl匹配
                Optional<IChnlSupport> serviceOptional = beanMap.values().stream()
                .filter(thirdServiceImpl -> delegateClass.isAssignableFrom(thirdServiceImpl.getClass()) && payChnl.equals(thirdServiceImpl.getPayChnl()))
                .findAny();
                // 如果找到了就是具体的实现类
                if (serviceOptional.isPresent()) {
                    service = serviceOptional.get();
                    serviceCache.put(payChnl, service);
                }
            }
        }
        if (service == null){
            System.out.println("cannot find service, payChnl = " + payChnl);
        }
        return service;
    }
}

3)定义IChnlSupport接口,ThirdProxyService实现类也实现IChnlSupport接口,原因是需要根据渠道名称匹配具体的实现类

public interface IChnlSupport {
    String getPayChnl();
}

@Service
public class GoogleThirdProxyServiceImpl implements ThirdProxyService, IChnlSupport {
    @Override
    public void doSomething(RequestData requestData) {
        System.out.println("google do something");
    }

    @Override
    public String getPayChnl() {
        return "google";
    }
}

@Service
public class HuaweiThirdProxyServiceImpl implements ThirdProxyService, IChnlSupport {
    @Override
    public void doSomething(RequestData requestData) {
        System.out.println("huawei do something");
    }

    @Override
    public String getPayChnl() {
        return "huawei";
    }
}

这样再次启动服务,就不会报错了,因为@Primary注解确定了BillService中注入的对象是ThirdProxyService的动态代理类

再通过接口调用doSomething方法时,就能根据传入的payChnl找到具体的实现类执行对应渠道的方法

添加控制器类

@RestController
public class BillController {
    @Autowired
    private BillService billService;

    @GetMapping("/doBill")
    public void doBill(@RequestBody RequestData requestData) {
        billService.doBill(requestData);
    }
}

调用http://127.0.0.1:8080/doBill,参数为{"payChnl": "google"}时,打印google do something;参数为{"payChnl": "huawei"}时,打印huawei do something

这样就实现了动态调用不同的实现类的效果

2.@Primary原理

SpringBoot启动时,在refresh方法中会调用invokeBeanFactoryPostProcessors扩展BeanDefinition,其中会调用到ConfigurationClassPostProcessor的processConfigBeanDefinitions,processConfigBeanDefinitions中会根据不同的情况扫描要注册的bean,源码loadBeanDefinitionsForConfigurationClass如下

private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    if (trackedConditionEvaluator.shouldSkip(configClass)) {
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
    }

    if (configClass.isImported()) {
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        // 处理@Bean方法注册
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }

    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

这里我们是通过ThirdProxyAutoConfig配置类注册的,那么会调用loadBeanDefinitionsForBeanMethod,这个方法中会调用AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata)处理相关的注解

当注册ThirdProxyService这个bean添加了@Primary注解,会将这个bean对应的BeanDefinition中的属性primary设置为true,processCommonDefinitionAnnotations源码如下:

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
    AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
    if (lazy != null) {
        abd.setLazyInit(lazy.getBoolean("value"));
    }
    else if (abd.getMetadata() != metadata) {
        lazy = attributesFor(abd.getMetadata(), Lazy.class);
        if (lazy != null) {
            abd.setLazyInit(lazy.getBoolean("value"));
        }
    }

    // 如果方法上有Primary注解,则设置primary属性为true
    if (metadata.isAnnotated(Primary.class.getName())) {
        abd.setPrimary(true);
    }
    AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
    if (dependsOn != null) {
        abd.setDependsOn(dependsOn.getStringArray("value"));
    }

    AnnotationAttributes role = attributesFor(metadata, Role.class);
    if (role != null) {
        abd.setRole(role.getNumber("value").intValue());
    }
    AnnotationAttributes description = attributesFor(metadata, Description.class);
    if (description != null) {
        abd.setDescription(description.getString("value"));
    }
}

在依赖注入时,首先根据类型找到容器所有的候选类,源码见DefaultListableBeanFactory#doResolveDependency

public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
                                  @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    // 省略部分代码...

    // 在容器中根据类型获取候选的class,matchingBeans是bean名称和类对象的集合
    Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
    if (matchingBeans.isEmpty()) {
        if (isRequired(descriptor)) {
            raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
        }
        return null;
    }

    String autowiredBeanName;
    Object instanceCandidate;

    // 如果匹配的bean有多个
    if (matchingBeans.size() > 1) {
        // 确定使用哪个候选的bean
        autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
        if (autowiredBeanName == null) {
            if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
                return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
            }
            else {
                return null;
            }
        }
        // 获取要注入的类型
        instanceCandidate = matchingBeans.get(autowiredBeanName);
    }

    // 省略其他代码...

    // 将instanceCandidate返回
}

determineAutowireCandidate方法中首先调用determinePrimaryCandidate会判断候选的bean中有没有被@Primary修饰的类,找到了就直接返回该类

protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
    Class<?> requiredType = descriptor.getDependencyType();
    // 首先判断有没有primaryCandidate
    String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
    if (primaryCandidate != null) {
        return primaryCandidate;
    }

    // 省略其他代码...
}

determinePrimaryCandidate方法中遍历每个候选bean,调用isPrimary判断有没有primaryBeanName

protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
    String primaryBeanName = null;
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
        String candidateBeanName = entry.getKey();
        Object beanInstance = entry.getValue();
        // 判断该beanDefinition中primary是否为true
        if (isPrimary(candidateBeanName, beanInstance)) {
            if (primaryBeanName != null) {
                boolean candidateLocal = containsBeanDefinition(candidateBeanName);
                boolean primaryLocal = containsBeanDefinition(primaryBeanName);
                if (candidateLocal && primaryLocal) {
                    throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
                                                              "more than one 'primary' bean found among candidates: " + candidates.keySet());
                }
                else if (candidateLocal) {
                    primaryBeanName = candidateBeanName;
                }
            }
            else {
                primaryBeanName = candidateBeanName;
            }
        }
    }
    return primaryBeanName;
}

isPrimary中获取beanName对应的BeanDefinition对象,判断BeanDefinition中的primary是否为true

protected boolean isPrimary(String beanName, Object beanInstance) {
    String transformedBeanName = transformedBeanName(beanName);
    if (containsBeanDefinition(transformedBeanName)) {
        // 判断beanDefinition中primary是否为true
        return getMergedLocalBeanDefinition(transformedBeanName).isPrimary();
    }
    BeanFactory parent = getParentBeanFactory();
    return (parent instanceof DefaultListableBeanFactory &&
            ((DefaultListableBeanFactory) parent).isPrimary(transformedBeanName, beanInstance));
}

至此,我们就知道了为什么在定义的bean上添加@Primary之后,即使有多个候选类Spring也会注入该bean的原因

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【基础】在GCC中编译和链接不是一个命令

在 GCC&#xff08;GNU Compiler Collection&#xff09;中&#xff0c;编译和链接不是一个命令。编译是将源代码转换为目标代码的过程。它主要进行语法检查、词法分析、生成中间代码等操作。链接是将多个目标文件和库文件组合成一个可执行文件的过程。在 GCC 中&#xff0c;通…

Cesium实现加载离线地形数据(nginx发布数据,cesiumLab地形切片数据)

实现效果如图&#xff1a; 详细步骤 1 下载地形数据&#xff08;DEM&#xff09; 下载地址&#xff1a;地理空间数据云 (gscloud.cn) 操作步骤&#xff1a; 注意&#xff1a;第3步可以自主选择DEM的分辨率&#xff0c;然后下载。 下载结果解压后如下图&#xff1a; 2 使用…

C语言 递归

递归指的是在函数的定义中使用函数自身的方法。 举个例子&#xff1a; 从前有座山&#xff0c;山里有座庙&#xff0c;庙里有个老和尚&#xff0c;正在给小和尚讲故事呢&#xff01;故事是什么呢&#xff1f;“从前有座山&#xff0c;山里有座庙&#xff0c;庙里有个老和尚&…

python3高级特性

1. 装饰器 装饰器是 Python 的一种高阶函数&#xff0c;它可以在不修改函数内部代码的情况下&#xff0c;给函数增加额外的功能。 案例&#xff1a;记录函数执行时间的装饰器 import time def timing_decorator(func): def wrapper(*args, **kwargs): start_time time.t…

lua学习笔记18(面相对象之多态)

print("*****************************面相对象多态*******************************") --相同方法不同执行逻辑 object{} object.id1 function object:new()local obj{}self.__indexself setmetatable(obj,self)return obj end function object:subClass(className)…

PLC程序远程上下载

在工业自动化领域&#xff0c;PLC&#xff08;可编程逻辑控制器&#xff09;扮演着至关重要的角色。然而&#xff0c;传统的PLC程序上传与下载方式往往受限于物理距离和现场环境&#xff0c;给工程师们带来了诸多不便。如今&#xff0c;随着远程技术的不断发展&#xff0c;PLC程…

基于XML配置bean(一)

文章目录 1.获取bean的两种方式1.通过id获取bean&#xff08;前面用过&#xff09;2.通过类型获取bean&#xff08;单例时使用&#xff09;1.案例2.代码1.beans.xml2.SpringBeanTest.java3.结果 3.注意事项 2.三种基本依赖注入方式1.通过属性配置bean&#xff08;前面用过&…

Eureka基础介绍和使用

目录 一.理论基础 二.父项目 2.1 新建父项目 2.2 管理依赖 三.子项目 3.1 新建子项目 3.2 注册中心Server依赖和启动类和配置文件 3.3 生产者Client 依赖和启动类和配置文件 3.5 消费者Custmer依赖和配置类、启动类和配置文件 四.心跳 五.公共资源项目 5.1新建实体…

BUG:vue表单验证校验不报错,必填都有信息,就是不能正常往下进行

vue表单验证未报错却出现异常 框架bug场景解决办法 框架 UI&#xff1a;element-UI 前端&#xff1a;vue2 bug场景 正常表单里面&#xff0c;有的信息要求必填或者加了一些限制&#xff0c;作为校验验证&#xff0c;只有走到校验才会执行其他行为&#xff0c;比如调用保存接…

labelimg安装和使用(解决闪退问题)

&#x1f308;个人主页&#xff1a;Rookie Maker &#x1f525; 系列专栏&#xff1a;计算机视觉 &#x1f3c6;&#x1f3c6;关注博主&#xff0c;随时获取更多关于IT的优质内容&#xff01;&#x1f3c6;&#x1f3c6; &#x1f600;欢迎来到我的代码世界~ &#x1f601; 喜…

C++修炼之路之list--C++中的双向循环链表

目录 前言 一&#xff1a;正式之前先回顾数据结构中的双向循环链表 二&#xff1a;list的简介 三&#xff1a;STL中list常用接口函数的介绍及使用 1.构造函数接口 2.list迭代器 范围for 3.数据的修改接口函数 4.list容量操作函数 5.list的迭代器失效 6.演示代码和测…

【网络编程】Web服务器shttpd源码剖析——线程池调度

hello &#xff01;大家好呀&#xff01; 欢迎大家来到我的网络编程系列之web服务器shttpd源码剖析——线程池调度&#xff0c;在这篇文章中&#xff0c;你将会学习到在Linux内核中如何创建一个自己的并发服务器shttpd&#xff0c;并且我会给出源码进行剖析&#xff0c;以及手绘…

allure2教程-3-测试报告定制

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节&#xff0c;我们学习一下pytestallure2生成html测试报告的方法&#xff0c;本小节我们学习一下allure2测试报告的定制。 allure2报告预览 预览网址&#xff1a;https://demo.qameta.io/allure/# allur…

[leetcode] minimum-falling-path-sum

. - 力扣&#xff08;LeetCode&#xff09; 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始&#xff0c;并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多…

归并排序了解吗?手撕一个我看看?

目录 1- 归并排序原理1-1 主要思想1-2 实现步骤 2- 归并排序代码实现(双指针)⭐ 归并排序 ——实现思路 3- ACM模式实现 1- 归并排序原理 1-1 主要思想 归并排序基于分治 将序列中待排序的数数字分为若干组&#xff0c;每个数字分为一组 将若干组两两合并&#xff0c;保证合…

3D模型处理的多进程并行【Python】

今天我们将讨论如何使用 Python 多进程来处理大量3D数据。 我将讲述一些可能在手册中找到的一般信息&#xff0c;并分享我发现的一些小技巧&#xff0c;例如将 tqdm 与多处理 imap 结合使用以及并行处理存档。 NSDT工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生…

【蓝桥杯2025备赛】素数判断:从O(n^2)到O(n)学习之路

素数判断:从O( n 2 n^2 n2)到O(n)学习之路 背景:每一个初学计算机的人肯定避免不了碰到素数&#xff0c;素数是什么&#xff0c;怎么判断&#xff1f; 素数的概念不难理解:素数即质数&#xff0c;指的是在大于1的自然数中&#xff0c;除了1和它本身不再有其他因数的自然数。 …

4.18作业

顺序栈&#xff1a; #include "seq_stack.h" seq_p creat_stack() //从堆区申请顺序栈的空间 {seq_p S(seq_p)malloc(sizeof(seq_stack));if(SNULL){printf("空间申请失败\n");return NULL;}bzero(S->data,sizeof(S->data));S->top-1;return S; …

OpenGL:图元

OpenGL的图元 点 GL_POINTS: 将顶点绘制成单个的点 线 GL_LINES:将顶点用于创建线段,2个点成为一条单独的线段。如果顶点个数是奇数,则忽略最后一个。 顶点:v0, v1, v2, v3, … , vn,线段:v0-v1, v2-v3, v4-v5, … , vn-1 - vn GL_LINE_STRIP:将顶点用于创建线段,…

在Linux系统中,禁止有线以太网使用NTP服务器进行时间校准的几种方法

目录标题 方法 1&#xff1a;修改NTP配置以禁止所有同步方法 2&#xff1a;通过网络配置禁用NTP同步方法 3&#xff1a;禁用NTP服务 在Linux系统中&#xff0c;如果想要禁止有线以太网使用NTP服务器进行时间校准&#xff0c;可以通过以下几种方法之一来实现&#xff1a; 方法 …
最新文章