1、使用框架的意义与Spring的主要内容

     随着软件结构的日益庞大,软件模块化趋势出现,软件开发也须要多人合作,随即分工出现。怎样划分模块,怎样定义接口方便分工成为软件project设计中越来越关注的问题。良好的模块化具有下面优势:可扩展、易验证、易维护、易分工、易理解、代码复用。

     优良的模块设计往往遵守“低耦合高内聚”的原则。而“框架”是对开发中良好设计的总结,把设计中常常使用的代码独立出来,所形成的一种软件工具。用户遵守它的开发规则,就能够实现良好的模块化,避免软件开发中潜在的问题。广义上的框架无处不再,一个常见的样例就是PC硬件体系结构,人们仅仅要依照各自须要的主板、显卡、内存等器件就能够随意组装成自己想要的电脑。而做主板的厂商不用关心做显卡厂商的怎么实现它的功能。软件框架也是如此,开发者仅仅要在Spring框架中填充自己的业务逻辑就能完毕一个模块划分清晰纷的系统。

     这里主要通过一个银行通知用户月收支记录的小样例来介绍轻型J2EE框架Spring的主要内容、它所解决的问题和实现的方法。

Spring框架主要能够分为3个核心内容:

        1、容器

        2、控制反转(IoC ,Inversion of Control)

        3、面向切面编程(AOP ,Aspect-Oriented Programming)

      样例中依次对这些特性进行介绍,描写叙述了软件模块化后存在的依赖与问题,以及Spring框架怎样解决这些问题。 

2、一个简单的样例程序

     如果有一个例如以下应用场景:(1)一个银行在每月的月初都须要向客户发送上个月的账单,账单发送的方式能够为纸质邮寄、或者短信方式。(2)另一个潜在的需求:为了安全起见,在每一个函数操作过程中都须要记录日志,记录參数传入是否正常,函数是否正常结束,以便出错时系统管理员查账。

     那么对这个需求进行简单实现。系统框图例如以下所看到的:

Spring框架简单介绍_spring框架

    首先定义一个账单输出的接口:




​1​

​//接口 ​


​2​

​public​​ ​​interface​​ ​​ReportGenerator{ ​


​3​

​public​​​​void​​​​generate(String[][] table) ;​


​4​

​}​


     实现“打印纸质账单”与“发送短信”两个详细功能:




​1​

​//账单报表实现类  ​


​2​

​public​​ ​​class​​ ​​PageReportGenerator implement ReportGenerator {​


​3​

​public​​​​void​​​​generate(String[][] table) {​


​4​

​log4j.info( ... );   ​​​​//输出日志 ​


​5​

​...打印操作,以便工作人员邮递给客户​


​6​

​log4j.info( ... );   ​​​​//输出日志 ​


​7​

​}  ​


​8​

​}​




​1​

​//短信报表实现类  ​


​2​

​public​​ ​​class​​ ​​SMSReportGenerator implement ReportGenerator {​


​3​

​public​​​​void​​​​generate(String[][] table) {​


​4​

​log4j.info( ... );​


​5​

​​


​6​

​log4j.info( ... );​


​7​

​} ​


​8​

​}​


    上层业务逻辑对上个月的账目进行统计并调用接口产生纸质或者短信结果:




​01​

​//上层业务中的服务类  ​


​02​

​public​​ ​​class​​ ​​ReportService{  ​


​03​

​private​​​​ReportGenerator reportGenerator =​​​​new​​​​SMSReportGenerator(); ​


​04​

​public​​​​void​​​​generateMonthlyReport(​​​​int​​​​year,​​​​int​​ ​​month) {  ​


​05​

​log4j.info( ... ); ​


​06​

​String[][] statistics =​​​​null​​​​;  ​


​07​

​... ​


​08​

​reportGenerator.generate(statistics); ​


​09​

​} ​


​10​

​} ​


这个实现源码请查看文章结尾附录中的"BankOld"。源码中与样例中程序略有差别:因为使用log4j须要引用外部的包,而且须要写配置文件,为了方便,源码中的日志输出用system.out.println()取代。

