我spring的博客分为分析篇和使用篇,这里属于分析篇。注重分析,以便日后回顾。
本文前言:要想搞清楚Spring的工作原理,首先需要还原一个场景需求,即我们在使用spring前,到底遇到了什么样的问题,才让我们转而使用spring框架。
场景模拟还原
1. 需要实现用户账户的保存功能。
这里采用普通的开发模拟,主要体现一种思想和结构。
2. 在没有使用spring之前,按照一般的开发思路,我采用的是这样的结构:
- Dao -> UserDao 数据持久层接口
- DaoImpl -> UserDaoImpl 数据持久层实现类
- service -> UserService 用户业务层接口类
- serviceImpl -> UserServiceImpl 用户业务层实现类
- ui -> Client 模拟客户端表现层
详细结构如图:
3. 类中具体代码内容如下(用简单的输出来表示模拟保存):
当然如果想使用一下原生的JDBC连接数据库,可以参考这篇文章,加以回顾:
JDBC连接数据库(原生方式)
- UserDao 数据持久层接口
/**
* 账户的持久层接口
* @author QingBang.Wang
*/
public interface UserDao {
/**
* 模拟保存
*/
void saveUser();
}
- DaoImpl -> UserDaoImpl 数据持久层实现类
import day02.User.Dao.UserDao;
/**
* @author QingBang.Wang
*/
public class UserDaoImpl implements UserDao {
public void saveUser(){
System.out.println("保存了账户");
}
}
- service -> UserService 用户业务层接口类
/**
* 业务层接口
* @author QingBang.Wang
*/
public interface UserService {
/**
* 模拟保存
*/
void saveUser();
}
- serviceImpl -> UserServiceImpl 用户业务层实现类
import day02.User.Dao.UserDao;
import day02.User.service.UserService;
/**
* 账户的业务层实现类
* @author QingBang.Wang
*/
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
public void saveUser(){
userDao.saveUser();
}
}
- ui -> Client 模拟客户端表现层
import day02.User.service.UserService;
/**
1. 控制层+模拟表现层
2. @author QingBang.Wang
*/
public class Client {
public static void main(String[] args) {
UserService us = new UserServiceImpl();
us.saveUser();
}
}
4. 运行测试
5. 在写好这些类后,问题便浮出水面:模块之间耦合度太高!请看以下截图:
正是这种 new 的方式,让各个模块间产生了很强的耦合度。依赖关系难以松解。
如何解决这个耦合度过高的问题呢?
在写JDBC代码的时候,我们曾使用了Class.forName()来加载,而不是使用new的方式来加载驱动,这为我们解决问题提供了一种解决思路。
因而,这里我们从这个角度进行入手:
初步思路的探索过程
1. 单纯的使用Class.forName(“类名”),好处和弊端是什么?
好处:能够在一定程度上实现解耦
弊端:括号里面的类名是写死的,失去了代码的灵活性
2.为了解决上述的弊端,我们利用配置文件的方式来解决:
配置文件常采用 xml 和properties 两种,由于properties的较为简洁,因而我使用properties来描述,后来的xml配置会在使用篇中介绍。
- 在resource文件中,创建bean.properties文件
UserService=day02.User.serviceImpl.UserServiceImpl
UserDao=day02.User.DaoImpl.UserDaoImpl
3.有了配置文件,我们就可以使用工厂模式来加载配置文件中的类
Bean 有着可重复使用组件的含义
- 创建Factory包,新建BeanFactory类
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @author QingBang.Wang
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//使用静态方法代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据Bean的名称获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
Object bean = null;
try {
String beanPath = props.getProperty(beanName);
//用反射的方式来创建对象
bean = Class.forName(beanPath).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
4.相应的,我们修改我们最初的工程代码
-
修改serviceImpl -> UserServiceImpl 用户业务层实现类
-
修改ui -> Client 模拟客户端表现层
5.此时,我们进行测试运行
表明初步解耦成功
6.然鹅,出现了一个新的问题,我们进行一个简单的测试:
修改客户端:
运行:
7.不知道大家眼不眼熟,这就是java中的多例!
这里不详细讲述多例和单例
当前实例就是多例的一种,servlet 则通常是一种单例
多例:简述将就是每次使用产生新的对象,拥有独立的成员变量
单例:每次都使用同一个对象
在我们的service和dao中全是多例,没有单例中可以改变的类成员,想要实现改变类成员,只能在方法中定义。
同时,由于每次使用都产生新的对象,然后销毁,因而多例的执行效率远不如单例!
我们知道,由于java中的垃圾回收机制,虚拟机中创建的对象,一段时间不使用就会被销毁,如果我们想要保存产生的对象,我们需要马上把它存起来,存起来的这个东西,我们就称为容器。
8. 到这里,我们自然而然的就引入了容器这一思想
那么,如何做到产生容器的效果呢?
没错,就是利用我们Java中非常重要的反射机制!
Map就要登场了
再次改良我们的解决方案
1.首先到BeanFactory中,值得注意的是,我们找到了多例产生的原因,如图
正是因为 newInstance()的存在。才会每次都产生新的对象出现。
bean = Class.forName(beanPath).newInstance();
每次都会调用默认构造函数创建新的对象
2. 我们对BeanFactory进行修改:
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* @author QingBang.Wang
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//定义一个Map,用于存放我们要创建的对象,称之为容器
private static Map<String,Object> beans;
//使用静态方法代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中所有的key
Enumeration keys = props.keys(); //Enumeration是枚举类型
//遍历枚举
while (keys.hasMoreElements()){
//取出每个key
String key = keys.nextElement().toString();
//根据key获取value
String beanPath = props.getProperty(key);
//反射创建对象
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//进行改造!
/**
* 单例模式,根据Bean的名称获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
/**
* 多例模式,根据Bean的名称获取bean对象
* @param beanName
* @return
*/
// public static Object getBean(String beanName){
// Object bean = null;
// try {
// String beanPath = props.getProperty(beanName);
// //用反射的方式来创建对象
// bean = Class.forName(beanPath).newInstance(); //每次都会调用默认构造函数创建对象
// } catch (Exception e) {
// e.printStackTrace();
// }
// return bean;
// }
}
需要注意的是:
以及
静态代码块只在类执行的时候加载一次,此时已经将对象创建完毕放入容器中,
再次创建对象时就可以实现从容器中取出来!
3. 此时,再次运行客户端进行测试
也就是说,此时,多例模式已经成功的转为单例模式,同时实现了容器的存储,对象完全没必要反复创建
类成员也可以重复使用了,当然如果不想重复使用,完全可以放到方法中。
谜底揭晓
我们再次改良的方案,其实就是我们常说的IOC,控制反转
那么在我们探索的阶段,经过了怎样的模式变化呢?
下图是我们最初的模型,也就是说,工程中不仅拥有着很强耦合度,同时也很难保持工程资源的独立性
下图则是我们采用容器后产生的效果:
结束
扩展思考:这种模式为什么叫控制反转呢?
在我们的类中,类拥有着决定是否采用new这种方式或者采用容器工厂种方式的权利,如果选择了new这种方式,那么这个类便决定了将创建对象的控制权交给自己,拥有着独立控制权。而采用了容器工厂这种方式,则决定了是将控制权交由工厂处理,类本身无法独立拥有控制权,其中的好处便是降低了程序间的依赖关系,实现了资源的有效管控,削减计算机程序的耦合
至此,整体的探索也就完成了,从中也体现出了Spring当中最最重要的概念–IOC
不得不佩服创造这一思想的巨人们,让我们得以站在了巨人的肩膀上。