Java开发框架:Spring IOC、Bean概述

315 阅读14分钟

Spring框架

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。

比如说 Spring 支持 IoC(Inversion of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

Spring框架的核心模块

image.png

Core Container

Spring框架的核心模块,主要提供IoC依赖注入功能的支持。Spring其他所有的功能都依赖该模块

  • spring-core:Spring框架的核心工具类

  • spring-beans:提供对bean的创建、配置和管理等都功能的支持

  • spring-context:提供对国际化、事件传播、资源加载等功能的支持

  • spring-expression:提供对表达式语言的支持,只依赖于core模块,可以单独使用。

AoP

  • spring-aspects:该模块为于AspectJ的集成提供支持。

  • spring-apo:提供了面向切面的编程实现。

  • spring-instrument:提供了为JVM添加代理的功能。使用场景非常有限。

Data Access/Integration

  • spring-jdbc:提供了对数据库访问的抽象JDBC。不同的数据库都有自己独立的API用于操作数据库,Java程序只需要和API交互,屏蔽了数据库的影响。

  • spring-tx:提供对事物的支持

  • spring-orm:提供对Hibernate、JPA、iBatis等ORM框架的支持。

  • spring-oxm:

  • spring-jsm:消息服务。

Spring Web

  • spring-web:对 Web 功能的实现提供一些最基础的支持。

  • spring-webmvc:提供对 Spring MVC 的实现。主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

  • spring-websocket:提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。

  • spring-webflux:提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。

谈谈自己对Spring IOC的理解

IOC(Inversion of Control:控制反转)的思想是将原来在程序中手动new一个对象的控制权交由Spring框架来管理。

将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件或者到SpringBoot时代更加简洁,只要配置好注解即可,完全不用考虑对象是如何被创建出来的。

Spring IOC容器

BeanFactory

BeanFactory和ApplicationContext是Spring框架提供的两个IOC容器,ApplicationContext是BeanFactory的子接口。ApplicationContext 容器包括 BeanFactory 容器的所有功能,所以通常不建议使用BeanFactory。

以下代码说明了如何使用BeanFactory

package com.tutorialspoint;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
public class MainApp {
   public static void main(String[] args) {
      XmlBeanFactory factory = new XmlBeanFactory
                             (new ClassPathResource("Beans.xml"));
      HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
      obj.getMessage();
   }
}
  • 第一步利用框架提供的 XmlBeanFactory()  API 去生成工厂 bean 以及利用 ClassPathResource()  API 去加载在路径 CLASSPATH 下可用的 bean 配置文件。XmlBeanFactory()  API 负责创建并初始化所有的对象,即在配置文件中提到的 bean。

  • 第二步利用第一步生成的 bean 工厂对象的 getBean()  方法得到所需要的 bean。 这个方法通过配置文件中的 bean ID 来返回一个真正的对象,该对象最后可以用于实际的对象。一旦得到这个对象,你就可以利用这个对象来调用任何方法。

ApplicationContext

最常被使用的 ApplicationContext 接口实现:

  • FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径。

  • ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。

  • XmlWebApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。

下面是文件 MainApp.java 的内容:

package com.tutorialspoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class MainApp {
   public static void main(String[] args) {
      ApplicationContext context = new FileSystemXmlApplicationContext
            ("C:/Users/ZARA/workspace/HelloSpring/src/Beans.xml");
      HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
      obj.getMessage();
   }
}

在主程序当中,我们需要注意以下两点:

  • 第一步生成工厂对象。加载完指定路径下 bean 配置文件后,利用框架提供的 FileSystemXmlApplicationContext API 去生成工厂 bean。FileSystemXmlApplicationContext 负责生成和初始化所有的对象,比如,所有在 XML bean 配置文件中的 bean。

  • 第二步利用第一步生成的上下文中的 getBean()  方法得到所需要的 bean。 这个方法通过配置文件中的 bean ID 来返回一个真正的对象。一旦得到这个对象,就可以利用这个对象来调用任何方法。

bean什么时候被实例化

  • 如果使用BeanFactory作为Spring Bean的工厂类,则所有的Bean都是在第一次使用该Bean(调用getBean)的时候实例化。

    • 这种方式无法发现一些Spring的配置问题。如果bean的某一个属性没有注入,BeanFactory加载后,直至第一次调用getBean方法才会抛出异常。
  • 如果使用ApplicationFactory作为工厂类,则分为以下情况:

    • 如果bean是singleton的,并且lazy-init为false,则ApplicationContext启动的时候就实例化该Bean,并且将实例化的bean放在一个map结构的缓存中,下次再用的时候直接从缓存中获取。

    • 如果scope是singleton,且lazy-init为true或者使用@Lazy注解将其设置为懒加载模式,则:容器在解析注册Bean定义时进行预实例化??

    • 如果scope是prototype,则在第一次使用该bean的时候实例化。

Bean的作用域有哪些

  • singleton:单例,Ioc容器中只有唯一的bean实例。

  • prototype:每次获取都会创建一个新的bean实例。也就是说,连续getBean()两次,得到的是不同的实例。

  • request:仅web应用可用。每次http请求都会产生一个新的bean,且仅在当前http request内生效

  • session:仅web应用可用。每次来自新session的http请求都会产生一个新的bean,仅在当前http session内有效。

  • application/gloable-session:仅web应用可用,不常见。每个web应用在启动时创建一个bean,仅在启动时间。

  • websocket:仅web应用可用,不常见。每次websocket回话产生一个新的bean。

如何配置bean的作用域

xml方式

<bean id = "..." class="..." scope="singleton"></bean>

注解方式:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Persion personPrototype(){
    return new Person();
}

Bean是否线程安全

Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。

最常用的两种作用域 是prototype 和 singleton 。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。

  • prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。

  • singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。

    • 如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)

    • 不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。

  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

