• Spring基础(3):复习


    为了让大家更容易接受我的一些观点,上一篇很多笔墨都用在了思路引导上,所以导致文章可能比较臃肿。

    这一篇来总结一下,会稍微精简一些,但整体趣味性不如第二篇。

    (上一篇说过了,目前介绍的2种注入方式的说法其实不够准确,后面源码分析时再详细介绍)

    主要内容:

    • 如何把对象交给Spring管理
    • 依赖注入
    • 自动装配
    • 、@Component还是@Bean
    • 聊一聊@ComponentScan

    如何把对象交给Spring管理

    首先明确2个概念:Spring Bean和Java Object。

    在Spring官方文档中,Bean指的是交给Spring管理、且在Spring中经历完整生命周期(创建、赋值、各种后置处理)的Java对象。

    Object指的是我们自己new的、且没有加入Spring容器的Java对象。

    笼统地讲,要把对象交给Spring管理大概有3种方式(其他方式以后补充):

    • XML配置:
    • 注解开发:@Component
    • 配置类:@Configuration+@Bean

    这里要稍微强调以下几点:

    首先,XML配置方式必须搭配ClassPathXmlApplicationContext,并把XML配置文件喂给它

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans
    5. http://www.springframework.org/schema/beans/spring-beans.xsd">
    6. <!-- 在xml中描述类与类的配置信息 -->
    7. <bean id="person" class="com.bravo.xml.Person">
    8. <property name="car" ref="car"></property>
    9. </bean>
    10. <bean id="car" class="com.bravo.xml.Car"></bean>
    11. </beans>

    1. public class Test {
    2. public static void main(String[] args) {
    3. // 由于是XML配置方式,对应的Spring容器是ClassPathXmlApplicationContext,传入配置文件告知Spring去哪读取配置信息
    4. ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
    5. // 从容器中获取Person
    6. Person person = (Person) applicationContext.getBean("person");
    7. System.out.println(person);
    8. }
    9. }

    其次,所谓的注解开发不是说只要打上@Component注解,Spring就会把这个注解类解析成BeanDefinition然后实例化放入容器,必须配合注解扫描。

    开启扫描的方式有2种:

    • (XML+注解)
    • @ComponentScan(@Configuration配置类+注解)

    大家可以把注解开发等同于@Component,只不过这个注解的解析必须开启扫描。所以,在我眼中@Component其实只是半吊子,必须依附于XML或者@Configuration配置类。

    最后,狭隘的JavaConfig风格可以等同于@Configuration+@Bean。此时,配置类上面的@ComponentScan并不是必须的。这取决于你是否要另外扫描@Component注解。一旦加了@ComponentScan,其实就是JavaConfig+注解了。

    1. @Configuration //表示这个Java类充当XML配置文件
    2. public class AppConfig {
    3. @Bean
    4. public Person person(){
    5. Person person = new Person();
    6. person.setCar(new Benz());
    7. return person;
    8. }
    9. }

    1. public class Test {
    2. public static void main(String[] args) {
    3. // AnnotationConfigApplicationContext专门搭配@Configuration配置类
    4. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    5. // 从容器中获取Person
    6. Person person = (Person) applicationContext.getBean("person");
    7. System.out.println(person);
    8. }
    9. }

    3种编程风格其实指的是把Bean交给Spring管理的3种方式:

    • @Component
    • @Configuration+@Bean

    至此,我们已经知道如何把Bean交给IOC。接下来,我们聊一聊DI。


    依赖注入

    虽然注入方式不止两种,但我们还是暂时按照两种方式复习

    • setter方法注入
    • 构造方法注入

    setter方法注入

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans
    5. http://www.springframework.org/schema/beans/spring-beans.xsd">
    6. <!-- 在xml中描述类与类的配置信息 -->
    7. <bean id="person" class="com.bravo.xml.Person">
    8. <!-- property标签表示,让Spring通过setter方法注入-->
    9. <property name="car" ref="car"></property>
    10. </bean>
    11. <bean id="car" class="com.bravo.xml.Car"></bean>
    12. </beans>

    Person

    1. public class Person {
    2. // Person依赖Car
    3. private Car car;
    4. // setter方法
    5. public void setCar(Car car) {
    6. this.car = car;
    7. System.out.println("通过setter方法注入...");
    8. }
    9. @Override
    10. public String toString() {
    11. return "Person{" +
    12. "car=" + car +
    13. '}';
    14. }
    15. }

    Car

    1. public class Car {
    2. }

    中配置,则类中必须提供setter方法。因为等于告诉Spring调用setter方法注入。

    构造方法注入

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans
    5. http://www.springframework.org/schema/beans/spring-beans.xsd">
    6. <!-- 在xml中描述类与类的配置信息 -->
    7. <bean id="person" class="com.bravo.xml.Person">
    8. <!-- constructor-arg标签表示,让Spring通过构造方法注入-->
    9. <constructor-arg ref="car"></constructor-arg>
    10. </bean>
    11. <bean id="car" class="com.bravo.xml.Car"></bean>
    12. </beans>

    Person

    1. public class Person {
    2. // Person依赖Car
    3. private Car car;
    4. // 有参构造
    5. public Person(Car car){
    6. this.car = car;
    7. System.out.println("通过构造方法注入...");
    8. }
    9. @Override
    10. public String toString() {
    11. return "Person{" +
    12. "car=" + car +
    13. '}';
    14. }
    15. }

    中配置,则类中必须提供对应参数列表的构造方法。因为等于告诉Spring调用对应的构造方法注入。

    什么叫对应参数列表的构造方法?比如上面配置的

    <constructor-arg ref="car">constructor-arg>

    则类中必须提供

    1. public Person(Car benz){
    2. this.car = benz;
    3. }

    参数多一个、少一个都不行,Spring只会找这个构造方法,找不到就报错!


    自动装配

    我们发现上面XML的依赖注入有点累赘。比如

    Person

    1. public class Person {
    2. // Person依赖Car
    3. private Car car;
    4. // setter方法
    5. public void setCar(Car car) {
    6. this.car = car;
    7. System.out.println("通过setter方法注入...");
    8. }
    9. @Override
    10. public String toString() {
    11. return "Person{" +
    12. "car=" + car +
    13. '}';
    14. }
    15. }

    其实类结构已经很好地描述了依赖关系:Person定义了Car字段,所以Person依赖Car。

    此时在中再写一遍

    1. <!-- 在xml中描述类与类的配置信息 -->
    2. <bean id="person" class="com.bravo.xml.Person">
    3. <!-- property标签表示,让Spring通过setter方法注入-->
    4. <property name="car" ref="car"></property>
    5. </bean>
    6. <bean id="car" class="com.bravo.xml.Car"></bean>

    就属于重复操作了。而且后期如果类结构发生改变,比如加了一个shoes字段,我们不仅要维护类结构本身,还要额外维护标签中的

    针对这种情况,Spring提出了自动装配。我们分三种编程风格讨论。

    1.XML的自动装配

    在XML中,自动装配可以设置全局和局部,即:对所有bean起效,还是对单个bean起效

    • 全局:default-autowire="byName"
    • 局部:autowire="byName"

    全局(XML文件中每一个bean都遵守byName模式的自动装配)

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans
    5. http://www.springframework.org/schema/beans/spring-beans.xsd"
    6. default-autowire="byName">
    7. <!-- 在xml中只定义bean,无需配置依赖关系 -->
    8. <bean id="person" class="com.bravo.xml.Person"></bean>
    9. <bean id="car" class="com.bravo.xml.Car"></bean>
    10. </beans>

    局部(只对当前有效)

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <beans xmlns="http://www.springframework.org/schema/beans"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://www.springframework.org/schema/beans
    5. http://www.springframework.org/schema/beans/spring-beans.xsd">
    6. <!-- 在xml中只定义bean,无需配置依赖关系 -->
    7. <bean id="person" class="com.bravo.xml.Person" autowire="byName"></bean>
    8. <bean id="car" class="com.bravo.xml.Car"></bean>
    9. </beans

    XML的自动装配,与之前的依赖注入相比,只有XML文件不同:

    • 去除之前依赖注入时配置的
    • 加上全局或局部的自动装配属性

    类结构要求还是和之前一样,该提供setter方法或者构造方法的,不能少。

    自动装配共4种模式:

    • byName
    • byType
    • constructor
    • no

    如果你选择byName或者byType,则需要提供setter方法。

    如果你选择constructor,则需要提供给构造方法。

    总之,对于XML而言,自动装配的作用是:只需写,不需要写里面的其他标签。

    2.注解开发的自动装配

    1. @Configuration //表示这个Java类充当XML配置文件
    2. @ComponentScan("com.bravo.annotation")//开启注解扫描
    3. public class AppConfig {
    4. }

    Person

    1. @Component
    2. public class Person {
    3. @Qualifier("benz")
    4. @Autowired
    5. private Car car;
    6. @Override
    7. public String toString() {
    8. return "Person{" +
    9. "car=" + car +
    10. '}';
    11. }
    12. }

    @Configuration配置类要搭配AnnotationConfigApplicationContext

    1. public class Test {
    2. public static void main(String[] args) {
    3. // AnnotationConfigApplicationContext专门搭配@Configuration配置类
    4. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    5. // 从容器中获取Person
    6. Person person = (Person) applicationContext.getBean("person");
    7. System.out.println(person);
    8. }
    9. }

    @Autowired默认是byType,如果找到多个相同的,会去匹配与当前字段同名的bean。没找到或者找到多个都会报错。

    上面演示的是@Autowired作用于成员变量上,其实我们也可以把@Autowired加在构造方法上,它也会自动注入bean。

    读完上面这句话两秒后,你意识到自己被骗了,于是反驳我:放你的屁,我从来没在构造方法上加@Autowired,而且即使不加,形参也能注入进来。

    是的,确实不加也注入进来了。

    在回答这个问题之前,我们先达成共识:不管我们new对象,还是Spring帮我们创建bean,都离不开构造方法。这一点没有异议吧?

    当你的类中只有一个默认无参构造方法时,Spring实例化时没得选,只能用无参构造创建bean。但是,如果类中有两个构造方法,比如:

    1. @Component
    2. public class Person {
    3. private Car car;
    4. private Shoes shoes;
    5. public Person(Car benz) {
    6. this.car = benz;
    7. }
    8. public Person(Car benz, Shoes shoes){
    9. this.car = benz;
    10. this.shoes = shoes;
    11. }
    12. @Override
    13. public String toString() {
    14. return "Person{" +
    15. "car=" + car +
    16. ", shoes=" + shoes +
    17. '}';
    18. }
    19. }

    此时,Spring会报错,因为它无法替你决定到底用哪个构造器创建bean。你要加上@Autowired,明确告诉Spring用哪个构造方法创建bean。

    当然,放在setter方法上也可以注入进来。具体细节,会在分析自动装配底层源码时介绍。

    3.JavaConfig的自动装配

    其实没必要把JavaConfig再单独分出一类,因为它底层其实也是@Component。所以和在@Component里使用@Autowired是一样的。

    AppConfig

    1. @Configuration //表示这个Java类充当XML配置文件
    2. @ComponentScan("com.bravo.javaconfig")//用来扫描Benz组件注入
    3. public class AppConfig {
    4. //把benz注入进来,用来设置给person
    5. @Autowired
    6. private Car benz;
    7. @Bean
    8. public Person person(){
    9. Person person = new Person();
    10. person.setCar(benz);
    11. return person;
    12. }
    13. }

    Person

    1. public class Person {
    2. private Car car;
    3. public void setCar(Car car) {
    4. this.car = car;
    5. }
    6. @Override
    7. public String toString() {
    8. return "Person{" +
    9. "car=" + car +
    10. '}';
    11. }
    12. }

    、@Component还是@Bean

    学习了把对象交给Spring管理的3种方式后,我们产生了疑惑:

    、@Component和@Bean该如何取舍呢?

    虽然@Bean和@Component都是注解,看起来是一家人,但其实@Bean和更接近。它俩的共同点是:

    类文件和bean定义分开

    什么意思呢?

    打个比方:

    @Component直接写在源码上,而bean标签和@Bean都是另写一个文件描述bean定义

    直接写源码上面,有什么不好吗?

    有好有坏。

    好处是:相对其他两种方式,@Component非常简洁。

    坏处是,如果你想要交给Spring管理的对象是第三方提供的,那么你无法改动它的源码,即无法在上面加@Component。更甚者,人家连源码都没有,只给了你jar包,怎么搞?

    网上花里胡哨的对比一大堆,但个人觉得就这个是最重要的。以后遇到不好加@Component的,能想到@Bean或者就行了。


    聊一聊@ComponentScan

    我们都知道,@ComponentScan和XML中的标签功能相同,都是开启注解扫描,而且可以指定扫描路径。

    AppConfig

    1. @Configuration //表示这个Java类充当XML配置文件
    2. @ComponentScan("com.bravo.javaconfig")
    3. public class AppConfig {
    4. }

    Person

    1. @Component
    2. public class Person {
    3. @Qualifier("benz")
    4. @Autowired
    5. private Car car;
    6. @Override
    7. public String toString() {
    8. return "Person{" +
    9. "car=" + car +
    10. '}';
    11. }
    12. }

    Benz

    1. @Component
    2. public class Benz implements Car {
    3. }

    Test

    1. public class Test {
    2. public static void main(String[] args) {
    3. // AnnotationConfigApplicationContext专门搭配@Configuration配置类
    4. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    5. // 从容器中获取Person
    6. Person person = (Person) applicationContext.getBean("person");
    7. System.out.println(person);
    8. }
    9. }

    目录结构

    测试结果

    接下来,我们做几个实验,来探讨一下@ComponentScan。

    实验一:不写@ComponentScan

    这个别试了,直接报错,因为压根没开启扫描,找不到Person。

    报错:找不到Person,说明没扫描到

    实验二:不指定路径,同包

    AppConfig

    1. @Configuration //表示这个Java类充当XML配置文件
    2. @ComponentScan //删除basePackages,不指定路径
    3. public class AppConfig {
    4. }

    测试结果

    还是能扫描到

    实验三:指定路径,不同包

    AppConfig

    1. @Configuration //表示这个Java类充当XML配置文件
    2. @ComponentScan("com.bravo.javaconfig")//扫描javaconfig包下的组件
    3. public class AppConfig {
    4. }

    把AppConfig类移到annotation包下,和Person等组件不同包:

    测试结果

    还是扫描到了,身在曹营心在汉,虽然配置类在annotation包下,但是路径指定了javaconfig

    实验四:不指定路径,不同包

    不试了,扫描不到。

    总结

    其实,背后的原理是下面这句代码:

    1. // AnnotationConfigApplicationContext专门搭配@Configuration配置类
    2. ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

    AnnotationConfigApplicationContext吃了AppConfig这个配置类后,会尝试去拿类上面的@ComponentScan注解:

    • 有注解(开启扫描)
      • 有路径:扫描指定路径
      • 没路径:默认扫描当前配置类所在包及其子包下组件
    • 没有注解(不开启扫描)

    我们回头分析一下四个实验:

    • 有注解
      • 有路径:扫描指定路径(实验三:指定路径,不同包,但是指定的路径是对的)
      • 没路径:默认扫描当前包及其子包下组件(实验二、四,默认扫描配置类所在包)
    • 没有注解(不扫描)
      • 报错(实验一:不写@ComponentScan)

    @ComponentScan在SpringBoot中的应用

    用过SpringBoot的朋友都知道,我们必须写一个启动类

    1. @SpringBootApplication
    2. public class SpringbootDemoApplication {
    3. public static void main(String[] args) {
    4. SpringApplication.run(SpringbootDemoApplication.class, args);
    5. }
    6. }

    而SpringBoot有一个不成文的规定:

    所有的组件必须在启动类所在包及其子包下,出了这个范围,组件就无效了。

    为什么会有这个规定呢?

    我们来看一下启动类上唯一的注解@SpringBootApplication,发现它其实是一个组合注解:

    @ComponentScan没有指定basePackages属性,也就是没有指定扫描路径。那么,按照上面的分析,默认扫描当前包及其子包下组件。

    这就是上面不成文规定的背后原因。

  • 相关阅读:
    【Java 进阶篇】JDBC DriverManager 详解
    代理IP与Socks5代理的技术奇妙之旅
    基于JavaWeb+SpringBoot+Vue超市管理系统的设计和实现
    什么是Jmeter ?Jmeter使用的原理步骤是什么?
    既能够用ffmpeg命令做RTSP流转RTMP流,又可以像调用avcodec/avfilter库一样逻辑编程
    Linux虚拟化指南:构建虚拟化环境
    vue2源码(二)-- Vue.set和Vue.delete
    html设置背景图大小,自动拉伸背景图
    excel表格怎么换行?单元格内换行的4个方法
    服务器相关概念1
  • 原文地址:https://blog.csdn.net/smart_an/article/details/134280177