3、Spring中的容器

        A、模块化后出现的问题与隐患

        如果随着project的复杂化,上面的样例须要分成两个模块,以便开发时分工,通常会以例如以下结构划分:

Spring框架简单介绍_模块化_02

    划分后再看原来的代码: 




​1​

​//上层业务中的服务类  ​


​2​

​public​​ ​​class​​ ​​ReportService{  ​


​3​

​private​​​​ReportGenerator reportGenerator =​​​​new​​​​SMSReportGenerator();​​​​//隐患 ​


​4​

 


​5​

​public​​​​void​​​​generateMonthlyReport(​​​​int​​​​year,​​​​int​​ ​​month) {  ​


​6​

​... ​


​7​

​}  ​


​8​

​}​


     在服务类有private ReportGenerator reportGenerator = new SMSReportGenerator();这么一行代码,ReportService类与SMSReportGenerator类不属于同一个模块,当开发者B对内部实现进行改动时,因为存在依赖,开发者A也要进行改动(比方之前喜欢短信收账单的客户感觉短信不够具体,希望以后改用邮件收账单,那么开发者B须要实现一个MailReportGenerator类,在开发者B改动代码是,开发者A也须要改代码------声明部分改动)。假设系统庞大new

SMSReportGenerator()大量使用的话,改动就会十分复杂,一个声明没有改动就会出现大的BUG。

    所以须要一种划分,让各个模块尽可能独立,当开发者B改动自己的模块时,开发者A不须要改动不论什么代码。

    B、问题出现的原因

    为样例中的程序画一个UML依赖图:

Spring框架简单介绍_开发者_03

    能够发现上述问题出现的原因主要是:模块A与模块B不但存在接口依赖,还存在实现依赖。ReportGenerator每次改动它的实现,都会对ReportService产生影响。那么须要重构消除这样的实现依赖。

    C、用容器解决这个问题

    消除实现依赖一般能够通过加入一个容器类来解决。在样例程序容器代码例如以下: 




​01​

​//容器类  ​


​02​

​public​​ ​​class​​ ​​Container {  ​


​03​

 


​04​

​public​​​​static​​​​Container instance; ​


​05​

 


​06​

​private​​​​Map<String, Object> components; ​


​07​

 


​08​

​public​​​​Container(){ ​


​09​

​component =​​​​new​​​​HashMap<String, Object>(); ​


​10​

​instance =​​​​this​​​​; ​


​11​

 


​12​

​ReportGenertor reportGenertor =​​​​new​​​​SMSReportGenertor(); ​


​13​

​components.put(“reportGenertor”, reportGenertor); ​


​14​

 


​15​

​ReportService reportService =​​​​new​​​​ReportService(); ​


​16​

​components.put(“reportService”, reportService); ​


​17​

​}  ​


​18​

 


​19​

​public​​​​Object getComponent(String id){ ​


​20​

​return​​​​components.get(id); ​


​21​

​}  ​


​22​

​}​


    使用容器后,模块A的ReportService的属性实现方法也发生了变化。




​01​

​//服务类变更,减少了耦合  ​


​02​

​public​​ ​​class​​ ​​ReportService{  ​


​03​

 


​04​

​//private ReportGenerator reportGenerator = new SMSReportGenerator(); ​


​05​

​private​​​​ReportGenerator reportGenerator = (ReportGenerator) Container.instance.getComponent(“reportGenerator”); ​


​06​

 


​07​

​public​​​​void​​​​generateMonthlyReport(​​​​int​​​​year,​​​​int​​ ​​month) {  ​


​08​

​... ​


​09​

​}  ​


​10​

​}​


     这种话,class都在容器中实现,使用者仅仅须要在容器中查找须要的实例,开发者改动模块B后(在模块中添加邮件报表生成类MailReportGenerator),仅仅须要在容器类中改动声明(把ReportGenertor

reportGenertor = new SMSReportGenertor();改为ReportGenertor reportGenertor = new

MailReportGenertor();)就可以,模块A不须要改动不论什么代码。一定程度上减少了模块之间的耦合。

