1、使用框架的意义与Spring的主要内容
随着软件结构的日益庞大,软件模块化趋势出现,软件开发也需要多人合作,随即分工出现。如何划分模块,如何定义接口方便分工成为软件工程设计中越来越关注的问题。良好的模块化具有以下优势:可扩展、易验证、易维护、易分工、易理解、代码复用。
优良的模块设计往往遵守“低耦合高内聚”的原则。而“框架”是对开发中良好设计的总结,把设计中经常使用的代码独立出来,所形成的一种软件工具。用户遵守它的开发规则,就可以实现良好的模块化,避免软件开发中潜在的问题。广义上的框架无处不再,一个常见的例子就是PC硬件体系结构,人们只要按照各自需要的主板、显卡、内存等器件就可以任意组装成自己想要的电脑。而做主板的厂商不用关心做显卡厂商的怎么实现它的功能。软件框架也是如此,开发人员只要在Spring框架中填充自己的业务逻辑就能完成一个模块划分清晰纷的系统。
这里主要通过一个银行通知用户月收支记录的小例子来介绍轻型J2EE框架Spring的主要内容、它所解决的问题和实现的方法。
Spring框架主要可以分为3个核心内容:
1、容器
2、控制反转(IoC ,Inversion of Control)
3、面向切面编程(AOP ,Aspect-Oriented Programming)
例子中依次对这些特性进行介绍,描述了软件模块化后存在的依赖与问题,以及Spring框架如何解决这些问题。
2、一个简单的例子程序
假设有一个如下应用场景:(1)一个银行在每月的月初都需要向客户发送上个月的账单,账单发送的方式可以为纸质邮寄、或者短信方式。(2)还有一个潜在的需求:为了安全起见,在每个函数操作过程中都需要记录日志,记录参数传入是否正常,函数是否正常结束,以便出错时系统管理员查账。
那么对这个需求进行简单实现。系统框图如下所示:
首先定义一个账单输出的接口:
2 |
public interface ReportGenerator{
|
3 |
public void generate(String[][] table) ;
|
实现“打印纸质账单”与“发送短信”两个具体功能:
2 |
public class PageReportGenerator implement ReportGenerator {
|
3 |
public void generate(String[][] table) {
|
2 |
public class SMSReportGenerator implement ReportGenerator {
|
3 |
public void generate(String[][] table) {
|
上层业务逻辑对上个月的账目进行统计并调用接口产生纸质或者短信结果:
02 |
public class ReportService{
|
03 |
private ReportGenerator reportGenerator = new SMSReportGenerator();
|
04 |
public void generateMonthlyReport( int year, int month) {
|
06 |
String[][] statistics = null ;
|
08 |
reportGenerator.generate(statistics);
|
这个实现源代码请查看文章结尾附录中的"BankOld"。源代码中与例子中程序略有区别:由于使用log4j需要引用外部的包,并且需要写配置文件,为了方便,源代码中的日志输出用system.out.println()代替。
3、Spring中的容器
A、模块化后出现的问题与隐患
假设随着工程的复杂化,上面的例子需要分成两个模块,以便开发时分工,一般会以如下结构划分:
划分后再看原来的代码:
2 |
public class ReportService{
|
3 |
private ReportGenerator reportGenerator = new SMSReportGenerator();
|
5 |
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、用容器解决问题
消除实现依赖一般可以通过添加一个容器类来解决。在例子程序容器代码如下:
02 |
public class Container {
|
04 |
public static Container instance;
|
06 |
private Map<String, Object> components;
|
09 |
component = new HashMap<String, Object>();
|
12 |
ReportGenertor reportGenertor = new SMSReportGenertor();
|
13 |
components.put(“reportGenertor”, reportGenertor);
|
15 |
ReportService reportService = new ReportService();
|
16 |
components.put(“reportService”, reportService);
|
19 |
public Object getComponent(String id){
|
20 |
return components.get(id);
|
使用容器后,模块A的ReportService的属性实现方法也发生了变化。
02 |
public class ReportService{
|
05 |
private ReportGenerator reportGenerator = (ReportGenerator) Container.instance.getComponent(“reportGenerator”);
|
07 |
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的写法如下:
03 |
public class ReportService{
|
08 |
private ReportGenerator reportGenerator;
|
10 |
public void setReportGenerator( ReportGenerator reportGenerator) {
|
11 |
this .reportGenerator = reportGenerator;
|
14 |
public void generateMonthlyReport( int year, int month) {
|
在容器中把依赖注入:
02 |
public class Container {
|
06 |
component = new HashMap<String, Object>();
|
08 |
ReportGenertor reportGenertor = new SMSReportGenertor();
|
09 |
components.put(“reportGenertor”, reportGenertor);
|
11 |
ReportService reportService = new ReportService();
|
12 |
reportService.setReportGenerator(reportGenerator);
|
13 |
components.put(“reportService”, reportService);
|
这样一来ReportService就不用管SMSReportGenertor在容器中是什么名字,模块A对于模块B只有接口依赖,做到了松耦合。
C、Spring IoC容器的XML配置
每个使用Spring框架的工程都会用到容器与控制反转,为了代码复用,Spring把通用的代码独立出来形成了自己的IoC容器供开发者使用:
与上面例子中实现的容器相比,Spring框架提供的IoC容器要远远复杂的多,但用户不用关心Spring
IoC容器的代码实现,Spring提供了一种简便的bean依赖关系配置方式------使用XML文件,在上面的例子中,配置依赖关系只要在工程根目录下的“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" >
|
08 |
< bean id = "smsReportGenerator" class = "bank.SMSReportGenerator" />
|
10 |
< bean id = "reportService" class = "bank.ReportService" >
|
11 |
< property name = "reportGenerator" ref = "smsReportGenerator" />
|
<?xml version="1.0" encoding="UTF-8"?>是标准的XML头,xmlns引用的是一些命名空间,两个一般在工程中自动生成。后面的内容由用户输入,主要表示实例化SMSReportGenerator,实例化ReportService并把SMSReportGenerator的对象smsReportGenerator赋值给ReportService的属性reportGenerator,完成依赖注入。
5、Spring中的面向切面编程
A、日志问题以及延伸
在例子的需求中有一条是:需要记录日志,以便出错时系统管理员查账。回顾例子中的代码,在每个方法中都加了日志操作:
02 |
public class ReportService{
|
04 |
public void generateMonthlyReport( int year, int month) {
|
06 |
String[ ][ ] statistics = null ;
|
08 |
reportGenerator.generate(statistics);
|
02 |
public class PageReportGenerator implement ReportGenerator {
|
04 |
public void generate(String[ ][ ] table) {
|
可以看出在每个方法的开始与结尾都调用了日志输出,这种零散的日志操作存在着一些隐患,会导致维护的困难。比如日志输出的格式发送了变化,那么无论模块A还是模块B的程序员都要对每个方法每个输出逐条修改,极容易遗漏,造成日志输出风格的不一致。又比如不用Log4j日志输出工具更换其他工具,如果遗漏一个将会出现严重BUG。
与日志输出相似的问题在编程中经常遇到,这种跨越好几个模块的功能和需求被称为横切关注点,典型的有日志、验证、事务管理等。
横切关注点容易导致代码混乱、代码分散的问题。而如何将很切关注点模块化是本节的重点。
B、代理模式
传统的面向对象方法很难实现很切关注点的模块化。一般的实现方式是使用设计模式中的代理模式。代理模式的原理是使用一个代理将对象包装起来,这个代理对象就取代了原有对象,任何对原对象的调用都首先经过代理,代理可以完成一些额外的任务,所以代理模式能够实现横切关注点。
可能在有些程序中有很多横切关注点,那么只需要在代理外再加几层代理即可。以银行账单为例介绍一个种用Java Reflection API动态代理实现的横切关注点模块化方法。系统提供了一个InvocationHandler接口:
2 |
public interface InvocationHandler {
|
3 |
public Object invoke(Object proxy, Method method, Object[] args) throw Throwable;
|
我们需要实现这个接口来创建一个日志代理,实现代码如下:
02 |
public class LogHandler implement InvocationHandler{
|
06 |
public LogHandler(Object target){
|
09 |
public Object invoke(Object proxy, Method method, Object[] args ) throw Throwable{
|
12 |
log4j.info(“开始:方法”+ method.getName() + “参数”+Arrays.toString(args) );
|
15 |
Object result = method.invoke(target, args);
|
18 |
log4j.info(“结束:方法”+ method.getName() + “返回值”+ result );
|
这样既可以使得日志操作不再零散分布于各个模块,易于管理。调用者可以通过如下方式调用:
03 |
public static void main(String[ ] args){
|
04 |
ReportGenerator reportGeneratorImpl = new SMSReportGenerator ();
|
07 |
ReportGenerator reportGenerator = (ReportGenerator ) Proxy.newProxyInstance(
|
08 |
reportGeneratorImpl.getClass().getClassLoader(),
|
09 |
reportGeneratorImpl.getClass().getInterfaces(),
|
10 |
new LogHandler(reportGeneratorImpl)
|
代理模式很好的实现了横切关注点的模块化,解决了代码混乱代码分散问题,但是我们可以看出用 Java Reflection API 实现的动态代理结构十分复杂,不易理解,Spring框架利用了代理模式的思想,提出了一种基于JAVA注解(Annotation)和XML配置的面向切面编程方法(AOP ,Aspect-Oriented Programming)简化了编程过程。
C、Spring AOP 使用方法
Spring AOP使用中需要为横切关注点(有些时候也叫切面)实现一个类,银行账单的例子中,切面的实现如下:
03 |
public class LogAspect{
|
05 |
@Before (“execution(* *.*(..))”)
|
06 |
public void LogBefore(JoinPoint joinPoint) throw Throwable{
|
07 |
log4j.info(“开始:方法”+ joinPoint.getSignature().getName() );
|
10 |
@After (“execution(* *.*(..))”)
|
11 |
public void LogAfter(JoinPoint joinPoint) throw Throwable{
|
12 |
log4j.info(“结束:方法”+ joinPoint.getSignature().getName() );
|
注解1表示这个类是一个切面,注解2中" * *.*(..)* "是一个通配符,表示在容器中所有类里有参数的方法。@Before(“execution(* *.*(..))”)表示在所有类里有参数的方法前调用切面中德 LogBefore() 方法。同理,注解3中@After(“execution(* *.*(..))”)表示在所有类里有参数的方法执行完后调用切面中的LogAfter()方法。
实现完切面类后,还需要对Spring工程中的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" >
|
11 |
< aop:aspectj-autoproxy />
|
12 |
< bean id = "smsReportGenerator" class = "bank.SMSReportGenerator" />
|
13 |
< bean id = "reportService" class = "bank.ReportService" >
|
14 |
< property name = "reportGenerator" ref = "smsReportGenerator" />
|
16 |
< bean class = "bank.LogAspect" />
|
这比之前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 攻略》
分享到:
相关推荐
通过阅读"SSH框架简介.doc"和"Spring框架简介.doc",你可以深入理解这两个框架的工作原理、优缺点及应用场景,为你的Java开发之路打下坚实基础。在学习过程中,实践是关键,尝试创建小型项目,将理论知识转化为实际...
提供Spring框架的基本功能,其主要组件是BeanFactory,是工厂模式的实现。 Spring 上下文。向Spring 框架提供上下文信息,包括企业服务,如 JNDI、EJB、电子邮件、国际化、校验和调度等。 Spring AOP。通过配置管理...
#### 一、Spring框架简介 Spring框架是目前最流行的Java企业级开发框架之一,它提供了一整套轻量级的解决方案,旨在简化企业级应用的开发。Spring的核心功能包括依赖注入(Dependency Injection, DI)、面向切面编程...
《Spring框架开发参考中文手册》是一本专门为Java开发者设计的指南,旨在帮助他们深入理解和熟练运用Spring框架进行软件开发。Spring作为Java企业级应用开发的主流框架,以其强大的功能、灵活的设计和广泛的社区支持...
我们从一个简单的容器开始,一步步的重构,最后实现一个基本的Spring框架的雏形,为了帮助我们更加深入的理解Spring的IoC的原理和源码。 详细内容见博文: 【SSH进阶之路】一步步重构容器实现Spring框架——从一个...
即Spring框架的概述 1。 介绍Spring框架 1.1。 依赖注入和控制反转 1.2。 模块 1.2.1。 核心容器 1.2.2。 数据访问/集成 1.2.3。 web 1.2.4。 AOP和仪表 1 2 5。 测试 1.3。 使用场景 1.3.1。 依赖关系管理和命名...
Spring框架是中国IT开发领域中最广泛使用的Java应用框架之一,它为构建高质量的、松耦合的、可测试的Java应用程序提供了全面的解决方案。这个“spring框架帮助文档”旨在为开发者提供详尽的指导,帮助他们克服在使用...
《Java Spring框架学习指南》是一份详尽的教育资源,旨在帮助开发者从零基础开始逐步掌握Spring框架,并深入理解其核心概念和技术。这份PDF文档全面覆盖了Spring框架的基础知识,包括IoC(Inversion of Control)...
Spring框架基础 Spring框架起源于Rod Johnson在2002年出版的《Expert One-on-One J2EE Design and Development》中的代码。这个框架融合了Java企业版(JEE)开发的最佳实践,与一流的第三方框架集成,并提供了简单...
**Java Spring 框架简介** Spring 是一个广泛使用的开源Java框架,主要设计用于简化企业级应用程序开发。它由Rod Johnson在2003年创建,最初作为一个轻量级的IoC(Inversion of Control,控制反转)容器,如今已...
**Spring框架简介** Spring框架是Java开发领域中最广泛使用的轻量级框架之一,自2003年首次发布以来,它极大地推动了企业级Java应用的发展。Spring的主要目标是简化Java开发,通过提供一个全面的编程和配置模型,...
Spring框架是Java开发中广泛应用的轻量级框架,它以其依赖注入(Dependency Injection,DI)和面向切面编程(Aspect-Oriented Programming,AOP)为核心特性,极大地简化了企业级应用的开发工作。这份"Spring框架...
Spring框架是由Rod Johnson创建的开源框架,旨在解决企业应用开发的复杂性。Spring框架的出现极大地简化了企业级应用的开发,并且不仅仅局限于服务器端的开发。Spring的核心概念之一是使用简单的JavaBean来实现之前...
Spring框架是中国著名的Java企业级应用开发框架,由Rod Johnson在其著作《Expert One-on-One J2EE Design and Development》中首次提出。Spring以其轻量级、模块化和强大的依赖注入特性而闻名,它极大地简化了Java...
Spring框架基础总结 一、Spring框架概述 Spring框架是一个开源的控制反转(IoC)和面向切面(AOP)的容器框架,旨在简化企业级Java应用的开发工作。Spring框架的核心价值在于其分层架构设计,使得开发者可以根据...
Spring 框架的设计目标是让对象之间的依赖关系转而用配置文件来管理,也就是他的依赖注入机制。这个注入关系在一个叫 Ioc 容器中管理,那 Ioc 容器中有又是什么就是被 Bean 包裹的对象。Spring 正是通过把对象包装在...
Spring框架是Java开发中广泛应用的一个开源技术,以其强大的功能和灵活性深受开发者喜爱。Spring框架的整体结构包括多个核心模块,这些模块提供了丰富的功能,使得开发者能够更高效地构建企业级应用程序。 1. ...