您的位置:首页 > 编程语言 > Java开发

spring学习(三)—AOP

2016-07-05 22:18 387 查看
Aspect Oriented Programming 面向切面编程。解耦是程序员编码开发过程中一直追求的。AOP也是为了解耦所诞生。

AOP/OOP

AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。

上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。

同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。

换而言之,OOD/OOP面向名词领域,AOP面向动词领域。

比如说,对于一个学生来说,他的成绩从期中到期末的比较,这只是在他个人方面比较,关注点在纵向比较 使用OOP的思想来实现比较方便,而对于全班学生期中成绩的比较,则是在横向比较,这时AOP的思想比较适合

面向切面编程(AOP)提供另外一种角度来思考程序结构,通过这种方式弥补了面向对象编程(OOP)的不足。 除了类(classes)以外,AOP提供了 切面。切面对关注点进行模块化,例如横切多个类型和对象的事务管理。 (这些关注点术语通常称作 横切(crosscutting) 关注点。)

Spring的一个关键的组件就是 AOP框架。 尽管如此,Spring IoC容器并不依赖于AOP,这意味着你可以自由选择是否使用AOP,AOP提供强大的中间件解决方案,这使得Spring IoC容器更加完善。

AOP的术语

切面(Aspect):首先,我们在项目中把一些重复的代码,我们可以移出来,我们称之为横切关注点,比如servlet中的权限管理,文件上传等。一个关注点的模块化,就是切面,切面可以使用xml配置和注解@Aspect方式实现

连接点(JoinPoint): 这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点。spring只支持方法连接点。

通知(Advice): 在切点的某个特定的连接点上执行的动作,在上一篇的文章中,我们使用了xml方式练习了几种通知,之后我们学习如何使用注解方式配置

切入点(Pointcut) :匹配连接点的断言,通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行

引入(introduction) :允许我们向现有的类添加新方法属性。

目标(target): 引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

AOP代理(proxy) :AOP框架创建的对象,用来实现切面契约,在spring中,AOP代理可以是jdk动态代理或cglib代理

织入(weaving) : 把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象。 这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

spring用代理类包裹切面,把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标bean。

通知的类型:

前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

抛出异常后通知(After throwing advice): 在方法抛出异常退出时执行的通知。

后通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。

跟AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽量简单的通知类型来实现需要的功能。 例如,如果你只是需要用一个方法的返回值来更新缓存,虽然使用环绕通知也能完成同样的事情, 但是你最好使用After returning通知而不是环绕通知。 用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。 比如,你不需要调用 JoinPoint(用于Around Advice)的 proceed() 方法,就不会有调用的问题。

Spring的AOP代理

Spring缺省使用J2SE 动态代理(dynamic proxies)来作为AOP的代理。这样任何接口都可以被代理。

Spring也支持使用CGLIB代理. 对于需要代理类而不是代理接口的时候CGLIB代理是很有必要的。 如果一个业务对象并没有实现一个接口,默认就会使用CGLIB。 此外,面向接口编程 也是一个最佳实践,业务对象通常都会实现一个或多个接口。

此外,还可以强制的使用CGLIB:我们将会在以后讨论这个问题,解释问什么你会要这么做。

在Spring 2.0之后,Spring可能会提供多种其他类型的AOP代理,包括了完整的生成类。这不会影响到编程模型。

@AspectJ支持

如果你使用Java 5的话,推荐使用Spring提供的@AspectJ切面支持,通过这种方式声明Spring AOP中使用的切面。 “@AspectJ”使用了Java 5的注解,可以将切面声明为普通的Java类。 AspectJ 5发布的 AspectJ project 中引入了这种@AspectJ风格。 Spring 2.0 使用了和AspectJ 5一样的注解,使用了AspectJ 提供的一个库来做切点(pointcut)解析和匹配。 但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver)。

启用@AspectJ支持

为了在Spring配置中使用@AspectJ aspects,你必须首先启用Spring对基于@AspectJ aspects的配置支持,自动代理(autoproxying)基于通知是否来自这些切面。 自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确认通知是否如期进行。

通过在你的Spring的配置中引入下列元素来启用Spring对@AspectJ的支持:

<!-- 启用aspectJ切面 注解配置通知 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- 用于使用注解配置bean  -->
<context:annotation-config></context:annotation-config>
<!-- 当使用注解配置bean时,以下配置会对当前包及其子包进行搜索,不再配置包中的bean将不起作用 -->
<context:component-scan base-package="com.lgh.annotation"></context:component-scan>


同时我们需要在应用程序的classpath中引入两个AspectJ库:aspectjweaver.jar 和 aspectjrt.jar。