4、Spring中的控制反转

 

    A、还存在的耦合

    使用容器后模块A与模块B之间的耦合降低了,可是通过UML依赖图能够看出模块A開始依赖于容器类:

Spring框架简单介绍_模块化_04

    之前的模块A对模块B的实现依赖通过容器进行传递,在程序中用(ReportGenerator) Container.instance.getComponent(“reportGenerator”)的方法取得容器中SMSReportGenertor的实例,这样的用字符(“reportGenerator”)指代详细实现类SMSReportGenertor 的方式并没有全然的解决耦合。所以在银行账单的样例中我们须要消除ReportService对容器Container的依赖。


    B、控制反转与依赖注入

    在我们常规的思维中,ReportService须要初始化它的属性private ReportGenerator reportGenerator就必须进行主动搜索须要的外部资源。不使用容器时,它须要找到SMSReportGenertor()的构造函数;当使用容器时须要知道SMSReportGenertor实例在容器中的命名。不管怎么封装,这样的主动查找外部资源的行为都必须知道怎样获得资源,也就是肯定存在一种或强或弱的依赖。那是否存在一种方式,让ReportService不再主动初始化reportGenerator,被动的接受推送的资源?

    这样的反转资源获取方向的思想被称为控制反转(IoC,Inversion of Control),使用控制反转后,容器主动地将资源推送给须要资源的类(或称为bean)ReportService,而ReportService须要做的仅仅是用一种合适的方式接受资源。控制反转的详细实现过程用到了依赖注入(DI,Dependecncy Injection)的设计模式,ReportService类接受资源的方式有多种,当中一种就是在类中定义一个setter方法,让容器将匹配的资源注入:setter的写法例如以下: 




​01​

​//为须要依赖注入的类增加一种被称为setter的方法  ​


​02​

 


​03​

​public​​ ​​class​​ ​​ReportService{  ​


​04​

 


​05​

​/*private ReportGenerator reportGenerator = ​


​06​

​(ReportGenerator) Container.instance.getComponent(“reportGenerator”); */​​ 


​07​

 


​08​

​private​​​​ReportGenerator reportGenerator; ​


​09​

 


​10​

​public​​​​void​​​​setReportGenerator( ReportGenerator reportGenerator) { ​


​11​

​this​​​​.reportGenerator = reportGenerator; ​


​12​

​}  ​


​13​

 


​14​

​public​​​​void​​​​generateMonthlyReport(​​​​int​​​​year,​​​​int​​ ​​month) {  ​


​15​

​...  ​


​16​

​}  ​


​17​

​}​


    在容器中把依赖注入: 




​01​

​//容器类    ​


​02​

​public​​ ​​class​​ ​​Container {  ​


​03​

 


​04​

​... ​


​05​

​public​​​​Container ( ) { ​


​06​

​component =​​​​new​​​​HashMap<String, Object>(); ​


​07​

​instance =​​​​this​​​​; ​


​08​

​ReportGenertor reportGenertor =​​​​new​​​​SMSReportGenertor(); ​


​09​

​components.put(“reportGenertor”, reportGenertor); ​


​10​

 


​11​

​ReportService reportService =​​​​new​​​​ReportService(); ​


​12​

​reportService.setReportGenerator(reportGenerator);​​​​//使用ReportService的setter方法注入依赖关系 ​


​13​

​components.put(“reportService”, reportService);​


​14​

​}  ​


​15​

​... ​


​16​

​}​


    这样一来ReportService就不用管SMSReportGenertor在容器中是什么名字,模块A对于模块B仅仅有接口依赖,做到了松耦合。

    C、Spring IoC容器的XML配置

    每一个使用Spring框架的project都会用到容器与控制反转,为了代码复用,Spring把通用的代码独立出来形成了自己的IoC容器供开发人员使用:

 Spring框架简单介绍_模块化_05

    与上面样例中实现的容器相比,Spring框架提供的IoC容器要远远复杂的多,但用户不用关心Spring

