Spring IOC官方文档学习笔记(九)之基于注解的容器配置,
Spring IOC官方文档学习笔记(九)之基于注解的容器配置,
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的初始化回调和销毁回调,在前面的章节中已经讨论过了,此处略
用户点评