Spring编程常见错误50例-Spring Bean依赖注入常见错误(下)

2023-09-18 15:39:47

@Value没有注入预期的值

问题

对于@Value可以装配多种类型的数据:

  • 装配对象:
@Value("#{student}")
private Student student;

@Bean
public Student student(){
    Student student = createStudent(1, "xie");
    return student;
}
  • 装配字符串:
@Value("我是字符串")
private String text;
  • 注入系统参数、环境变量或者配置文件中的值:
@Value("${ip}")
private String ip
  • 注入其他Bean属性:
@Value("#{student.name}")  // student是bean的ID
private String name;

但是使用该注解时遇到以下场景会出现问题:在控制器类中引用配置类中的属性时部分值返回错误

username=admin
password=pass
@RestController
@Slf4j
public class ValueTestController {
    @Value("${username}")
    private String username;

    @Value("${password}")
    private String password;

    @RequestMapping(path = "user", method = RequestMethod.GET)
    public String getUser(){
       return username + ","  + ", " + password;  // username返回的是运行这段程序的计算机用户名,password能正确返回
    };

}
原因

从下面代码中可以看到@Value的工作分为三个核心步骤:

// DefaultListableBeanFactory#doResolveDependency
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
                                  @Nullable Set<String> autowiredBeanNames, 
                                  @Nullable TypeConverter typeConverter) throws BeansException {
    // ...
        // ①寻找@Value
        Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
        if (value != null) {
            // ②解析value值
            if (value instanceof String) {
                String strVal = resolveEmbeddedValue((String) value);
                BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
                value = evaluateBeanDefinitionString(strVal, bd);
            }
            // ③转化Value解析的结果到装配的类型
            TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
            try {
                return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
            }
            catch (UnsupportedOperationException ex) {
                ...
            }
        }

    // ...
}
  • 寻找@Value:判断属性字段是否标记为@Value
// QualifierAnnotationAutowireCandidateResolver#findValue
@Nullable
protected Object findValue(Annotation[] annotationsToSearch) {
    if (annotationsToSearch.length > 0) {   // qualifier annotations have to be local
        AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
            // valueAnnotationType即为@Value
            AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
        if (attr != null) {
            return extractValue(attr);
        }
    }
    return null;
}
  • 解析@Value的字符串值:如果字段标记了@Value,则可拿到对应的字符串值,然后就可以根据字符串值去做解析,解析结果可能是字符串,也可能是对象

  • 将解析结果转化为要装配的对象的类型:

分析完对应的步骤后,可以定位到问题原因在解析@Value指定字符串过程中,对${xxx}的查找不局限在application.properties,而是针对多个源,这些源在启动时被有序固定,所以在查找时也是按序查找的。当查找到systemEnvironment时发有个username和配置文件中的重合
在这里插入图片描述

// PropertySourcesPropertyResolver#getProperty
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
        for (PropertySource<?> propertySource : this.propertySources) {
            if (logger.isTraceEnabled()) {
                logger.trace("Searching for key '" + key + "' in PropertySource '" +
                             propertySource.getName() + "'");
            }
            Object value = propertySource.getProperty(key);
            if (value != null) {
                if (resolveNestedPlaceholders && value instanceof String) {
                    value = resolveNestedPlaceholders((String) value);
                }
                logKeyFound(key, propertySource, value);
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Could not find key '" + key + "' in any property source");
    }
    return null;
}
解决方式

避免存在与系统或环境变量有同名的配置

myname=admin
password=pass

错乱的注入集合

问题

假设存在多个学生Bean,需找出来并存储到List里并在控制器类中输出:

// 可以理解为收集方式
@Bean
public Student student1(){
    return createStudent(1, "psj1");
}

@Bean
public Student student2(){
    return createStudent(2, "psj2");
}

private Student createStudent(int id, String name) {
    Student student = new Student();
    student.setId(id);
    student.setName(name);
    return student;
}
private List<Student> students;

public StudentController(List<Student> students){
    this.students = students;
}

@RequestMapping(path = "students", method = RequestMethod.GET)
public String listStudents(){
    return students.toString();
};

此时需要再增加学生Bean,换了一种方式注入集合类型:

// 可以理解为直接装配方式
@Bean
public List<Student> students(){
    Student student3 = createStudent(3, "psj3");
    Student student4 = createStudent(4, "psj4");
    return Arrays.asList(student3, student4);
}

但上述两种方式都存在,只会输出前面两个学生

原因
  • 进行第一种装配方式时(即收集方式),主要分为以下过程:
// DefaultListableBeanFactory#resolveMultipleBeans
@Nullable
private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
                                    @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
    final Class<?> type = descriptor.getDependencyType();

    if (descriptor instanceof StreamDependencyDescriptor) {
        ...
        // 装配stream
        return stream;
    }
    else if (type.isArray()) {
        ...
        // 装配数组
        return result;
    }
    else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
        // 装配集合
        // 获取集合的元素类型
        Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
        if (elementType == null) {
            return null;
        }
        // 根据元素类型查找所有的bean
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
                                                                   new MultiElementDescriptor(descriptor));
        if (matchingBeans.isEmpty()) {
            return null;
        }
        if (autowiredBeanNames != null) {
            autowiredBeanNames.addAll(matchingBeans.keySet());
        }
        // 转化查到的所有bean放置到集合并返回
        TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
        Object result = converter.convertIfNecessary(matchingBeans.values(), type);
        ...
        return result;
    }
    else if (Map.class == type) {
        ...
        // 解析map
        return matchingBeans;
    }
    else {
        return null;
    }
}
  • 进行第二种装配方式时(即直接装配方式),具体过程在DefaultListableBeanFactory#findAutowireCandidates
  • 当同时满足这两种装配方式时,从下面代码中可以看出它们是不能共存的:
// DefaultListableBeanFactory#doResolveDependency
// 采用收集方式
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
    return multipleBeans;
}
// 上述方式不执行才会执行直接装配方式
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
解决方式

对于同一个集合对象的注入不要混合多种注入方式

参考

极客时间-Spring 编程常见错误 50 例

https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class3

更多推荐

从入门到出师,关于学习RPA的建议!

随着人工智能技术的不断发展,RPA(RoboticProcessAutomation)作为一种新型的自动化生产工具,正逐渐成为IT领域的热门话题。越来越多的初学者和职场人士开始关注和学习RPA技术,以提升个人技能和职业竞争力。一、了解RPA基础知识学习RPA之前,需要了解其基础知识。包括什么是RPA,RPA的应用场景,

面试题:RocketMQ 如何保证消息不丢失,如何保证消息不被重复消费?

文章目录1、消息整体处理过程Producer发送消息阶段手段一:提供SYNC的发送消息方式,等待broker处理结果。手段二:发送消息如果失败或者超时,则重新发送。手段三:broker提供多master模式,即使某台broker宕机了,保证消息可以投递到另外一台正常的broker上。Broker处理消息阶段手段四:提供

LeetCode19.删除链表的倒数第N个节点

我先用的第一种方法,先第一次遍历算出有节点数num,然后第二次遍历找到第num-n个节点,删除它的下一个节点,也就是第num-n节点.next=num-n节点.next.next(),然后需要注意的是找到第num-n个节点,指针需要从头节点移动num-n-1次,但是后来一直报空指针异常,我反复的检查,一步一步自己推,死

身份和访问管理解决方案:混合型IAM

对于依赖于本地IT基础结构和传统安全模型的组织,可以更轻松地验证和授权企业网络内的所有内容,包括设备、用户、应用程序和服务器。尝试从公司网络外部获取访问权限的用户使用虚拟专用网络(VPN)和网络访问控制(NAC)进行身份验证。随着云和远程工作的日益普及,新的企业架构正在重新定义边界。数据还存储在公司墙外,用户可以通过公

看期权哪个软件更好用?数据比较全面直观的那种?

在介绍期权看盘软件之前,我们先来了解一下期权交易的发展史。2015年,国内首只期权上市交易,2019年深交所期权上市,期权市场越来越火,期权分仓软件也是横空出世发展至今,下文介绍看期权哪个软件更好用?数据比较全面直观的那种?常用的看盘期权软件有:掌上财富、东方财富、同花顺、通达信、文华财经等。一般来说,多数行情走势软件

合同被篡改,被变更,被调换风险大?君子签电子合同有效化解

纸质合同签署文件类型多,签署量大,人为干预较多,合同被篡改,被变更,被调换风险大,难以防范和避免。请注意,出现以下几个情况,代表你已经遭遇合同“调包计”了!1、合同内容被PS篡改利用PS工具可以轻易将预先商定好的合同内容,包括合同金额、时间、日期、数量、报酬、利率等进行修改,还可以对骑缝印文进行拼接处理,盖印痕迹。内容

计算机网络 实验二 交换机的基本配置

实验二交换机的基本配置实验目的•掌握交换机的配置方式及切换命令;•掌握交换机端口的基本配置;•掌握交换机mac地址的查看与管理方法。实验设备以太网交换机一台服务器一台PC机五台配置电缆、网线若干网络拓扑及IP地址分配给计算机Pc0~Pc4配置IP地址,分别是192.168.1.1、192.168.1.2、192.168

【SpringCloud】微服务技术栈入门2 - Nacos框架与Feign

目录Nacos下载Nacos并运行配置NacosNacos集群Nacos负载均衡Nacos环境隔离Nacos注册细节Nacos更多配置项快速上手自动更新Feign取代RestTemplateFeign自定义配置性能优化Nacos下载Nacos并运行首先下载对应的release包,主要要选择已经打包编译好的nacos-s

MongoDB(二)基础操作 创建、删除等

mongodb有一个特点,如果某个库,库下面没数据(mongodb成集合),该库等于不存在的mongodb只要创建一个库,在库下写入数据,该库才会生成mongoshe[-h=host-p=xxx]创建数据库use数据库名#如果数据库名已经存在,则表示切换到这个数据库,如果没有,则创建,但不是持久化到磁盘,查看有权限查看

Linux的常见指令

目录pwd命令ls指令mkdir指令touch指令cd指令rmdir指令&&rm指令man指令nanocp指令mv指令cat指令more指令less指令head指令tail指令grep指令热键zip/unzip指令tar指令uname–r指令输出重定向图形化界面和命令行操作本质都是对操作系统进行直接或间接的操作pwd命

租用独立服务器有哪些常见的误区?

租用独立服务器有哪些常见的误区?如今,租用独立服务器的市场随着idc行业良好的发展趋势而变得越来越广泛,其最明显的地方在于出现了许多的代理商,而成为代理商的门槛非常低,这样一来就会出现许多问题,导致很多企业在面对层出不穷的代理商做选择时,都会非常困扰,因为租用服务器还存在后续使用的售后、机器的质量保障等等,不泛一些不良

热文推荐