使用maven进行添加jar包:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.2.17.RELEASE</version>
</dependency>

<!-- aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.5</version>

</dependency>
<!-- aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.5</version>
</dependency>


声明一个切面

在启用@AspectJ支持的情况下,在application context中定义的任意带有一个@Aspect切面(拥有@Aspect注解)的bean都将被Spring自动识别并用于配置在Spring AOP。 以下例子展示了为了完成一个不是非常有用的切面所需要的最小定义:

下面是在application context中的一个常见的bean定义,这个bean指向一个使用了 @Aspect 注解的bean类:

<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>


下面是 TxManager类定义,使用了 org.aspectj.lang.annotation.Aspect 注解。

package com.lgh.annotation.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect

public class TxManager {

...

}


切面(用 @Aspect 注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。

声明一个切入点(pointcut)

回想一下,切入点决定了连接点关注的内容,使得我们可以控制通知什么执行。 Spring AOP只支持Spring bean方法执行连接点。所以你可以把切入点看做是匹配Spring bean上的方法执行。 一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。 在@AspectJ中,一个切入点实际就是一个普通的方法定义提供的一个签名,并且切入点表达式使用 @Pointcut注解来表示。 这个方法的返回类型必须是 void。 如下的例子定义了一个切入点’transfer’,这个切入点匹配了任意名为”transfer”的方法执行:

@Pointcut(“execution(* transfer(..))”)

结和通知使用注解方式写的话就是:

@Before(“execution(* com.lgh.annotation.dao..(..))”)

支持的切入点指定者

Spring AOP 支持在切入点表达式中使用如下的AspectJ切入点指定者:


其他的切入点类型

完整的AspectJ切入点语言支持额外的切入点指定者,但是Spring不支持这个功能。 他们分别是call, initialization, preinitialization, staticinitialization, get, set, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this 和 @withincode。 在Spring AOP中使用这些指定者将会导致抛出IllegalArgumentException异常。

Spring AOP支持的切入点指定者可能在将来的版本中得到扩展,不但支持更多的AspectJ 切入点指定者(例如”if”),还会支持某些Spring特有的切入点指定者,比如”bean”(用于匹配bean的名字)。

execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指定者。

within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的appolication object)是指定类型的实例。

args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

@target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中执行的对象的类已经有指定类型的注解。

@args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型有指定类型的注解。

@within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

@annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题有某种给定的注解。

因为Spring AOP限制了连接点必须是方法执行级别的,pointcut designators的讨论也给出了一个定义,这个定义和AspectJ的编程指南中的定义相比显得更加狭窄。 除此之外,AspectJ它本身有基于类型的语义,在执行的连接点’this’和’target’都是指同一个对象,也就是执行方法的对象。 Spring AOP是一个基于代理的系统,并且严格区分代理对象本身(对应于’this’)和背后的目标对象(对应于’target’)

合并切入点表达式

切入点表达式可以使用using ‘&&’, ‘||’ 和 ‘!’来合并.还可以通过名字来指向切入点表达式。 以下的例子展示了三种切入点表达式: anyPublicOperation(在一个方法执行连接点代表了任意public方法的执行时匹配); inTrading(在一个代表了在交易模块中的任意的方法执行时匹配) 和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。

@Pointcut(“execution(public * *(..))”)

private void anyPublicOperation() {}

