SpringIOC&AOP
Spring是为简化企业级系统开发而诞生的,Spring框架为J2EE应用常见的问题提供了简单、有效的解决方案,使用Spring,你可以用简单的POJO(Plain Old Java Object)来实现那些以前只有EJB才能实现的功能。这样不只是能简化服务器端开发,任何Java系统开发都能从Spring的简单、可测试和松耦合特征中受益。可以简单的说,Spring是一个轻量级的反向控制(IoC)和面向切面编程(AOP)容器框架[3]。Spring IoC,借助于依赖注入设计模式, 使得开发者不用理会对象自身的生命周期及其关系,而且能够改善开发者对J2EE模式的使用;Spring AOP,借助于Spring实现的拦截器,开发者能够实现以声明的方式使用企业级服务,比如安全性服务、事务服务等。Spring IoC和 Spring ; AOP组合,一起形成了Spring,这样一个有机整体,使得构建轻量级的J2EE架构成为可能,而且事实证明,非常有效。没有Spring IoC的Spring AOP是不完善的,没有Spring AOP的Spring IoC是不健壮的。本文是以Spring架构的成功的实际商务系统项目为背景,阐述了反向控制原理和面向切面的编程技术在Spring框架中的应用,同时 抽取适量代码示意具体应用,并和传统开发模式进行对比,展示了Spring framework的简单,高效,可维护等优点。
1、Spring IoC 1.1 反向控制原理
反向控制是Spring框架的核心。但是,反向控制是什么意思?到底控制的什么方面被反向 了呢?2004年美国专家Martin Fowler发表了一篇论文《Inversion of Control Containers and the Dependency Injection pattern》阐述了这个问题,他总结说是获得依赖对象的方式反向了,根据这个启示,他还为反向控制提出了一个更贴切的名字:Dependency Injection(DI 依赖注入)。
通常,应用代码需要告知容器或框架,让它们找到自身所需要的类,然后再由应用代码创建待使 用的对象实例。因此,应用代码在使用实例之前,需要创建对象实例。然而,IoC模式中,创建对象实例的任务交给IoC容器或框架(实现了IoC设计模式的 框架也被称为IoC容器),使得应用代码只需要直接使用实例,这就是IoC。相对IoC 而言,“依赖注入”的确更加准确的描述了这种设计理念。所谓依赖注入,即组件之间的依赖关系由容器在运行期决定,形象的来说,即由容器动态的将某种依赖关 系注入到组件之中。
1.2 IoC在Spring中的实现
任何重要的系统都需要至少两个相互合作的类来完成业务逻辑。通常,每个对象都要自己负责得 到它的合作(依赖DI)对象。你会发现,这样会导致代码耦合度高而且难于测试。使用IoC,对象的依赖都是在对象创建时由负责协调系统中各个对象的外部实体提 供的,这样使软件组件松散连接成为可能。
DI依赖注入
DI(依赖注入)是IoC实现的重要技术,有如下2中方式:
1) setter方式注入
2) 构造方式注入
3)接口注入(不是很常用,这里就不讲了)
4)注解注入
注入类型有如下几种:简单值、集合、bean对象 Ioc和DI的关系? 我们认为Spring是具有IoC特性的框架。 实现IoC是由Spring容器来完成的,Spring容器通过依赖注入DI建立 起对象(组件、Bean) 乊间的关系。 我们可以这样理解:DI是IoC实现的一种手段,Ioc通过DI来实现。下面示意了Spring IoC 应用,步骤如下:
定义一个接口Dao.java
package Dao; public interface Dao { public void say(); }
一个Dao的实现类StuDaoImpl.java
package Dao.Impl; import Dao.Dao; public class StuDaoImpl implements Dao{ public void say() { System.out.println("我是学生"); } }
一个Dao的实现类TeaDaoImpl.java
package Dao.Impl; import Dao.Dao; public class TeaDaoImpl implements Dao{ public void say() { System.out.println("我是老师"); } }
一个Service的Say.java类
package Dao.Service; import Dao.Dao; public class Say { private Dao tea; public void setTea(Dao tea) { this.tea = tea; } /*public Say(Dao tea){//构造器注入的时候用 this.tea=tea; }*/ public void saywork(){ tea.say(); } }
一个测试类Test.java
package Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import Dao.Service.Say; public class test { public static void main(String[] args) { String conf="applicationContext.xml"; ApplicationContext ac=new ClassPathXmlApplicationContext(conf); Say say=(Say)ac.getBean("say"); say.saywork(); } }
Spring的配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans default-lazy-init="true"> <bean id="student" class="Dao.Impl.StuDaoImpl" > </bean> <bean id="teacher" class="Dao.Impl.TeaDaoImpl" > </bean> <bean id="say" class="Dao.Service.Say"> <property name="tea" ref="teacher"></property><!--setter注入法--> <!--<constructor-arg index="0" ref="teacher"></constructor-arg><!--构造器注入法--> </bean> </beans>
最后因为id=teacher通过<property name="tea" ref="teacher"></property>注入到say类中,所以程序输出“我是老师”
如果想输出"我是学生"那么直接改一下配置文件就好了,不需要改动源文件的内容。
注解方式配置
常用配置方式有XML文档配置,还有一种是通过注解方式配置。 采用注解方式的目的就是为了简化XML配置文件。 注解方式(也叫注释)是JDK5版本提供的,之前的版本丌支持。 Spring2.5版本后支持注解方式,之前的版本不支持。
组件自动扫描功能
首先需要在applicationContext.xml中添加<context:component-scan/>
1) 扫描Bean组件的注解,替代xml中的<bean>元素的定义。
@Service 用于Service业务组件
@Control 用于Action控制组件
@Respository 用于DAO数据访问组件
@Component 用于其他组件
Bean组件扫描到容器后,
默认名字为类名(首字母小写)如果需要自定义名称可以使用@Service("id名")
2) 依赖注入的注解标记
@Resource 按名称@Resource(name="id名")
@AutoWired 按名称
@Autowired
@Qualifier("id名")
3)其他注解
@Scope 等价于<bean scope="">
@PostConstruct 等价于<bean init-method="">
@PreDestroy 等价于<bean destroy-method="">
我们不采用xml的方式注入属性了,采用注解的方式注入
1) 使用工程spring1
2) 采用注解的方式
a. 修改applicationContext.xml
Xml文档中丌再配置bean了,我们引入新的标签 <context:component-scan>标签的作用是迚行组件自劢扫描 注意,使用此标签的前提是必须具有xmlns:context命名空间 注意,和之前的applicationContext.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" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation=" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd"> <context:component-scan base-package="Dao"></context:component-scan> </beans>
修改StuDaoImpl.java
package Dao.Impl; import org.springframework.stereotype.Service; import Dao.Dao; @Service public class StuDaoImpl implements Dao{ public void say() { System.out.println("我是学生"); } }
修改TeaDaoImpl.java
package Dao.Impl; import org.springframework.stereotype.Service; import Dao.Dao; @Service("tea")//别名 public class TeaDaoImpl implements Dao{ public void say() { System.out.println("我是老师"); } }
修改Say.java
package Dao.Service; import javax.annotation.Resource; import org.springframework.stereotype.Service; import Dao.Dao; @Service public class Say { @Resource private Dao tea; /*public Say(Dao tea){ this.tea=tea; }*/ public void saywork(){ tea.say(); } }
修改测试类Test.java
package Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import Dao.Service.Say; public class test { public static void main(String[] args) { String conf="applicationContext.xml"; ApplicationContext ac=new ClassPathXmlApplicationContext(conf); Say say=(Say)ac.getBean("say"); say.saywork(); } }
结果是一样的
仔细观察一下上面的代码,可以看到:
(1)我们的组件并不需要实现框架指定的接口,因此可以轻松的将组件从Spring中脱离,甚至不需要任何修改,这在基于EJB框架实现的应用中是难以想象的。
(2)组件间的依赖关系减少,极大改善了代码的可重用性。Spring的依赖注入机制,可以在运行期为组件配置所需资源,而无需在编写组件代码时就加以指定,从而在相当程度上降低了组件之间的耦合。
2、Spring AOP
2.1 面向切面编程基础
通常,系统由很多组件组成,每个组件负责一部分功能,然而,这些组件也经常带有一些除了核 心功能之外的附带功能 。系统服务如日志、事务管理和安全经常融入到一些其他功能模块中。这些系统服务通常叫做交叉业务,这是因为它们总是分布在系统的很多组件中。通过将这些业 务分布在多个组件中,给我们的代码引入了双重复杂性。
(1) 实现系统级业务的代码在多个组件中复制。这意味着如果你要改变这些业务逻辑,你就必须到各个模块去修改。就算把这些业务抽象成一个独立模块,其它模块只是调用它的一个方法,但是这个方法调用也还是分布在很多地方。
(2) 组件会因为那些与自己核心业务无关的代码变得杂乱。一个向地址录中添加条目的方法应该只关心如何添加地址,而不是关心它是不是安全或支持事务的。
此时,我们该怎么办呢?这正是AOP用得着的地方。AOP帮助我们将这些服务模块化,并把它们声明式地应用在需要它们的地方,使得这些组件更加专注于自身业务,完全不知道其它涉及到的系统服务。
这里的概念切面,就是我们要实现的交叉功能,是应用系统模块化的一个方面或领域。切面的最 常见例子就是日志记录。日志记录在系统中到处需要用到,利用继承来重用日志模块是不合适的,这样,就可以创建一个日志记录切面,并且使用AOP在系统中应 用。下图展示了切面应用方式
图表 1 应用切面
其中,通知Advice是切面的实际实现。连接点Joinpoint是应用程序执行过程中 插入切面的地点,这个地点可以是方法调用,异常抛出,甚至可以是要修改的字段,切面代码在这些地方插入到你的应用流程中,添加新的行为。切入点 Pointcut定义了Advice应该应用在那些连接点,通常通过指定类名和方法名,或者匹配类名和方法名式样的正则表达式来指定切入点。
2.2 AOP在Spring中的实现
基于AOP,业界存在各种各样的AOP实现,比如,JBoss AOP、Spring AOP、ASPectJ、 Aspect Werkz等。各自实现的功能也不一样。AOP实现的强弱在很大程度上取决于连接点模型。目前,Spring只支持方法级的连接点。这和一些其他AOP框 架不一样,如AspectJ和JBoss,它们还提供了属性接入点,这样可以防止你创建特别细致的通知,如对更新对象属性值进行拦截。然而,由于 Spring关注于提供一个实现J2EE服务的框架,所以方法拦截可以满足大部分要求,而且Spring的观点是属性拦截破坏了封装,让Advice触发在属性值改变而不是方法调用上无疑是破坏了这个概念。
Spring的AOP框架的关键点如下:
(1)Spring实现了AOP联盟接口。在Spring AOP中,存在如下几种通知(Advice)类型
Before通知:在目标方法被调用前调用,涉及接口org.springFramework.aop.MethodBeforeAdvice;
After通知:在目标方法被调用后调用,涉及接口为org.springframework.aop.AfterReturningAdvice;
Throws通知:目标方法抛出异常时调用,涉及接口org.springframework.aop.MethodBeforeAdvice;
Around通知:拦截对目标对象方法调用,涉及接口为org.aopalliance.intercept.MethodInterceptor。
(2)用Java编写Spring通知,并在Spring的配置文件中,定义在什么地方应用通知的切入点。
(3)Spring的运行时通知对象。代理Bean只有在第一次被应用系统需要的时候才被 创建。如果你使用的是ApplicationContext,代理对象在BeanFactory载入所有Bean的时候被创建。Spring有两种代理创 建方式。如果目标对象实现了一个或多个接口暴露的方法,Spring将使用JDK的 java.lang.reflect.Proxy类创建代理。这个类让Spring动态产生一个新的类,它实现所需的接口,织入了通知,并且代理对目标对 象的所有请求。如果目标对象没有实现任何接口,Spring使用CGLIB库生成目标对象的子类。在创建这个子类的时候,Spring将通知织入,并且将 对目标对象的调用委托给这个子类。此时,需要将Spring发行包lib/cglib目录下的JAR文件发布到应用系统中。
2.3 Spring AOP的优势
借助于Spring AOP,Spring IoC能够很方便的使用到非常健壮、灵活的企业级服务,是因为Spring AOP能够提供如下几方面的优势:
(1)允许开发者使用声明式企业服务,比如事务服务、安全性服务;EJB开发者都知道,EJB组件能够使用J2EE容器提供的声明式服务,但是这些服务要借助于EJB容器,而Spring AOP却不需要EJB容器,借助于Spring的事务抽象框架就可以这些服务。
(2)开发者可以开发满足业务需求的自定义切面;
(3)开发Spring AOP Advice很方便。因为这些AOP Advice仅是POJO类,借助于Spring提供的ProxyFactoryBean,能够快速的搭建Spring AOP Advice。
下面我们来看一个SpringAOP的列子
新建接口DeptService.java
package Service; public interface DeptService { public void delete(); public void save(); }
新建DeptService的实现类DeptServiceImpl.java
package Service; public class DeptServiceImpl implements DeptService{ public void delete() { System.out.println("删除部门"); } public void save() { System.out.println("部门保存"); } }
新建一个部门员工的操作监控类LoggerAop.java
package aop; import java.text.SimpleDateFormat; import java.util.Date; import org.aspectj.lang.ProceedingJoinPoint; import Util.PropertiesUtil; public class LoggerAop { public void mybefore(){ } //retVal用于获取目标对象方法的返回值 public void myafterReturning(Object retVal){ } //最终通知 public void myafterFinal(){ } //异常通知 public void mythrowing(Exception e){ } //环绕通知 public Object logger( ProceedingJoinPoint pjp) throws Throwable{ //获取目标对象类型 String clz=pjp.getTarget().getClass().getSimpleName(); //获取要调用的目标方法 String method=pjp.getSignature().getName(); String optMsg=PropertiesUtil.getOptMsg(clz+"."+method); SimpleDateFormat stm=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println("张三执行了"+stm.format(new Date())+"时间做了"+optMsg+"操作"); Object retval=pjp.proceed();//调用目标对象方法 return retval; } }
新建测试类Test.java
package Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import Service.DeptService; import Service.UserService; import Service.UserServiceImpl; public class test { public static void main(String[] args) { String conf="applicationContext.xml"; ApplicationContext ac=new ClassPathXmlApplicationContext(conf); /*UserService mother=(UserService)ac.getBean("UserService"); mother.login(); mother.regist();*/ DeptService dept=(DeptService)ac.getBean("deptService"); dept.delete(); dept.save(); } }
新建Spring的配置文件applicationContext.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" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation=" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd"> <bean id="UserService" scope="prototype" class="Service.UserServiceImpl"> <!-- setter注入这样写 --> <property name="userdao" ref="userDAO"></property> </bean> <bean id="userDAO" scope="prototype" class="Dao.Impl.UserDaoImpl"></bean> <!-- AOP应用 --> <bean id="loggerAop" class="aop.LoggerAop" scope="prototype"> </bean> <bean id="deptService" class="Service.DeptServiceImpl" scope="prototype"></bean> <aop:config> <!-- 切入点,确定目标对象 --> <aop:pointcut expression="within(Service.*)" id="loggerPointcut"/> <aop:aspect id="loogerAspect" ref="loggerAop"> <!-- 方法执行的时候要先执行这个 --> <aop:around method="logger" pointcut-ref="loggerPointcut"/> </aop:aspect> </aop:config> </beans>
运行程序:
7) 运行Test 如上,已经将UserService纳入Spring容器的管理中。 我们现在又这样的需求,要为UserSerice的操作增加日志记录功能。 我们需要为更新、保存、删除操作增加记录日志功能,那么记录日志功能就属于切面功能。
3、结语
本文详细阐述了Spring背后的IoC原理和AOP技术,以实际成功项目为背景,抽取简 短片断,展示了Spring架构J2EE应用系统的原理。Spring IoC借助于依赖注入机制,减轻了组件之间的依赖关系,同时也大大提高了组件的可移植性,组件得到了更多的重用机会。借助于Spring AOP,使得开发者能声明式、基于元数据访问企业级服务,AOP合理补充了OOP技术,Spring AOP合理地补充了Spring IoC容器。Spring IoC与Spring AOP组合,使得Spring成为成功的J2EE架构框架,并能与标准的EJB等标准对抗,EJB不再是必需品。Spring已经冲入了J2EE的核心, 将引领整个J2EE开发、架构的方向。