Day604.@Value注入问题&集合类型注入问题 -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day604.@Value注入问题&集合类型注入问题 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

@Value注入问题&集合类型注入问题

讲到Spring的反转注入,必然知道他的强大,当这次今天阿昌总结的两种问题,当@Value和Spring注入集合类型有可能会发生的问题如下:

一、@Value 没有注入预期的值

当使用@Value,大部分的人可能觉得他只会用于在去注入String类型的场景,当其实他也可以去注入对象类型。

对于对象类型的注入,大部分人都会选择去使用@Autowired 或者 @Resource的方式去注入,而不会选择@Value。

举例@Autowired 和 @Value 的区别,前者可以在使用注解的时候不给予参数注明,而后者必须要求以表达式的方式去指定要注入的内容的位置。


@Value注解源码 内容如下:↓

public @interface Value 
   /**
    * The actual value expression &mdash; for example, <code>#systemProperties.myProp</code>.
    */
   String value();

我们一般都会因为 @Value 常用于 String 类型的装配而误以为 @Value 不能用于非内置对象的装配,实际上这是一个常见的误区

@Value("#student")
private Student student;

如上的方式可以在ioc容器中寻找beanid为student的bean实例进行注入


当然,使用 @Value 更多是用来装配 String,而且它支持多种强大的装配方式,典型的方式参考下面的示例:

//注册正常字符串
@Value("我是字符串")
private String text; 

//注入系统参数、环境变量或者配置文件中的值
@Value("$ip")
private String ip

//注入其他Bean属性,其中student为bean的ID,name为其属性
@Value("#student.name")
private String name;

所以上面就会涉及到,他去哪个配置源去取对应的内容进行注入?


当我们在application.properties中配置如下配置,

username=admin
password=pass

再用@Value去取的时候会出现问题!

@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 时,我们会发现 password 正确返回了,但是 username 返回的并不是配置文件中指明的 admin,而是运行这段程序的计算机用户名。

很明显,使用 @Value 装配的值没有完全符合我们的预期。


那为什么呢?
在Spring对应@Value注解的处理的源码如下:


@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
      @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException 
    //省略其他非关键代码
    Class<?> type = descriptor.getDependencyType();
      //寻找@Value
      Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
      if (value != null) 
         if (value instanceof String) 
            //解析Value值
            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 的字符串值

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

    • 拿到数据后,并赋值装配给我们指定的对象

那上面的问题我们就可以知道为什么username的取值会出现问题:刚好系统环境变量(systemEnvironment)中含有同名的配置

所以命名时,我们一定要注意不仅要避免和环境变量冲突,也要注意避免和系统变量等其他变量冲突,这样才能从根本上解决这个问题。


二、集合类型注入错乱问题

1、收集装配

对于Spring本身的特别强大的,他可以用于对于bean对象进行收集装配合集类型的注入,举例如下:↓

@Bean
public Student student1()
    return createStudent(1, "xie");


@Bean
public Student student2()
    return createStudent(2, "fang");


private Student createStudent(int id, String name) 
    Student student = new Student();
    student.setId(id);
    student.setName(name);
    return student;

有了集合类型的自动注入后,我们就可以把零散的学生 Bean 收集起来了,代码示例如下:

@RestController
@Slf4j
public class StudentController 

    private List<Student> students;//他会直接收集ioc容器中的bean,并装配到这个集合中

	//我们这里使用的构造器注入装配,所以不需要使用@Autowired
    public StudentController(List<Student> students)
        this.students = students;
    

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


2、直接装配

当然我们也可以直接使用如下的方式直接进行直接装配的方式:↓

@Bean
public List<Student> students()
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student3, student4);
 

3、当两种方式同时存在的问题

当Spring对于集合类型装配的源码如下:


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;
   

如要的步骤如下:

  • 获取集合类型的元素类型
    • 针对本案例,目标类型定义为 List students,所以元素类型为 Student,获取的具体方法参考代码行:
  • 根据元素类型,找出所有的 Bean
    • 有了上面的元素类型,即可根据元素类型来找出所有的 Bean,关键代码行如下:
  • 将匹配的所有的 Bean 按目标类型进行转化

Spring在装配对于的方式如上面两种方式的选择上的源码如下:

Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) 
   return multipleBeans;

Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);

当resolveMultipleBeans转换解析成功后就直接return了。我们看到Spring的做法是这两种装配集合的方式是不能同存的。

结合本案例,当使用收集装配方式来装配时,能找到任何一个对应的 Bean,则返回,如果一个都没有找到,才会采用直接装配的方式。说到这里,你大概能理解为什么后期以 List 方式直接添加的 Student Bean 都不生效了吧。

那么可知解决方案,就是使用一个方式进行对集合类型方式的注入。

直接装配:↓

@Bean
public List<Student> students()
    Student student1 = createStudent(1, "xie");
    Student student2 = createStudent(2, "fang");
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student1,student2,student3, student4);

收集方式:↓

    @Bean
    public Student student1()
        return createStudent(1, "xie");
    
    @Bean
    public Student student2()
        return createStudent(2, "fang");
    
    @Bean
    public Student student3()
        return createStudent(3, "liu");
    
    @Bean
    public Student student4()
        return createStudent(4, "fu");
    

在对于同一个集合对象的注入上,混合多种注入方式是不可取的,这样除了错乱,别无所得。


那最后,在案例 2 中,我们初次运行程序获取的结果如下:[Student(id=1, name=xie), Student(id=2, name=fang)]那么如何做到让学生 2 优先输出呢?

  1. 在代码层,直接去改变他们两个new出来的顺序
  2. 添加@Order(number)注解,number越小优先级越高,越靠前
  3. @DependsOn 使用它,可使得依赖的Bean如果未被初始化会被优先初始化。

以上是关于Day604.@Value注入问题&集合类型注入问题 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

JAVA零基础小白入门上手教程day15-泛型&File

Python集合&文件操作Day03

Day603.Bean选取问题&找不到问题 -Spring编程常见错误

JAVA零基础小白学习免费教程day13-Collection&数据结构

JAVA零基础小白学习免费教程day13-Collection&数据结构

JAVA零基础小白学习免费教程day14-Set&HashMap