Spring(一)Spring基础与SpringIOC

2024-03-24  本文已影响0人  烦远远

文章内容来源:拉勾教育Java高薪训练营(侵权联系删除)

一、spring概述

1.1 spring简介

  Spring 是分层的 full-stack(全栈) 轻量级开源框架,以 IoC 和 AOP 为内核,提供了展现层 SpringMVC 和业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,已经成为使用最多的 Java EE 企业应用开源框架。
  Spring 官方网址:http://spring.io

1.2 spring优势

1.3 spring的核心结构

spring核心模块

二、Spring IOC

2.1 IOC的基本概念

2.1.1 什么是IOC

  IoC Inversion of Control (控制反转/反转控制),注意它是⼀个技术思想,不是⼀个技术实现。
描述的事情:Java开发领域对象的创建,管理的问题。
传统开发方式:比如类A依赖于类B,往往会在类A中new⼀个B的对象。
IOC思想下开发方式:我们不用自己去new对象了,而是由IOC容器(Spring框架)去帮助我们实例化对象并且管理它,我们需要使用哪个对象,去问IOC容器要即可。我们丧失了⼀个权利(创建、管理对象的权利),得到了⼀个福利(不用考虑对象的创建、管理等⼀系列事情)
为什么叫做控制反转?
控制:指的是对象创建(实例化、管理)的权利。
反转:控制权交给外部环境了(Spring框架、IOC容器)。


springIOC

2.1.2 IOC解决了什么问题

IoC解决对象之间的耦合问题。
比如 Service层需要引入Dao,如果没有IOC,我们需要手动去在service里面手动new一个Dao的实现类。
这样service层跟Dao层代码就耦合在一起了,如果Dao的逻辑发生重大变化,我们新增加了Dao的实现类,我们要把Service里面跟Dao耦合的代码都要改一遍。不符合面向接口开发的原则。
当我们将类的控制权交给Spring的时候我们只需要声明接口即可,耦合度较小。


代码解耦

2.1.3 IOC和DI的关系

DI:Dependancy Injection(依赖注入)。
怎么理解:
IOC和DI描述的是同⼀件事情,只不过角度不⼀样罢了。
IOC是站在对象的角度:对象实例化及其管理的权利交给了(反转)给了容器。
DI是站在容器的角度:容器会把对象依赖的其他对象注入(送进去),比如A对象实例化过程中因为声明了一个B类型的属性,那么就需要容器把B对象注入给A。

2.2 IOC基础

2.2.1 启动容器的方式

1.Java环境下启动IoC容器:

ClassPathXmlApplicationContext:从类的根路径下加载配置文件(推荐使用)。
FileSystemXmlApplicationContext:从磁盘路径上加载配置文件。
AnnotationConfigApplicationContext:纯注解模式下启动Spring容器。

2.WEB环境下启动IoC容器:

a.从xml启动容器
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
 <display-name>Archetype Created Web Application</display-name>
 <!--配置Spring ioc容器的配置⽂件-->
 <context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>classpath:applicationContext.xml</param-value>
 </context-param>
 <!--使⽤监听器启动Spring的IOC容器-->
 <listener>
 <listener�class>org.springframework.web.context.ContextLoaderListener</listener�class>
 </listener>
</web-app>
b.从配置类启动容器
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
 <display-name>Archetype Created Web Application</display-name>
 <!--告诉ContextloaderListener知道我们使用注解的方式启动ioc容器-->
 <context-param>
 <param-name>contextClass</param-name>
 <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
 </context-param>
 <!--配置启动类的全限定类名-->
 <context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>com.lagou.edu.SpringConfig</param-value>
 </context-param>
 <!--使⽤监听器启动Spring的IOC容器-->
 <listener>
 <listener-class>org.springframework.web.context.ContextLoaderListener</listener�class>
 </listener>
</web-app>

2.2.2 bean的实例化方式

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

a.使用无参构造函数
在默认情况下,它会通过反射调用无参构造函数来创建对象。如果类中没有无参构造函数,将创建失败。

<!--配置service对象-->
<bean id="userService" class="com.lagou.service.impl.TransferServiceImpl">
</bean>

b.使用静态方法创建
在实际开发中,我们使用的对象有些时候并不是直接通过构造函数就可以创建出来的,它可能在创建的过程中会做很多额外的操作。此时会提供⼀个创建对象的方法,恰好这个方法是static修饰的方法,即是此种情况。
例如,我们在做Jdbc操作时,会用到java.sql.Connection接口的实现类,如果是mysql数据库,那么用的就是JDBC4Connection,但是我们不会去写 JDBC4Connection connection = newJDBC4Connection() ,因为我们要注册驱动,还要提供URL和凭证信息,⽤ DriverManager.getConnection 方法来获取连接。
那么在实际开发中,尤其早期的项⽬没有使用Spring框架来管理对象的创建,但是在设计时使用了工厂模式解耦,那么当接入pring之后,工厂类创建对象就具有和上述例子相同特征,即可采用此种方式配置。

