ssm框架之Spring(上)

你是否也曾在 Java 开发中陷入这样的困境:手动管理对象依赖时,代码里满是繁琐的new关键字,一旦需求变动,牵一发而动全身;想要实现事务控制,又得在业务逻辑中嵌入大量重复的事务处理代码,既臃肿又难以维护?如果你对这些场景深有体会,那么你一定听说过甚至正在使用一个改变了 Java 开发格局的框架 ------Spring。从 2003 年首次发布至今,Spring 早已不是简单的 "工具类集合",它以 "简化开发、降低耦合" 为核心,构建起了一套覆盖从后端服务到前端交互、从单体应用到微服务架构的完整生态,成为全球数百万 Java 开发者的 "标配"。今天,我们就一起走进 Spring 的世界,看看这个 "开发神器" 究竟如何解决我们的痛点,又为何能在二十多年的技术迭代中始终保持活力,尤其聚焦其核心思想之一 ------IOC(控制反转)的魅力。

一、核心概念

1,IOC(Inversion of Control)控制反转

使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。

2,Spring技术对IOC思想进行了实现

Spring提供了一个容器,称为IOC容器,用来充当IOC思想的"外部"。

IOC容器负责对象的创建,初始化等一系列工作,被创建或被管理的对象在IOC容器中统称为Bean。

3,DI(Dependency Injection)依赖注入

在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入。

二、基于XML配置的Spring应用

1,Bean的常用配置概览

2,SpringBean的配置详解

1,Bean的基础配置

例如:配置UserDaoImpl由Spring容器负责管理

复制代码
<bean id="userDao" class="com.Dao.Impl.UserDaoImpl"/>

此时存储到Spring容器(singleObjects单例池)中的Bean的beanName是userDao,值是UserDaoImpl对象,可以根据beanName获取Bean实例

复制代码
applicationContext.getBean("userDao");

如果不配置id,则Spring会把当前Bean实例的全限定名作为beanName

2,Bean的别名配置(少用)

可以为当前Bean指定多个别名,根据别名也可以获得Bean对象

复制代码
<bean id="userDao" name="aaa,bbb" class="com.mxw.Dao.Impl.UserDaoImpl"/>

此时多个名称都可以获得UserDaoImpl实例对象

复制代码
userDao userdao = (userDao) applicationContext.getBean("userDao"); userDao userdao1 = (userDao) applicationContext.getBean("aaa"); userDao userdao2 = (userDao) applicationContext.getBean("bbb");
3,Bean的范围配置------scope

默认情况下,单纯的Spring环境Bean的作用范围有两个:Singleton和Prototype

singleton:单例,默认值,Spring容器创建的时候,就会进行Bean的实例化,并存储到容器内部的单例池中,每次getBean时都是从单例池中获取相同的Bean实例;

prototype:原型,Spring容器初始化时不会创建Bean实例,当调用getBean时才会实例化Bean,每次getBean都会创建一个新的Bean实例。(不会存储到容器内部的单例池中)

4,Bean的延迟加载

当lazy-init设置为true时为延迟加载,也就是当Spring容器创建的时候,不会立即创建Bean实例,等到用到时在创建Bean实例并存储到单例池中去,后续再使用该Bean直接从单例池获取即可,本质上该Bean还是单例的。

复制代码
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl" lazy-init="true"/>
5,Bean的初始化和销毁方法配置(不常用)

Bean在被实例化后,可以执行指定的初始化方法完成一些初始化的操作,Bean在销毁之前也可以执行指定的销毁方法完成一些操作,初始化方法名称和销毁方法名称通过以下配置:

复制代码
// xml配置内容 <bean id="userDao" class="com.Dao.Impl.UserDaoImpl" init-method="init" destroy-method="destroy"/>

// 测试类内容 //ApplicationContext类中没有关闭方法,需要使用ClassPathXmlApplicationContext类 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("ApplicationContext.xml"); userDao userdao = (userDao) applicationContext.getBean("userDao"); // 显式关闭 applicationContext.close();

扩展:除此之外,我们还可以通过实现InitializingBean接口,完成一些Bean的初始化操作。

6,Bean的实例化配置

Spring的实例化方式主要如下两种:

1)构造方式实例化:底层通过构造方法对Bean进行实例化

