Spring IOC官方文档学习笔记(九)之基于注解的容器配置
  oTtxkpRl76Le 2023年11月01日 60 0

1.基于注解的配置与基于xml的配置

(1) 在xml配置文件中,使用<context:annotation-config></context:annotation-config>标签即可开启基于注解的配置,如下所示,该标签会隐式的向容器中添加ConfigurationClassPostProcessor,AutowiredAnnotationBeanPostProcessor,CommonAnnotationBeanPostProcessor,PersistenceAnnotationBeanPostProcessor,RequiredAnnotationBeanPostProcessor这5个后置处理器,用于处理注解

<beans ....>

    <!-- 开启基于注解的配置,该标签不常用,常用下面的<context:component-scan />标签 -->
    <context:annotation-config></context:annotation-config>

    <!-- 开启注解扫描,它不仅有着 <context:annotation-config />标签相同的效果,还提供了一个base-package属性用来指定包扫描路径,将路径下所扫描到的bean注入到容器中 -->
    <!-- <context:component-scan base-package="cn.example.spring.boke"></context:component-scan> -->
</beans>

(2) Spring同时支持基于注解的配置与基于xml的配置,可以将两者混合起来使用;注解配置会先于xml配置执行,因此,基于xml配置注入的属性值会覆盖掉基于注解配置注入的属性值,如下所示

//定义一个普通bean
@Component(value = "exampleA")
public class ExampleA {
    
    //通过注解,注入属性值
    @Value("Annotation injected")
    private String str;

    public void setStr(String str) {
        this.str = str;
    }

    public String getStr() {
        return str;
    }
}

<!-- xml配置文件 -->
<beans ....>

    <context:component-scan base-package="cn.example.spring.boke"></context:component-scan>

    <!-- 通过xml,注入属性值,注意:这里bean的id与上面基于注解所提供的bean的id是一致的 -->
    <bean id="exampleA" class="cn.example.spring.boke.ExampleA">
        <property name="str" value="xml inject"></property>
    </bean>
</beans>

//测试,打印结果为 xml inject,证明注解方式会先于xml方式执行
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("boke/from.xml");
System.out.println(((ExampleA) ctx.getBean("exampleA")).getStr());

2.@Required

(1) @Required注解用于setter方法上,表示某个属性值必须被注入,若未注入该属性值,则容器会抛出异常,从Spring 5.1版本开始,该注解已被弃用,Spring目前推荐使用构造函数注入来注入这些非空依赖项,如下所示

//ExampleA有一个非空属性str
public class ExampleA {

    private String str;

    @Required
    public void setStr(String str) {
        this.str = str;
    }
}

<!-- xml配置文件 -->
<beans ....>
    <context:annotation-config></context:annotation-config>

    <bean id="exampleA" class="cn.example.spring.boke.ExampleA">
        <!-- 必须要设置str属性值,如果将下面这条标签注释掉,那么启动时容器会抛出异常 -->
        <property name="str" value="must"></property>
    </bean>
</beans>

3.@Autowired

(1) @Autowired可用于构造函数上,用于注入非空依赖项,如下

//有两个普通的bean: ExampleA和ExampleC
@Component
public class ExampleA { }

@Component
public class ExampleC { }

//例一:此时ExampleB有且仅有一个构造函数,那么对于该构造函数,加不加@Autowired都一样,因为Spring只能通过该构造函数来创建ExampleB对象
@Component
public class ExampleB {

    //@Autowired  //写不写都一样
    public ExampleB(ExampleA exampleA) {
        System.out.println("注入:" + exampleA);
    }
}

//例二:此时ExampleB有多个构造函数,那么我们必须使用@Autowired来注解其中某一个构造函数,以此来指定容器在创建ExampleB对象时使用哪个构造函数
@Component
public class ExampleB {
    
    public ExampleB(ExampleA exampleA) {
        System.out.println("注入:" + exampleA);
    }

