Spring基础 IOC,自动装配

424 阅读16分钟

参考文章: blog.csdn.net/cy973071263…

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/684490…

IOC概念

浅谈IOC

粗略概括:控制反转就是把创建和管理 bean 的过程转移给了第三方。而这个第三方,就是 Spring IoC Container,对于 IoC 来说,最重要的就是容器

容器负责创建、配置和管理 bean,也就是它管理着 bean 的生命,控制着 bean 的依赖注入。

工厂模式:因为项目中每次创建对象是很麻烦的,所以我们使用 Spring IoC 容器来管理这些对象,需要的时候你就直接用,不用管它是怎么来的、什么时候要销毁,只管用就好了。

Bean 其实就是包装了的 Object,无论是控制反转还是依赖注入,它们的主语都是 object,而 bean 就是由第三方包装好了的 object。

IOC的好处

  1. 不用自己组装,拿来就用。
  2. 享受单例的好处,效率高,不浪费空间。
  3. 便于单元测试,方便切换mock组件。
  4. 便于进行AOP操作,对于使用者是透明的。
  5. 统一配置,便于修改。

深入ioc

Spring IoC 就是通过 javabeanset() 方法注入的

  • 控制反转,控制了什么?反转是什么? bean 的创建、管理的权利,控制 bean 的整个生命周期。 把这个控制的权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。

  • ListableBeanFactory(获取多个bean的工厂),这个 Listable 的意思就是,通过这个接口,我们可以获取多个 Bean,大家看源码会发现,最顶层 BeanFactory 接口的方法都是获取单个 Bean 的。
  • ApplicationContext 继承了 HierarchicalBeanFactory(多层bean工厂)Hierarchical 单词本身已经能说明问题了,也就是说我们可以在应用中起多个 BeanFactory,然后可以将各个 BeanFactory 设置为父子关系。
  • AutowireCapableBeanFactory(自动装配能力的bean工厂) 这个名字中的 Autowire 大家都非常熟悉,它就是用来自动装配 Bean 用的,但是仔细看上图,ApplicationContext 并没有继承它,不过不用担心,不使用继承,不代表不可以使用组合,如果你看到 ApplicationContext 接口定义中的最后一个方法 getAutowireCapableBeanFactory() 就知道了。
  • DefaultListableBeanFactory,在Spring中,实际上是把DefaultListableBeanFactory作为一个默认的功能完整的IoC容器来使用的。包含了基本IoC容器所具有的重要功能。

ApplicationContext

ApplicationContextBeanFactory的基础上集成了MessageSource, ApplicationEventPublisher, ResourcePatternResolver这几个接口,这些接口为ApplicationContext提供了以下BeanFactory不具备的新特性:

  1. 支持不同的信息源。我们看到ApplicationContext扩展了MessageSource接口,这些信息源的扩展功能可以支持国际化的实现,为开发多语言版本的应用提供服务。
  2. 访问资源。这一特性体现在对ResourceLoader和Resource的支持上,这样我们可以从不同地方得到Bean定义资源。这种抽象使用户程序可以灵活地定义Bean定义信息,尤其是从不同的I/O途径得到Bean定义信息。
  3. 支持应用事件。继承了接口ApplicationEventPublisher,从而在上下文中引入了事件机制。这些事件和Bean的生命周期的结合为Bean的管理提供了便利。
  4. 在ApplicationContext中提供的附加服务。这些服务使得基本IoC容器的功能更丰富。因为具备了这些丰富的附加功能,使得ApplicationContext与简单的BeanFactory相比,对它的使用是一种面向框架的使用风格,所以一般建议在开发应用时使用ApplicationContext作为IoC容器的基本形式。

BeanDefinition

BeanDefinition抽象了我们对Bean的定义,是让容器起作用的主要数据类型。对IoC容器来说,BeanDefinition就是对依赖反转模式中管理的对象依赖关系的数据抽象,也是容器实现依赖反转功能的核心数据结构,依赖反转功能都是围绕对这个BeanDefinition的处理来完成的。

