我们知道,Java方面的高级程序员一定得掌握Spring的技能,其中包括Spring 依赖注入(IOC),面向切面(AOP),和数据库的整合(比如和Hibernate整合或声明式事务等)以及Spring MVC架构。其中,Spring的依赖注入是重中之重,在面试时,面试官一定会问这方面的问题。

      根据我的培训和面试经验,这方面的知识点虽然不难(初学者估计最多3天就能看明白并调通程序),但要把其中的各混淆点(也就是面试点)讲清楚不容易,换句话说,初级程序员在学习Spring IOC这部分的知识时,或多或少会走些弯路,在通常情况下,会经过一次或多次面试的失败(就是交学费)后才能理顺这部分的知识体系。


--------------------------------------------------------------------------------------------------------------------

 1 通过一个简单但易学的案例来了解依赖注入

        步骤一 开发提供服务的SayHello.java程序。        


1 package com;
 2 public class SayHello {
 3     private HelloWorldSpring helloWorldSpring;
 4     public HelloWorldSpring getHelloWorldSpring() {
 5         return helloWorldSpring;
 6     }
 7     public void setHelloWorldSpring(HelloWorldSpring helloWorldSpring) {
 8         this.helloWorldSpring = helloWorldSpring;
 9     }
10     public void sayHello(){
11         System.out.println("Say Hello:" + helloWorldSpring.sayHello());
12         }
13     }



       第10行定义了一个sayHello的方法,在这方法里,调用了在第3行定义的helloWorldSpring对象,输出一串文字。

       这里有一个比较有意思的现象,虽然在第4行和第7行针对helloWorldSpring对象定义了get和set的方法,但在第11行使用helloWorldSpring对象之前,始终没有用new关键字初始化这个对象,那么按照以往的经验,会不会出现空指针异常呢?

        别着急,先看下HelloWorldSpring这个类里有没有特殊的动作。

        步骤二 定义HelloWorldSpring.java这个类。

 



1 package com;
2 public class HelloWorldSpring {
3     private String sayContent;    
4     public String sayHello() {
5         System.out.println("HelloWorld Spring!");
6         return "Hello World Spring";
7     }
8 }



        这里直接在第4行定义了sayHello的方法,也没看到特殊的代码。接下来看一下在SpringMain这个类里是如何调用的。

        步骤三 开发调用者SpringMain.java,请大家注意一下调用sayHello方法的方式。




1 package com;
 2 import org.springframework.context.ApplicationContext;
 3 import org.springframework.context.support.ClassPathXmlApplicationContext;
 4 public class SpringMain {
 5     public static void main(String[] args) {
 6         ApplicationContext context = new ClassPathXmlApplicationContext(
 7                 "conf/applicationContext-*.xml");
 8 
 9     SayHello sayHello = (SayHello) context.getBean("SayHello");
10     sayHello.sayHello();        
11     }
12 }



        关键代码在第9行和第10行,是通过一个getBean方法,获得了SayHello对象的一个实例,随后调用了其中的sayHello方法。在这之前,通过第6行的代码,装载了一个配置文件。

       戏法变到这里,大家看到了两个不可思议的地方。第一,在SayHello类里,始终没有初始化HelloWorldSpring对象,就直接用了。第二,在SpringMain类里,没有像往常那样用SayHello sayHello = new SayHello();  的方法初始化对象,而是通过getBean的方式来获得类的实例。

      其实,这里大家已经能看到“低耦合”的写法了。让我们最后看完Spring的配置文件再来综合体验IoC的好处。

      步骤四 编写Spring的配置文件applicationContext-service-api.xml,关键代码如下




1 <bean id="HelloWorldSpring" class="com.HelloWorldSpring">
2 </bean>
3 <bean id="SayHello" class="com.SayHello" >
4 <property name="helloWorldSpring" ref="HelloWorldSpring" />
5 </bean>



        在第1行和第2行里,定义了一个Bean。在Spring里,一个Bean往往和一个类所对应,这里id是HelloWorldSpring的这个Bean是和com.HelloWorldSpring这个类所对应。

        而在第3行到第5行里,用id是SayHello这个Bean对应com.SayHello这个类,请大家注意第4行,用内置property这个方式,把HelloWorldSpring这个类内嵌到SayHello类里。

   2 IoC的特点,不用New就可以初始化类

       SpringMain.java的主要代码如下。