    //如果注释掉该@Autowired注解,启动容器时Spring会抛出异常,因为它不知道该选择哪个构造函数
    //由于该构造函数添加了@Autowired注解,因此容器会选择这个构造函数来创建ExampleB对象
    @Autowired
    public ExampleB(ExampleC exampleC) {
        System.out.println("注入:" + exampleC);
    }
}

<!-- xml配置文件 -->
<beans ....>
    <context:component-scan base-package="cn.example.spring.boke"></context:component-scan>
</beans>

(2) 此外,@Autowired还可用于成员变量和成员方法上

@Component
public class ExampleA { }

@Component
public class ExampleC { }

@Component
public class ExampleB {
    
    //成员变量
    @Autowired
    private ExampleA exampleA;

    private ExampleC exampleC;
    
    //普通的成员方法
    @Autowired
    public void inject(ExampleA exampleA) {
        System.out.println("向普通成员方法inject注入了:" + exampleA);
    }
    
    //setter方法
    @Autowired
    public void setExampleC(ExampleC exampleC) {
        System.out.println("向setter方法注入了:" + exampleC);
        this.exampleC = exampleC;
    }
    
    public ExampleA getExampleA() {
        System.out.println("注入成员变量:" + this.exampleA);
        return exampleA;
    }
}

<!-- xml配置文件 -->
<beans ....>
    <context:component-scan base-package="cn.example.spring.boke"></context:component-scan>
</beans>

//启动容器
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("boke/from.xml");
System.out.println(((ExampleB) ctx.getBean("exampleB")).getExampleA());

//结果如下
向普通成员方法inject注入了:cn.example.spring.boke.ExampleA@60dcc9fe
向setter方法注入了:cn.example.spring.boke.ExampleC@16e7dcfd
注入成员变量:cn.example.spring.boke.ExampleA@60dcc9fe

(3) @Autowired注解不会作用于static fields和static methods之上,如下

//在上面的例子中,其他保持不变,修改ExampleB如下
@Component
public class ExampleB {

    @Autowired
    private static ExampleA exampleA;

    public static ExampleA getExampleA() {
        return exampleA;
    }

    @Autowired
    public static void inject(ExampleC exampleC) {
        System.out.println("注入了:" + exampleC);
    }
}

//此时,我们再次启动容器,会发现控制台只会打印一个null,除此之外,还会有两段警告:Autowired annotation is not supported on static fields: private static cn.example.spring.boke.ExampleA cn.example.spring.boke.ExampleB.exampleA 和 
//Autowired annotation is not supported on static methods: public static void cn.example.spring.boke.ExampleB.inject(cn.example.spring.boke.ExampleC)
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("boke/from.xml");
System.out.println(((ExampleB) ctx.getBean("exampleB")).getExampleA());

(4) @Autowired注解还可用于收集同一类型的bean,如下所示

//Example接口有两个实现类:ExampleA和ExampleC
public interface Example { }

@Component
public class ExampleA implements Example { }

@Component
public class ExampleC implements Example { }

//现在,有另一个普通类ExampleB,想要获取到容器中所有Example的实现类,则直接使用@Autowired注解即可,容器会帮助我们收集
@Component
public class ExampleB {

    @Autowired
    private Example[] exampleArray;

    @Autowired
    private Set<Example> exampleSet;
    
    //对于Map集合,key为Bean的名称
    @Autowired
    private Map<String, Example> exampleMap;

    @Override
    public String toString() {
        return "ExampleB{" +
                "exampleArray=" + Arrays.toString(exampleArray) +
                ", exampleSet=" + exampleSet +
                ", exampleMap=" + exampleMap +
                '}';
    }
}

//启动容器,打印ExampleB如下
ExampleB{exampleArray=[cn.example.spring.boke.ExampleA@7219ec67, cn.example.spring.boke.ExampleC@45018215], exampleSet=[cn.example.spring.boke.ExampleA@7219ec67, cn.example.spring.boke.ExampleC@45018215], exampleMap={exampleA=cn.example.spring.boke.ExampleA@7219ec67, exampleC=cn.example.spring.boke.ExampleC@45018215}}

此外,如果我们想要控制bean在有序集合中的顺序,只需实现Ordered接口或使用@Order注解即可,其值越小,bean在有序集合中的排序越靠前