ClassPathXmlApplicationContext 的容器初始化我们大致分为下面几步:

  1. BeanDefinitionResource 定位 这里的Resource定位 是通过继承ResourceLoader 获得的,ResourceLoader代表了加载资源的一种方式,正是策略模式的实现。

  2. 从 Resource中解析、载入BeanDefinition

  3. BeanDefinition 在IoC 容器中的注册

dependency injection (依赖注入)

什么是依赖? 什么是注入?

依赖:程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。

配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系

当然,IoC 也可以通过其他的方式来实现,而 DI 只是 Spring 的选择。

IoC 和 DI 也并非 Spring 框架提出来的,Spring 只是应用了这个设计思想和理念到自己的框架里去。

总结一句话:Spring是使用控制反转(IOC)思想,依赖注入(DI)实现的

spring八大模块

每个「绿框」,对应一个模块,总共8个模块;「黑色包」,表示要实现这个模块的 jar 包。

Core 模块是 Spring 的核心,Spring 的所有功能都依赖于这个 jar 包,Core 主要是实现 IoC 功能,那么说白了 Spring 的所有功能都是借助于 IoC 实现的。

快速使用ioc

  1. maven导入:
<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.3.RELEASE</version>
    </dependency>
  1. 创建一个javabean类,生成get,set方法
public class AmdBean {
    private String mainboard;
    private String Cpu;
  1. 创建配置文件
<?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">

    <bean id="amd" class="com.hyx.bean.AmdBean" >
        <property name="cpu" value="5800x"></property>
        <property name="mainboard" value="5700xt"></property>
    </bean>

</beans>
  1. 执行文件进行验证
    @Test
    public void AmdTest() {

        // 加载配置文件,这里使用的是类路径加载
        ApplicationContext context=new ClassPathXmlApplicationContext("services.xml");

        // 获取bean对象
        AmdBean amd = context.getBean("amd",AmdBean.class);

        System.out.println(amd.getCpu());
        System.out.println(amd.getMainboard());

    }

推荐配置方法

1. xml文件配置bean

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml");

使用ApplicationContext引入配置,它默认不支持注解注入bean,可于xml文件中打开这一配置

2. 注解配置

新建配置类,于类上写入注解,指明需要扫描的位置

// 将该类设置为一个配置类,该类就相当于一个XML
@Configurable
@ComponentScan("com.hyx.bean")
public class Config {
}

使用AnnotationConfigApplicationContext注入注解的配置,它默认支持注解扫描

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);

推荐注册bean的方式

1. xml文件注册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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="amd" class="com.hyx.bean.AmdBean" >
        <property name="cpu" value="5800x"></property>
        <!-- 使用ref注入的话,后面写的是已经在bean上注册的对应id名称的对象,就是ref=(另一个bean的id) -->
        <property name="mainboard" value="5700xt"></property>
    </bean>

    <bean id="amdService" class="com.hyx.bean.AmdService">
        <property name="amdBean" ref="amd"></property>
    </bean>
    
</beans>

XML中还有一个depends-on 这个可以规定bean在容器中的创建顺序。

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

通过depends-on配置,那么manager就会先于beanOne创建,但是这两个bean之间是没有依赖关系的。这种操作的应用场景就是如果beanOne的初始化需要用到manager的一些信息或者beanOne中的一些静态方法需要用到manager的信息,那么就得保证manager先于beanOne创建。但是这两个bean之间并没有依赖关系,所以只能使用depends-on的方法

2. 使用注解注册bean

@Component("amd")
public class AmdBean {

    @Value("@5700xt")
    private String mainboard;
    @Value("@5800x")
    private String Cpu;
    }

自动装配

什么是bean的自动装配?

Spring容器能够自动装配相互合作的bean,这意味着容器不需要和配置,能通过Bean工厂自动处理bean之间的协作。

自动装配的方法:

  • no/default
  • byName
  • byType
  • constructor

使用XML来进行自动装配

1. byType

通过类型来查找相应的依赖关系

<?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="byType">
 
    <bean id="dao" class="priv.cy.dao.IndexDaoImpl"></bean>
 
    <bean id="service" class="priv.cy.dao.IndexService"></bean>
</beans>