构造方式实例化Bean又分为无参构造方法实例化和有参构造方法实例化,Spring中配置的几乎都是无参构造方式,以下是有参构造方式实例化Bean:

复制代码
public UserDaoImpl(String name){System.out.println(name);}

有参构造在实例化Bean时,需要参数的注入,通过标签,嵌入在标签内部提供构造参数,如下:

复制代码
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl"> <!-- "name"表示参数名 "haohao"表示给参数赋的值 --> <constructor-arg name="name" value="haohao"/> </bean>

工厂方式实例化:底层通过调用自定义的工厂方法对Bean进行实例化,又分为如下三种:

1,静态工厂方法实例化Bean

复制代码
// 无参数版
// factory类中内容
public class MyBeanFactory {
    public static userDao userDao(){
        // 利用工厂方法创建对象的好处:
        // Bean创建之前可以进行一些其他业务逻辑操作
        
        return new UserDaoImpl();
    }
}
// xml文件内容
<bean id="userDao1" class="com.mxw.factory.MyBeanFactory" factory-method="userDao"></bean>

// 有参数版
// factory类中内容
public class MyBeanFactory {
    public static userDao userDao(String name,int age){
        return new UserDaoImpl();
    }
}
// xml文件内容
<bean id="userDao1" class="com.mxw.factory.MyBeanFactory" factory-method="userDao">
    <constructor-arg name="name" value="haohao"></constructor-arg>
    <constructor-arg name="age" value="18"></constructor-arg>
</bean>

2,实例工厂方法实例化Bean

java 复制代码
// 无参数版
// factory类中内容
public class MyBeanFactory2 {
    public userDao userDao(){
        return new UserDaoImpl();
    }
}
// xml文件内容
// 实例方法需要现有实例对象才能调用 1,配置工厂对象 2,配置工厂方法
<!-- 配置工厂对象 -->
<bean id="myBeanFactory2" class="com.mxw.factory.MyBeanFactory2"></bean>

<bean id="userDao2" factory-bean="myBeanFactory2" factory-method="userDao"></bean>
// 有参数版
// factory类中内容
public class MyBeanFactory2 {
    public userDao userDao(String username){
        return new UserDaoImpl();
    }
}
// xml文件内容
<!-- 配置工厂对象 -->
<bean id="myBeanFactory2" class="com.mxw.factory.MyBeanFactory2"></bean>

<bean id="userDao2" factory-bean="myBeanFactory2" factory-method="userDao">
    <constructor-arg name="username" value="tom"></constructor-arg>
</bean>

注意:只要是产生bean对象的方法内部参数配置都可以使用标签

3,实现FactoryBean规范延迟实例化Bean

java 复制代码
// factory类中内容
public class MyBeanFactory3 implements FactoryBean<userDao> {
    @Override
    public userDao getObject() throws Exception {
        return new UserDaoImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return userDao.class;
    }
}
// xml文件内容
// 不用配置工厂方法,由于是一种规范,所以默认调用getObject方法
<bean id="userDao3" class="com.mxw.factory.MyBeanFactory3"></bean>

注意:对象不像之前一样保存在beanFactory中的signaltonObjects中(signaltonObjects中保存的是MyBeanFactory3),而是保存在beanFactory中的factoryBeanObjectCache中。并且是在调用了getbean方法时才调用getObject方法返回对象。(用到的时候产生bean对象)

7,Bean的依赖注入配置

Bean的依赖注入有两种方式:

其中,ref是reference的缩写形式,翻译为:涉及,参考的意思,用于引用其他Bean的id。value用于注入普通属性值。

依赖注入的数据类型有如下三种:

1)普通数据类型,例如:String,int,boolean等,通过value属性指定。

2)引用数据类型,例如:UserDaoImpl,DataSource等,通过ref属性指定。

3)集合数据类型,例如:List,Map,Properties等。

java 复制代码
// userServiceImpl类中内容
public class userServiceImpl implements userService {
    // 注入List
    private List<String> stringList;
    private List<userDao> daoList;

    public void setStringList(List<String> stringList) {
        this.stringList = stringList;
    }

    public void setDaoList(List<userDao> daoList) {
        this.daoList = daoList;
    }
}
// xml文件内容
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl">
        <property name="stringList">
            <list>
                <value>1</value>
                <value>2</value>
                <value>3</value>
            </list>
        </property>
        <property name="daoList">
            <list>
                <!-- 方式一 -->
                <bean class="com.mxw.Dao.Impl.UserDaoImpl"></bean>
                <!-- 方式二 -->
                <ref bean="userDao"></ref>
            </list>
        </property>
    </bean>
