【小家Spring】细说Spring IOC容器的自动装配(@Autowired),以及Spring4.0新特性之【泛型依赖注入】的源码级解析(下)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】细说Spring IOC容器的自动装配(@Autowired),以及Spring4.0新特性之【泛型依赖注入】的源码级解析(下)

泛型依赖注入


有了上面的源码解析,详细下面的案例结果,我们是能够猜到的:


// 向容器内注入Bean(此处忽略)
    @Autowired
    private GenericBean<String, Object> objectGenericBean;  // 这样注入报错:说找不到Bean
//
    @Autowired
    private GenericBean objectGenericBean; // 依然报错,但是和上面报错不同,这里报错是找到了2个,匹配不到
// 因此这种情况要特别特别特别的注意:如果字段名不是objectGenericBean,而是objectGeneric,就不会报错了,具体原因参考上文。。。


这里面为了协助理解,附图解释:


image.png


从上图可以看出,如果我们注入的时候不指定的泛型,它就是两个 ,属于通配符。所以能够匹配容器里的同类型的所有的Bean,所以如果筛选不出来only one,那就报错了。(因此,如果容器了只有这一个类型的Bean,那就木有问题,就是它了


接下来再看看这个case:


// 我向容器里只注入一个该类型的Bean,
    @Bean
    public GenericBean objectGeneric() {
        return new GenericBean<Object, Object>("obj1", 2);
    }
// 注入方式一:
    @Autowired
    private GenericBean objectGenericBean; //GenericBean(t=obj1, w=2)  显然,这是最正常不过的注入了
// 注入方式二:
    @Autowired
    private GenericBean<Integer, Integer> integerGenericBean; // GenericBean(t=obj1, w=2) 也没有任何问题,可以正常注入,但需要下面这个情况:
  // 我们可以注入任意泛型标注的(以及不用泛型标注的bean,但是使用时候需要注意)
    @Override
    public Object hello() {
        System.out.println(integerGenericBean);
        //java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
        // 这里不能用Integer直接接收,只能使用Object接受,否则一定报错~
        Integer t = integerGenericBean.getT();
        Integer w = integerGenericBean.getW();
        return "service hello";
    }
// 注入方式三: 看看注入多次,是否是同一个Bean(泛型不同的情况下)
    @Autowired
    private GenericBean<Integer, Integer> integerGenericBean;
    @Autowired
    private GenericBean<String, String> stringGenericBean;
    @Override
    public Object hello() {
        // 这样直接比较会编译报错哦
        //System.out.println(integerGenericBean == stringGenericBean);
        //但是我们这样来看看到底是不是同一个对象
        System.out.println(System.identityHashCode(integerGenericBean)); //72085469
        System.out.println(System.identityHashCode(stringGenericBean)); //72085469
        return "service hello";
    }
因此我们可以大胆的说:注入的就是同一个Bean。哪怕泛型不同,也是同一个对象
毕竟Spring管理的Bean,默认都是单例的


Spring Boot中RedisTemplate<Object, Object>和StringRedisTemplate的注入问题


最近有个小伙伴问我问题,说项目中他们注入RedisTemplate的时候,好像可以随便注入,有的同时注入StringRedisTemplate,有的注入RedisTemplate,有的注入RedisTemplate<String,Object>。。。


相信如果没见到这篇文章之前,很多小伙伴也不能理解,但是有了上面的解释,相信一看就知道啥原因了。看了看他们的配置文件,有自己配置注入的RedisTemplate,且没有给泛型。所以按照上面的例子的说明,我们是可以注入任意泛型的RedisTemplate的,但是使用的时候需要注意~~~~~~~


下面我们来看看,Spring Boot自动为我们注入的情况:


  // 容器中不存在name为redisTemplate的Bean,这个会注入
  @Bean
  @ConditionalOnMissingBean(name = "redisTemplate")
  public RedisTemplate<Object, Object> redisTemplate(
      RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }
  // 当前容易没有StringRedisTemplate 类型的(注意不是RedisTemplate类型)Bean,它就会注入
  // 说明一点:StringRedisTemplate 是RedisTemplate的子类
  @Bean
  @ConditionalOnMissingBean 
  public StringRedisTemplate stringRedisTemplate(
      RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }


若我们自己什么都不配置,那么我们只能这么注入,且只能这两种情况。


    // 这样注入是失败的,因为容器中并没有该类型的(泛型类型)的Bean
    //@Autowired
    //private RedisTemplate<Integer, Integer> integerRedisTemplate;
    @Autowired
    private RedisTemplate<Object, Object> objectRedisTemplate;
    @Autowired
    private RedisTemplate<String, String> stringRedisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate2;
    @Test
    public void fun1() {
        System.out.println(objectRedisTemplate); //org.springframework.data.redis.core.RedisTemplate@3b27b497
        // 下面两个Bean,显然其实是同一个Bean,都是SpringBoot为我们配置的StringRedisTemplate
        System.out.println(stringRedisTemplate); //org.springframework.data.redis.core.StringRedisTemplate@b1534d3
        System.out.println(stringRedisTemplate2); //org.springframework.data.redis.core.StringRedisTemplate@b1534d3
    }
//从这里面我们可以看出,Spring IOC容器里是只有两个RedisTemplate的=====


如果我们自己手动注入一个不带泛型的Bean呢?


// 我们自己注入一个Bean  RedisTemplate
    //备注:这里bean名称不要叫为redisTemplate,让SpringBoot也能注入(为了测试)
    //生产环境:建议名称就叫redisTemplate,因为我们既然自定义了,就没必要再多余注入一个了
    @Bean
    public RedisTemplate myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
// 单元测试一番:
    @Autowired
    private ApplicationContext applicationContext;
    @Test
    public void fun1() {
        applicationContext.getBeansOfType(RedisTemplate.class)
                .forEach((k, v) -> System.out.println(k + "-->" + v));
        //备注:若我们注册Bean名称为redisTemplate,那就只会有两个的
        //myRedisTemplate-->org.springframework.data.redis.core.RedisTemplate@6a818392
        //redisTemplate-->org.springframework.data.redis.core.RedisTemplate@489091bd
        //stringRedisTemplate-->org.springframework.data.redis.core.StringRedisTemplate@512d6e60   
    }

@Autowired和@Resource的区别


直观的错误理解:


@Autowired根据类型进行注入,若没有找到该类型Bean会报错

@Autowired根据类型进行注入, 若出现多个类型的Bean,会报错

@Resource根据名称进入注入


解答这些误解(给出正确答案):

1.@Autowired根据类型进行注入这话没毛病,但是若没有找到该类型的Bean,若设置了属性required=false也是不会报错的


2.@Autowired注入若找到多个类型的Bean,也不会报错,比如下面三种情况,都不会报错~

// 向容器中注入两个相同类型的Bean,并且都不使用@Primary标注
@Configuration
public class RootConfig {
    @Bean
    public Parent parentOne() {
        return new Parent();
    }
    @Bean
    public Parent parentTwo() {
        return new Parent();
    }
}
注入方式如下:
    @Autowired
    private Parent parent;
// 若什么都不处理,会异常:NoUniqueBeanDefinitionException: No qualifying bean of type 'com.fsx.bean.Parent' available: expected single matching bean but found 2: parentOne,parentTwo
// 方案一:向容器注入Bean的时候加上@Primary注解(略)
// 方案二:使用@Qualifier(略)
// 方案三:使得字段名,和容器里的Bean名称一致,比如改成下面字段名,就不会报错了
    @Autowired
    private Parent parentOne;
    @Autowired
    private Parent parentTwo;


关于方案三:因为大多数小伙伴都认为@Autowired注解找到两个Bean就直接报错了,是不被允许的。没想到最后它还会根据字段名进行一次过滤,完全找不到再报错。

因为我使用的Spring版本为:5.0.6.RELEASE 因此有可能是版本原因Spring做了这步处理,这里我也不再去测试从哪个版本开始支持的了,若有小伙伴清楚了,欢迎留言告知,万分感激~


需要说明的是,它和@Qualifier的区别:他们的生效阶段不一样。

@Qualifier:它在寻早同类型的Bean的时候就生效了,在方法findAutowireCandidates这里去寻找候选的Bean的时候就生效了,只会找出一个(或者0个出来)

@Autowired自带的根据字段名匹配:发生在若找出多个同类型Bean的情况下,会根据此字段名称determine一个匹配上的出来


@Resource·装配顺序解释:


  • 如果既没有指定name,又没有指定type,则自动先按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配
  • 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
  • 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
  • 如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常
  • 需要注意的是:@Resource并不支持@Primary

@Resource并不支持@Primary这句话此处做出说明:在Spring4.2之前它是不支持的,但是在4.2之后,@Resource已经全面支持了@Primary以及提供了对@Lazy的支持。


注意:它对@Lazy支持并不是ContextAnnotationAutowireCandidateResolver来处理的,全部由CommonAnnotationBeanPostProcessor这个类里面的方法处理


它的核心处理办法是ResourceElement.inject,最终调用ResourceElement.getResourceToInject方法:

  private class ResourceElement extends LookupElement {
    @Override
    protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) {
      return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName));
    }
  }


