Spring依赖注入DI
开头语:本文是我整理网上各种资料后整合而成的一篇文章。文中涉及到了代码重构、面向接口编程、持久化和工厂设计模式的内容。
1. Spring是什么?
Spring是一个开源框架,是于2003年兴起的一个轻量级Java开发框架。它是为了解决企业应用开发的复杂性而创建的。
说它是轻量级的是因为从jar包大小与开销两方面而言Spring都是轻量的。
- Spring框架目的:解决企业应用开发的复杂性
- Spring框架应用范围:任何Java应用
为什么选择使用Spring?现实往往是这样的:
- 前辈的代码用了Spring
- 经常听到Spring的名字,应该很高端
- 技术文章经常提到Spring
- 很多开源框架都支持Spring
- 很多国内外公司都在用Spring
事实上Spring能带给我们很多—Spring特性:
- 方便解耦,简化开发—通过Spring提供的IoC容器,我们可以将对象之间的依赖关系交由Spring进行控制,避免硬编码所造成的过度耦合
- —面向切面AOP编程的支持—利用Spring可以轻松的进行面向切面编程
- —声明式事务的支持—在Spring中,我们可以从单调烦闷的事务管理中解耦出来,通过声明式方式灵活地进行事物的管理,提高开发效率和质量
- 方便程序的测试—利用Spring,测试不再是昂贵的操作
- 方便集成各种优秀框架–Spring可以降低各种框架的使用难度,Spring提供了对各种优秀框架的直接支持:Struts,Hibernate、Hessian、Quartz等
- 降低Java EE API的使用难度–Spring对很多难用的Java EE API(如JDBC、JavaMail,远程调用等)提供了一个封装层,这使Java EE API的使用难度大为降低了
- Spring MVC—Spring提供了一个优秀的Web框架,它在框架设计、扩展性、灵活性等方面都超越了Struts、WebWork等MVC框架
2. IoC/DI介绍
2.1 为什么介绍IoC/DI?
IoC/DI是Spring框架的核心(没有之一),可以说它是Spring在开源框架界走红的原因。
IoC控制反转是Spring容器的内核,AOP、声明式事务等功能都在此基础上开花结果的。
2.2 引子—用户注册的例子
由于IoC/DI的概念比较难解释,下面使用一个代码重构的例子来逐步引出IoC的概念。
我们先看看需求:实现一个用户注册信息持久化的类。功能:
- 保存用户注册的信息;
- 根据用户的名称获得该注册用户。
虽然功能简单,但它对持久化方式的要求却非常的灵活:
- 支持内存中持久化,供测试、演示使用。
- 支持持据化到文本文件中。
- 支持持久化到数据库中。
- 未来还可能需要支持使用开源框架进行持久化,比如:iBATIS、Hibernate等。
用户注册的例子—实现
如何去设计、实现我们这个持久化类呢?
我们遵循软件开发的原则“首先让它跑起来,再去优化(重构)它”,我们首先实现最简单的在内存中持久化用户信息。
既然我们要保存和取得用户信息,首先应该设计用户类。代码如下:
//User.java
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方法
......
}
持久化类有两个方法,分别在内存中保存和获取User对象。代码如下:
//MemoryUserDao.java
public class MemoryUserDao {
private static Map<String, User> users = new HashMap<String, User>();
public void saveUser(User user){
users.put(user.getName(),user);
}
public User queryUser(String userName){
return users.get(userName);
}
}
用户持久化类完成之后,我们就可以在业务逻辑代码中使用它了。例如:用户注册时,用户注册类UserRegisterService代码片断如下:
//UserRegisterService.java
public class UserRegisterService{
private MemoryUserDao userDao = new MemoryUserDao();
public boolean register(User user){
...
MemoryUserDao userDao = new MemoryUserDao();
userDao.saveUser(user);
...
}
}
可是,现在如果要在文本文件中持久化User,又该如何实现呢?实现一个TextUserDao类,这个并不困难。但业务逻辑代码将面临重大灾难:需要找到所有使用过MemoryUserDao的业务逻辑类,将他们中的MemoryUserDao逐个手工修改为 TextUserDao,并且重新编译,当然以前的测试也必须全部从头来过!
人生的浩劫只是刚刚开始,因为根据前面的需求我们至少要分别实现四种持久化方式!这时,你一定和我一样在期待着救世主的早日降临——接口(Interface)。
面向接口编程
什么是接口?
¨ 接口定义了行为的协议,这些行为在继承接口的类中实现。
¨ 接口定义了很多方法,但是没有实现它们。类履行接口协议并实现所有定义在接口中的方法。
¨ 接口是一种只有声明没有实现的特殊类。
接口的优点:
¨ 使用者不必知道其使用对象的具体所属类。
¨ 一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
¨ 对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
¨ 松散藕合(loosens coupling)。
¨ 增加了重用的可能性。
接口的缺点:
设计的复杂性略有增加
(用户持久化类)重构第一步——面向接口编程
1、 设计用户持久化类的接口UserDao,代码如下:
//UserDao.java
public interface UserDao {
public void add(User user);
public User query(String name);
}
2、 具体的持久化来必须要继承UserDao接口,并实现它的所有方法。我们还是首先实现内存持久化的用户类:
//MemoryUserDao.java
public class MemoryUserDao implements UserDao{
private static Map<String, User> users = new HashMap<String, User>();
public void add(User user){
users.put(user.getName(),user);
}
public User query(String userName){
return users.get(userName);
}
}
这时,业务逻辑这边的UserRegister代码又该如何实现呢?
UserDao userDao = new MemoryUserDao();
userDao.add(user);
如果我们再切换到文本的持久化实现TextUserDao,业务逻辑代码仍然需要手工修改。虽然我们已经使用了面向对象的多态技术,对象userDao方法的执行都是针对接口的调用,但userDao对象的创建却依赖于具体的实现类,比如上面MemoryUserDao。这样我们并没有完全实现前面所说的“使用者不必知道其使用对象的具体所属类”。
如何解决业务逻辑对象依赖具体实现类的问题呢?下面该是我们的工厂(Factory)模式出场了!
重构第二步——工厂(Factory)模式
我们使用一个工厂类来实现userDao对象的创建,这样业务逻辑只要知道这一个工厂类就可以了,不用依赖任何具体的UserDao实现。创建userDao对象的工厂类UserDaoFactory代码如下:
//UserDaoFactory.java
public class UserDaoFactory {
public static UserDao createUserDao(){
return new MemoryUserDao();
}
}
业务逻辑这边的UserRegister代码片断如下:
UserDao userDao = UserDaoFactory.createUserDao();
userDao.add(user);
现在如果再要更换持久化方式,比如使用文本文件持久化用户信息。就算有再多的业务逻辑代码调用了用户持久化对象我们都不用担心了。因为业务逻辑和用户持久化对象的具体实现完全解耦。我们唯一要修改的只是一个UserDaoFactory类。
重构第三步——工厂(Factory)模式的改进
到这里人生的浩劫已经得到了拯救。但我们仍不满足,因为假如将内存持久化改为文本文件持久化仍然有着硬编码的存在——UserDaoFactory类的修改。代码的修改就意味着重新编译、打包、部署甚至引入新的Bug。所以,我们不满足,因为它还不够完美!
如何才是我们心目中的完美方案?至少要消除更换持久化方式时带来的硬编码。具体实现类的可配置不正是我们需要的吗?我们在一个属性文件中配置UserDao的实现类,例如:
在属性文件中可以配置UserDao实现类的全名,如userDao = com.test.MemoryUserDao。然后UserDaoFactory将从这个属性文件中取得UserDao实现类的全名,再通过Java的反射Class.forName(className).newInstance()语句来自动创建一个UserDao接口的具体实例。UserDaoFactory代码如下:
//UserDaoFactory.java
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实现类名,将它设置为你需要更换的持久化类名即可。
我们终于可以松下一口气了?不,矛盾仍然存在。我们引入了接口,引入了工厂模式,让我们的系统高度的灵活和可配置,同时也给开发带来了一些复杂度:
1、本来只有一个实现类,后来却要为这个实现类引入了一个接口。
2、引入了一个接口,却还需要额外开发一个对应的工厂类。
3、工厂类过多时,增加了管理、维护成本。
当然,面接口编程是实现软件的可维护性和可重用行的重要原则已经勿庸置疑。这样,第一个复杂度问题是无法避免的,再说一个接口的开发和维护的工作量是微不足道的。但后面两个复杂度的问题,我们是完全可以解决的:工厂模式的终极方案——IoC模式。
重构第四步-IoC容器
使用IoC容器,用户注册类UserRegisterService不用主动创建UserDao实现类的实例。由IoC容器主动创建UserDao实现类的实例,并注入到用户注册类中。我们下面将使用Spring提供的IoC容器来管理我们的用户注册类。
用户注册类UserRegisterService的部分代码如下:
//UserRegisterService.java
public class UserRegisterService {
private UserDao userDao = null;//由容器注入的实例对象
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
// UserRegisterService的业务方法
}
在其它的UserRegisterService方法中就可以直接使用userDao对象了,它的实例由Spring容器主动为它创建。但是,如何组装一个UserDao的实现类到UserRegisterService中呢?哦,Spring提供了配置文件来组装我们的组件。Spring的配置文件applicationContext.xml代码片断如下:
<!--applicationContext.xml-->
<bean id="userRegisterService" class="com.dev.spring.simple.UserRegisterService">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.dev.spring.simple.MemoryUserDao"/>
现在业务这边如何获取UserRegister的实例呢,来看下面的代码?
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserRegisterService userRegisterService = (UserRegisterService)ctx.getBean("userRegisterService");
assertNotNull(userRegisterService);
//...
}
}
哈哈,一切变得如此简单。Spring容器已经帮我们创建了2个bean id分别叫做userDao和userRegisterService的实例,并且已经把userDao实例注入到了userRegisterService实例中去了。
2.3 什么是控制反转(IoC)/依赖注入(DI)?
现在是时候解释什么是IoC/DI了:
控制反转(IoC=Inversion of Control)IoC,它将类和类之间的依赖从代码中脱离出来,用配置的方式进行依赖关系描述,由IoC容器负责以来类之间的创建、拼接、管理、获取等工作。用白话来讲,就是由容器控制程序之间的(依赖)关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在:(依赖)控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。
IoC也称为好莱坞原则(Hollywood Principle):“Don’t call us, we’ll call you”。即,如果大腕明星想演节目,不用自己去找好莱坞公司,而是由好莱坞公司主动去找他们(当然,之前这些明星必须要在好莱坞登记过)。
正在业界为IoC争吵不休时,大师级人物Martin Fowler也站出来发话,以一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》为IoC正名,至此,IoC又获得了一个新的名字:“依赖注入 DI(Dependency Injection)”。
相对IoC 而言,“依赖注入”的确更加准确的描述了这种古老而又时兴的设计理念。从名字上理解,所谓依赖注入,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关系注入到组件之中。
例如前面用户注册的例子。UserRegisterService依赖于UserDao的实现类,在最后的改进中我们使用IoC容器在运行期动态的为UserRegisterService注入UserDao的实现类。即UserRegisterService对UserDao的依赖关系由容器注入,UserRegisterService不用关心UserDao的任何具体实现类。如果要更改用户的持久化方式,只要修改配置文件applicationContext.xm即可。
依赖注入机制减轻了组件之间的依赖关系,同时也大大提高了组件的可移植性,这意味着,组件得到重用的机会将会更多。
结束语
恭喜你读完这篇文章算是对Spring入门了,不过Spring框架所涉及的内容远远不止这些,还有很多东西可以继续探索的哦。
还没有评论,来说两句吧...