Spring系列(一)——Spring的入门之IOC、DI和基本配置讲解
Spring可以说是框架学习中绕不过去的一座山,也是当前面试或者是工作中开发所必须要掌握和熟练的技术。本篇文章将对主要对Spring的IOC(控制反转)和DI(依赖注入)概念进行介绍和演练,同时针对常用的标签进行讲解,希望对各位读者有所帮助。
一、Spring的介绍
按照以往的风格,在学习一门新技术的时候,最好先了解一下这门技术的作用和起源。
先说说什么是Spring
Spring是分层的 Java SE/EE应用 full-stack 轻量级开源框架,以 IOC(Inverse Of Control:反转控制)和AOP(Aspect Oriented Programming:面向切面编程)为内核。
Spring提供了展现层 SpringMVC和持久层 Spring JDBCTemplate以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。
简单来说,Spring是满足Java SE/EE开发需求,简化我们开发流程的框架,它除了自身体系中的Spring MVC和JDBC Template之外,还能很方便地整合其他第三方框架,使得我们的开发变得更加简洁,能够更加专注于业务逻辑的实现。
再说说Spring的发展历程
自1997年IBM提出EJB的思想到2006年EJB3发布,EJB一直是遵循J2EE规范,被许多项目采用的解决方案之一。但是EJB的实现非常复杂,在业务代码之外,需要生成大量的描述性代码。这给开发效率带来了很大的问题。同时EJB的测试和框架集成的工作也十分的麻烦。在2004年,Rod John(Spring 之父)在其《Expert One-to-One J2EE Development without EJB》作品中阐述了 J2EE 开发不使用 EJB的解决方式,这也就是Spring 雏形的由来。同年,Spring1.0诞生,而后经过不断完善,其功能和生态不断发展增强,也越来越受到开发者的青睐。2017 年 09 月,Spring 5.0 发布。
本篇文章对Spring的描述也是基于5.0版本进行的。
二、Spring的优势和体系结构
(一)Spring的优势(看看就行)
1)方便解耦,简化开发
通过 Spring 提供的 IoC容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度耦合。
用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
2)AOP 编程的支持
通过 Spring的 AOP 功能,方便进行面向切面编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松实现。
3)声明式事务的支持
可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务管理,提高开发效率和质量。
4)方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可做的事情。
5)方便集成各种优秀框架
Spring对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的支持。
6)降低 JavaEE API 的使用难度
Spring对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的使用难度大为降低。
7)Java 源码是经典学习范例
Spring的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对Java 设计模式灵活运用以及对 Java技术的高深造诣。它的源代码无意是 Java 技术的最佳实践的范例。
(二)Spring的体系结构
我们从下面的图片可以看到,最底下的Test模块表示spring框架的可测试性,我们主要需要关注的是倒数第二层的核心容器,Beans表示spring的bean管理,Core是spring的核心代码,Context是spring容器的上下文,SpEL表示spring的表达式。自顶向上分别对应持久层和web层两个模块,持久层中有JDBC,事务等功能,Web模块中包含了WebSocket和Servlet等功能。
三、Spring的IOC
(一)IOC的快速入门
IOC的英文全称是inverse of controller,也就是中文的控制反转。其实含义也很好理解,我们以前写代码时创建对象的方法,一般都是直接“new”对象出来使用,而控制反转就是将对象的管理者从开发人员转移到了Spring身上,由Spring帮我们管理我们的对象。
这样做有什么好处呢?
最明显的好处是降低了代码间耦合度。我们可以看一下下面的代码:
模拟controller处理用户请求,调用service方法的过程
- Controller层
public class UserController {
// 模拟controller调用service
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.save();
}
}
- Service层(接口和实现)
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
public void save() {
UserDao userDao = new UserDaoImpl();
userDao.save();
}
}
- Dao层(接口和实现)
public interface UserDao {
void save();
}
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save方法执行了...");
}
}
在上面的代码中,我们自然可以利用多态的特性,使用"new"的方式创建service和dao接口的对应实现子类,来完成对应方法的调用。但这样的话会存在一个问题:假如现在我们需要更换掉某一个接口的实现子类,我们就不得不在代码中修改其实现子类,当需要替换的地方比较多时,我们就需要花费比较多的时间在修改代码上面。
那么,有没有更好的一种解决方式呢?
答案自然是有的,在原先对象的创建是通过我们手动new创建出来的,如果我们把(子类)对象的创建交由spring来控制,让它来管理具体的实现类。
下面我们就来具体的进行实现吧:
步骤一:引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
步骤二:创建spring配置文件
spring配置文件的文件名可以自定义,但习惯性我们会将文件命名为applicationContext.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">
<!--配置userDao接口实现子类-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口实现子类-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl"></bean>
</beans>
步骤三:将对象的创建改为从读取配置文件中读取
- Controller层
public class UserController {
// 模拟controller调用service
public static void main(String[] args) {
// 给定配置文件的路径和文件名
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) app.getBean("userService");
userService.save();
}
}
- Service层
public class UserServiceImpl implements UserService {
public void save() {
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) app.getBean("userDao");
userDao.save();
}
}
经过这样处理之后,如果后面的开发中需要更换接口的实现子类,我们只需要改一下配置文件即可。本质上spring的IOC做的工作就是对原有的逻辑进行了抽取和封装而已。
(二)Bean标签的使用
我们在上一小节的spring配置文件中,使用了<bean>标签进行实现类的配置。默认情况下它调用的是类中的无参构造函数,如果没有无参构造函数则不能创建成功。
同时,id 属性表示Bean实例在Spring容器中的唯一标识(id属性具有唯一性);class属性表示id标识具体对应的实现类。
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl"></bean>
作用范围
spring不仅能够帮助我们管理对象的创建,还可以控制对象的作用范围。
| 取值范围 | 说明 |
|---|---|
| singleton | 单例 |
| prototype | 多例的 |
| request | WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中 |
| session | WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中 |
| global session | WEB 项目中,应用在 Portlet 环境,如果没有 Portlet 环境那么globalSession 相当于 session |
关于单例和多例我们很好理解,二者的区别在于前者只会创建一个实例对象,后者会每次调用时都会创建一个实例对象。我们可以用下面的代码来简单的进行演示:
- 配置对象作用范围
<!--配置userDao接口实现子类-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl" scope="singleton"></bean>
<!--配置 userService 接口实现子类-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" scope="prototype"></bean>
- 编写测试代码
public static void scopeTest(){
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService1 = (UserService) app.getBean("userService");
UserService userService2 = (UserService) app.getBean("userService");
UserDao userDao1 = (UserDao) app.getBean("userDao");
UserDao userDao2 = (UserDao) app.getBean("userDao");
System.out.println("userService1:" + userService1);
System.out.println("userService2:" + userService2);
System.out.println("userDao1:" + userDao1);
System.out.println("userDao2:" + userDao2);
}
除了实例化个数的区别外,不同的作用范围对应的实例化时机和对应bean的生命周期。
我们还可以通过设置对象的初始化和销毁方法,来满足特定场景下的需求:
init-method:初始化对象时执行的方法
destroy-method销毁对象时执行的方法
比如我们给userDaoImpl设置对应的初始化和销毁方法
- 在spring配置文件配置bean的对应属性
<!--配置userDao接口实现子类-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl" scope="singleton" init-method="initMethod" destroy-method="destroyMethod" ></bean>
- 在类中定义具体的初始化和销毁方法
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save方法执行了...");
}
public void initMethod(){
System.out.println("userServiceImpl初始化了...");
}
public void destroyMethod(){
System.out.println("userServiceImpl已经销毁了...");
}
}
- 在方法中进行调用
public class SpringTest {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) app.getBean("userDao");
userDao.save();
((ClassPathXmlApplicationContext)app).close();
}
}
Bean实例化的三种方式
(1)无参构造方法实例化
在之前的学习中我们知道,默认情况下spring是通过无参构造方法来创建对象的,但如果bean中没有默认无参构造函数,将会创建失败
<!--配置userDao接口实现子类-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
(2)工厂静态方法实例化
- 创建工厂类
public class StaticFactoryBean {
public static UserDao createUserDao(){
return new UserDaoImpl();
}
}
- spring配置文件
<!--配置userDao接口实现子类-->
<bean id="userDao" class="com.qiqv.dao.UserDaoFactoryBean" factory-method="getUserDaoInstance"></bean>
(3)工厂实例方法实例化
工厂的非静态方法返回Bean实例
- 创建工厂类实例方法
public class UserDaoFactoryBean {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
- 将工厂类配置为bean实例创建方式
<!--工厂类-->
<bean id="userFactory" class="com.qiqv.dao.UserDaoFactoryBean"></bean>
<!--配置userDao接口实现子类-->
<bean id="userDao" factory-bean="userFactory" factory-method="getUserDao" ></bean>
五、Spring的DI(依赖注入)讲解
依赖注入(Dependency Injection):它是 Spring 框架核心 IOC 的具体实现。
在上一节中有过这么一个案例,我们在配置文件中配置了userDao和userService对应的实现类,然后在每次调用的时候创建applicationContext上下文对象来获取对应的实例。创建多次上下文对象是一种浪费资源的做法,我们在调用某个方法时,我们就知道了具体要获取哪些实现类,只是因为分层的存在使得获取实现的动作变得不连贯。在这种场景下我们自然会希望有一种方法可以在调用某个方法的时候一次性将所有需要的bean给创建出来,这就是Spring的依赖注入的由来。
(一)Spring依赖注入的方式
(1)构造方法注入
- 在配置文件中使用
constructor-arg标签进行依赖注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口实现子类-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" >
<constructor-arg name="userDao" ref="userDao"></constructor-arg>
</bean>
- 在实现类中定义变量和构造方法
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
userDao.save();
}
}
有了这种方式后,以后我们就不需要每次获取对象都创建applicationContext上下文对象了。
(2) set方法注入
- 在配置文件中使用
property标签进行依赖注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口实现子类-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" >
<!-- name表示实体类中属性的名称,ref表示配置文件中具体实现类的id标识 -->
<property name="userDao" ref="userDao"></property>
</bean>
- 在实现类中定义变量和set方法
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
userDao.save();
}
}
(3) p名称空间注入(比较少用,了解即可)
P命名空间注入本质也是set方法注入,但比起上述的set方法注入更加方便,主要体现在配置文件中,如下: 首先,需要引入P命名空间:
xmlns:p="http://www.springframework.org/schema/p"
然后将注入方式从property标签改为p属性注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口实现子类-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" ></bean>
具体的UserServiceImpl类还是需要拥有userDao属性的set方法,这里就不再展示。
(二)Spring依赖注入的数据类型
上面的操作,都是注入的引用Bean,处了对象的引用可以注入,普通数据类型,集合等都可以在容器中进行注入。
(1)普通数据类型的引入
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="age" value="15"></property>
<property name="name" value="xiaoming"></property>
</bean>
(2)集合数据类型的引入
List<String>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="strList" >
<list>
<value>xiaoming</value>
<value>xiaohong</value>
<value>xiaolan</value>
</list>
</property>
</bean>
List<User>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userList">
<list>
<ref bean="user1"></ref>
<ref bean="user2"></ref>
</list>
</property>
</bean>
<bean id="user1" class="com.qiqv.pojo.User">
<property name="name" value="xiaoming"></property>
<property name="age" value="15"></property>
</bean>
<bean id="user2" class="com.qiqv.pojo.User">
<property name="name" value="xiaohong"></property>
<property name="age" value="41"></property>
</bean>
Map<String,User>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userMap">
<map>
<entry key="user1" value-ref="user1"></entry>
<entry key="user2" value-ref="user2"></entry>
</map>
</property>
</bean>
<bean id="user1" class="com.qiqv.pojo.User">
<property name="name" value="xiaoming"></property>
<property name="age" value="15"></property>
</bean>
<bean id="user2" class="com.qiqv.pojo.User">
<property name="name" value="xiaohong"></property>
<property name="age" value="41"></property>
</bean>
Properties
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userMap">
<props>
<prop key="name1">xiaoming</prop>
<prop key="name2">xiaohong</prop>
<prop key="name3">xiaolan</prop>
</props>
</property>
</bean>
六、引入其他配置文件
实际开发中,Spring的配置内容非常多,这就导致Spring配置很繁杂且体积很大,所以,可以将部分配置拆解到其他配置文件中,而在Spring主配置文件通过import标签进行加载
<import resource="applicationContext-xxx.xml"/
七、ApplicationContext继承体系的介绍
在前面的代码演示中,我们使用ApplicationContext接口的实现类来作为加载spring配置文件的启动类,那么该接口所处的继承体系到底是怎么样的呢?我们可以参考一下下面的图片:

