【从0开始学Spring——Spring基础-IOC】

简介

Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。Spring 官网:https://spring.io/

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring 官网列出的 Spring 的 6 个特征:

  • 核心技术 :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试 :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。
  • 数据访问 :事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持 : Spring MVC和Spring WebFlux Web框架。
  • 集成 :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言 :Kotlin,Groovy,动态语言。

基础组件

在这里插入图片描述

  • Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。
  • Spring Aspects : 该模块为与AspectJ的集成提供支持。
  • Spring AOP :提供了面向切面的编程实现。
  • Spring JDBC : Java数据库连接。
  • Spring JMS :Java消息服务。
  • Spring ORM : 用于支持Hibernate等ORM工具。
  • Spring Web : 为创建Web应用程序提供支持。
  • Spring Test : 提供了对 JUnit 和 TestNG 测试的支持。

组件详情

IOC

IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。 IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。

Spring IoC的初始化过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQw6IHY2-1639742651263)(https://secure1.wostatic.cn/static/h1TUQeBJHQKwh3W314gnmF/image.png)]

IOC-注解驱动与组件扫描

注解驱动

在 xml 驱动的 IOC 容器中,咱使用的是 ClassPathXmlApplicationContext ,它对应的是类路径下的 xml 驱动。

对于注解配置的驱动,那自然可以试着猜一下,应该是 Annotation 开头的,ApplicationContext 结尾。那就是下面咱介绍的 AnnotationConfigApplicationContext

注解驱动需要的是配置类。一个配置类就可以类似的理解为一个 xml 。配置类没有特殊的限制,只需要在类上标注一个 @Configuration 注解即可。

在 xml 中,咱声明 Bean 是通过 <bean> 标签。

// @Configuration注解 == applicationContext.xml
@Configuration
public class QuickstartConfiguration {
  // @Bean注解 == <bean>标签
  @Bean("name")
  public Person person() {
      return new Person();
  }
}
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
        
  <bean id="person" class="com.bean.Person"/>
</beans>


@Configuration等于xml
QuickstartConfiguration .class等于applicationContext.xml
@Bean等于
Person等于class=“com.bean.Person”
“name”等于id=“person”

注意:如果指定了bean的Name则为其指定的,如果没有指定则默认@Bean注解下方法名的首字母小写


组件注册与组件扫描

注解注册注解:@Component,即代表该类会被注册到 IOC 容器中作为一个 Bean 。

相当于xml中:

@Component
// 如果不指定组件名称则默认为person
public class Person {}
<bean class="com.bean.Person"/>

组件扫描

在配置类上额外标注一个 @ComponentScan ,并指定要扫描的路径,它就可以扫描指定路径包及子包下的所有 @Component 组件

@Configuration
@ComponentScan("com.bean")
public class ComponentScanConfiguration {
    
}

如果不指定扫描路径,则默认扫描本类所在包及子包下的所有 @Component 组件

springboot服务建议写在主启动类上面。

传统SSM服务可以写在xml上。

<context:component-scan base-package="com"/>

拓展:@Configuration注解内部也引用了@Component注解,所以它也会被加载到IOC容器,作为一个Bean。这就是与xml文件不同的区别。

xml驱动与注解驱动互通

目的是实现一方引用另一方。

xml引入注解

在 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/context 
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启注解配置 -->
    <context:annotation-config />
    <bean class="com.linkedbear.spring.annotation.d_importxml.config.AnnotationConfigConfiguration"/>
</beans>

注解引入xml

在注解配置中引入 xml ,需要在配置类上标注 @ImportResource 注解,并声明配置文件的路径:

@Configuration
@ImportResource("classpath:annotation/beans.xml")
public class ImportXmlAnnotationConfiguration {
    
}

依赖查找

用户根据name OR type在IOC容器中获取组装好的Bean

先看一个对象交给IOC容器管理

public class MysqlDAOImpl implements DemoDAO {}
<bean id="mysqlDao" class="dao.Impl.MysqlDAOImpl"/>

上述代码表示将该对象交给IOC容器管理,成为了一个bean对象。

名称解释
idbean的名称
classbean的类型

byName

根据bean的id取查找bean。

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MysqlDAOImpl mysqlDao = (MysqlDAOImpl) context.getBean("mysqlDao");
System.out.println(mysqlDao);

byType

根据bean的class去查找bean

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MysqlDAOImpl mysqlDao = (MysqlDAOImpl) context.getBean(MysqlDAOImpl.class);
System.out.println(mysqlDao)

ofType

如果一个接口有多个实现,而咱又想一次性把这些都拿出来,那 getBean 方法显然就不够用了,需要使用额外的方式。

而ofType可以实现传入一个接口 / 抽象类,返回容器中所有的实现类 / 子类。

<bean id="mysqlDao" class="dao.Impl.MysqlDAOImpl"/>
<bean id="oracleDao" class="dao.Impl.OracleDAOImpl"/>
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Map<String, DemoDAO> ofTypes = context.getBeansOfType(DemoDAO.class);
        ofTypes.forEach((beanName,bean) -> {
            System.out.println(beanName + " : " + bean.toString());
        });
    }
}