<!--    该处bean又上面引用 -->
    <bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl"></bean>


// userServiceImpl类中内容
public class userServiceImpl implements userService {

    // 注入set
    private Set<String> stringSet;
    private Set<userDao> daoSet;

    public void setDaoSet(Set<userDao> daoSet) {
        this.daoSet = daoSet;
    }

    public void setStringSet(Set<String> stringSet) {
        this.stringSet = stringSet;
    }
}
// xml文件内容
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl">
    <property name="stringSet">
        <set>
            <value>1</value>
            <value>2</value>
            <value>3</value>
        </set>
    </property>
    <property name="daoSet">
        <set>
            <!--    注入set的两种方式和注入list的两种方式一样,这里只写第一种     -->
            <bean class="com.mxw.Dao.Impl.UserDaoImpl"></bean>
        </set>
    </property>
    <!--Map注入-->
<property name="objects">
    <map>
        <entry key="张明" value="1"></entry>
        <entry key="李白" value="2"></entry>
        <entry key="王伟" value="3"></entry>
    </map>
</property>
</bean>


// userServiceImpl类中内容
public class userServiceImpl implements userService {

    // 注入properties
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public void show() {
        System.out.println(properties);
    }
}
// xml文件内容
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl">
    <property name="properties">
        <props>
            <prop key="p1">ppp</prop>
            <prop key="p2">ppp</prop>
        </props>
    </property>
</bean>

扩展:自动装配方式

如果被注入的属性类型是Bean引用的话,那么可以在标签中使用autowire属性去配置自动注入方式,属性值有两个:

byName:通过属性名自动装配,即去匹配setXxx与id="xxx"(name="xxx")是否一致;

byType:通过Bean的类型从容器中匹配,匹配出多个相同Bean类型时,报错。

java 复制代码
  // 1,byName
  // userServiceImpl类中内容
  public class userServiceImpl implements userService {

    private userDao userDao;

    public void setUserDao(userDao userDao) {
        this.userDao = userDao;
    }
}
// xml文件内容
// 根据id="userDao"的beanName和setUserDao的"UserDao"一样,所以将第二个bean对象注入到第一个bean对象中。
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl" autowire="byName"></bean>
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl"></bean>

// 1,byType
// userServiceImpl类中内容不变
// xml文件内容
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl" autowire="byType"></bean>
// 如果设置多个同类型的bean会报错(报错内容:No qualifying bean of type 'com.mxw.Dao.userDao' available: expected single matching bean but found 2: userDao,userDao1)
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl"></bean>
<bean id="userDao1" class="com.mxw.Dao.Impl.UserDaoImpl"></bean>
8,Spring的其他配置标签

Spring的xml标签大体上分为两类,一种是默认标签,一种是自定义标签

默认标签:就是不用额外导入其他命名空间约束的标签,例如标签

Spring的默认标签用到的是Spring的默认命名空间

该命名空间约束下的默认标签如下:

自定义标签:就是需要额外引入其他命名空间约束,并通过前缀引用的标签,例如标签

标签,除了经常用的作为根标签外,还可以嵌套在根标签内,使用profile属性切换开发环境

java 复制代码
<!--    配置测试环境下,需要加载的Bean实例-->
    <beans profile="test"></beans>
<!--    配置开发环境下,需要加载的Bean实例-->
    <beans profile="dev"></beans>

可以使用以下两种方式指定被激活的环境:

使用命令行动态参数,虚拟机参数未知加载-Dspring.profiles.active=test

使用代码的方式设置环境变量System.setProperty("spring.profiles.active","test")

java 复制代码
// 测试类中内容

// 指定开发环境 (如果指定了环境那么只有环境里面的和公共部分的生效,如果没有指定环境那么只有公共环境的生效)
System.setProperty("spring.profiles.active","dev");

<import>标签,用于导入其他配置文件,项目变大后,就会导致一个配置文件内容过多,可以将一个配置文件根据业务模块进行拆分,拆分后,最终通过标签导入到一个主配置文件中,项目加载主配置文件就连同导入的文件一并加载了