Bean的生命周期

image.png

参考这个食用:Spring IOC容器源码分析

DI(依赖注入)

依赖注入(DI)  是IOC思想的一种实现方式。Spring容器也是通过这种形式管理Bean的。比如:A类要依赖B类,A类不再直接创建B类的实例,而是把这种依赖关系配置在外部xml文件或者通过注解配置依赖关系,然后由Spring容器根据配置信息创建、管理bean类。

依赖注入有以下几种方式:

手动注入

构造器注入

@Service
public class AService {
    BService bService;
    @Autowired
    public AService(BService bService) {
        this.bService = bService;
    }
}

如果类只有一个构造方法,那么 @Autowired 注解可以省略;如果类中有多个构造方法,那么需要添加上 @Autowired 来明确指定到底使用哪个构造方法。

构造器注入需要结合配置文件食用

 <bean name="AService" class="com.busyboy.service.AService">
     <constructor-arg index="0" ref="BService"/>
 </bean>

定义bean:AService,依赖BService,并通过构造器注入

setter方法注入

@Service
public class AService {
    BService bService;

    @Autowired
    public void setBService(BService bService) {
        this.bService = bService;
    }
}

setter方法注入也需要结合配置文件食用

 <bean name="AService" class="com.busyboy.service.AService">
     <property name="aService" ref="aService"/>
 </bean>

上述两种xml方式的依赖注入,现在其实很少用到,就不多做介绍。详细可以看:W3C School Spring教程

自动装配

xml方式

Spring 容器可以在不使用<constructor-arg><property> 元素的情况下自动装配相互协作的 bean 之间的关系,这样可以减少xml文件的一些配置。自动装配有byName、byType和构造器注入三种方式。

自动装配原理:在创建Bean的过程中,在填充属性时,Spring会去解析当前类,把当前类的所有方法都解析出来,Spring会去解析每个方法得到对应的PropertyDescriptor对象,PropertyDescriptor中有几个属性:

public class PropertyDescriptor extends FeatureDescriptor {
    private Reference<? extends Class<?>> propertyTypeRef;
    private final MethodRef readMethodRef;
    private final MethodRef writeMethodRef;
    private Reference<? extends Class<?>> propertyEditorClassRef;
    private boolean bound;
    private boolean constrained;
    private String baseName;
    private String writeMethodName;
    private String readMethodName;
}
  • name:这个name并不是方法的名字,而是拿方法名字进过处理后的名字

    • 如果方法名字以“get”开头,比如“getXXX”,那么name=XXX
    • 如果方法名字以“is”开头,比如“isXXX”,那么name=XXX
    • 如果方法名字以“set”开头,比如“setXXX”,那么name=XXX
  • readMethodRef:表示get方法的Method对象的引用

  • readMethodName:表示get方法的名字

  • writeMethodRef:表示set方法的Method对象的引用

  • writeMethodName:表示set方法的名字

  • propertyTypeRef:如果有get方法那么对应的就是返回值的类型,如果是set方法那么对应的就是set方法中唯一参数的类型