withAnnotation注解查找

IOC 容器除了可以根据一个父类 / 接口来找实现类,还可以根据类上标注的注解来查找对应的 Bean 。下面咱来测试包含注解的 Bean 如何被查找。

声明一个注解:@Color

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Color {

}

创建三个bean对象:Red,Blue,Dog。给Red,Blue添加上@Color注解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZqDKcSl-1639742651264)(https://secure1.wostatic.cn/static/uXW1U2iD1x2jWp4RJyXTBV/image.png)]

测试:

public class MainTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Map<String, Object> beans = context.getBeansWithAnnotation(Color.class);
        beans.forEach((beanName,bean) -> {
            System.out.println("beanName:" + beanName + ", bean:" + bean);
        });
    }
}

获取IOC容器中的所有Bean

要用到 ApplicationContext 的另一个方法了:getBeanDefinitionNames

这个方法是通过id去查找bean的,而并非是name。

测试:

public class MainTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Map<String, Object> beans = context.getBeansWithAnnotation(Color.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
}

延时查找

实现缺省策略:通过applicationContext去查询所获取的bean是否存在,存在则获取,不存在做后续操作,比如创建。

用途

我想获取一个 Bean 的时候,你可以先不给我报错,先给我一个包装让我拿着,回头我自己用的时候再拆开决定里面有还是没有,这样是不是就省去了 IOC 容器报错的麻烦事了呢?

实现

在 SpringFramework 4.3 中引入了一个新的 API :ObjectProvider ,它可以实现延迟查找。

ObjectProvider 中还有一个方法:getIfAvailable ,它可以在找不到 Bean 时返回 null 而不抛出异常。使用这个方法,就可以避免上面的问题了。

public class LazyLookupApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Cat cat = ctx.getBean(Cat.class);
        System.out.println(cat);
        // 下面的代码会报Bean没有定义 NoSuchBeanDefinitionException
        // Dog dog = ctx.getBean(Dog.class);
    
        // 这一行代码不会报错
        ObjectProvider<Dog> dogProvider = ctx.getBeanProvider(Dog.class);
        Dog dog = dogProvider.getIfAvailable();
        if (dog == null) {
            dog = new Dog();
        }
    }
}

依赖注入

创建的 Bean 都是不带属性的!如果我要创建的 Bean 需要一些预设的属性,那该怎么办呢?那就涉及到 IOC 的另外一种实现了,就是依赖注入
还是延续 IOC 的思想,如果你需要属性依赖,不要自己去找,交给 IOC 容器,让它帮你找,并给你赋上值。

简单属性注入

public class Person {
    private String name;
    private Integer age;
    // getter and setter ......
}
<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>

通过依赖查找获取Person实例查询到的结果:

Person{name='test-person-byset', age=18}

Setter注入