<!--使用静态方法创建对象的配置方式-->
<bean id="userService" class="com.lagou.factory.BeanFactory"
 factory-method="getTransferService"></bean>

c.使用实例化方法创建
此种方式和上面静态方法创建其实类似,区别是用于获取对象的方法不再是static修饰的了,而是类中的一个普通方法。此种方式比静态方法创建的几率要高⼀些。在早期开发的项目中,工厂类中的方法有可能是静态的,也有可能是非静态方法,当是非静态方法时,即可采用下面的配置方式。

<!--使⽤实例方法创建对象的配置方式-->
<bean id="beanFactory" class="com.lagou.factory.instancemethod.BeanFactory"></bean>
<bean id="transferService" factory-bean="beanFactory" factory-method="getTransferService"></bean>

(2) xml和注解模式下bean的实例化

1)实际企业开发中,纯xml模式使用已经很少了。
2)引入注解功能,不需要引入额外的jar。
3)xml+注解结合模式,xml文件依然存在,所以spring IOC容器的启动仍然从加载xml开始。
4)哪些bean的定义写在xml中,哪些bean的定义使用注解。
第三方jar中的bean定义在xml,比如德鲁伊数据库连接池。
自己开发的bean定义使用注解,比如@service等。

a.xml中标签与注解的对应(IoC)

xml形式 对应注解形式
标签 @Component("accountDao"),注解加在类上bean的id属性内容直接配置在注解后面如果不配置,默认定义个这个bean的id为类的类名首字母小写;另外,对分层代码开发提供了@Componenet的三种别名@Controller、@Service、@Repository分别用于控制层类、服务层类、dao层类的bean定义,这四个注解的用法完全⼀样,只是为了更清晰的区分而已
标签的scope属性 @Scope("prototype"),默认单例,注解加在类上
标签的init-method属性 @PostConstruct,注解加在方法上,该方法就是初始化后调用的方法
标签的destory-method属性 @Scope("prototype"),默认单例,注解加在类上
标签的scope属性 @PreDestory,注解加在方法上,该方法就是销毁前调⽤的方法

b.DI 依赖注入的注解实现方式

@Autowired(推荐使用)
@Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired。
@Autowired采取的策略为按照类型注入。
如上代码所示,这样装配回去spring容器中找到类型为AccountDao的类,然后将其注入进来。这样会产生⼀个问题,当⼀个类型有多个bean值的时候,会造成无法选择具体注入哪⼀个的情况,这个时候我们需要配合着@Qualifier使用。@Qualifier告诉Spring具体去装配哪个对象。

public class TransferServiceImpl {
 @Autowired
 @Qualifier(name="jdbcAccountDaoImpl") 
 private AccountDao accountDao; }

@Resource
@Resource 注解由 J2EE 提供,需要导入包 javax.annotation.Resource。
@Resource 默认按照 ByName 自动注入。

public class TransferService {
 @Resource 
 private AccountDao accountDao;
 @Resource(name="studentDao") 
 private StudentDao studentDao;
 @Resource(type="TeacherDao") 
 private TeacherDao teacherDao;
 @Resource(name="manDao",type="ManDao") 
 private ManDao manDao;
}

(3) 纯注解模式下bean的实例化

改造xm+注解模式,将xml中遗留的内容全部以注解的形式迁移出去,最终删除xml,从Java配置类启动类加上对应注解。

2.2.3 不同作用范围的Bean生命周期

bean的生命周期:实例化——属性赋值——初始化——销毁

单例模式:singleton

对象出生:当创建容器时,对象就被创建了。
对象活着:只要容器在,对象⼀直活着。
对象死亡:当销毁容器时,对象就被销毁了。
⼀句话总结:单例模式的bean对象生命周期与容器相同。

多例模式:prototype

对象出生:当使用对象时,创建新的对象实例。
对象活着:只要对象在使用中,就⼀直活着。
对象死亡:当对象长时间不用时,被java的垃圾回收器回收了。
⼀句话总结:多例模式的bean对象,spring框架只负责创建,不负责销毁。

2.3 IOC高级特性

2.3.1 lazy-Init 延迟加载

