为了让大家更容易接受我的一些观点,上一篇很多笔墨都用在了思路引导上,所以导致文章可能比较臃肿。
这一篇来总结一下,会稍微精简一些,但整体趣味性不如第二篇。
(上一篇说过了,目前介绍的2种注入方式的说法其实不够准确,后面源码分析时再详细介绍)
主要内容:
- 如何把对象交给Spring管理
- 依赖注入
- 自动装配
- <bean>、@Component还是@Bean
- 聊一聊@ComponentScan
如何把对象交给Spring管理
首先明确2个概念:Spring Bean和Java Object。
在Spring官方文档中,Bean指的是交给Spring管理、且在Spring中经历完整生命周期(创建、赋值、各种后置处理)的Java对象。
Object指的是我们自己new的、且没有加入Spring容器的Java对象。
笼统地讲,要把对象交给Spring管理大概有3种方式(其他方式以后补充):
- XML配置:<bean>
- 注解开发:@Component
- 配置类:@Configuration+@Bean
这里要稍微强调以下几点:
首先,XML配置方式必须搭配ClassPathXmlApplicationContext,并把XML配置文件喂给它
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 在xml中描述类与类的配置信息 -->
<bean id="person" class="com.bravo.xml.Person">
<property name="car" ref="car"></property>
</bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
</beans>
public class Test {
public static void main(String[] args) {
// 由于是XML配置方式,对应的Spring容器是ClassPathXmlApplicationContext,传入配置文件告知Spring去哪读取配置信息
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
// 从容器中获取Person
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}
其次,所谓的注解开发不是说只要打上@Component注解,Spring就会把这个注解类解析成BeanDefinition然后实例化放入容器,必须配合注解扫描。
开启扫描的方式有2种:
- <context:component-scan>(XML+注解)
- @ComponentScan(@Configuration配置类+注解)
大家可以把注解开发等同于@Component,只不过这个注解的解析必须开启扫描。所以,在我眼中@Component其实只是半吊子,必须依附于XML或者@Configuration配置类。
最后,狭隘的JavaConfig风格可以等同于@Configuration+@Bean。此时,配置类上面的@ComponentScan并不是必须的。这取决于你是否要另外扫描@Component注解。一旦加了@ComponentScan,其实就是JavaConfig+注解了。
@Configuration //表示这个Java类充当XML配置文件
public class AppConfig {
@Bean
public Person person(){
Person person = new Person();
person.setCar(new Benz());
return person;
}
}
public class Test {
public static void main(String[] args) {
// AnnotationConfigApplicationContext专门搭配@Configuration配置类
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取Person
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}
3种编程风格其实指的是把Bean交给Spring管理的3种方式:
- <bean>
- @Component
- @Configuration+@Bean
至此,我们已经知道如何把Bean交给IOC。接下来,我们聊一聊DI。
依赖注入
虽然注入方式不止两种,但我们还是暂时按照两种方式复习
- setter方法注入
- 构造方法注入
setter方法注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 在xml中描述类与类的配置信息 -->
<bean id="person" class="com.bravo.xml.Person">
<!-- property标签表示,让Spring通过setter方法注入-->
<property name="car" ref="car"></property>
</bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
</beans>
Person
public class Person {
// Person依赖Car
private Car car;
// setter方法
public void setCar(Car car) {
this.car = car;
System.out.println("通过setter方法注入...");
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
Car
public class Car {
}
<bean>中配置<property>,则类中必须提供setter方法。因为<property>等于告诉Spring调用setter方法注入。
构造方法注入
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 在xml中描述类与类的配置信息 -->
<bean id="person" class="com.bravo.xml.Person">
<!-- constructor-arg标签表示,让Spring通过构造方法注入-->
<constructor-arg ref="car"></constructor-arg>
</bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
</beans>
Person
public class Person {
// Person依赖Car
private Car car;
// 有参构造
public Person(Car car){
this.car = car;
System.out.println("通过构造方法注入...");
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
<bean>中配置<constructor-arg>,则类中必须提供对应参数列表的构造方法。因为<constructor-arg>等于告诉Spring调用对应的构造方法注入。
什么叫对应参数列表的构造方法?比如上面配置的
<constructor-arg ref="car"></constructor-arg>
则类中必须提供
public Person(Car benz){
this.car = benz;
}
参数多一个、少一个都不行,Spring只会找这个构造方法,找不到就报错!
自动装配
我们发现上面XML的依赖注入有点累赘。比如
Person
public class Person {
// Person依赖Car
private Car car;
// setter方法
public void setCar(Car car) {
this.car = car;
System.out.println("通过setter方法注入...");
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
其实类结构已经很好地描述了依赖关系:Person定义了Car字段,所以Person依赖Car。
此时在<bean>中再写一遍
<!-- 在xml中描述类与类的配置信息 -->
<bean id="person" class="com.bravo.xml.Person">
<!-- property标签表示,让Spring通过setter方法注入-->
<property name="car" ref="car"></property>
</bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
就属于重复操作了。而且后期如果类结构发生改变,比如加了一个shoes字段,我们不仅要维护类结构本身,还要额外维护<bean>标签中的<property>。
针对这种情况,Spring提出了自动装配。我们分三种编程风格讨论。
1.XML的自动装配
在XML中,自动装配可以设置全局和局部,即:对所有bean起效,还是对单个bean起效
- 全局:default-autowire="byName"
- 局部:autowire="byName"
全局(XML文件中每一个bean都遵守byName模式的自动装配)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
default-autowire="byName">
<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
</beans>
局部(只对当前<bean>有效)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 在xml中只定义bean,无需配置依赖关系 -->
<bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
<bean id="car" class="com.bravo.xml.Car"></bean>
</beans
XML的自动装配,与之前的依赖注入相比,只有XML文件不同:
- 去除之前依赖注入时配置的<property>或<constructor-arg>
- 加上全局或局部的自动装配属性
类结构要求还是和之前一样,该提供setter方法或者构造方法的,不能少。
自动装配共4种模式:
- byName
- byType
- constructor
- no
如果你选择byName或者byType,则需要提供setter方法。
如果你选择constructor,则需要提供给构造方法。
总之,对于XML而言,自动装配的作用是:只需写<bean>,不需要写<bean>里面的其他标签。
2.注解开发的自动装配
@Configuration //表示这个Java类充当XML配置文件
@ComponentScan("com.bravo.annotation")//开启注解扫描
public class AppConfig {
}
Person
@Component
public class Person {
@Qualifier("benz")
@Autowired
private Car car;
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
@Configuration配置类要搭配AnnotationConfigApplicationContext
public class Test {
public static void main(String[] args) {
// AnnotationConfigApplicationContext专门搭配@Configuration配置类
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取Person
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}
@Autowired默认是byType,如果找到多个相同的,会去匹配与当前字段同名的bean。没找到或者找到多个都会报错。
上面演示的是@Autowired作用于成员变量上,其实我们也可以把@Autowired加在构造方法上,它也会自动注入bean。
读完上面这句话两秒后,你意识到自己被骗了,于是反驳我:放你的屁,我从来没在构造方法上加@Autowired,而且即使不加,形参也能注入进来。
是的,确实不加也注入进来了。
在回答这个问题之前,我们先达成共识:不管我们new对象,还是Spring帮我们创建bean,都离不开构造方法。这一点没有异议吧?
当你的类中只有一个默认无参构造方法时,Spring实例化时没得选,只能用无参构造创建bean。但是,如果类中有两个构造方法,比如:
@Component
public class Person {
private Car car;
private Shoes shoes;
public Person(Car benz) {
this.car = benz;
}
public Person(Car benz, Shoes shoes){
this.car = benz;
this.shoes = shoes;
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
", shoes=" + shoes +
'}';
}
}
此时,Spring会报错,因为它无法替你决定到底用哪个构造器创建bean。你要加上@Autowired,明确告诉Spring用哪个构造方法创建bean。
当然,放在setter方法上也可以注入进来。具体细节,会在分析自动装配底层源码时介绍。
3.JavaConfig的自动装配
其实没必要把JavaConfig再单独分出一类,因为它底层其实也是@Component。所以和在@Component里使用@Autowired是一样的。
AppConfig
@Configuration //表示这个Java类充当XML配置文件
@ComponentScan("com.bravo.javaconfig")//用来扫描Benz组件注入
public class AppConfig {
//把benz注入进来,用来设置给person
@Autowired
private Car benz;
@Bean
public Person person(){
Person person = new Person();
person.setCar(benz);
return person;
}
}
Person
public class Person {
private Car car;
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
<bean>、@Component还是@Bean
学习了把对象交给Spring管理的3种方式后,我们产生了疑惑:
<bean>、@Component和@Bean该如何取舍呢?
虽然@Bean和@Component都是注解,看起来是一家人,但其实@Bean和<bean>更接近。它俩的共同点是:
类文件和bean定义分开
什么意思呢?
打个比方:
@Component直接写在源码上,而bean标签和@Bean都是另写一个文件描述bean定义
直接写源码上面,有什么不好吗?
有好有坏。
好处是:相对其他两种方式,@Component非常简洁。
坏处是,如果你想要交给Spring管理的对象是第三方提供的,那么你无法改动它的源码,即无法在上面加@Component。更甚者,人家连源码都没有,只给了你jar包,怎么搞?
网上花里胡哨的对比一大堆,但个人觉得就这个是最重要的。以后遇到不好加@Component的,能想到@Bean或者<bean>就行了。
聊一聊@ComponentScan
我们都知道,@ComponentScan和XML中的<context:component-scan>标签功能相同,都是开启注解扫描,而且可以指定扫描路径。
AppConfig
@Configuration //表示这个Java类充当XML配置文件
@ComponentScan("com.bravo.javaconfig")
public class AppConfig {
}
Person
@Component
public class Person {
@Qualifier("benz")
@Autowired
private Car car;
@Override
public String toString() {
return "Person{" +
"car=" + car +
'}';
}
}
Benz
@Component
public class Benz implements Car {
}
Test
public class Test {
public static void main(String[] args) {
// AnnotationConfigApplicationContext专门搭配@Configuration配置类
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 从容器中获取Person
Person person = (Person) applicationContext.getBean("person");
System.out.println(person);
}
}
目录结构
测试结果
接下来,我们做几个实验,来探讨一下@ComponentScan。
实验一:不写@ComponentScan
这个别试了,直接报错,因为压根没开启扫描,找不到Person。
报错:找不到Person,说明没扫描到
实验二:不指定路径,同包
AppConfig
@Configuration //表示这个Java类充当XML配置文件
@ComponentScan //删除basePackages,不指定路径
public class AppConfig {
}
测试结果
还是能扫描到
实验三:指定路径,不同包
AppConfig
@Configuration //表示这个Java类充当XML配置文件
@ComponentScan("com.bravo.javaconfig")//扫描javaconfig包下的组件
public class AppConfig {
}
把AppConfig类移到annotation包下,和Person等组件不同包:
测试结果
还是扫描到了,身在曹营心在汉,虽然配置类在annotation包下,但是路径指定了javaconfig
实验四:不指定路径,不同包
不试了,扫描不到。
总结
其实,背后的原理是下面这句代码:
// AnnotationConfigApplicationContext专门搭配@Configuration配置类
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
AnnotationConfigApplicationContext吃了AppConfig这个配置类后,会尝试去拿类上面的@ComponentScan注解:
- 有注解(开启扫描)
- 有路径:扫描指定路径
- 没路径:默认扫描当前配置类所在包及其子包下组件
- 没有注解(不开启扫描)
我们回头分析一下四个实验:
- 有注解
- 有路径:扫描指定路径(实验三:指定路径,不同包,但是指定的路径是对的)
- 没路径:默认扫描当前包及其子包下组件(实验二、四,默认扫描配置类所在包)
- 没有注解(不扫描)
- 报错(实验一:不写@ComponentScan)
@ComponentScan在SpringBoot中的应用
用过SpringBoot的朋友都知道,我们必须写一个启动类
@SpringBootApplication
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
而SpringBoot有一个不成文的规定:
所有的组件必须在启动类所在包及其子包下,出了这个范围,组件就无效了。
为什么会有这个规定呢?
我们来看一下启动类上唯一的注解@SpringBootApplication,发现它其实是一个组合注解:
@ComponentScan没有指定basePackages属性,也就是没有指定扫描路径。那么,按照上面的分析,默认扫描当前包及其子包下组件。
这就是上面不成文规定的背后原因。