public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {
  ...
  protected Object buildLazyResourceProxy(final LookupElement element, final @Nullable String requestingBeanName) {
    TargetSource ts = new TargetSource() {
      @Override
      public Class<?> getTargetClass() {
        return element.lookupType;
      }
      @Override
      public boolean isStatic() {
        return false;
      }
      @Override
      public Object getTarget() {
        return getResource(element, requestingBeanName);
      }
      @Override
      public void releaseTarget(Object target) {
      }
    };
    ProxyFactory pf = new ProxyFactory();
    pf.setTargetSource(ts);
    if (element.lookupType.isInterface()) {
      pf.addInterface(element.lookupType);
    }
    ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory ?
        ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader() : null);
    return pf.getProxy(classLoader);
  }
  protected Object getResource(LookupElement element, @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException {
    ...
    return autowireResource(this.resourceFactory, element, requestingBeanName);
  }
  protected Object autowireResource(BeanFactory factory, LookupElement element, @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException {
      ...
      resource = beanFactory.resolveDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
      ...
  }
  ...
}


它最终也是依赖于beanFactory.resolveDependency()去处理,所以对@Primary也提供了支持~~~


在Spring生态下开发,强烈建议全部使用Spring技术栈的注解,而不推荐使用JSR规范或者JavaEE提供的注解(毕竟Spring对自己亲儿子的支持是最佳的)


JSR330提供的注解@Inject和@Named和@Autowired和@Qualifire一模一样,他们可以混用(唯一区别是:只有Spring的亲儿子@Autowired只是required = false)

@Autowired等三个注解都可议注入List、Map等等。但是要求必须至少有一个,否则请写@Autowired(required = false),显然此时如果你用的@Resource或者@Inject就只能眼睁睁看着它报错了~所以推荐大家使用Spring的亲儿子:@Autowired吧~~~ 并且大都情况下推荐构造函数注入


泛型依赖注入的另一优点实例(Base基类设计)


泛型依赖注入在书写Base基类的时候,有非常大的用处,可以省略不少的代码,更好的规划和设计代码。

我在书写公司Base类的时候,很好的使用到了这一优点。


下面简单贴出代码如下:(不做过多解释了)

// 我定义的BaseDao接口如下:
public interface IBaseDBDao<T extends BaseDBEntity<T, PK>, PK extends Number>{ ... }
// 定义的BaseService如下:
public interface IBaseDBService<T extends BaseDBEntity<T, PK>, PK extends Number> { ... }
// 因为service层,所以我肯定更希望的是提供一些基础的、共用的实现,否则抽取的意义就不大了,因此此处就用到了泛型依赖注入:
//BaseServiceImpl基础共用实现如下:
public abstract class BaseDBServiceImpl<T extends BaseDBEntity<T, PK>, PK extends Number> implements IBaseDBService<T, PK> {
  // 这里就是泛型依赖注入的核心,子类无需再关心dao层的注入,由基类完成dao的注入即可,非常的自动化,切方便管理
  // 这里子类继承,对对应的注入到具体类型的Dao接口代理类,而不用子类关心
  // 如果这是在Spring4之前,我之前做就得提供一个abstract方法给子类,让子类帮我注入给我,我才能书写公用逻辑。
  //然后这一把泛型依赖注入,大大方便了继承者的使用
  // 可以说完全做到了无侵入、赋能的方式加强子类
    @Autowired
    private IBaseDBDao<T, PK> baseDao;
}


冷知识:使用@Value进行依赖注入


其实上面已经提到了,AutowiredAnnotationBeanPostProcessor不仅处理@Autowired也处理@Value,所以向这么写,使用@Value注解也是能够实现依赖注入的:

@Configuration
public class RootConfig {
    @Bean
    public Person person() {
        return new Person();
    }
  // 这样就能够实现依赖注入了~~~
    @Value("#{person}")
    private Person person;
}


细节:

1、只能是#{person}而不能是${person}

2、person表示beanName,因此请保证此Bean必须存在。比如若写成这样@Value("#{person2}")就报错:


Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'person2' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
  at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)
  at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)


