文档章节

Spring框架简介

Day
 Day
发布于 2011/12/26 11:00
字数 3983
阅读 5239
收藏 62

 

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

 

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

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

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

Spring框架主要可以分为3个核心内容:

        1、容器

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

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

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

2、一个简单的例子程序

 

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

     那么对这个需求进行简单实现。系统框图如下所示:

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

//接口
public interface ReportGenerator{
    public void generate(String[][] table) ;
}

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

//账单报表实现类 
public class PageReportGenerator implement ReportGenerator {
    public void generate(String[][] table) {
        log4j.info( ... );    //输出日志 
        ...打印操作,以便工作人员邮递给客户
        log4j.info( ... );    //输出日志 
    } 
}
//短信报表实现类 
public class SMSReportGenerator implement ReportGenerator {
    public void generate(String[][] table) {
        log4j.info( ... );
        ...短信发送操作
        log4j.info( ... );
     }
}

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

//上层业务中的服务类 
public class ReportService{ 
    private ReportGenerator reportGenerator = new SMSReportGenerator(); 
    public void generateMonthlyReport(int year, int month) { 
        log4j.info( ... ); 
        String[][] statistics = null ; 
        ... 
        reportGenerator.generate(statistics); 
    }
} 
这个实现源代码请查看文章结尾附录中的"BankOld"。源代码中与例子中程序略有区别:由于使用log4j需要引用外部的包,并且需要写配置文件,为了方便,源代码中的日志输出用system.out.println()代替。

3、Spring中的容器

 

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

        假设随着工程的复杂化,上面的例子需要分成两个模块,以便开发时分工,一般会以如下结构划分:

    划分后再看原来的代码: 

//上层业务中的服务类 
public class ReportService{ 
    private ReportGenerator reportGenerator = new SMSReportGenerator(); //隐患 

    public void generateMonthlyReport(int year, int month) { 
        ... 
    } 
}

     在服务类有private ReportGenerator reportGenerator = new SMSReportGenerator();这么一行代码,ReportService类与SMSReportGenerator类不属于同一个模块,当开发人员B对内部实现进行修改时,由于存在依赖,开发人员A也要进行修改(比如之前喜欢短信收账单的客户感觉短信不够详细,希望以后改用邮件收账单,那么开发人员B需要实现一个MailReportGenerator类,在开发人员B修改代码时,开发人员A也需要改代码------声明部分修改)。如果系统庞大new
SMSReportGenerator()大量使用的话,修改就会十分复杂,一个声明没有修改就会出现大的BUG。

    所以需要一种划分,让各个模块尽可能独立,当开发人员B修改自己的模块时,开发人员A不需要修改任何代码。

    B、问题出现的原因

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

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

    C、用容器解决问题

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

//容器类 
public class Container { 

    public static Container instance; 

    private Map<String, Object> components; 

    public Container(){ 
        component = new HashMap<String, Object>(); 
        instance = this; 

        ReportGenertor reportGenertor = new SMSReportGenertor(); 
        components.put(“reportGenertor”, reportGenertor); 

        ReportService reportService = new ReportService(); 
        components.put(“reportService”, reportService); 
    } 

    public Object getComponent(String id){ 
        return components.get(id); 
    } 
}

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

//服务类变更,降低了耦合 
public class ReportService{ 

    //private ReportGenerator reportGenerator = new SMSReportGenerator(); 
    private ReportGenerator reportGenerator = (ReportGenerator) Container.instance.getComponent(“reportGenerator”); 

    public void generateMonthlyReport(int year, int month) { 
        ... 
    } 
}

     这样的话,class都在容器中实现,使用者只需要在容器中查找需要的实例,开发人员修改模块B后(在模块中增加邮件报表生成类MailReportGenerator),只需要在容器类中修改声明(把ReportGenertor
reportGenertor = new SMSReportGenertor();改为ReportGenertor reportGenertor = new
MailReportGenertor();)即可,模块A不需要修改任何代码。一定程度上降低了模块之间的耦合。

4、Spring中的控制反转

 

    A、还存在的耦合

    使用容器后模块A与模块B之间的耦合减少了,但是通过UML依赖图可以看出模块A开始依赖于容器类:

    之前的模块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的写法如下: 

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

public class ReportService{ 

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

    private ReportGenerator reportGenerator; 

    public void setReportGenerator( ReportGenerator reportGenerator) { 
        this.reportGenerator = reportGenerator; 
    } 

    public void generateMonthlyReport(int year, int month) { 
        ...  
    } 
}

    在容器中把依赖注入: 

//容器类   
public class Container { 
     