java 复制代码
<!--    导入用户模块配置文件-->
    <import resource="classpath:UserModuleApplicationContext.xml"></import>
<!--    导入商品模块配置文件-->
    <import resource="classpath:UserModuleApplicationContext.xml"></import>

<alias>标签是为某个Bean添加别名,与在标签上使用name属性添加别名的方式一样。

java 复制代码
// xml文件内容
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl"></bean>
<alias name="userDao" alias="aaa"></alias>

在beanFactory中维护着一个名为aliasMap集合,存储别名和beanName之间的映射关系

9,Spring的get方法

java 复制代码
// 测试类中内容
// 根据beanName获取容器中的Bean实现,需要手动强转
userService userService = (userService) applicationContext.getBean("userService");
// 根据Bean类型去容器中匹配对应的Bean实例,如存在多个匹配Bean则报错
userDao userDao = applicationContext.getBean(userDao.class);
// 根据beanName获取容器中的Bean实例,指定Bean的Type类型
userService userService1 = applicationContext.getBean("userService", userService.class);

3,Spring配置非自定义Bean

以上在xml配置的Bean都是自己定义的,例如UserDaoImpl。但是在实际开发中有些功能类并不是我们自己定义的,而是使用的第三方jar包中的,那么,这些Bean要想让Spring进行管理,也需要对其进行配置。

配置非自定义的Bean需要考虑如下两个问题:

1,被配置的Bean的实例化方式是什么?无参构造,有参构造,静态工厂还是实例工厂方式;

2,被配置的Bean是否需要注入必要属性。

三、Bean实例化的基本流程

Spring容器在进行初始化时,会将xml配置的的信息封装成一个BeanDefinition对象(一个bean对应一个BeanDefinition对象),所有的BeanDefinition存储到一个名为beanDefinitionMap的Map集合中,Spring框架在对该Map进行遍历,使用反射创建Bean实例对象,创建好的Bean对象存储在一个名为singletonObjects的Map集合中,当调用getBean方法时则最终从该Map集合中取出Bean实例对象返回。

DefaultListableBeanFactory对象内部维护着一个Map用于存储封装好的BeanDefinitionMap

java 复制代码
public class DefaultListBeanFactory extends ... implements ...{
    // 存储<bean>标签对应的BeanDefinition对象
    // key:是Bean的beanName,value:是Bean定义对象BeanDefinition
    private final Map<String,BeanDefinition> beanDefinitionMap;
}

Spring框架会取出beanDefinitionMap中的每个BeanDefinition信息,反射构造方法或调用指定的工厂方法生成Bean实例对象,所以只要将BeanDefinition注册到beanDefinitionMap这个Map中,Spring就会进行对应的Bean的实例化操作。

Bean实例及单例池singletonObjects,beanDefinitionMap中的BeanDefinition会被转化成对应的Bean实例对象,存储到单例池singletonObjects中去,在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中,维护着singletonObjects。

java 复制代码
// 部分源码
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

总结(Bean实例化的基本流程)

1,加载xml配置文件,解析获取配置中的每个的信息,封装成一个个的BeanDefinition对象;

2,将BeanDefinition存储在一个名为beanDefinitionMap的Map中;

3,ApplicationContext底层遍历beanDefinitionMap,创建Bean实例对象;

4,创建好的Bean实例对象,被存储在一个名为singletonObjects的Map中;

5,当执行applicationContext.getBean(beanName)时,从singletonObjects去匹配Bean实例返回。

四、SpringBean的生命周期

Spring Bean的生命周期是从Bean实例化之后,即通过反射创建出对象之后,到Bean成为一个完整的对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期,Spring Bean的生命周期大体上分为三个阶段:

1,Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;

2,Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法等。

3,Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池singletonObjects中去了,即完成了Spring Bean的整个生命周期。

1,Spring Bean的初始化阶段

Spring Bean的初始化过程涉及如下几个过程:

1,Bean实例的属性填充

2,Aware接口属性注入

3,BeanPostProcessor的before()方法回调

4,InitialzingBean接口的初始化方法回调

5,自定义初始化方法init回调

6,BeanPostProcessor的after()方法回调

1)Bean实例的属性填充

BeanDefinition中有对当前Bean实体的注入信息通过属性propertyValues进行了存储,例如UserService的属性信息如下(beanFactory->beanDefinitionMap->某个beaDefinition对象->value->properValues):