Bean的延迟加载(延迟创建)
ApplicationContext 容器的默认行为是在启动服务器时将所有 singleton bean 提前进行实例化。
提前实例化意味着作为初始化过程的⼀部分,ApplicationContext 实例会创建并配置所有的singletonbean。
比如:

<bean id="testBean" class="cn.lagou.LazyBean" />
该bean默认的设置为: <bean id="testBean" calss="cn.lagou.LazyBean" lazy-init="false" />

lazy-init="false",立即加载,表示在spring启动时,立刻进行实例化。
如果不想让⼀个singleton bean 在 ApplicationContext实现初始化时被提前实例化,那么可以将bean
设置为延迟实例化。

<bean id="testBean" calss="cn.lagou.LazyBean" lazy-init="true" />

设置 lazy-init 为 true 的 bean 将不会在 ApplicationContext 启动时提前被实例化,而是第⼀次向容器通过getBean 索取 bean 时实例化的。如果⼀个设置了立即加载的 bean1,引用了⼀个延迟加载的 bean2 ,那么 bean1 在容器启动时被实例化,bean2 由于被 bean1 引用,所以也被实例化,这种情况也符合延时加载的 bean 在第⼀次调用时才被实例化的规则。也可以在容器层次中通过在 元素上使用 "default-lazy-init" 属性来控制延时初始化。
如下面配置:

<beans default-lazy-init="true">
 <!-- no beans will be eagerly pre-instantiated... -->
</beans>

如果⼀个 bean 的 scope 属性为 scope="pototype" (多例)时,即使设置了 lazy-init="false",容器启动时也不会实例化bean,而是调用 getBean 方法实例化的。
应用场景
(1)开启延迟加载⼀定程度提高容器启动和运转性能。
(2)对于不常使用的 Bean 设置延迟加载,这样偶尔使用的时候再加载,不必要从⼀开始该 Bean 就占用资源。

2.3.2 FactoryBean 和 BeanFactory

BeanFactory

BeanFactory是容器的顶级接口,定义了容器的⼀些基础行为,负责生产和管理Bean的⼀个工厂,具体使用它下面的子接口类型,比如ApplicationContext;

FactoryBean

Spring中Bean有两种,一种是普通Bean,一种是工厂Bean(FactoryBean),FactoryBean可以生成某⼀个类型的Bean实例(返回给我们),也就是说我们可以借助于它自定义Bean的创建过程。
Bean创建的三种方式中的静态方法和实例化方法和FactoryBean作用类似,FactoryBean使用较多,尤其在Spring框架⼀些组件中会使用,还有其他框架和Spring框架整合时使用。
下面我们可以通过FactoryBean实现自定义Bean的创建过程,完成复杂bean的创建。
如图:Spring预置的FactoryBean中定义了如下方法:

import org.springframework.lang.Nullable;

// 可以让我们自定义Bean的创建过程(完成复杂Bean的定义)
public interface FactoryBean<T> {
 @Nullable
 // 返回FactoryBean创建的Bean实例,如果isSingleton返回true,则该实例会放到Spring容器的单例对象缓存池中Map
 T getObject() throws Exception;
 @Nullable
 // 返回FactoryBean创建的Bean类型
 Class<?> getObjectType();
 // 返回作⽤域是否单例
 default boolean isSingleton() {
   return true;
 } 
}

Company类:

package com.lagou.edu.pojo;

public class Company {

    private String name;

    private String address;

    private int scale;

    public String getName() {
        return name;
    }

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

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getScale() {
        return scale;
    }

    public void setScale(int scale) {
        this.scale = scale;
    }

    @Override
    public String toString() {
        return "Company{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", scale=" + scale +
                '}';
    }
}

CompanyFactoryBean

package com.lagou.edu.factory;


import com.lagou.edu.pojo.Company;
import org.springframework.beans.factory.FactoryBean;

public class CompanyFactoryBean implements FactoryBean<Company>{

    private String companyInfo; // 公司名称,地址,规模
    
    public void setCompanyInfo(String companyInfo) {
        this.companyInfo = companyInfo;
    }
    @Override
    public Company getObject() throws Exception {
        // 模拟创建复杂对象Company
        Company company = new Company();
        String[] strings = companyInfo.split(",");
        company.setName(strings[0]);
        company.setAddress(strings[1]);
        company.setScale(Integer.parseInt(strings[2]));
        return company;
    }
    @Override
    public Class<?> getObjectType() {
        return Company.class;
    }
    @Override
    public boolean isSingleton() {
        return true;
    }
}