IoC容器的代码实现,Spring提供了一种简便的bean依赖关系配置方式------使用XML文件,在上面的样例中,配置依赖关系仅仅要在project根文件夹下的“application.xml”编辑例如以下内容:




​01​

​<?​​​​xml​​​​version​​​​=​​​​"1.0"​​​​encoding​​​​=​​​​"UTF-8"​​​​?> ​


​02​

​<​​​​beans​​   ​​xmlns​​​​=​​​​"http://www.springframework.org/schema/beans"​


​03​

​xmlns:xsi​​​​=​​​​"http://www.w3.org/2001/XMLSchema-instance"​


​04​

​xmlns:p​​​​=​​​​"http://www.springframework.org/schema/p"​


​05​

​xsi:schemaLocation="http://www.springframework.org/schema/beans​


​06​

​http://www.springframework.org/schema/beans/spring-beans-3.0.xsd" >​


​07​

 


​08​

​<​​​​bean​​​​id​​​​=​​​​"smsReportGenerator"​​​​class​​​​=​​​​"bank.SMSReportGenerator"​​​​/>​


​09​

 


​10​

​<​​​​bean​​​​id​​​​=​​​​"reportService"​​​​class​​​​=​​​​"bank.ReportService"​​​​>  ​


​11​

​<​​​​property​​​​name​​​​=​​​​"reportGenerator"​​​​ref​​​​=​​​​"smsReportGenerator"​​​​/> ​


​12​

​</​​​​bean​​​​>​


​13​

​</​​​​beans​​​​>​


        <?xml version="1.0" encoding="UTF-8"?>是标准的XML头,xmlns引用的是一些命名空间,两个一般在project中自己主动生成。后面的内容由用户输入,主要表示实例化SMSReportGenerator,实例化ReportService并把SMSReportGenerator的对象smsReportGenerator赋值给ReportService的属性reportGenerator,完毕依赖注入。

5、Spring中的面向切面编程

 

    A、日志问题以及延伸

    在样例的需求中有一条是:须要记录日志,以便出错时系统管理员查账。回想样例中的代码,在每一个方法中都加了日志操作: 




​01​

​//服务类  ​


​02​

​public​​ ​​class​​ ​​ReportService{  ​


​03​

​... ​


​04​

​public​​​​void​​​​generateMonthlyReport(​​​​int​​​​year,​​​​int​​ ​​month) {  ​


​05​

​log4j.info( ... );  ​​​​//记录函数的初始状况參数等信息 ​


​06​

​String[ ][ ] statistics =​​​​null​​​​;  ​


​07​

​... ​


​08​

​reportGenerator.generate(statistics); ​


​09​

​log4j.info( ... );  ​​​​//记录函数的运行状况与返回值 ​


​10​

​}  ​


​11​

​}​




​01​

​//凭条报表实现类    ​


​02​

​public​​ ​​class​​ ​​PageReportGenerator implement ReportGenerator { ​


​03​

 


​04​

​public​​​​void​​​​generate(String[ ][ ] table) { ​


​05​

 


​06​

​log4j.info( ... );      ​​​​//记录函数的初始状况參数等信息 ​


​07​

​…打印操作 ​


​08​

​log4j.info( ... );      ​​​​//记录函数的运行状况与返回值 ​


​09​

​}  ​


​10​

​}​


    能够看出在每一个方法的開始与结尾都调用了日志输出,这样的零散的日志操作存在着一些隐患,会导致维护的困难。比方日志输出的格式发送了变化,那么不管模块A还是模块B的程序猿都要对每一个方法每一个输出逐条改动,极easy遗漏,造成日志输出风格的不一致。又比方不用Log4j日志输出工具更换其它工具,假设遗漏一个将会出现严重BUG。

    与日志输出相似的问题在编程中常常遇到,这样的跨越好几个模块的功能和需求被称为横切关注点,典型的有日志、验证、事务管理等。

