Spring基础(3):复习

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

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

(上一篇说过了,目前介绍的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属性,也就是没有指定扫描路径。那么,按照上面的分析,默认扫描当前包及其子包下组件。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值