对于
ApplicationContext接口而言,其主要的实现类有以下三个:
(1)ClassPathXmlApplicationContext
它是从类的根路径下加载配置文件 推荐使用这种
(2)FileSystemXmlApplicationContext
它是从磁盘路径上加载配置文件,配置文件可以在磁盘的任意位置。
(3)AnnotationConfigApplicationContext
当使用注解配置容器对象时,需要使用此类来创建 spring 容器。它用来读取注解。
八、Properties标签的使用
我们知道,使用原始JDBC的API进行操作的话,我们需要在代码中对drivername、username等信息进行手动配置,但这种硬编码在之后需要修改配置的时候会比较麻烦,所以演变到后面的封装到.properties文件中。这样我们就可以将JBDC配置写在配置文件中,但这样还不够简便,我们还是需要通过getProperty的方式对参数进行封装,通过封装得到数据库连接对象Connection,当然了更多时候我们都是将参数封装到连接池中。
但现在有了spring之后,这部分的工作就可以交由spring来完成:
- 引入数据库驱动依赖和C3P0连接池依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
- 配置spring核心配置文件
applicationContext.xml
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
- 在代码中进行测试
public class SpringTest {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
DataSource dataSource = (DataSource) app.getBean("dataSource");
System.out.println(dataSource);
}
}
我们虽然通过配置的方式将JDBC的参数写在了配置文件中,但我们习惯上为了解耦,还是会将这些数据库连接参数另外封装到一个jdbc.properties文件中,这时候我们就可以使用context:property-placeholder标签来导入properties文件中的参数到spring配置文件中。
首先,需要引入context命名空间和约束路径:
命名空间:
xmlns:context="http://www.springframework.org/schema/context"
约束路径:
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
然后在核心配置文件中导入和使用我们的配置,我们可以在property标签中使用${ }的方式来读取property文件key对应的value值。
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${user.driverClass}"></property>
<property name="jdbcUrl" value="${user.jdbcUrl}"></property>
<property name="user" value="${user.user}"></property>
<property name="password" value="${user.password}"></property>
</bean>
说在最后
实际上,在了解IOC和DI的作用之后,掌握并快速上手并不难,对于程序员来说学习更重要的意义在于知道这门技术存在的背景和解决问题的思想,知晓这门技术的优劣,这样对自己的技术成长可能更有帮助。
参考资料
Spring诞生前夕的世界:
https://www.jianshu.com/p/5783dc936516
浙公网安备 33010602011771号