【序言】
关于IOC,对于用Spring的人来说这是个随时挂在嘴边的词,也是面试或者对Spring深入研究的人所绕不过去的坎,但是越是这种高频核心点,在网上查找的资料反而大都千篇一律,很难有直击心灵,让人恍然大悟的佳作,颇有灯下黑的感觉。
本着知其然,知其所以然的心理,本来想自己写一篇关于IOC的前世今生,希望自己搞明白为什么要用IOC以及IOC为我们带来哪些好处的文章,但是偶然在网上找到一篇佳作,感觉就是自己想要的。更重要的是从作者字里行间,能感受到对技术的一种执着,我想这对我们这些后辈同行者来说,不免是一种激励,因文章年代较久且结尾略显仓促,遂转载并予以修改和注释
原文链接:http://blog.sina.com.cn/s/blog_40e28e4b01000a1j.html
【IOC的应用场景--WHY&WHAT】
用户需求:用户注册和查询
功能:
1、 保存用户注册的信息;
2、 根据用户的名称获得该注册用户。
场景一 :简单功能实现
我们遵循软件开发的原则“首先让它跑起来,再去优化(重构)它”,我们首先实现最简单的在内存中持久化用户信息。
既然我们要保存和取得用户信息,首先应该设计用户类。代码如下:
public class User {
private Long id;
private String name;
private String password;
private String group;
public User(String name,String password){
this.name = name;
this.password = password;
}
//相应的get/set方法
………..
}
针对用户信息,写一个持久化类,来实现将用户信息保存到内存和查询两个功能
public class MemoryUserPersist {
private static Map users = new HashMap();
static{
User defaultAdmin = new User("Moxie","pass");
users.put(defaultAdmin.getName(),defaultAdmin);
}
public MemoryUserPersist (){
}
public void saveUser(User user){
users.put(user.getName(),user);
}
public User LoadUser(String userName){
return (User)users.get(userName);
}
}
基本的功能类完成后,我们就可以在客户端(并非真实的客户端,任何一个调用方都可以称为是客户端)使用它了。例如:客户端用户注册时,UserRegister代码片断如下:
MemoryUserPersist userPersist = new MemoryUserPersist ();
userPersist.saveUser(user);
可是,现在如果要在文本文件中持久化User,又该如何实现呢?
场景二:支持内存和文本持久化
实现一个TextUserPersist类,这个并不困难。但客户端代码将面临重大灾难:找到所有使用过MemoryUserPersist的客户端类,将他们中的MemoryUserPersist逐个手工修改为 TextUserPersist,并且重新编译,当然以前的测试也必须全部从头来过!
人生的浩劫只是刚刚开始,因为我们要分别实现多种持久化方式!此时,客户端迫切需要与服务端持久化类的解耦,我只负责存储与调用,至于存到哪我不想管,这个救世主就是——接口(Interface)。
面向接口编程
什么是接口?
接口定义了行为的协议,这些行为在继承接口的类中实现。
接口定义了很多方法,但是没有实现它们。类履行接口协议并实现所有定义在接口中的方法。
接口是一种只有声明没有实现的特殊类。
接口的优点:
Client不必知道其使用对象的具体所属类。
一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
松散藕合(loosens coupling)。
增加了重用的可能性。
接口的缺点:
设计的复杂性略有增加
1. 根据接口来实现上面的优化,设计持久化的接口
public interface UserDao {
public void save(User user);
public User load(String name);
}
2、 具体的持久化来必须要继承UserDao接口,并实现它的所有方法。我们还是首先实现内存持久化的用户类:
public class MemoryUserDao implements UserDao{
private static Map users = new HashMap();;
static{
User user = new User("Moxie","pass");
users.put(user.getName(),user);
}
public void save(User user) {
users.put(user.getId(),user);
}
public User load(String name) {
return (User)users.get(name);
}
}
MemoryUserDao的实现代码和上面的MemoryUserPersist基本相同,唯一区别是MemoryUserDao类继承了UserDao接口,它的save()和load()方法是实现接口的方法。同样的方式我们再实现一个TestUserDao类继承UserDao接口,
这时,客户端UserRegister的代码又该如何实现呢?
UserDao userDao = new MemoryUserDao();
// UserDao userDao = new TextUserDao();
userDao.save(user);
现在的实现起码能保证客户端在调用UserDao的时候不用关心服务端存储是在内存还是文本,达到在客户端内部初步解耦的目的,但是客户端定义的对象还要指定具体的实现类,仍然没能实现客户端与服务端的解耦。
那如何实现彻底的客户端与服务端解耦呢?
场景三:客户端与服务端要彻底解耦
实现客户端与服务端彻底解耦,最好是所有的判断都在服务端,客户端只需要调用服务端的一个对象,但是客户端不能直接调用服务端的接口,那么就必须让服务端自己根据场景去判断并生成正确的实例,对设计模式稍微熟悉的小伙伴这时候就会想到一种设计模式--工厂模式,它的应用场景就是根据不同的条件去实例化不同对象,将判断从客户端转移到生产的服务端。
简单说一下工厂模式
1)没有工厂时代:客户端自己创建实例
2)简单工厂模式:客户端只对接一个工厂,创建实例交给工厂,但是需要增加服务类型,就要修改工厂支持
3)工厂方法模式:客户端对接符合工厂标准的多个子工厂(即继承),根据自己需求来指定工厂
我们使用一个简单工厂类来实现userDao对象的创建,这样客户端只要知道这一个工厂类就可以了,不用依赖任何具体的UserDao实现。创建userDao对象的工厂类UserDaoFactory代码如下:
public class UserDaoFactory {
public static UserDao createUserDao(String type){
if ("m".equals(type)){
return new MemoryUserDao();
}else{
return new TextUserDao();
}
}
}
客户端UserRegister代码片断如下:
UserDao userDao = UserDaoFactory. CreateUserDao();
userDao.save(user);
现在如果再要更换持久化方式,比如使用文本文件持久化用户信息。就算有再多的客户代码调用了用户持久化对象我们都不用担心了。因为客户端和用户持久化对象的具体实现完全解耦。我们唯一要修改的只是一个UserDaoFactory类。
但是,在服务端我们还是要对工厂进行修改,这种硬编码就意味着功能修改代码就要变动,代码的修改就意味着重新编译、打包、部署甚至引入新的Bug。
如何完全消除服务端硬编码?
场景四:消除服务端工厂硬编码
如何才是我们心目中的完美方案?至少要消除更换持久化方式时带来的硬编码。我们第一反应应该是反射+可配置,具体实现类的可配置不正是我们需要的吗?我们在一个属性文件中配置UserDao的实现类,例如:
在属性文件中可以这样配置:userDao = com.test.MemoryUserDao。UserDao的工厂类将从这个属性文件中取得UserDao实现类的全名,再通过Class.forName(className).newInstance()语句来自动创建一个UserDao接口的具体实例。UserDaoFactory代码如下:
public class UserDaoFactory {
public static UserDao createUserDao(){
String className = "";
// ……从属性文件中取得这个UserDao的实现类全名。
UserDao userDao = null;
try {
userDao = (UserDao)Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return userDao;
}
通过对工厂模式的优化,我们的方案已近乎完美。如果现在要更换持久化方式,不需要再做任何的手工编码,只要修改配置文件中的userDao实现类名,将它设置为你需要更换的持久化类名即可。
我们终于可以松下一口气了?不,矛盾仍然存在。我们引入了接口,引入了工厂模式,让我们的系统高度的灵活和可配置,同时也给开发带来了一些复杂度:
- 本来只有一个实现类,后来却要为这个实现类引入了一个接口。
- 引入了一个接口,却还需要额外开发一个对应的工厂类。
- 工厂类过多时,管理、维护非常困难。
场景五:工厂管理优化(IOC)
当然,面向接口编程是实现软件的可维护性和可重用行的重要原则已经勿庸置疑。这样,第一个复杂度问题是无法避免的,再说一个接口的开发和维护的工作量是微不足道的。
但后面两个复杂度的问题,如果能提取一个共同的工厂类,可以读取配置信息,并能返回对应的实例,就能解决这两个问题,但是这样又给我们带来新的麻烦,一个工厂类怎么可能返回不同类型的实例呢?此时,我们可以定义一个包装类(BeanDefination),将需要产生的实例作为该类的属性信息,此时无论什么样的类,用户都能通过getBean方法获取,也就达到我们彻底解耦+消除硬编码+管理问题了。
但是,事情还远没有结束,优化是永无止境的,如果我每次使用Bean都去getBean一下,说实话不仅烦而且Low,于是DI应用而生,实现一处注入,处处使用。至此,如果仅仅是解决上面这些问题,那么实现这些功能的IOC已经满足需求,对整个流程的优化也算结束了。
【自定义IOC/DI的实现--HOW】
下面我们通过自定义IOC/DI框架,来看看IOC/DI能否解决我们上面的问题,整体的思路是:
- 在配置文件添加需要创建的Bean信息
- 自定义DI接口Inject,并通过@Inject注入实例
- CustomBeanFactory在初始化时,根据配置文件生成Bean实例,并注入bean的Map---完成IOC功能
- 在CustomBeanFactory初始化过程中,通过扫描@Inject接口,设置包含该注解的属性为对应的实例---完成DI功能
- 通过getBean方法调用
代码的整体结构如图:
其中,Controller,Domain,Repository,Service下面的代码不做介绍,直接贴代码
public class UserController {
@Inject
private UserService userService;
public void save()
{
userService.save();
}
public String load() {
return userService.load();
}
}
public class UserService {
@Inject
private MemUserRepositoryImpl memUserRepository;
public void save(){
memUserRepository.save();
}
public String load() {
return memUserRepository.load();
}
}
public class MemUserRepositoryImpl {
@Inject
private User user;
public void save(){
user.setName("test0");
user.setGroup("test1");
user.setPassword("123");
}
public String load(){
return user.toString();
}
}
public class User {
private String name;
private String password;
private String group;
// getter,setter..省略
}
@Inject的注解如下所示,用来进行DI功能
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
配置文件中添加需要创建的Bean,当然,这么做只是为了省事,一般是通过Xml或者注解的方式来实现Bean的注入的。
user=com.example.spring.zyq.customioc.Domain.User
userRepository=com.example.spring.zyq.customioc.Repository.MemUserRepositoryImpl
userService=com.example.spring.zyq.customioc.Service.UserService
userController=com.example.spring.zyq.customioc.Controller.UserController
重点看一下工厂类的实现,此处只为展示IOC/DI功能实现,不涉及单例,循环依赖等问题的实现。
public class CustomBeanFactory {
private static Properties prop = new Properties();
private static Map<Class,Object> beans = new HashMap<>();
// 构造函数内完成Bean的创建
public CustomBeanFactory() {
try {
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("application.properties");
prop.load(inputStream);
Enumeration<Object> keys = prop.keys();
//对于配置文件中配置类路径,循环创建实例
while (keys.hasMoreElements()){
String key = keys.nextElement().toString();
String beanPath = prop.getProperty(key);
Class clazz = Class.forName(beanPath);
circleAssembleBean(clazz);
}
}catch (Exception e){
e.printStackTrace();
}
}
private <Q> Q circleAssembleBean(Class<Q> clazz) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
Q instance = null;
if (beans.get(clazz)==null){
instance = clazz.newInstance();
beans.put(clazz,instance);
//查找字段中是否有字段被@Inject注解,存在则递归实例化
Field[] fields = clazz.getDeclaredFields();
for (Field field:fields) {
Inject inject = field.getAnnotation(Inject.class);
if (inject!=null){
Object object = circleAssembleBean(field.getType());
if(!field.isAccessible()){
field.setAccessible(true);
}
//依赖注入,可以理解为对设置的变量进行赋值,比如Controller中的private UserService userService,此处将userService之前引用为null,此时替换为对应的实例引用
field.set(beans.get(clazz),object);
}
}
}
return instance;
}
public Object getBean(Class clazz){
return beans.get(clazz);
}
在main函数中进行调用
public class CustomIocApplication {
public static void main(String[] args) {
CustomBeanFactory customBeanFactory = new CustomBeanFactory();
UserController userController = (UserController) customBeanFactory.getBean(UserController.class);
userController.save();
System.out.println(userController.load());
}
}
则输出如图所示:
至此,完成整个自定义IOC/DI功能,仅需一个Factory就能完成所有类的实例化,且能自动注入。当然,Spring的IOC/DI比这要复杂得多,而且IOC发展到现在,已经不仅仅是解决工厂管理优化的问题,作为整个Spring的核心,它还兼顾性能优化,功能拓展等其他角色,于是,比如状态翻转本身,比如单例,比如基于IOC的AOP实现等,所以,学习IOC也不能单从某一方面,而把它看成是一种综合解决方案更合适。