@Bean
public Person person() {
    Person person = new Person();
    person.setName("test-person-anno-byset");
    person.setAge(18);
    return person;
}
<bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
    <property name="name" value="test-person-byset"/>
    <property name="age" value="18"/>
</bean>

构造注入

@Bean
public Person person() {
    return new Person("test-person-anno-byconstructor", 18);
}
<bean id="person" class="com.linkedbear.spring.basic_di.b_constructor.bean.Person">
    <constructor-arg index="0" value="test-person-byconstructor"/>
    <constructor-arg index="1" value="18"/>
</bean>

注解式属性注入

@Component下属性注入

@Component
public class Black {
    @Value("black-value-anno")
    private String name;
    
    @Value("0")
    private Integer order;
}

外部配置文件引入-@PropertySource

resources目录下创建red.properties文件

**(一)**
red.name=red-value-byproperties
red.order=1
**(三)   ** 
    @Value("${red.name}")
    private String name;
    
    @Value("${red.order}")
    private Integer order;
**(二)**
@Configuration
// 顺便加上包扫描
@ComponentScan("com.bean")
@PropertySource("classpath:red.properties")
public class InjectValueConfiguration {
    
}

SpEL表达式

SpEL 全称 Spring Expression Language ,它从 SpringFramework 3.0 开始被支持,它本身可以算 SpringFramework 的组成部分,但又可以被独立使用。它可以支持调用属性值、属性参数以及方法调用、数组存储、逻辑计算等功能。