    ... 
    public Container ( ) { 
        component = new HashMap<String, Object>(); 
        instance = this; 
        ReportGenertor reportGenertor = new SMSReportGenertor(); 
        components.put(“reportGenertor”, reportGenertor); 

        ReportService reportService = new ReportService(); 
        reportService.setReportGenerator(reportGenerator); //使用ReportService的setter方法注入依赖关系 
        components.put(“reportService”, reportService);
    } 
    ... 
}

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

    C、Spring IoC容器的XML配置

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

 

    与上面例子中实现的容器相比,Spring框架提供的IoC容器要远远复杂的多,但用户不用关心Spring
IoC容器的代码实现,Spring提供了一种简便的bean依赖关系配置方式------使用XML文件,在上面的例子中,配置依赖关系只要在工程根目录下的“application.xml”编辑如下内容:

<?xml version="1.0" encoding="UTF-8"?> 
<beans    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd" >

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

    <bean id="reportService" class="bank.ReportService">  
       <property name="reportGenerator" ref="smsReportGenerator" /> 
    </bean>
 </beans>

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

5、Spring中的面向切面编程

 

    A、日志问题以及延伸

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

//服务类 
public class ReportService{ 
    ... 
    public void generateMonthlyReport(int year, int month) { 
        log4j.info( ... );   //记录函数的初始状况参数等信息 
        String[ ][ ] statistics = null ; 
        ... 
        reportGenerator.generate(statistics); 
        log4j.info( ... );   //记录函数的执行状况与返回值 
    } 
}

 

//凭条报表实现类   
public class PageReportGenerator implement ReportGenerator { 

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

        log4j.info( ... );       //记录函数的初始状况参数等信息 
        …打印操作 
        log4j.info( ... );       //记录函数的执行状况与返回值 
    } 
}

    可以看出在每个方法的开始与结尾都调用了日志输出,这种零散的日志操作存在着一些隐患,会导致维护的困难。比如日志输出的格式发送了变化,那么无论模块A还是模块B的程序员都要对每个方法每个输出逐条修改,极容易遗漏,造成日志输出风格的不一致。又比如不用Log4j日志输出工具更换其他工具,如果遗漏一个将会出现严重BUG。

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

    横切关注点容易导致代码混乱、代码分散的问题。而如何将很切关注点模块化是本节的重点。
 
    B、代理模式

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

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

//系统提供的代理接口 
public interface InvocationHandler { 
    public Object invoke(Object proxy, Method method, Object[] args) throw Throwable; 
}

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

//日志代理实现   
public class LogHandler implement InvocationHandler{ 

    private Object target; 

    public LogHandler(Object target){ 
        this.target = target; 
    } 
    public Object invoke(Object proxy, Method method, Object[] args ) throw Throwable{ 

        //记录函数的初始状况参数等信息 
        log4j.info(“开始:方法”+ method.getName() + “参数”+Arrays.toString(args) );
 

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

        //记录函数的执行状况与返回值 
        log4j.info(“结束:方法”+ method.getName() + “返回值”+ result ); 

    }
 }

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

//主函数   
public class Main{ 
    public static void main(String[ ] args){ 
        ReportGenerator reportGeneratorImpl  = new SMSReportGenerator (); 

        //通过系统提供的Proxy.newProxyInstance创建动态代理实例 
        ReportGenerator reportGenerator = (ReportGenerator ) Proxy.newProxyInstance(  
            reportGeneratorImpl.getClass().getClassLoader(), 
            reportGeneratorImpl.getClass().getInterfaces(), 
            new LogHandler(reportGeneratorImpl)
        ) ; 
        ...
    }
}

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

    C、Spring AOP 使用方法

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

//切面模块实现   
@Aspect    //注解1 
public class LogAspect{ 

    @Before(“execution(* *.*(..))”)    //注解2 
    public void LogBefore(JoinPoint joinPoint)  throw Throwable{ 
        log4j.info(“开始:方法”+ joinPoint.getSignature().getName() ); 
    } 

    @After(“execution(* *.*(..))”)     //注解3 
    public void LogAfter(JoinPoint joinPoint)  throw Throwable{ 
        log4j.info(“结束:方法”+ joinPoint.getSignature().getName() ); 
    }
}

     注解1表示这个类是一个切面,注解2中" * *.*(..)* "是一个通配符,表示在容器中所有类里有参数的方法。@Before(“execution(* *.*(..))”)表示在所有类里有参数的方法前调用切面中德 LogBefore() 方法。同理,注解3中@After(“execution(* *.*(..))”)表示在所有类里有参数的方法执行完后调用切面中的LogAfter()方法。
    实现完切面类后,还需要对Spring工程中的application.xml进行配置以便实现完整的动态代理: 

<?xml version="1.0" encoding="UTF-8"?> 
<beans    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"    
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" > 

    <aop:aspectj-autoproxy /> 
    <bean id="smsReportGenerator" class="bank.SMSReportGenerator" /> 
    <bean id="reportService" class="bank.ReportService"> 
        <property name="reportGenerator" ref="smsReportGenerator" /> 
    </bean> 
    <bean class="bank.LogAspect" />