spring会进行扫描,如果一个bean内有依赖的对象,比如上面例子中,假设IndexService内有对于IndexDaoImpl的依赖对象,spring在扫描IndexService时,会先去看看有没有对于IndexDaoImpl的bean注册,如果有的话直接注入。这就是按照类型进行自动装配

该方法有一个问题:

在有多个可以装配的对象存在时,spring会报错,因为他不知道应该装配哪一个

注意:使用自动装配的时候默认使用的是无参构造方法来创建对象,所以要确保所有的bean都有无参构造方法,要么不显示地写构造方法,要么显示写出无参构造方法

2. byName

通过指定bean的Name来进行自动装配,如果bean没有设定name,默认name为bean的id

package priv.cy.dao;
 
public class IndexService {
    private IndexDao dao;
    public void service() {
        dao.test();
    }
    public void setDao(IndexDao dao) {
        this.dao = dao;
    }
}

这种情况下装配的方式为:

spring扫描到要装配对象的set方法,如此处为setDao(),他会将set去掉,将剩下的字符首字符小写,此方法中为dao,它现在就会去扫描对应name值为dao的bean,注入进该bean

3. no和default

两个都是一个意义: 手动装配,如果没有手动装配会报空指针错误

4. constructor

构造函数自动装配:

public class IndexService {
    private IndexDao dao;
 
    public IndexService (IndexDao dao) {
        this.dao = dao;
    }
 
    public void service() {
        dao.test();
    }
    public void setDao(IndexDao dao) {
        this.dao = dao;
    }
}

该装配需要实现构造函数,构造函数内传入的值就是要注入的bean,spring会去容器里寻找这个bean,如果未实现构造函数则报错

局部自动装配

    <bean id="amdService" class="com.hyx.bean.AmdService" autowire="byName">
        <property name="amdBean" ref="amd"></property>
    </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"
        default-autowire="byType">
 
    <bean id="dao" class="priv.cy.dao.IndexDaoImpl"></bean>
    <bean id="dao1" class="priv.cy.dao.IndexDaoImpl1"></bean>
    <bean id="service" class="priv.cy.dao.IndexService" autowire="byName"></bean>
</beans>

这种写法就可以实现service这个bean使用byName进行自动装配,其他的bean使用byType进行自动装配。这样就可以防止service的bean使用byType时发现有多个相同类型的bean都可以注入到service中,出现冲突报错了

bean命名规则

使用注解的时候,bean的命名规则

  1. 在使用@Component、@Repository、@Service、@Controller等注解创建bean时,如果指定bean的name,则是指定的名称.
  2. 如果不指定bean的name,bean名称的默认规则是类名的首字母小写,如SysConfig - sysConfig,Tools - tools。
  3. 如果类名前两个或以上个字母都是大写,那么bean名称与类名一样,如RBACUserLog - RBACUserLog,RBACUser - RBACUser,RBACRole - RBACRole。

使用XML来配置spring容器的时候bean标签写上id,然后如果不指定name的话,那么bean的name默认和id一样

自定义命名规则

命名规则是通过BeanNameGenerator这个接口来实现的,我们可以通过实现这个接口来完成自己的命名规则,编写好命名规则类,然后告诉在告诉spring要扫描位置的同时,指定命名规则类就可以了

指定扫描类:

注解:

@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}

XML:

 <beans>
        <context:component-scan base-package="org.example"
            name-generator="org.example.MyNameGenerator" />
  </beans>

使用注解进行自动装配

  1. @Autowired默认先使用的是byTypebyType使用的是属性的类型,如果byType会出现报错的话,再就会再使用byName,这里使用的name也是属性的属性名,用属性的属性名来去spring容器中匹配查找相同的bean name
  2. @Resource默认是使用的是byName,但是这里的byName和之前讲XML自动装配的byName不太一样,之前的byName都是通过setter方法名来匹配name的,但是@Resource使用的是属性名来匹配name的,和setter方法没有关系。@Resource可以指定使用byName还是byType。

但是@Resource可以指定使用bytype匹配:

@Resource(type = IndexDaoImpl1.class)

直接指定使用byType,并且使用IndexDaoImpl1.class这个类,这样不管怎么样service这个类注入的已经是IndexDaoImpl1这个实现类