(5) 默认情况下,当容器中不存在对应类型的bean时,@Autowired注解会抛出异常,此时,我们可以指定该注解中的required属性为false,来允许该依赖项可为空

@Component
public class ExampleB {

    //此时,即使容器中没有ExampleA这个bean,也不会抛出异常
    @Autowired(required = false)
    private ExampleA exampleA;
}

//上面的required = false可用@Nullable注解进行替换,同样表示允许该依赖项可为空
//注:上下这两段代码等价
@Component
public class ExampleB {

    @Autowired
    @Nullable
    private ExampleA exampleA;
}

(6) 此外,对于容器所提供的一些特殊依赖项,比如:BeanFactory, ApplicationContext, Environment等,可直接使用@Autowired进行注入而无需实现它们对应的Aware接口

3.@Primary

(1) 在前面的例子中,我们使用@Autowired注解来收集同一类型的bean至集合中,但如果我们只想选择其中的某一个bean来进行依赖项的注入的话,就可以使用@Primary注解,如下

//Example接口有两个实现类:ExampleA和ExampleC
public interface Example { }

@Component
public class ExampleA implements Example { }

@Component
public class ExampleC implements Example { }

@Component
public class ExampleB {
    //此时,由于容器中有两个Example实例,容器不知道该注入哪个给我们的成员变量example,便会抛出异常
    @Autowired
    private Example example;
}

//对于上面出现的选择困难问题,我们便可以使用@Primary来指定优先被选择的bean,如下所示,表明在选择困难时优先选择ExampleA实例,此时容器会将它注入到ExampleB中的成员变量example
@Component
@Primary
public class ExampleA implements Example {

}

4.@Qualifier

(1) @Qualifier的作用与@Primary作用相似,它也是用于指定一个优先被选择对象,如下所示

@Component("exampleA")
public class ExampleA implements Example { }

@Component("exampleC")
public class ExampleC implements Example { }

@Component
public class ExampleB {
    
    //此时,容器中类型为Example的bean有两个,因此我们可以使用@Qualifier注解来指定希望被注入的bean的名称,此处为exampleA,那么容器会将exampleA这个bean注入进来,从而解决了选择困难问题
    @Autowired
    @Qualifier("exampleA")
    private Example example;
}


//对上面这个例子,我们将ExampleC类稍微变更一下,给它加上@Primary注解,其余保持不变
@Component("exampleC")
@Primary
public class ExampleC implements Example { }

//此时,exampleA和exampleC哪个会被选择呢?  答案是exampleA,因为@Qualifier注解的优先级更高

(2) @Qualifier注解也可用于条件过滤

//现在,假设我有3个类:ExampleA,ExampleB和ExampleC,它们都实现了Example接口,都是Example类型
@Component
@Qualifier("inject")
public class ExampleA implements Example { }

@Component
public class ExampleB implements Example { }

@Component
@Qualifier("inject")
public class ExampleC implements Example { }

//现在,我有另一个普通类Main,想将ExampleA和ExampleC收集起来,把ExampleB排除在外,那么便可使用@Qualifier注解进行标记,如上所示,在ExampleA和ExampleC上加了@Qualifier注解
@Component
public class Main {
    //再用@Qualifier注解标记被注入的集合,那么容器就会将相应的bean自动注入进行,此处我们就用了@Qualifier注解进行了过滤
    @Autowired
    @Qualifier("inject")
    private List<Example> exampleList;

    @Override
    public String toString() {
        return "Main{" +
                "exampleList=" + exampleList +
                '}';
    }
}

//启动,获取Main对象并打印,可见容器中的ExampleB对象并未被注入进来
Main{exampleList=[cn.example.spring.boke.ExampleA@58a90037, cn.example.spring.boke.ExampleC@74294adb]} 

(3) @Qualifier还可用来分组,如下例

//自定义注解MyQualifier用于分组,其中分组参数有两个,分别为group和rank,只有这两个参数均相等时,才能算作是一组内的元素
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyQualifier {
    char group();

    int rank();
}