</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框架作用。当然Spring除了容器、控制反转、面向切面之外还有许多其他功能,但都是在这三个核心基础上实现的。

 

通过Spring框架来重构银行月账单例子的源代码在附录的“BankSpring”中。

附件


[1] 例子中不用Spring的实现BankOld源代码 

[2] 使用Spring后的实现BankSpring源代码

参考文献


 [1] Gary Mak ,《Spring 攻略》

© 著作权归作者所有

Day

Day

粉丝 34
博文 14
码字总数 10261
作品 0
杭州
程序员
私信 提问
加载中

评论(12)

yuantingjun
yuantingjun
真的太棒了!!!
一气化三清
一气化三清
很不错
一气化三清
一气化三清
很不错
ulyn
ulyn
支持一个 不错
小波1126
小波1126
将的很好!
盐巴2012
盐巴2012
楼主描述的非常好,恍然大悟...感谢并收藏
一剑风徽
一剑风徽
一个例子就把spring的三大功能说出来了
xianming
xianming

引用来自“Day”的评论

引用来自“刘宪明”的评论

谢谢楼主,讲的不错,不过我想我问问,你是用什么画图工具,画上面的图的?可以告诉我吗?谢谢

用PPT和visio画的~

谢谢你的回复
Day
Day 博主

引用来自“刘宪明”的评论

谢谢楼主,讲的不错,不过我想我问问,你是用什么画图工具,画上面的图的?可以告诉我吗?谢谢

用PPT和visio画的~
xianming
xianming
谢谢楼主,讲的不错,不过我想我问问,你是用什么画图工具,画上面的图的?可以告诉我吗?谢谢
新书上架:《Spring Boot 开发实战》(基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战)

新书上架:《Spring Boot 开发实战》 — 基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战 京东下单链接 https://item.jd.com/31178320122.html 天猫下单链接 https://detail...

程序员诗人
2018/08/05
0
0
SpringMVC基础及应用-李守红

本文视频观看地址:http://www.wisdomdd.cn/Wisdom/resource/articleDetail.htm?resourceId=1141 视频大纲 ³SpringMVC简介和环境搭建 ³第一个SpringMVC实例 ³SpringMVC配置讲解 ³SpringM...

智慧点点
2018/08/26
27
0
新书上架:《Spring Boot 开发实战》基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战

新书上架:《Spring Boot 开发实战》 — 基于 Kotlin + Gradle + Spring Boot 2.0 的企业级服务端开发实战 京东下单链接 https://item.jd.com/31178320122.html 天猫下单链接 https://detail...

程序员诗人
2018/08/22
0
0
【Spring boot实践】Spring boot 认识

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Rsx/article/details/89003901 文章目录 Spring boot 简介 Spring boot是spring 家族中的一个全新的框架,它用...

不专业得Cook-任晓帅
04/03
0
0
SSM-SpringMVC-01:SpringMVC是什么?SpringMVC执行流程

------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- SpringMVC简介:   SpringMVC也叫Spring Web mvc,属于表现层的框架。Spring MVC是Spring框架的一部分,是在Spring3.0后...

晨曦dawn
2018/03/19
0
0

没有更多内容

加载失败,请刷新页面

加载更多

插入排序算法

《Java算法总纲目录》 1、定义     元素被分为有序区和无序区两部分。最初有序区只有一个元素。每次从无序区中选择一个元素,插入到有序区的位置,直到无序区变空。 2、代码 public c...

木九天
26分钟前
3
0
ApacheCN 翻译/校对/笔记整理活动进度公告 2019.10.18

注意 请贡献者查看参与方式,然后直接在 ISSUE 中认领。 翻译/校对三个文档就可以申请当负责人,我们会把你拉进合伙人群。翻译/校对五个文档的贡献者,可以申请实习证明。 请私聊片刻(52981...

ApacheCN_飞龙
28分钟前
4
0
Hands-on! 如何给 TiDB 添加新系统表

作者:黄东旭 “TiDB,你已经是一个成熟的数据库了,该学会用自己的 SQL 查自己的状态了。” 对于一个成熟的数据库来说,通过 SQL 来查询系统本身的状态再正常不过,对于 MySQL 来说 INFOMA...

TiDB
34分钟前
4
0
SpringBoot admin+Eureka+钉钉通知

SpringBoot admin+Eureka+钉钉通知 一、效果 登录账号+密码 监控服务 查看实时日志 钉钉通知 二、什么是Spring Boot Admin ? Spring Boot Admin是一个开源社区项目,用于管理和监控SpringB...

小白的成长
37分钟前
18
0
docker-rabbitmq

docker pull rabbitmqmkdir -p /rabbitmqdocker run -d \--name rabbitmq \--hostname rabbitmq \-v /rabbitmq:/var/lib/rabbitmq \-e RABBITMQ_DEFAULT_USER=root \-e RA......

李琼涛
39分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部