@Pointcut(“within(com.xyz.someapp.trading..*”)

private void inTrading() {}

@Pointcut(“anyPublicOperation() && inTrading()”)

private void tradingOperation() {}就上所示的,从更小的命名组件来构建更加复杂的切入点表达式是一种最佳实践。 当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。 (比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点。 成员可视性访问规则不影响到切入点的 匹配。

切入点定义一个你可以在任何需要切入点表达式的地方可引用的切面 ,比如在项目中对数据库进行增删改查,这时用到了事务管理,这是一段重复代码,spring可以帮我们把重复代码提走使用HibernateTransactionManager类对事务进行控制,这时调用业务层时使用xml配置则是:

<bean id="mySessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml"></property>
</bean>

<!-- 事务管理 器-->
<bean id="myManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"></property>
</bean>

<!-- 配置事务管理特性 -->
<tx:advice id="txAdvice" transaction-manager="myManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>

</tx:advice>
<!--
execution : 方法执行时候
1* 不限制返回值
2* 任意类
3* 任意方法
4..任意方法

-->
<aop:config>
<aop:pointcut expression="execution(* com.lgh.shop.admin.biz.impl.*.*(..))" id="ps"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="ps"/>
</aop:config>


注解示例

package com.lgh.annotation.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect
public class TxManager {
// 前置通知(Before advice)
@Before("execution(* com.lgh.annotation.dao.*.*(..))")
public void beginTx() {
System.out.println("开启事务 1");
}

// 返回后通知(After returning advice)
@AfterReturning("execution(* com.lgh.annotation.dao.*.*(..))")
public void commitTx() {
System.out.println("提交事务 1");
}

// 抛出后通知(After throwing advice)
@AfterThrowing("execution(* com.lgh.annotation.dao.*.*(..))")
public void rollbackTx() {
System.out.println("出现异常,回滚事务 1");
}

/*
* 后通知(After (finally) advice)
*
* 不论一个方法是如何结束的,在它结束后(finally)
* 后通知(After (finally) advice)都会运行。 使用 @After
* 注解来声明。这个通知必须做好处理正常返回和异常返回两种情况。
* 通常用来释放资源。
*/
@After("execution(* com.lgh.annotation.dao.*.*(..))")
public void finallyTx() {
System.out.println("关闭session 1");
}

}


"execution(* com.lgh.annotation.dao.*.*(..))"


这段代码表示当dao层的任意类调用任意方法时通知执行

此时我们需要一个UserDao

package com.lgh.annotation.dao;

public interface UserDao {

void save();
void del();
}


实现类:

package com.lgh.annotation.dao.impl;

import org.springframework.stereotype.Service;

import com.lgh.annotation.dao.UserDao;
/*
* 这里使用了注解方式配置bean.
* */
@Service(value="userDao")
public class UserDaoImpl implements UserDao {

@Override
public void save() {

System.out.println("保存商品");
}

@Override
public void del() {
System.out.println("删除商品");
//这里人为的制造了一个除零异常
int i = 3/0;

}

}


测试:

package com.lgh.annotation.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.lgh.annotation.dao.UserDao;

public class SpringAnnoTest {

public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) ac.getBean("userDao");
userDao.save();

System.out.println("---------------------------");
userDao.del();
}

}


结果:

开启事务 1
保存商品
关闭session 1
提交事务 1
---------------------------
开启事务 1
删除商品
关闭session 1
出现异常,回滚事务 1
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.lgh.annotation.dao.impl.UserDaoImpl.del(UserDaoImpl.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)


我们看到当我们调用dao层类的方法时,执行了通知

有时我们不需要配置这么多的通知,只配一个环绕通知,就能完成所有事情

配置环绕通知

package com.lgh.annotation.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect

public class TxAroundManager {

/*
* 有时我们不需要配置这么多的通知,
* 只配一个环绕通知,就能完成所有事情
* */

@Around("execution(* com.lgh.annotation.dao.*.*(..))")
public void AroundTx(ProceedingJoinPoint pjp) throws Throwable{
try {
System.out.println("开启事务 2");
pjp.proceed();
System.out.println("提交事务 2");
} catch (Exception e) {
System.out.println("出现异常,回滚事务 2");
}finally {
System.out.println("关闭session 2");
}

}

//  @AfterThrowing("execution(* com.lgh.annotation.dao.*.*(..))")
//  public void rollbackTx(){
//      System.out.println("出现异常,回滚事务 2");
//  }
//  @After("execution(* com.lgh.annotation.dao.*.*(..))")
//  public void finallyTx(){
//      System.out.println("关闭session 2");
//  }
//
}


xml配置:

<!--
先把上一个切面注起来
<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>
-->
<bean name="txAround" class="com.lgh.annotation.aspect.TxAroundManager"></bean>


结果:

开启事务 2
保存商品
提交事务 2
关闭session 2
---------------------------
开启事务 2
删除商品
出现异常,回滚事务 2
关闭session 2


通知顺序

当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序规则是什么呢?

<bean name="tx" class="com.lgh.annotation.aspect.TxManager"></bean>
<bean name="txAround" class="com.lgh.annotation.aspect.TxAroundManager"></bean>


开启事务 1
开启事务 2
保存商品
提交事务 2
关闭session 2
关闭session 1
提交事务 1
---------------------------
开启事务 1
开启事务 2
删除商品
出现异常,回滚事务 2
关闭session 2
关闭session 1
提交事务 1


这时默认执行xml配置前面的,我们使用注解@Order可以对其控制

@Order(1),括号中的数字越小,优先级越高,先执行,

@Aspect
@Order(5)
public class TxManager {}

@Aspect
@Order(1)
public class TxAroundManager {}


结果:

开启事务 2
开启事务 1
保存商品
关闭session 1
提交事务 1
提交事务 2
关闭session 2
---------------------------
开启事务 2
开启事务 1
删除商品
关闭session 1
出现异常,回滚事务 1
出现异常,回滚事务 2
关闭session 2
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息