可以看出,报的是Spel的解析时候找不到Bean的错~


@Value结合el表达式也有这样的能力(原理是StandardBeanExpressionResolver和StandardEvaluationContext),只是一般我们不这么干。


@Value(#{})与@Value(${})的区别


    1.@Value(#{}): SpEL表达式

    @Value("#{}") 表示SpEl表达式通常用来获取bean的属性,或者调用bean的某个方法。当然还有可以表示常量


   2.@Value(${}):获取配置文件中的属性值


它俩可以结合使用:比如:@Value("#{'${spring.redis.cluster.nodes}'.split(',')}")是一个结合使用的案例~ 这样就可以把如下配置解析成List了

spring.redis.cluster.nodes=10.102.144.94:7535,10.102.144.94:7536,10.102.144.95:7535,10.102.144.95:7536,10.102.148.153:7535,10.102.148.153:7536
相关文章
|
1月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
1月前
|
XML Java 数据格式
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
这篇文章是Spring5框架的入门教程,详细讲解了IOC容器中Bean的自动装配机制,包括手动装配、`byName`和`byType`两种自动装配方式,并通过XML配置文件和Java代码示例展示了如何在Spring中实现自动装配。
Spring5入门到实战------6、IOC容器-Bean管理XML方式(自动装配)
|
1月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
1月前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
|
18天前
|
设计模式 测试技术 PHP
深入解析 Laravel 中的依赖注入
【8月更文挑战第31天】
7 0
|
18天前
|
开发者 Java
Play Framework深度解析:依赖注入的神秘力量,如何助力Web应用架构优化?答案即将揭晓!
【8月更文挑战第31天】依赖注入(DI)是现代软件开发的关键技术,用于分离对象创建与依赖关系,提升代码的可维护性和可测试性。Play Framework是一款高性能Java Web框架,内置了基于Google Guice的DI支持。本文探讨Play Framework中DI的最佳实践,包括定义组件、构造函数注入、字段注入以及作用域控制和自定义绑定等高级特性,帮助开发者轻松构建结构清晰、可维护性高的Web应用。
29 0
|
29天前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
2月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
2月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
96 0
|
21天前
|
缓存 Java 数据库连接
Spring Boot 资源文件属性配置,紧跟技术热点,为你的应用注入灵动活力!
【8月更文挑战第29天】在Spring Boot开发中,资源文件属性配置至关重要,它让开发者能灵活定制应用行为而不改动代码,极大提升了可维护性和扩展性。Spring Boot支持多种配置文件类型,如`application.properties`和`application.yml`,分别位于项目的resources目录下。`.properties`文件采用键值对形式,而`yml`文件则具有更清晰的层次结构,适合复杂配置。此外,Spring Boot还支持占位符引用和其他外部来源的属性值,便于不同环境下覆盖默认配置。通过合理配置,应用能快速适应各种环境与需求变化。
27 0

推荐镜像

更多