Spring在进行属性注入时,会分为如下几种情况:

注入普通属性,String,int或存储基本类型的集合时,直接通过set方法的反射设置进去

注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完整整个生命周期)后,在进行注入操作;

注入双向对象引用属性时,涉及了循环引用(循环依赖)问题;

多个实体之间相互依赖并形成闭环的情况就叫做"循环依赖",也叫做"循环引用"

java 复制代码
// userDaoImpl文件内容
private userDao userDao;
public void setUserDao(userDao userDao) {
    this.userDao = userDao;
}
// userServiceImpl文件内容
private userServiceImpl userService;
public void setUserService(userServiceImpl userService) {
    this.userService = userService;
}
// xml文件内容
<bean id="userService" class="com.mxw.Service.Impl.userServiceImpl">
    <property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.mxw.Dao.Impl.UserDaoImpl">
    <property name="userService" ref="userService"></property>
</bean>

Spring提供了三级缓存存储完整Bean实例和半成品Bean实例,用于解决循环引用问题

在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map:

java 复制代码
// 1,最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 2,早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用,称之为"二级缓存"
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 3,单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

UserService和UserDao循环依赖的过程结合上述三级缓存详细描述:

1)UserService实例化对象,但尚未初始化,将UserService存储到三级缓存;

2)UserService属性注入,需要UserDao,从缓存中获取,没有UserDao;

3)UserDao实例化对象,但尚未初始化,将UserDao存储到三级缓存;

4)UserDao属性注入,需要UserService,从三级缓存获取UserService(从一级缓存开始查找),UserService从三级缓存移入二级缓存;

5)UserDao执行其他生命周期过程,最终成为了一个完整Bean,存储到一级缓存,删除二三级缓存;

6)UserService注入UserDao;

7)UserService执行其他生命周期过程,最终成为了一个完整Bean,存储到一级缓存,删除二三级缓存。

2)常用的Aware接口

Aware接口是一种框架辅助属性注入的一种思想,其他框架中也可以看到类似的接口。框架具备高度封装性,我们接触到的一般都是业务代码,一个底层功能API不能轻易的获取到,但是这不意味着永远用不到这些对象,如果用到了,就可以使用框架提供的类似Aware的接口,让框架给我们注入该对象。

2,SpringIOC整体流程总结

从解决依赖注入的 "痛点",到支撑微服务架构的 "全能",Spring 的价值从来不止于 "简化代码",更在于它以 IOC 为核心搭建的一套灵活、稳定且持续进化的 "生态体系"。无论是初入行的开发者理解对象管理逻辑,还是深耕多年的架构师优化系统耦合度,IOC 都是探索 Spring 绕不开的关键。当然,Spring 的学习之路并非一蹴而就,从 IOC 容器的原理到 AOP 的实际应用,从 Spring Boot 的快速开发到 Spring Cloud 的分布式治理,每一步深入都能带来新的收获。希望这篇文章能成为你探索 Spring 的起点,接下来不妨亲手搭建一个 Spring 项目,在实践中感受 IOC 如何简化依赖管理 ------ 毕竟,最好的学习方式,永远是 "动手去做"。JUST DO IT。

相关推荐
消失的旧时光-19438 小时前
Android ble理解
java·kotlin
晨晖28 小时前
SpringBoot的yaml配置文件,热部署
java·spring boot·spring
冒泡的肥皂8 小时前
MVCC初学demo(二
数据库·后端·mysql
追逐时光者8 小时前
一款基于 .NET WinForm 开源、轻量且功能强大的节点编辑器,采用纯 GDI+ 绘制无任何依赖库仅仅100+Kb
后端·.net
鬼火儿8 小时前
1.2 redis7.0.4安装与配置开机自启动
java·后端
小马哥编程8 小时前
【软考架构】案例分析-对比MySQL查询缓存与Memcached
java·数据库·mysql·缓存·架构·memcached
一 乐8 小时前
高校后勤报修系统|物业管理|基于SprinBoot+vue的高校后勤报修系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·毕设
朝新_8 小时前
【SpringMVC】SpringMVC 小案例:加法计算器初步理解前后端接口交互与数据处理
java·笔记·spring·交互·javaee
逻极8 小时前
Rust数据类型(上):标量类型全解析
开发语言·后端·rust