xml 配置

 <!--测试自定义FactoryBean-->
    <bean id="companyBean" class="com.lagou.edu.factory.CompanyFactoryBean">
        <property name="companyInfo" value="拉勾,中关村,500"/>
    </bean>

测试,获取FactoryBean产生的对象

Object companyBean = applicationContext.getBean("companyBean");
System.out.println("bean:" + companyBean);
// 结果如下
bean:Company{name='拉勾', address='中关村', scale=500}

测试,获取FactoryBean,需要在id之前添加“&”

Object companyBean = applicationContext.getBean("&companyBean");
System.out.println("bean:" + companyBean);
// 结果如下
bean:com.lagou.edu.factory.CompanyFactoryBean@53f6fd09

2.4 IOC循环依赖

Spring框架通过其自身的三级缓存机制解决了循环依赖的问题。对于Spring中的循环依赖主要有两种情况:

1 构造器注入导致的循环依赖

Spring不能解决构造器注入的循环依赖,因为在构造器注入时,Spring容器需要先创建一个Bean A的对象,而在构造A时就需要注入另一个Bean B的对象,但此时B尚未创建或者正在创建中,这就构成了死循环。因此,为了避免构造器循环依赖,建议使用 setter 或 field 注入来代替构造器注入。

2 Setter/Field注入导致的循环依赖

Spring容器可以解决基于Setter或Field注入的循环依赖问题。这是因为Spring容器在初始化Bean的过程中采用了三级缓存策略来管理单例Bean的创建状态:
一级缓存(singletonObjects):存放完全初始化好的单例Bean。
二级缓存(earlySingletonObjects):存放提前曝光(early exposed)的单例Bean,这些Bean还未完成所有的初始化,但对象已经被实例化。
三级缓存(singletonFactories):存放Bean的工厂对象,可以用来生产未完成初始化的Bean。
当Spring容器创建Bean时,首先会尝试从一级缓存中获取,如果获取不到,则尝试创建新的Bean实例。在创建Bean的过程中,会先将半成品对象(仅实例化但未完成依赖注入的对象)放入二级缓存中,接着处理其他依赖项。如果有循环依赖发生,就可以从二级缓存中获取到彼此的引用,待所有依赖注入完成后,再将Bean从二级缓存移到一级缓存中。

下面举两个例子:

2.1 普通类循环依赖:

不涉及工厂方法创建对象的依赖,这种情况使用二级缓存就可以解决
假设有一个Spring应用中有两个单例Bean ServiceA 和 ServiceB,它们之间存在循环依赖:
ServiceA 类中有一个成员变量 serviceB,表示它依赖于 ServiceB。
ServiceB 类中也有一个成员变量 serviceA,表示它依赖于 ServiceA。

@Service
public class ServiceA {
    private ServiceB serviceB;

    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private ServiceA serviceA;

    @Autowired
    public void setServiceA(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

Spring是如何使用三级缓存解决这样的循环依赖呢?

1.创建ServiceA:
2.创建ServiceB:
3.回溯到ServiceA:

这样一来,即使存在循环依赖,Spring也能通过三级缓存机制保证依赖关系的正确建立,且不会陷入无限递归的创建过程中。注意,以上描述的是setter注入的情况,构造器注入的循环依赖Spring是无法解决的.

2.2 工厂方法类循环依赖:

在工厂方法模式下,Spring的确可以利用三级缓存来解决循环依赖问题。下面是一个简化的例子来展示这一过程:

假设我们有两个互相依赖的Bean:ComponentA 和 ComponentB,并且通过工厂方法来创建它们

@Service
public class ComponentA {

    private final ObjectFactory<ComponentB> componentBFactory;

    @Autowired
    public ComponentA(ObjectFactory<ComponentB> componentBFactory) {
        this.componentBFactory = componentBFactory;
    }

    public void doSomething() {
        ComponentB componentB = componentBFactory.getObject();
        // ...
    }
}

@Service
public class ComponentB {

    private final ComponentA componentA;

    @Autowired
    public ComponentB(ComponentA componentA) {
        this.componentA = componentA;
    }
}

现在我们来看Spring如何处理循环依赖:

1.创建ComponentA:
2.创建ComponentB:
3。使用三级缓存:
4.继续创建ComponentA:
5.最终处理:

通过三级缓存,Spring能够在创建 ComponentA 的过程中保存一个能够生成 ComponentB 实例的工厂对象,这样即使在创建过程中存在循环依赖,也可以暂时绕过,等到真正需要 ComponentB 实例时再通过工厂对象去创建。这样既保持了依赖关系的正确性,也避免了循环引用带来的初始化问题。

上一篇 下一篇

猜你喜欢

热点阅读