1     ApplicationContext context = new ClassPathXmlApplicationContext("conf/applicationContext-*.xml");
2     SayHello sayHello = (SayHello) context.getBean("SayHello");
3     sayHello.sayHello();

  

       运行SpringMain.java时,首先是把配置文件里定义的信息装载到context类里;接着在第2行里,通过context.getBean方法,根据配置文件的定义,获取ID为SayHello(即class是com.SayHello)这个类;随后在第3行里使用这个类的sayHello方法。从代码中大家可以看出,这里同样没有用到new,而是根据配置文件来初始化类。

       没有使用new,就意味着低耦合,具体而言,就是SayHello、HelloWorldSpring和SpringMain这三个类之间的耦合度很低。

      假设有三个团队在开发维护这三个类,如果用常规new方法来创建类,比如在SayHello类里用HelloWorldSpring helloWorldSpring = new HelloWOrldSpring();,那么一旦管理HelloWorldSpring类的团队要修改调用的接口,比如new的构造函数需要带参数,那么SayHello类的管理者就不得不受无妄之灾,也需要修改本身的代码。

      要知道在公司里,修改代码并且发布到生产环境,要经过很烦琐且很严格的审批流程,必须要经历代码审查、代码提交、测试人员测试、领导审批、最终发布以及发布后检查这些步骤。如果用刚才看到的通过配置文件装载类,在本地代码里没有new的这套开发方式,那么如果一个团队修改了代码,其他团队就有可能不必改动代码,这样就可以很大程度上避免不必要的工作。

 

      3 控制翻转和依赖注入

      控制翻转的英文名字叫IoC(Inversion of Control),依赖注入英文名叫DI(Dependency Injection),下面通过下表来看一下它们的概念。

       



概念名

含义

表现形式

控制翻转(IoC,控制反转)

类之间的关系,不用代码控制,而是由Spring容器(也就是Spring的jar包)来控制。控制权由代码翻转到容器里,这叫控制翻转

在初始化对象时,在代码里无须new,而是把类之间的关系写到配置文件里

依赖注入(DI)

在代码运行时,如果我们要在一个类里使用(也叫注入)另一个类,比如在上述的SayHello类里要初始化另外一个HelloWorldSpring类,那么这种注入就是依赖于配置文件的

同样是把类之间的调用关系写到配置文件里,在运行时,会根据配置文件,把HelloWorldSpring这个类注入SayHello里

        通过上面的描述,能看到它们其实是从不同的角度讲述的同一件事情。依赖注入强调类的注入是由Spring容器在运行时完成,而控制反转强调类之间的关系是由Spring容器控制。

       从这两个名词可知, Spring给我们带来了一种全新的编程理念,即不用new也可以创建和使用对象。这种开发方式让我们能像搭积木一样组装不同的类,组装后的类之间的耦合度很低,一个类的修改可以不影响(或者影响度很小)其他的类,这样就可以避免一个小修改带来的一大串连锁反应。

       大家在了解Spring的时候,一定请理解“低耦合”这个好处,这本来是面向对象思想带给我们的好处,在Spring开发的过程中我们确实能感受到。

 

      4 读取配置文件的各种方式

       在Spring里,通常在配置文件中描述各类以及类之间的包含关系,在使用的时候,会先加载配置文件,Spring的内核会读取配置文件,随后动态地组装各类。

       通过下表来总结一下读取配置文件的各种方式,它们之间没有优劣之分,大家可以挑选个最适用的,具体来讲,没有特殊情况,就可以用ClassPathXmlApplicationContext。

 



类名

例子

XmlBeanFactory

Resource resource = new ClassPathResource("bean.xml"); 