//定义ExampleA属于A1组
@Component
@MyQualifier(group = 'A', rank = 1)
public class ExampleA implements Example { }

//定义ExampleB也属于A1组
@Component
@MyQualifier(group = 'A', rank = 1)
public class ExampleB implements Example { }

//定义ExampleC属于A2组
@Component
@MyQualifier(group = 'A', rank = 2)
public class ExampleC implements Example { }

//Main类,用于收集不同组类的元素
@Component
public class Main {
    
    //收集属于A1组的元素
    @Autowired
    @MyQualifier(group = 'A', rank = 1)
    private List<Example> a1List;

    //收集属于A2组的元素
    @Autowired
    @MyQualifier(group = 'A', rank = 2)
    private List<Example> a2List;

    @Override
    public String toString() {
        return "Main{" +
                "a1List=" + a1List +
                ", a2List=" + a2List +
                '}';
    }
}

//启动容器,获取Main对象并打印,可见不同组的元素都已收集完毕
Main{a1List=[cn.example.spring.boke.ExampleA@130f889, cn.example.spring.boke.ExampleB@1188e820], a2List=[cn.example.spring.boke.ExampleC@641147d0]}

5.泛型可做自动装配限定符

//有一个泛型接口Store
public interface Store<T> { }

//该泛型接口有两个实现类分别为StoreInteger和StoreString,它们都是Store类型
@Component
public class StoreInteger implements Store<Integer> { }

@Component
public class StoreString implements Store<String> { }

//普通类Main
@Component
public class Main {
    //此时,容器中有两个类型为Store的bean,因为我们指定了泛型参数,因此,泛型参数就成了限定符,故StoreInteger被注入给s1,而StoreString被注入给s2,避免出现选择困难问题
    @Autowired
    private Store<Integer> s1;

    @Autowired
    public Store<String> s2;
}


//对上面这个例子,我们把Main类改改,取消成员变量中的泛型参数
@Component
public class Main {
    //此时,由于取消了泛型参数,容器就不知道该往s1或s2中注入哪个bean了,此时启动容器,会发现容器抛出NoUniqueBeanDefinitionException异常
    @Autowired
    private Store s1;

    @Autowired
    public Store s2;
}

6.CustomAutowireConfigurer

(1) 在上面介绍@Qualifier注解时,我们自定义了一个注解@MyQualifier用于分组,它会生效的根本原因是因为我们在该注解上添加了@Qualifier注解,现在,通过CustomAutowireConfigurer(本质是一个BeanFactoryPostProcessor),我们可以不再依赖@Qualifier注解,而是用另一种方法,如下

//自定义注解@MyQualifier,注意这是一个普通的注解,它没有像上面的一样被@Qualifier注解修饰
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyQualifier { }

//使用我们的自定义注解@MyQualifier,用于确定优先被选择的对象,该注解会被注册给CustomAutowireConfigurer以便生效
@Component
@MyQualifier
public class ExampleA implements Example { }

@Component
public class ExampleB implements Example { }

@Component
public class ExampleC {
    @Autowired
    @MyQualifier
    private Example example;
}
<!-- xml配置文件 -->
<beans ....>

    <context:component-scan base-package="cn.example.spring.boke"></context:component-scan>
    
    <!-- 为了使我们的@MyQualifier注解的优先被选择作用生效,我们得将它注册给CustomAutowireConfigurer,如下 -->
    <bean id="customAutowireConfigurer"
          class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
        <property name="customQualifierTypes">
            <set>
                <!-- @MyQualifier注解的全限定路径 -->
                <value>cn.example.spring.boke.MyQualifier</value>
            </set>
        </property>
    </bean>
</beans>

//启动容器,可以发现ExampleA对象被注入给了ExampleC

(2) 容器是如何决定自动装配候选对象的?

  • 看每个bean的autowire-candidate属性值,为false则表示该bean不参与候选

  • 看beans标签上的default-autowire-candidates属性值,为false则表示该beans标签下的所有bean都不参与候选

  • 在CustomAutowireConfigurer上注册的自定义注解和@Qualifier注解所标注的bean会被优先选择