get方法的定义是: 方法参数个数为0个,并且 (方法名字以"get"开头 或者 方法名字以"is"开头并且方法的返回类型为boolean)

set方法的定义是: 方法参数个数为1个,并且 (方法名字以"set"开头并且方法返回类型为void)

byName

xml配置文件中,通过autowire="byName"的方式自动装配。Spring会在容器中查找名称与属性名称相同的bean,并自动注入到属性(是说B类依赖的A类对象,是作为B类的一个属性存在)中。

比如:在配置文件中,如果一个 bean 定义设置为自动装配 byName,并且它包含 spellChecker 属性(即,它有一个 setSpellChecker(...)  方法),那么 Spring 就会查找定义名称为 spellChecker 的 bean,并且用它来设置这个属性。

Spring在通过byName自动填充属性时的流程是:

  1. 找到所有set方法所对应的XXX部分的名字

  2. 根据XXX部分的名字去获取bean

(什么时候查找这个bean?编译时还是用到这个bean的时候?)

举例如下。这里是 TextEditor.java 文件的内容:

package com.tutorialspoint;
public class TextEditor {
   // byName方式装配,需要从容器中找到名称为spellChecker的bean进行装配
   private SpellChecker spellChecker;
   private String name;
   // 还是要有set方法
   public void setSpellChecker( SpellChecker spellChecker ){
      this.spellChecker = spellChecker;
   }
   public SpellChecker getSpellChecker() {
      return spellChecker;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getName() {
      return name;
   }
   public void spellCheck() {
      // SpellChecker类里面有一个checkSpelling方法。
      spellChecker.checkSpelling();
   }
}

下面是在正常情况下的配置文件 Beans.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-3.0.xsd">

   <!-- Definition for textEditor bean -->
   <bean id="textEditor" class="com.tutorialspoint.TextEditor">
       <property name="spellChecker" ref="spellChecker" />
       <property name="name" value="Generic Text Editor" />
   </bean>

   <!-- Definition for spellChecker bean -->
   <bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
   </bean>

</beans>

但是,如果你要使用自动装配 “byName”,那么你的 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-3.0.xsd">

   <!-- Definition for textEditor bean -->
   <bean id="textEditor" class="com.tutorialspoint.TextEditor" autowire="byName">
      <property name="name" value="Generic Text Editor" />
   </bean>

   <!-- Definition for spellChecker bean -->
   <bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
   </bean>

</beans>

byType

在 XML 配置文件中 bean 的 autowire 属性设置为 byType(即autowire="byName")。如果容器中存在一个与指定属性类型相同的bean,那么将与该属性自动装配;如果存在多个该类型bean,那么抛出异常,并指出不能使用byType方式进行自动装配;如果没有找到相匹配的bean,则什么事都不发生

例如,在配置文件中,如果一个 bean 定义设置为自动装配 byType,并且它包含 SpellChecker类型的 spellChecker属性,那么 Spring 就会查找类型为 SpellChecker 的 bean,并且用它来设置这个属性。

Spring通过byType方式自动注入属性的流程是:

  1. 获取当前类的set方法中唯一参数的参数类型,并根据该类型去容器中获取bean

  2. 如果找到多个,会报错

这里是 TextEditor.java 文件的内容:

package com.tutorialspoint;
public class TextEditor {
   // 需要从容器中找到类型为'SpellChecker'的bean进行装配
   private SpellChecker spellChecker;
   private String name;
   public void setSpellChecker( SpellChecker spellChecker ) {
      this.spellChecker = spellChecker;
   }
   public SpellChecker getSpellChecker() {
      return spellChecker;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getName() {
      return name;
   }
   public void spellCheck() {
      spellChecker.checkSpelling();
   }
}

下面是另一个依赖类文件 SpellChecker.java 的内容:

package com.tutorialspoint;
public class SpellChecker {
   public SpellChecker(){
      System.out.println("Inside SpellChecker constructor." );
   }
   public void checkSpelling() {
      System.out.println("Inside checkSpelling." );
   }   
}

下面是在正常情况下的配置文件 Beans.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-3.0.xsd">