Spring框架简单介绍_开发者_06

    横切关注点导致代码混乱、代码分散的问题。而怎样将非常切关注点模块化是本节的重点。

 

    B、代理模式

    传统的面向对象方法非常难实现非常切关注点的模块化。一般的实现方式是使用设计模式中的代理模式。代理模式的原理是使用一个代理将对象包装起来,这个代理对象就代替了原有对象,不论什么对原对象的调用都首先经过代理,代理可以完毕一些额外的任务,所以代理模式可以实现横切关注点。

Spring框架简单介绍_spring_07

    可能在有些程序中有非常多横切关注点,那么仅仅须要在代理外再加几层代理就可以。以银行账单为例介绍一个种用Java Reflection API动态代理实现的横切关注点模块化方法。系统提供了一个InvocationHandler接口: 




​1​

​//系统提供的代理接口  ​


​2​

​public​​ ​​interface​​ ​​InvocationHandler {  ​


​3​

​public​​​​Object invoke(Object proxy, Method method, Object[] args)​​​​throw​​​​Throwable; ​


​4​

​}​


    我们须要实现这个接口来创建一个日志代理,实现代码例如以下:




​01​

​//日志代理实现    ​


​02​

​public​​ ​​class​​ ​​LogHandler implement InvocationHandler{  ​


​03​

 


​04​

​private​​​​Object target; ​


​05​

 


​06​

​public​​​​LogHandler(Object target){ ​


​07​

​this​​​​.target = target; ​


​08​

​}  ​


​09​

​public​​​​Object invoke(Object proxy, Method method, Object[] args )​​​​throw​​​​Throwable{ ​


​10​

 


​11​

​//记录函数的初始状况參数等信息 ​


​12​

​log4j.info(“開始:方法”+ method.getName() + “參数”+Arrays.toString(args) );​


​13​

 


​14​

 


​15​

​Object result = method.invoke(target, args); ​


​16​

 


​17​

​//记录函数的运行状况与返回值 ​


​18​

​log4j.info(“结束:方法”+ method.getName() + “返回值”+ result ); ​


​19​

 


​20​

​} ​


​21​

​}​


     这样既能够使得日志操作不再零散分布于各个模块,易于管理。调用者能够通过例如以下方式调用: 




​01​

​//主函数    ​


​02​

​public​​ ​​class​​ ​​Main{  ​


​03​

​public​​​​static​​​​void​​ ​​main(String[ ] args){ ​


​04​

​ReportGenerator reportGeneratorImpl  =​​​​new​​​​SMSReportGenerator (); ​


​05​

 


​06​

​//通过系统提供的Proxy.newProxyInstance创建动态代理实例 ​


​07​

​ReportGenerator reportGenerator = (ReportGenerator ) Proxy.newProxyInstance(  ​


​08​

​reportGeneratorImpl.getClass().getClassLoader(), ​


​09​

​reportGeneratorImpl.getClass().getInterfaces(), ​


​10​

​new​​​​LogHandler(reportGeneratorImpl)​


​11​

​) ; ​


​12​

​...​


​13​

​} ​


​14​

​}​


    代理模式非常好的实现了横切关注点的模块化,攻克了代码混乱代码分散问题,可是我们能够看出用 Java Reflection API 实现的动态代理结构十分复杂,不易理解,Spring框架利用了代理模式的思想,提出了一种基于JAVA注解(Annotation)和XML配置的面向切面编程方法(AOP ,Aspect-Oriented Programming)简化了编程过程。

    C、Spring AOP 用法

    Spring AOP使用中须要为横切关注点(有些时候也叫切面)实现一个类,银行账单的样例中,切面的实现例如以下:




​01​

​//切面模块实现    ​


​02​

​@Aspect​​    ​​//注解1  ​


​03​