也可以直接指定名字来查找装配

 @Resource(name = "indexDaoImpl1")

spring懒加载(懒汉模式)

bean在调用的时候才会被加载

使用懒加载的前提是这个bean的作用域必须是单例的。

lazy-init。告诉spring容器是否以懒加载的方式创造对象。用的时候才加载构造,不用的时候不加载

取值:true(懒,真正调用到的时候再加载)、false(非懒,已启动spring容器就创建对象)、default(懒)

 <bean id="dao" class="cn.java.ioc.MW" lazy-init="default" ></bean>

注解配置:

@Lazy
@Bean("dao")
public Dao dao() {
    System.out.println("dao");
    return new Dao();
}

优缺点

  • 懒加载:对象使用的时候才去创建,节省资源,但是不利于提前发现错误。

  • 非懒加载:容器启动的时候立刻创建对象。消耗资源。利于提前发现错误。

  • 当scope=“prototype” (多例)时,默认以懒加载的方式产生对象。

  • 当scope=“singleton” (单例)时,默认以非懒加载的方式产生对象。

spring作用域

spring容器中的bean可以分为五个范围。所有范围的名称都是说明的,

1.singleton:这种bean范围是默认的,这种范围确保不管接受到多个请求,每个容器中有一个bean的实例,单例模式由bean factory自身来维护。

2.prototype:原先通过范围与单例范围相反,为每一个bean请求提供一个实例。

3.request:在请求bean范围内会为每一个来自客户端的网络请求创建一个实例,在请求完成之后,bean会失效并被垃圾回收器回收。该属性仅对HTT请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。

4.session:与请求范围类似,确保每个session中的bean的实例在session过期后bean会随之消失。该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。Session

5.global-session:global-session和portlet公用全局存储变量的话,那么这全局变量需要存储在global-session中。该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。

可以通过@Scope(" ")注解来设置

spring容器bean的作用域默认是单例,非懒加载的

作用域引发的问题

一个类依赖另一个类,但是这两个的作用域是不同的,就会出现问题。

@Service
@Scope("singleton")
public class IndexService {
    @Autowired
    private IndexDao dao;
 
    public void service() {
        System.out.println("service" + this.hashCode());
        System.out.println("dao" + dao.hashCode());
    }
 
    public void setDao(IndexDao dao) {
        this.dao = dao;
    }
}
@Repository
@Scope("prototype")
public class IndexDaoImpl implements IndexDao {
    @Override
    public void test() {
        System.out.println("Impl 0");
    }
}

service是单例的,而它依赖的dao是原型的,但是最终的结果确实dao的原型作用域失效,这就是作用域不同进行依赖错产生的问题。因为service是单例的,他在spring容器中只会创建一次,而它依赖的对象是随着它的加载而加载的,所以service只加载一次,即使dao是原型的,也只会被加载一次。

解决方式

  1. ApplicationContextAware接口

对象实现ApplicationContextAware接口,这个还需要覆写一个set方法,和引入ApplicationContext对象

在service类每一次需要使用依赖的这个dao时,就通过ApplicationContext对象重新取一次这个bean,这样dao就可以依旧保持原型的作用域,每一次使用它就会是一个新bean,而service对象一直保持单例不变。 实现:

@Service
@Scope("singleton")
public class IndexService implements ApplicationContextAware {
 
    private ApplicationContext applicationContext;
 
    @Autowired   //加不加这个注解都无所谓了,反正最后使用的也不是这个dao对象  用ApplicationContext这个Autowired可加可不加
    private IndexDao dao;
 
    public void service() {
        System.out.println("service" + this.hashCode());
		//每次调用都使用applicationContext对象重新获取IndexDaoImpl的bean
        System.out.println("dao" + applicationContext.getBean("indexDaoImpl").hashCode());
    }
    public void setDao(IndexDao dao) {
        this.dao = dao;
    }
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

但是这种实现spring接口的方法,会大大增加项目的耦合度,侵入性太强,使代码严重spring的API,spring的设计理念之一就是非侵入性,所以spring还提供了Lookup Method Injection这个方法来解决问题