BeanFactory factory = new XmlBeanFactory(resource); 

ClassPathXmlApplicationContext

ApplicationContext factory=new ClassPathXmlApplicationContext("conf/appcontext.xml"); 

用文件系统类来读取

FileSystemXmlApplicationContext

ApplicationContext factory=new FileSystemXmlApplicationContext("classpath:appcontext.xml"); 



              5 单例和多例

        我们知道,Spring的容器会在程序运行时,根据配置文件自动地创建(或者叫实例化)具体的Java类(也叫class,或叫Bean)。在配置文件里,可以设置创建文件时是否用单例的方式,如果没有设置,则会自动用默认的单例的方式来创建文件。如果不想用单例,则可以通过如下两种语法来修改,它们是等价的。

        <bean id="SayHello" class="com.SayHello" singleton="false"> 或者

         <bean id="SayHello" class="com.SayHello" scope="prototype">

       在实际项目中,一般用单例模式来创建无状态的Bean,而对于有状态的Bean,一般不用这种模式。所谓无状态的Bean,是指没有能够标识它目前状态属性的Bean,比如共享单车,A用好以后,可以放入共享池(即放到马路边上),B可以继续使用。由于没有供某个特定的用户使用,所以也就不能保持某一用户的状态,所以叫无状态Bean。相反,如果针对个人的自行车,那么会有个状态来表明是个人的。

      讲到这里,请大家确认如下概念,并不是我们首先设置了singleton是false,所以Spring容器才用单例的方式,恰恰相反,根据实际的需求,待创建的类可以被其他多个类共享,因此我们才设置singleton是false。是先有需求再有具体的实现。

    这个知识点可以说是Spring面试的必考点,下面通过下表来对比一下两者的差别

     



列别

实际用例

特点

有状态Bean

我们访问网站登录后都有自己的用户名和密码,系统可以用一个有状态的Bean来记录我们的访问信息,比如来源IP访问页面列表和访问时间等

会为每次调用创建一个实例,一旦调用结束,比如用户离开了网站,则该Bean就会被销毁

无状态Bean

数据库连接的通用类,其他类可以用它来获取数据库连接并进行操作

可以在缓冲池里只维护一个实例,无须创建和销毁操作,性能高,但是线程不安全



 

 

  6 论面试

        当年我追过一本小说,叫天择,里面有个故事情节,皇帝请主角吃饭,让主角点菜,主角点的不是龙肝凤胆,也不是山珍海味,是两个家常菜,炒青菜和蛋炒饭,如下我引用的是书中原话:

       天下万事万物,都是有一个从简单到复杂,又从复杂趋向于简单的过程,用道家的话来说,就是天下大道,以简驭繁,用佛家的话来说,就是看山是山,看山不是山,看山仍是山的三大境界。蛋炒饭和炒青菜这两样东西每个人都吃了不知道多少次,但是,正因为如此,能够在这平凡当中做出来令人难忘的伟大味道,这才是顶尖的高手的境界!

     学习Spring IOC这平凡的知识点,也会经历过上述”从简单到复杂“的过程,作者根据多年面试培训(甚至包括写书)的经验,从纷繁复杂的Spring IOC的诸多知识点中提炼出针对初级程序员有用的上述内容,不能说是顶尖高手,但至少也经过沉淀,对大家多少有些帮助,也一定能帮助大家少走些弯路(这也是本文申请加入首页的理由)。

      面试时也这样,面试官会在乎候选人掌握多少知识点(广度),更在乎对于知识点的深度,如果候选人能从IOC这种平凡的知识点里说出自己的高深体会,这样反而能更打动面试官。

        在这方面可以说出如下的要点:

      1 基本概念(谁都会说)

      2 结合项目说明怎么用IOC,以及IOC的好处(不用new就能用,低耦合),这大家可以结合本文里提到的案例说明

      3 一些外围的知识点,比如如何导入配置文件

      4 特别地,请讲述单例和多例,并请结合具体例子说明在项目里的用法。