spring的IOC AOP
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
1、控制反转(IOC)/依赖注入(DI):
在传统的程序设计中,当调用者需要被调用者的协助时,通常由调用者来创建被调用者的实例。但在spring里创建被调用者的工作不再由调用者来完成,因此控制反转(IoC),为什么称为反转呢?反转是相对于正向而言的,那么什么算是正向的呢?考虑一下常规情况下的应用程序,如果要在A里面使用C,你会怎么做呢?当然是直接去创建C的对象,也就是说,是在A类中主动去获取所需要的外部资源C,这种情况被称为正向的。那么什么是反向呢?就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中。
创建被调用者实例的工作通常由spring容器来完成,然后注入调用者,因此也被称为依赖注入(DI),所以其实依赖注入和控制反转是表达是同一个概念,只是描述的角度不同而已,依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
Spring 解决了一个非常关键的问题他可以让你把对象之间的依赖关系转而用配置文件来管理,也就是他的依赖注入机制。而这个注入关系在一个叫 Ioc 容器中管理,那 Ioc 容器中就是被 Bean 包裹的对象。Spring 正是通过把对象包装在 Bean 中而达到对这些对象管理以及一些列额外操作的目的。
用图例来说明一下,先看没有IoC/DI的时候,常规的A类使用C类的示意图,如图1所示:
图1 常规A使用C示意图
当有了IoC/DI的容器后,A类不再主动去创建C了,如图2所示:
图2 A类不再主动创建C
而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向的注入到A类中,如图3所示:
图3 有IoC/DI容器后程序结构示意图
1.1、Spring容器
Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化 Bean 并建立 Bean 之间的依赖关系。Spring 的 IoC 容器在完成这些底层工作的基 础上,还提供了 Bean 实例缓存、生命周期管理、Bean 实例代理、事件发布、资源装载等 高级服务。 Spring提供了多种容器的实现,分为两类,Bean工厂(由org.springframework.beans.factory.BeanFactory 接口定义)(最简单的容器,提供了基础的依赖注入支持) 和应用上下文(由org.springframework.context.ApplicationContext接口定义)(建立在Bean工厂基础之上,提供了系统架构服务,如从属性文件中读取文本信息,向相关的事件监听器发布事件)。
1.1.1、Bean Factory
Bean 工厂(com.springframework.beans.factory.BeanFactory)是 Spring 框架最核心的接 口,它提供了高级 IoC 的配置机制。 Bean工厂,不像其他工厂模式的实现,只能分发一种类型的对象,Bean工厂可以创建和分发各种类型的Bean。Bean工厂在实例化这些对象的时候还创建它们之间的关联关系(DI)。Bean工厂还要参与Bean的生命周期中,调用用户定义好的初始化和销毁方法。
Spring中有几种BeanFactory的实现方式,最常用的是org.springframework.beans.factory.xml.XmlBeanFactory.要创建XmlBeanFactory需要传递一个org.springframework.core.io.Resource实例给构造函数。
BeanFactory factory = new XmlBeanFactory(new FileSystemResource("c:/beans.xml"));
MyBean myBean = (MyBean)factory.getBean("myBean");
bean工厂延迟载入所有的单实例Bean,直到getBean()方法被调用的时候Bean才被创建。
1.1.2、ApplicationContext
建立在Bean工厂基础之上,提供了系统架构服务,如从属性文件中读取文本信息,向注册为监听器的Bean发送事件。ApplicationContext 的 主 要 实 现 类 是ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext,前者默认从类路径加载配置文件,后者默认从文件系统 中装载配置文件, 除了提供的一些附加的功能外,与BeanFactory 另一个重要的区别是关于单实例载入:bean工厂延迟载入所有的单实例Bean,直到getBean()方法被调用的时候Bean才被创建。上下文,会在上下文启动后预载入所有的单实例bean。
1.2、Bean的装配
1.2.1、使用XML Bean的装配
定义Car实现类
public class Car { private String color ; private Engine engine public Car() {} public Car(String color, Engine engine) { this.engine = engine; this.color = color } public void drive() { System.out.println("hu hu hu ….."); } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public Engine getEngine() { return engine; } public void setEngine(Engine engine) { this.engine = engine; } }
构造函数注入
<bean id = "bmw" class = "class.test.Car"> <constructor-arg value = “Black”>//普通类型 <constructor-arg ref = "turbineEngine">//其他bean, </bean>
可以注入内部bean,参见下面的setter注入。
setter方法注入
<bean id = "audi" class = "class.test.Car" <property name = "color" value = "White">//普通类型 <property name = " engine" ref = "naturalEngine">//其他bean </bean> <bean id = "Benz" class = "class.test.Car" <property name = "color" value = "Red">//普通类型 <property name = " engine"> //注入内部类 <bean class = "com.test.tool.BenzEngine" /> </bean>
自动装配:
显式的装配会导致大量的XML,Spring自动装配,只需要设置自动装配的<bean>中的autowire属性
<bean id = "kenny" class = "com.springinaciton.springidol.Instrumentalist" autowire = "autodetect">
Spring提供了四种自动装配类型:
- byName:在容器中寻找和需要自动装配的属性名相同的Bean(或ID)。如果没有找到这个属性就没有装配上。
- byType:在容器中寻找一个与自动装配的属性类型相同的Bean。如果没有找到这个属性就没有装配上。如果找到超过一个则抛出org.springframework.beans.factory.UnsatisfiedDependencyException异常。
- constructor:在容器中查找与需要自动装配的Bean的构造函数参数一致的一个或多个Bean。如果存在不确定Bean或构造函数容器会抛出org.springframework.beans.factory.UnsatisfiedDependencyException异常。
- autodetect:首先尝试使用constuctor来自动装配,然后使用byType方式。不确定性的处理与constructor方式和byType方式一样,都抛出org.springframework.beans.factory.UnsatisfiedDependencyException异常。
Spring 默认情况下bean不会被自动装配的,但可以通过在spring配置文件的根元素<beans>中设置default autowire,就可以将所有的Bean设置为自动装配。在Spring对Bean自动装配的过程中很容易出现不确定性,这些不确定性会导致程序无法运行。那么在我们的实际应用中要避免出现装配的不确定性。避免装配的不确定性的最好的方法就是使用显示装配和自动装配的混合方法。对有二义性的Bean使用显示装配,对没有二义性的Bean使用自动装配。
Bean范围化:
Spring默认所有的bean都是单一的。但为使得每次都能产生一个新的Bean实例,可以声明Bean的scope属性为prototype。这样当注入此bean时候,都会是不同的实例。
Spring范围化选项:
- singleton 定义Bean的范围为每个Spring容器一个实例(默认值)。
- Prototype 允许Bean可以多次被实例化(使用一次创建一个实例)。
- request Bean的范围是HTTP请求。只有在使用有web能力的spring上下文时才有效。
- session Bean的范围是HTTP会话。只有在使用有web能力的spring上下文时才有效。
- global-session Bean的范围是HTTP全局会话。只有在portlet上下文才有效。
初始化和销毁Bean的时候执行一些动作,spring提供了两种方式
1、用init-method 和 destroy-method参数申明<bean>,如果在<beans>中申明这个两个参数,可以为上下文中所有的bean定义初始化和销毁时执行的方法。
2、类继承InitializingBean和DisposableBean接口
1.2.2、基于注解的Bean的装配
在一个稍大的项目中,如果组件采用xml的bean定义来配置,显然会增加配置文件的体积,查找以及维护起来也不太方便。 而且基于XML的Bean的装配,Bean的定义信息和Bean实现类是分离的,采用基于注解的配置方式时,Bean定义信息是通过在Bean实现类上标注解实现。
- @Service用于标注业务层组件
- @Controller用于标注控制层组件
- @Repository用于标注数据访问组件,即DAO组件
- @Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
Spring2.5为我们引入了组件自动扫描机制,他在类路径下寻找标注了上述注解的类,并把这些类纳入进spring容器中管理。
applicationContext.xml:
<context:component-scan base-package="com.sankuai.meituan"/>
webmvc-config.xml:
<context:component-scan base-package="com.sankuai.meituan.**.web"/>
ps.
因为applicationContext是mvc context的父容器,mvc context可以引用applicationContext的bean,而applicationContext无法引用到mvc的bean,
spring查找bean,会现在当前context中查找,如果没有满足的,再到父容器查找,
applicationContext是在web.xml中配置的ContentLoader监听器启动的,当xml启动时加载,并按照一个约定的key放在java的ServletContext中,然后mvc 的servlet初始化时,先从ServletContext中按照约定的key取出来,以它为父容器,去创建mvc的容器。
@Service注解的类不能在springmvc.xml(对应crm中的webmvc-config.xml)中扫描,因为springmvc.xml与applicationContext.xml不是同时加载,如果不进行这样的设置,那么,spring就会将所有带@Service注解的类都扫描到容器中,等到加载applicationContext.xml的时候,会因为容器已经存在Service类,使得cglib将不对Service进行代理,直接导致的结果就是在applicationContext 中的事务配置不起作用,发生异常时,无法对数据进行回滚。
自动装配:
@Autowired
@Autowired是Spring 提供的,需导入
Package:org.springframework.beans.factory.annotation.Autowired;
只按照byType 注入。
@Resource
@Resource默认按 byName 自动注入,是J2EE提供的, 需导入Package:
javax.annotation.Resource;
@Resource有两个中重要的属性:name和type ,而Spring将@Resource注解的name属性解析为bean的
名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用
type属性时则使用 byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用by
Name自动注入策略。
@Resource装配顺序
(1). 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常;
(2). 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常;
(3). 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常;
(4). 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;
@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入。
总结:基于XML方式的Bean的装配适合于Bean的实现类来源于第三方类库,如DataSource,JdbcTemplate等,因无法在类中标注注解,而基于注解的配置,较适合于当前项目中开发的Bean实现类。
2、面向切面编程(AOP):
在面向对象编程(OOP)思想中,我们将事物纵向抽象成一个个的对象。而在面向切面编程中,我们将一个个对象某些类似的方面横向抽象成一个切面,对这个切面进行一些如权限验证,事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。Spring的一个关键的组件就是 AOP框架。 尽管如此,Spring IoC容器并不依赖于AOP,这意味着你可以自由选择是否使用AOP,AOP提供强大的中间件解决方案,这使得Spring IoC容器更加完善。
AOP几个术语:
通知(Advice):定义了切面,如Security,定义了切面的“什么”和“何时”
连接点(Joinpoint): 程序在执行过程中可以插入切面的点
切入点(Pointcut):通知要织如入的一个或多个连接点,定义了切面的“何地”
切面(Aspect):通知和切入点的结合
引入(Introduction):允许我们向现有的类添加新方法和属性,让现有的类具有新的行为和状态
目标(Target):被通知的对象,如果没有AOP,这个对象必须包含自己的主要逻辑和交叉业务的逻辑。
代理(Proxy):是向目标对象应用通知之后被创建的对象,对于客户端来说目标对象和代理对象是没有区别的。
织入(Weaving):把切面应用到目标对象来创建代理对象的过程。
三个重要的AOP框架
AspectJ(http://eclipse.org/aspectj)
AspectJ是语言级的AOP实现,2001年由Xerox PARC的AOP小组发布,目前版本已经更新到1.6。AspectJ扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。主页位于http://www.eclipse.org/aspectj。
JBoss AOP (http://labs.jboss.com/portal/jobssaop/index.html)
2004年作为JBoss应用程序服务器框架的扩展功能发布,可以从这个地址了解到JBoss AOP的更多信息:http://www.jboss.org/products/aop。
Spring AOP (http://www.springframework.org )
Spring AOP使用纯Java实现,它不需要专门的编译过程,不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。Spring并不尝试提供最完整的AOP实现,相反,它侧重于提供一种和Spring IoC容器整合的AOP实现,用以解决企业级开发中的常见问题。在Spring中,我们可以无缝地将Spring AOP、IoC和AspectJ整合在一起。
下面主要介绍Spring AOP,Spring对AOP的支持具有以下的4种情况:经典的基于代理的AOP(各版本Spring),@AspectJ注解驱动的切面(仅Spring 2.0),纯POJO切面(仅Spring 2.0),注入式的AspectJ切面(各版本Spring)。
定义目标类
public interface Performer { public void perform() throws PerformanceException; } public class Singer implements Performer { private String name; private String song; public Singer(String name) { this(name, null); } public Singer(String name, String song) { this.name = name; this.song = song; } public void perform() throws PerformanceException { if(song == null) { throw new PerformanceException(name + " doesn't have a song to sing."); } System.out.println(name + " is singing " + song); } public void setSong(String song) { this.song = song; }
定义观众类
public class Audience { public Audience() {} public void watchPerformance(ProceedingJoinPoint jp) { System.out.println("The audience is taking their seats."); System.out.println("The audience is turning off their cellphones"); try { jp.proceed(); System.out.println("CLAP CLAP CLAP CLAP CLAP"); } catch (Throwable throwable) { System.out.println("Boo! We want our money back!"); } } public void takeSeats() { System.out.println("The audience is taking their seats."); } public void turnOffCellPhones() { System.out.println("The audience is turning off their cellphones"); } public void applaud() { System.out.println("CLAP CLAP CLAP CLAP CLAP"); } public void demandRefund() { System.out.println("Boo! We want our money back!"); } }
2.1、经典的基于代理的AOP
创建通知
public class AudienceAdvice implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice { public AudienceAdvice() {} //前通知,在方法之前调用 public void before(Method method, Object[] args, Object target) throws Throwable { audience.takeSeats(); audience.turnOffCellPhones(); } //返回后通知,在成功返回后执行 public void afterReturning(Object rtn, Method method, Object[] args, Object target) throws Throwable { audience.applaud(); } //抛出后通知,在抛出异常后执行 public void afterThrowing(Throwable throwable) { audience.demandRefund(); } public void afterThrowing(Method method, Object[] args, Object target, Throwable throwable) { audience.demandRefund(); } // injected private Audience audience; public void setAudience(Audience audience) { this.audience = audience; } }
创建周围通知
相当于前通知,返回后通知,抛出后通知的结合,在spring里,周围通知是由MethodInterceptor接口定义的。周围通知的好处是能以简洁的方式在一个方法里定义所有的通知。当然如果单独使用前通知或者后通知,周围通知的优势就减弱了。后通知只能够对被通知方法的返回值进行检查,不能修改,利用周围通知不仅可以检查,还可以修改返回值,让我们可以在把方法的返回值传递给调用者之前进行一些处理。
public class AudienceAroundAdvice implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { try { //方法调用之前执行 audience.takeSeats(); audience.turnOffCellPhones(); //调用目标方法 Object returnValue = invocation.proceed(); //在成功返回之后执行 audience.applaud(); return returnValue; } catch (PerformanceException throwable) { //在出现异常之后执行 audience.demandRefund(); throw throwable; } } // injected private Audience audience; public void setAudience(Audience audience) { this.audience = audience; } }
定义切点与通知者
声明正则表达式切点
<bean id="performancePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="pattern" value=".*perform" /> </bean>
把定义的切点与通知关联
<bean id="audienceAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" value="audienceAdvice"/> <property name="pointcut" value="performancePointcut" /> </bean>
联合切点与通知
<bean id="audienceAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="audienceAdvice" /> <property name="pattern" value=".*perform" /> </bean>
定义AspectJ切点
<bean id="audienceAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor"> <property name="advice" ref="audienceAdvice" /> <property name="expression" value="execution(* *.perform(..))" /> </bean>
创建代理
<bean id="singer" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="singerTarget" /> <property name="proxyInterfaces" value="com.springinaction.springidol.Performer" /> <property name="interceptorNames" value="audienceAdvisor" /> </bean>
这里我们需要把实际的singer Bean 重命名为singerTarget,而且需要为所有代理的bean XML。
Spring自动代理被通知的Bean:
在spring AOP里,通知者把通知和切点关联起来,从而完整地定义了一个切面。但是切面在spring里是以代理的方式实现的。好在Spring提供了BeanPostProcessor的一个方便实现:DefaultAdvisorAutoProxyCreator,它会自动检查通知者的切点是否匹配Bean的方法,并且使用通知的代理来替换这个Bean的定义。它自动用匹配的通知者代理Bean。为了使用DefaultAdvisorAutoProxyCreator,只需要在配置如下的Bean:
创建自动代理
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
2.2、基于@AspectJ 注解驱动切面的自动代理
@Aspect public class Audience { public Audience() {} @Pointcut("execution(* *.perform(..))") public void performance(){} @Before("performance()") public void takeSeats() { System.out.println("The audience is taking their seats."); } @Before("performance()") public void turnOffCellPhones() { System.out.println("The audience is turning off their cellphones"); } @AfterReturning("performance()") public void applaud() { System.out.println("CLAP CLAP CLAP CLAP CLAP"); } @AfterThrowing("performance()") public void demandRefund() { System.out.println("Boo! We want our money back!"); } }
配置下面这个元素,这个元素会在spring上下文创建一个AnnotationAwareAspectJAutoProxyCreator,从而根据@Pointcut注解定义的切点来自动代理匹配Bean。
<aop:aspectj-autoproxy>
2.3、定义纯粹的POJO切面
<aop:config> <aop:pointcut id="performance" expression="execution(* *.perform(..))" /> <aop:aspect ref="audience"> <aop:around method="aroundAdvice" pointcut-ref="performance" /> <aop:before method="takeSeats" pointcut-ref="performance" /> <aop:before method="turnOffCellPhones" pointcut-ref="performance" /> <aop:after-returning method="applaud" pointcut-ref="performance" /> <aop:after-throwing method="demandRefund" pointcut-ref="performance" /> </aop:aspect> </aop:config>
自动的使用JDK动态代理或者CGLIB来为目标对象创建代理,不要自己去定义自己的AutoProxyCreator
如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。
如果你希望强制使用CGLIB代理
<aop:config proxy-target-class="true"> ... </aop:config>