​public​​ ​​class​​ ​​LogAspect{  ​


​04​

 


​05​

​@Before​​​​(“execution(* *.*(..))”)   ​​​​//注解2 ​


​06​

​public​​​​void​​​​LogBefore(JoinPoint joinPoint) ​​​​throw​​​​Throwable{ ​


​07​

​log4j.info(“開始:方法”+ joinPoint.getSignature().getName() ); ​


​08​

​}  ​


​09​

 


​10​

​@After​​​​(“execution(* *.*(..))”)    ​​​​//注解3 ​


​11​

​public​​​​void​​​​LogAfter(JoinPoint joinPoint) ​​​​throw​​​​Throwable{ ​


​12​

​log4j.info(“结束:方法”+ joinPoint.getSignature().getName() ); ​


​13​

​} ​


​14​

​}​


     注解1表示这个类是一个切面,注解2中" * *.*(..)* "是一个通配符,表示在容器中全部类里有參数的方法。@Before(“execution(* *.*(..))”)表示在全部类里有參数的方法前调用切面中德 LogBefore() 方法。同理,注解3中@After(“execution(* *.*(..))”)表示在全部类里有參数的方法运行完后调用切面中的LogAfter()方法。

    实现完切面类后,还须要对Springproject中的application.xml进行配置以便实现完整的动态代理: 




​01​

​<?​​​​xml​​​​version​​​​=​​​​"1.0"​​​​encoding​​​​=​​​​"UTF-8"​​​​?> ​


​02​

​<​​​​beans​​   ​​xmlns​​​​=​​​​"http://www.springframework.org/schema/beans"​


​03​

​xmlns:xsi​​​​=​​​​"http://www.w3.org/2001/XMLSchema-instance"​


​04​

​xmlns:p​​​​=​​​​"http://www.springframework.org/schema/p"​


​05​

​xmlns:aop​​​​=​​​​"http://www.springframework.org/schema/aop"​​    


​06​

​xsi:schemaLocation="http://www.springframework.org/schema/beans ​


​07​

​http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ​


​08​

​http://www.springframework.org/schema/aop ​


​09​

​http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" > ​


​10​

 


​11​

​<​​​​aop:aspectj-autoproxy​​​​/> ​


​12​

​<​​​​bean​​​​id​​​​=​​​​"smsReportGenerator"​​​​class​​​​=​​​​"bank.SMSReportGenerator"​​​​/> ​


​13​

​<​​​​bean​​​​id​​​​=​​​​"reportService"​​​​class​​​​=​​​​"bank.ReportService"​​​​> ​


​14​

​<​​​​property​​​​name​​​​=​​​​"reportGenerator"​​​​ref​​​​=​​​​"smsReportGenerator"​​​​/> ​


​15​

​</​​​​bean​​​​> ​


​16​

​<​​​​bean​​​​class​​​​=​​​​"bank.LogAspect"​​​​/>​


​17​

​</​​​​beans​​​​>​


    这比之前IoC依赖关系配置的XML文件多了:xmlns:aop=http://www.springframework.org/schema/aop;http://www.springframework.org/schema/aop;http://www.springframework.org/schema/aop/spring-aop-3.0.xsd

这3个主要是声明XML中用于AOP的一些标签, <bean class="bank.LogAspect" /> 是在容器中声明LogAspect切面,<aop:aspectj-autoproxy />用于自己主动关联非常切关注点(LogAspect)与核心关注点(SMSReportGenerator,ReportService)。不难发现Spring AOP的方法实现横切关注点得模块化要比用Java Reflection API简单非常多。

6、Spring总结

 

         银行月账单报表样例通过使用Spring框架后变成了例如以下结构:

Spring框架简单介绍_spring_08

    在Spring框架的基础上原来存在耦合的程序被分成松耦合的三个模块。不管那个模块修改,对其它模块不须要额外修改。这就完毕了一种良好的架构,使软件易理解,模块分工明白,为软件的扩展、验证、维护、分工提供了良好基础。这就是Spring框架作用。当然Spring除了容器、控制反转、面向切面之外还有更多功能,但都是在这三个核心基础上实现的。