Spring框架的AOP/IOC/容器核心原理实例详解

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

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

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

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

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

        1)容器

        2)控制反转(IoC ,Inversion ofControl)

        3)面向切面编程(AOP ,Aspect-OrientedProgramming)

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

二、一个简单的例子程序

  假设有一个如下应用场景:

(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); 
    }
} 
注:实际上这是一个面向接口编程的一个简单小实例
三、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),只需要在容器类中修改声明,模块A不需要修改任何代码。一定程度上降低了模块之间的耦合。

(即把  ReportGenertor reportGenertor = new SMSReportGenertor();   

  改为:     ReportGenertor reportGenertor = new MailReportGenertor();)

四、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的写法如下:


<span style="font-size:14px;">//为需要依赖注入的类加入一种被称为setter的方法 
</span><span style="font-size: 16px;">
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) { 
        ...  
    } 
}</span>
在容器中把依赖注入:

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,完成依赖注入。

五、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简单很多。
六、Spring总结
 银行月账单报表例子通过使用Spring框架后变成了如下结构:

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


文章来源:https://my.oschina.net/myriads/blog/37922


  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: SpringAOPIOC实现原理AOP(面向切面编程)是Spring框架一个重要特性,它通过在运行时动态地将代码切入到类的方法中,实现了对业务逻辑的解耦和横向切割。SpringAOP实现原理是基于动态代理和字节码生成技术,通过在运行时动态地生成代理对象,将切面逻辑织入到目标对象的方法中。 IOC(控制反转)是Spring框架的另一个重要特性,它通过将对象的创建和依赖关系的管理交给Spring容器来实现,从而实现了对象之间的解耦和灵活性。SpringIOC实现原理是基于反射和XML配置文件,通过读取配置文件中的信息,动态地创建对象并注入依赖关系,从而实现了对象的管理和控制。 ### 回答2: Spring框架作为Java企业应用中广泛使用的开发框架,其内部实现的AOPIOC功能是其较为重要的组成部分。AOP(面向切面编程)和IOC(控制反转)是两个不同的概念,但它们之间存在紧密联系。在回答任何一个问题之前,需要更深入地了解这些术语以及它们的目的。 首先,了解AOP的实现原理AOP的主要原理是将整个应用程序分解成特定的相关部分,并标识出这些部分的职责。然后,AOP框架将横切关注点(Crosscutting Concerns)划分为一个个切面(Aspect),并实现将目标对象在运行时交织上这些切面的功能。这个交织的过程称为织入(Weaving),这样应用程序中的某些功能可以在运行时动态地添加到对象中,而无需对其进行静态编码。在Spring框架中,主流的AOP实现采用代理模式(Proxy)。它将目标对象和切面运作在代理对象之间,代理对象则在运行时插入切面功能。 其次,了解IOC的实现原理。控制反转是指将对象间的依赖关系的控制权从目标对象自身转移到容器中管理,从而实现对象的松耦合。Spring框架是典型的IOC容器,它利用依赖注入(Dependency Injection)技术,通过在配置文件(XML或JavaConfig)中定义对象之间的依赖关系,在容器实例化对象并完成之间的依赖注入。在依赖注入中,容器将对象之间的依赖关系检测到,并自动为其中的依赖添加实例。这样,程序员就不需要在代码中显式地将对象之间的依赖关系硬编码,而可以通过注解、XML配置文件或JavaConfig等方式来管理这些对象。 综上所述,AOPIOCSpring框架核心概念之一。通过使用Spring框架AOPIOC功能,开发人员可以编写更简洁、高效的代码,并更容易地实现面向对象编程的最佳实践。 ### 回答3: SpringAOPIOC(控制反转)是Spring框架中非常重要的两个部分,能够让开发人员更好的实现面向切面编程和解耦,提高代码可维护性和灵活性。 首先,让我们来看看SpringIOC实现原理SpringIOC是控制反转的实现,即通过容器管理对象之间的依赖关系,而不是在代码中直接通过new关键词来创建对象。SpringIOC实现的核心思想是依赖注入(Dependency Injection,DI),即对象通过Setter、构造函数、注解等方式获取它所需要的依赖对象。通过Spring容器来管理和维护所有应用程序需要的对象实例和依赖关系。在实现IOC的时候,Spring会通过配置文件或者注解方式把对象的创建、初始化和依赖注入关系都详细地描述出来,然后在运行时,通过反射机制自动创建对象,再将依赖注入进去,以此实现对象的控制权转由Spring容器来掌控。这样的实现可以减轻程序员的工作负担,提高程序的复用性和扩展性。 其次,SpringAOP实现原理也是很重要的。AOP可以让程序员将一些横切逻辑和业务处理逻辑分离出来,达到代码解耦的目的。在Spring中,AOP使用代理(Proxy)的方式实现,通过对需要执行的目标方法进行拦截,并在目标方法执行前后执行额外的逻辑,如开启/关闭事务、安全认证、日志记录等。代理类可以使用JDK自带的动态代理机制或CGLIB等第三方代理框架来生成,而切面对象则负责维护切面所需要拦截的目标对象、目标方法、切面执行顺序等信息。SpringAOP框架为切面提供丰富的表达式语言来指定需要拦截的方法,如通配符、正则表达式和AspectJ注解等。 因此,SpringAOPIOC实现原理都是非常重要的,它们的实现可以让应用程序代码更灵活、更容易维护,同时也使得程序员的工作更加高效。但是需要注意的是,在使用SpringAOPIOC时,要注意配置的正确性和合理性,否则可能会带来一些潜在的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值