当存在多个候选者时,将根据primary属性值确定: 如果恰好有一个候选者的primary属性为true,那么该候选者会被优先选择

7.@Resource

(1) Spring还支持JSR-250中的注解@Resource用于自动装配,它的功能同@Autowired,但与@Autowired不同的是,它优先按照bean的名称进行匹配选择,如果找不到,再按照类型匹配选择

8.@Value

(1) @Value注解常用来注入外部属性值

//在项目的resources目录下,有一个application.properties文件,其配置内容如下
my.name=aaaaa

//为了读取到这个配置文件内的内容,我们得定义一个配置类(使用@Configuration注解标注),同时使用@PropertySource注解来指定我们配置文件的路径(一般都采用classpath类路径)
@Configuration
@PropertySource("classpath:application.properties")
public class Config {

}

//现在,使用@Value注解就可以读取到配置文件中的配置属性了
@Component
public class ExampleA {
    //使用${}语法,填入配置文件中的某个键值
    @Value("${my.name}")
    private String name;

    public String getName() {
        return name;
    }
}

//启动容器
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("boke/from.xml");
System.out.println(ctx.getBean(ExampleA.class).getName());

//打印结果如下,可见我们配置文件中的属性已被成功读取到了
aaaaa

//此时,清空application.properties文件,让里面什么也没有,再次启动容器,打印如下,可见默认情况下,如果容器无法解析,它就会直接将@Value注解的参数值注入到属性中
${my.name}

在上面这个例子中,name被赋予了值${my.name},这显然不是我们所期望的结果,如果我们期望严格控制不存在的值,可声明一个PropertySourcesPlaceholderConfigurer类型的bean至容器中,此时,如果容器无法解析某个配置项,那么容器将会抛出异常

//修改上面例子中的Config配置类,如下,其他不动
@Configuration
@PropertySource("classpath:application.properties")
public class Config {

    //向容器中注入一个PropertySourcesPlaceholderConfigurer类型的bean
    //注意:此时该@Bean方法必须是static方法
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

//此时再次启动容器,会发现容器抛出IllegalArgumentException异常,提醒我们配置项解析失败

(2) SpringBoot会默认为我们提供PropertySourcesPlaceholderConfigurer,无需我们单独配置

(3) Spring会提供内置的类型转换工具,来解析配置项并将其转换为目标类型,当然我们也可以自己提供转换类(ConversionService),如下所示

//application.properties配置如下
my.name=AaBBc

//自定义转换工具MyConverter,用于将所有的大写字母转成小写,需要实现Converter接口
public class MyConverter implements Converter<String, String> {
    @Override
    public String convert(String s) {
        //大写转小写
        return s.toLowerCase(Locale.CHINA);
    }
}

@Configuration
@PropertySource("classpath:application.properties")
public class Config {
    
    //向容器中添加自定义类型转换工具
    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyConverter());
        return conversionService;
    }
}

//ExampleA类不变
@Component
public class ExampleA {

    @Value("${my.name}")
    private String name;

    public String getName() {
        return name;
    }
}

//启动容器,打印name如下,可见所有的大写字母均已转换成小写
aabbc

(4) @Value注解支持SpEL表达式,用于复杂计算

@Component
public class ExampleA {
    //#{ } 用于SpEL表达式,注意是 `#` 而不是之前的 `$`
    //此时,map={key=100, hh=300}
    @Value("#{{'key': 100, 'hh': 300}}")
    private Map<String, Integer> map;
}

9.@PostConstruct与@PreDestroy

(1) @PostConstruct与@PreDestroy分别用于bean的初始化回调和销毁回调,在前面的章节中已经讨论过了,此处略

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  2Vtxr3XfwhHq   2024年05月17日   54   0   0 Java
  Tnh5bgG19sRf   2024年05月20日   109   0   0 Java
  8s1LUHPryisj   2024年05月17日   46   0   0 Java
  aRSRdgycpgWt   2024年05月17日   47   0   0 Java
oTtxkpRl76Le