   <!-- Definition for textEditor bean -->
   <bean id="textEditor" class="com.tutorialspoint.TextEditor">
      <property name="spellChecker" ref="spellChecker" />
      <property name="name" value="Generic Text Editor" />
   </bean>

   <!-- Definition for spellChecker bean -->
   <bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
   </bean>

</beans>

但是,如果你要使用自动装配 “byType”,那么你的 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-3.0.xsd">

   <!-- Definition for textEditor bean -->
   <bean id="textEditor" class="com.tutorialspoint.TextEditor" autowire="byType">
      <property name="name" value="Generic Text Editor" />
   </bean>

   <!-- Definition for spellChecker bean -->
   <!--注意这个id配置的是类名,不是对象名了-->
   <bean id="SpellChecker" class="com.tutorialspoint.SpellChecker">
   </bean>

</beans>

constructor

通过构造函数自动装配。autowire="constructor"。 这个不详细说了,详见:W3C School Spring教程

其实构造方法注入相当于byType+byName,普通的byType是根据set方法中的参数类型去找bean,找到多个会报错,而constructor就是通过构造方法中的参数类型去找bean,如果找到多个会根据参数名确定

注解的方式(重点,目前常用的)

使用@Autowired 或者 @Resource 进行注入时,被注入的bean,不一定非要用比如@Component、@Controller这种注解标注,这几个注解是SpringBoot框架里才有的。所以在Spring中使用@Autowired的时候,被注入的bean,仍然定义在xml文件中。

@Autowired

当使用@Autowired时,会根据byType进行装配。所以,要确保在beans中的bean的class必须具有唯一性。

public class Person {
    private String name;
    @Autowired
    private School school;
    @Autowired
    private Family family;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public School getSchool() {
        return school;
    }


    public Family getFamily() {
        return family;
    }

}

beans.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/>


    <beans>
        <bean id="school" class="com.sdpei.beans.School"/>
        <bean id="family" class="com.sdpei.beans.Family"/>
        <bean id="person" class="com.sdpei.beans.Person">
            <property name="name" value="寒江"/>
        </bean>

    </beans>
</beans>

如果beans中的class不存在唯一性的时候,需要使用@Autowired+@Qualilier(value="xxbeanid")进行自动创配 xml文件:

<beans>
    <bean id="school" class="com.sdpei.beans.School"/>
    <bean id="school2" class="com.sdpei.beans.School"/>
    <bean id="family" class="com.sdpei.beans.Family"/>
    <bean id="family2" class="com.sdpei.beans.Family"/>
    <bean id="person" class="com.sdpei.beans.Person">
        <property name="name" value="寒江"/>
    </bean>

</beans>
public class Person {
    private String name;
    @Autowired
    @Qualifier(value = "school2")
    private School school;

    @Autowired
    @Qualifier(value = "family2")
    private Family family;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public School getSchool() {
        return school;
    }


    public Family getFamily() {
        return family;
    }

}

@Resource

默认使用byName的方式实现自动装配,若通过byName找不到,那么会使用byType的方式进行自动化装配。

如果beans中有两个class相同的类或者一个接口有两个实现类,则需要通过以下两种方式指定?:

@Resource(value=" xxx"):value为bean的id。

或者

Autowired+@Qualilier("xxbeanid")

两者的区别(还得再梳理下)

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。

  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。

  • 当一个接口存在多个实现类的情况下,@Autowired 和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显式指定名称,@Resource可以通过 name 属性来显式指定名称。

  • @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

需要再补一下注入的原理,什么时候加载之类的

可以参考这个文章:juejin.cn/post/703299…

各种注入方式的优缺点

详见参考文章

参考文章(依赖注入)

  1. 面试官常问的Spring依赖注入和Bean的装配问题,今天给大家讲清楚!
  2. 面试突击77:Spring 依赖注入有几种?各有什么优缺点?
  3. 【Spring源码】-Spring中依赖注入的方式
  4. 【Spring】使用xml文件实现自动装配;使用注解@Autowired和@Resource实现自动装配
  5. @Autowired和@Resource的区别

参考文章

  1. JavaGuide面试题
  2. 一文彻底明白IOC及Bean