SpEL 的语法统一用 #{} 表示,花括号内部编写表达式语言。

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IuJpOz5L-1639742651267)(https://secure1.wostatic.cn/static/mT2Vsoh3WXE97zTNSzusC2/image.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qe0JxCQP-1639742651269)(https://secure1.wostatic.cn/static/nWvzrUT4YvvwQVkDAgZjnu/image.png)]

自动注入@Autowired

在 Bean 中直接在 属性 / setter 方法 上标注 @Autowired 注解,IOC 容器会按照属性对应的类型,从容器中找对应类型的 Bean 赋值到对应的属性上,实现自动注入。

使用该注解的前提是注入的属性必须是已经在IOC容器中所管理,否则在程序运行时会找不到该Bean,启动失败。如果想不让程序抛异常可以给@Autowired 注解上加一个属性:required = false

    @Autowired(required = false)
    private Person person;

@Qualifier:指定Bean注入的名称

我们IOC容器里出现两个Person的对象的时候,再注入时控制台会报错:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.linkedbear.spring.basic_di.d_complexfield.bean.Person' available: expected single matching bean but found 2: administrator,master

IOC 容器发现有两个类型相同的 Person ,它也不知道注入哪一个了,索性直接 “我选择死亡” ,就挂了。

解决方式:

@Qualifier 注解的使用目标是要注入的 Bean ,它配合 @Autowired 使用,可以显式的指定要注入哪一个 Bean :

    @Autowired
    @Qualifier("administrator")
    private Person person;

同时我们也可能得出结论,@Autowired这个注解是根据Bean的类型去IOC容器中查找的。

JSR250-@Resource

@Resource 也是用来属性注入的注解,它与 @Autowired 的不同之处在于:@Autowired**** 是按照类型注入,@Resource 是直接按照属性名 / Bean的名称注入

JSR330-@Inject

JSR330 也提出了跟 @Autowired 一样的策略,它也是按照类型注入。不过想要用 JSR330 的规范,需要额外导入一个依赖:

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

剩下的使用方式就跟 SpringFramework 原生的 @Autowired + @Qualifier 一样了:

@Component
public class Cat {
    
    @Inject // 等同于@Autowired
    @Named("admin") // 等同于@Qualifier
    private Person master;
}

回调注入

。。。

延时注入

setter的延迟注入

@Component
public class Dog {
    
    private Person person;
    
    @Autowired
    public void setPerson(ObjectProvider<Person> person) {
        // 有Bean才取出,注入
        this.person = person.getIfAvailable();
    }

构造器的延迟注入

@Component
public class Dog {
    
    private Person person;
    
    @Autowired
    public Dog(ObjectProvider<Person> person) {
        // 如果没有Bean,则采用缺省策略创建
        this.person = person.getIfAvailable(Person::new);
    }

属性字段的延迟注入

    @Autowired
    private ObjectProvider<Person> person;
    
    @Override
    public String toString() {
        // 每用一次都要getIfAvailable一次
        return "Dog{" + "person=" + person.getIfAvailable(Person::new) + '}';
    }
    
    

Bean作用域

为什么会有作用域的概念:
说白了就是资源有限,如果一个资源同时被多个地方访问(如全局常量),那就可以把作用域提的很高;反之,如果一个资源伴随着一个时效性强的、带强状态的动作,那这个作用域就应该局限于一个动作,不能被这个动作之外干扰。

SpringFramework 中内置了 6 种作用域(5.x 版本):

作用域类型概述
singleton一个 IOC 容器中只有一个【默认值】
prototype每次获取创建一个
request一次请求创建一个(仅Web应用可用)
session一个会话创建一个(仅Web应用可用)
application一个 Web 应用创建一个(仅Web应用可用)
websocket一个 WebSocket 会话创建一个(仅Web应用可用)

singleton:单实例Bean

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wWoQ2DHv-1639742651270)(https://secure1.wostatic.cn/static/j69VR1cVCA1xoNsUrP164L/image.png)]

SpringFramework 中默认所有的 Bean 都是单实例的,即:一个 IOC 容器中只有一个

prototype:原型Bean

Spring 官方的定义是:**每次对原型 Bean 提出请求时,都会创建一个新的 Bean 实例。**这里面提到的 ”提出请求“ ,**包括任何依赖查找、依赖注入的动作,都算做一次 ”**提出请求“ 。由此咱也可以总结一点:如果连续 getBean() 两次,那就应该创建两个不同的 Bean 实例;向两个不同的 Bean 中注入两次,也应该注入两个不同的 Bean 实例。SpringFramework 的官方文档中也给出了一张解释原型 Bean 的图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrQt4lrf-1639742651271)(https://secure1.wostatic.cn/static/3fPWPVimNvPYt9x6Q4AQoY/image.png)]

Bean生命周期

生命周期阶段

在这里插入图片描述

  • 创建 / 实例化阶段:此时会调用类的构造方法,产生一个新的对象
  • 初始化阶段:此时对象已经创建好,但还没有被正式使用,可能这里面需要做一些额外的操作(如预初始化数据库的连接池)
  • 运行使用期:此时对象已经完全初始化好,程序正常运行,对象被使用
  • 销毁阶段:此时对象准备被销毁,已不再使用,需要预先的把自身占用的资源等处理好(如关闭、释放数据库连接)
  • 回收阶段:此时对象已经完全没有被引用了,被垃圾回收器回收

在spring阶段我们能干预的就只有初始化和销毁两个阶段。

控制Bean生命周期的三种方式

init-method & destroy-method@PostConstruct & @PreDestroyInitializingBean & DisposableBean
执行顺序最后最先中间
组件耦合度无侵入(只在 <bean>@Bean 中使用)与 JSR 规范耦合与 SpringFramework 耦合
容器支持xml 、注解原生支持注解原生支持,xml需开启注解驱动xml 、注解原生支持
单实例Bean
原型Bean只支持 init-method

对于原型 Bean 的生命周期,使用的方式跟上面是完全一致的,只是它的触发时机就不像单实例 Bean 那样了。

单实例 Bean 的生命周期是陪着 IOC 容器一起的,容器初始化,单实例 Bean 也跟着初始化(当然不绝对,后面会介绍延迟 Bean );容器销毁,单实例 Bean 也跟着销毁。原型 Bean 由于每次都是取的时候才产生一个,所以它的生命周期与 IOC 容器无关。


版权声明:本文为weixin_45411287原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。