  1. Lookup Method Injection 实现:
@Service
@Scope("singleton")
public class IndexService {
//  @Autowired     使用Lookup必须要将Autowired去掉,否则会出错
    private IndexDao dao;
 
    public void service() {
        System.out.println("service" + this.hashCode());
        System.out.println("dao" + getDao().hashCode());
    }
 
    @Lookup
    public IndexDao getDao() {
        return null;
    }
}

这里只需要添加一个get方法,用来每一次调用时获取全新的dao对象,方法名不重要,自定义就行,返回值就是你要获取的对象类型,只需要在这个方法上面添加@Lookup注解,这个方法内部什么逻辑代码都不需要写,直接写一个return null就可以了。在每次获取dao对象的位置调用这个方法就可以了。还有一点就是因为是通过这个方法来获取dao的bean了,所以也就不需要@Autowired这个注解进行注入了,要将其删除,不删除的话会出现报错。

@Lookup是通过方法的返回值类型,根据这个类型从spring容器中查找这个bean,如果找到了就直接通过这个方法返回回来。但是这里就有可能出现前面讲的多个相同类型的bean冲突问题

可以通过指定bean的name来指定要注入的对象:

  @Lookup("indexDaoImpl")

生命周期与回调

生命周期的回调

生命周期回调有三种:

  • Initialization Callbacks(初始化回调)
  • Destruction Callbacks(销毁回调)
  • Default Initialization and Destroy Methods(默认初始化和销毁方法)

初始化回调与销毁回调

初始化回调需要对象实现InitializingBean接口,并重写afterPropertiesSet方法,销毁回调需要对象实现DisposableBean接口,并且重写destroy方法

@Component("amd")
public class AmdBean implements InitializingBean , DisposableBean {

    @Value("@5700xt")
    private String mainboard;
    @Value("@5800x")
    private String Cpu;
    // 初始化回调
    @Override
    public void afterPropertiesSet() throws Exception {

        System.out.println("初始化回调");
    }

    // 销毁回调
    @Override
    public void destroy() throws Exception {

        System.out.println("销毁回调");
    }
 }

使用xml与注解实现回调

XML(init-method和destroy-method)

在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"
 
       default-init-method="init"
       default-destroy-method="destory" >
 
    <bean id="dao" class="priv.cy.dao.IndexDaoImpl1"></bean>
</beans>

 default-init-method="init" --- 初始化回调

 default-destroy-method="destory" --- 销毁回调

注解 (@PostConstruct和@PreDestory)

@PostConstruct 由JSR-250提供,在构造函数执行完之后执行,等价于xml配置文件中bean的initMethod

@PreDestory 由JSR-250提供,在Bean销毁之前执行,等价于xml配置文件中bean的destroyMethod

使用以上两个注解直接用在回调方法上,也能实现自定义的回调方法:

@Repository
public class IndexDaoImpl1 implements IndexDao {
    public IndexDaoImpl1() {
        System.out.println("Constructor");
    }
 
    @PostConstruct
    public void init() {
        System.out.println("init");
    }
 
    @PreDestory
    public void destory() {
        System.out.println("destory");
    }
}

byType多个同类型的Bean冲突解决办法

@Primary

这个是主数据源标签,出现冲突的情况下优先使用这个bean进行装配

@Repository
@Primary
public class IndexDaoImpl1 implements IndexDao {
    public IndexDaoImpl1() {
        System.out.println("Constructor 1");
    }
    @PostConstruct
    public void init() {
        System.out.println("init 1");
    }
    public void destory() {
        System.out.println("destory 1");
    }
}

@Qualifier

@Qualifier这个注解可以直接指定要注入哪个bean,它通过name去指定bean

@Service
public class IndexService {
    @Qualifier("indexDaoImpl1")
    private IndexDao indexDao;
}

Java自带的一些注解可以替代spring的注解来完成相同的功能,比如@Resource,这个就属于jdk的注解,但是也能实现自动装配的功能。

索引加速spring扫描类的速度

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.4.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

添加了这个之后spring项目在编译的时候就会将所有要扫描的类添加到一个索引当中,以后再扫描就不需要使用类路径一个个扫描了,而是直接使用索引进行扫描,这样大大加快了扫描速度。