配色: 字号:
Spring之AOP基本概念及配置AOP
2017-01-20 | 阅:  转:  |  分享 
  
Spring之AOP基本概念及配置AOP



为什么使用AOP



传统方法



AOP前前奏



首先考虑一个问题,假设我们要设计一个计算器,有如下两个需求:-在程序运行期间追踪正在放生的活动-希望计算器只能处理正数的运算通常我们会用如下代码进行实现:定义一个接口:

publicinterfaceArithmeticCalculator{



intadd(inti,intj);

intsub(inti,intj);



intmul(inti,intj);

intdiv(inti,intj);



}

实现类(在实现类中加入具体方法的实现,即正数的操作和日志功能,通过System.out.println输出实现):

publicclassArithmeticCalculatorLoggingImplimplementsArithmeticCalculator{



@Override

publicintadd(inti,intj){

System.out.println("Themethodaddbeginswith["+i+","+j+"]");

intresult=i+j;

System.out.println("Themethodaddendswith"+result);

returnresult;

}



@Override

publicintsub(inti,intj){

System.out.println("Themethodsubbeginswith["+i+","+j+"]");

intresult=i-j;

System.out.println("Themethodsubendswith"+result);

returnresult;

}



@Override

publicintmul(inti,intj){

System.out.println("Themethodmulbeginswith["+i+","+j+"]");

intresult=ij;

System.out.println("Themethodmulendswith"+result);

returnresult;

}



@Override

publicintdiv(inti,intj){

System.out.println("Themethoddivbeginswith["+i+","+j+"]");

intresult=i/j;

System.out.println("Themethoddivendswith"+result);

returnresult;

}

传统方法存在的问题



-代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀.每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点.-代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码.如果日志需求发生变化,必须修改所有模块.

使用动态代理解决上述问题



代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象.任何对原始对象的调用都要通过代理.代理对象决定是否以及何时将方法调用转到原始对象上.

/

动态代理

@authorMegustas



/

publicclassArithmeticCalculatorLoggingProxy{



//要代理的对象

privateArithmeticCalculatortarget;



publicArithmeticCalculatorLoggingProxy(ArithmeticCalculatortarget){

super();

this.target=target;

}



//返回代理对象

publicArithmeticCalculatorgetLoggingProxy(){

ArithmeticCalculatorproxy=null;

//代理对象由哪一个类加载器加载

ClassLoaderloader=target.getClass().getClassLoader();

//代理对象的类型,即其中有哪些方法

Class[]interfaces=newClass[]{ArithmeticCalculator.class};

//当调用代理对象其中的方法时,该执行的代码

InvocationHandlerh=newInvocationHandler(){

/

proxy:正在返回的那个代理对象,一般情况下,在invoke方法中不使用该对象

method:正在被调用的方法

args:调用方法传入的参数

/

@Override

publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)

throwsThrowable{

StringmethodName=method.getName();

//打印日志,此种方式对日志进行维护和更改就十分简洁

System.out.println("[before]Themethod"+methodName+"beginswith"+Arrays.asList(args));



//调用目标方法

Objectresult=null;



try{

//前置通知

//invoke:通过函数名反射相应的函数

result=method.invoke(target,args);

//返回通知,可以访问到方法的返回值

}catch(NullPointerExceptione){

e.printStackTrace();

//异常通知,可以访问到方法出现的异常

}



//后置通知.因为方法可以能会出异常,所以访问不到方法的返回值



//打印日志

System.out.println("[after]Themethodendswith"+result);



returnresult;

}

};



/

loader:代理对象使用的类加载器。

interfaces:指定代理对象的类型.即代理代理对象中可以有哪些方法.

h:当具体调用代理对象的方法时,应该如何进行响应,实际上就是调用InvocationHandler的invoke方法

以下是代理对象,不同于基本对象通过new生成

/

proxy=(ArithmeticCalculator)Proxy.newProxyInstance(loader,interfaces,h);



returnproxy;

}

但是写动态代理,难度却不小,不是很容易掌握。

AOP简介



AOP(Aspect-OrientedProgramming,面向切面编程):是一种新的方法论,是对传统OOP(Object-OrientedProgramming,面向对象编程)的补充.

AOP的主要编程对象是切面(aspect),而切面模块化横切关注点(即对象里放入的是一个个横切关注点的方法).

在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类.这样一来横切关注点就被模块化到特殊的对象(切面)里.

AOP的好处:每个事物逻辑位于一个位置,代码不分散,便于维护和升级业务模块更简洁,只包含核心业务代码.



这里写图片描述



这里的一个个需求,例如验证参数、日志功能等都是横切关注点,我们将横切关注点抽取出来



通过切面和业务逻辑的结合实现目标功能,即为面向切片编程。

AOP术语



切面(Aspect):横切关注点(一个个具体需求)(跨越应用程序多个模块的功能)被模块化的特殊对象

通知(Advice):切面必须要完成的工作(比如说切面需要完成验证,即切面中的每一个方法)

目标(Target):被通知的对象(即业务逻辑)

代理(Proxy):向目标对象应用通知之后创建的对象(将切面和目标混合)

连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如ArithmethicCalculator#add()方法执行前的连接点,执行点为ArithmethicCalculator#add();方位为该方法执行前的位置

切点(pointcut):每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。

AOP配置



用注解方式声明切面



首先介绍一个AOP框架,AspectJ,是Java社区里最完整最流行的AOP框架,在Spring2.0以上版本中,可以使用基于AspectJ注解来配置AOP。



要在spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:aopalliance.jar、aspectj.weaver.jar和spring-aspects.jar,即导入包:

com.springsource.NET.sf.cglib-2.2.0.jar

com.springsource.org.aopalliance-1.0.0.jar

com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar

commons-logging-1.1.1.jar

spring-aop-4.0.0.RELEASE.jar

spring-aspects-4.0.0.RELEASE.jar

spring-beans-4.0.0.RELEASE.jar

spring-context-4.0.0.RELEASE.jar

spring-core-4.0.0.RELEASE.jar

spring-expression-4.0.0.RELEASE.jar



将aopSchema添加到根元素中.



要在SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML元素

当SpringIOC容器侦测到Bean配置文件中的元素时,会自动为与AspectJ切面匹配的Bean创建代理.

要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为Bean实例.当在SpringIOC容器中初始化AspectJ切面之后,SpringIOC容器就会为那些与AspectJ切面相匹配的Bean创建代理.在AspectJ注解中,切面只是一个带有@Aspect注解的Java类.通知是标注有某种注解的简单的Java方法.



AspectJ支持的5中类型的通知注解



@Before:前置通知,在方法执行之前执行

@After:后置通知,在方法执行之后执行

@AfterRunning:返回通知,在方法返回结果之后执行

@AfterThrowing:异常通知,在方法抛出异常之后

@Around:环绕通知,围绕着方法执行



/

日志切面

@authorMegustas



/

//把这个类声明为一个切面:首先需要把该类放入到IOC容器中,通过注解@Component、再声明为一个切面,通过注解@Aspect,并且在配置文件中加入配置

//通过Order注解来指定切面的优先级,优先级数字越小代表优先级越高,越先执行

@Order(2)

@Aspect

@Component

publicclassLoggingAspect{



//这个方法在哪些类的哪些方法前执行,通过注解来规定

//声明该方法是一个前置通知,在目标方法开始之前执行,".add"方法说明在add方法之前执行,"."则表示在包下所有方法之前执行

@Before("execution(publicintcom.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int,int))")

publicvoidbeforeMethwww.baiyuewang.netod(JoinPointjoinPoint){

StringmethodName=joinPoint.getSignature().getName();

Listargs=Arrays.asList(joinPoint.getArgs());

System.out.println("Themethod"+methodName+"beginswith"+args);

}



//后置通知:在目标方法执行后(无论是否发生异常),执行的通知

//在后置通知中还不能访问目标目标方法执行的结果,执行结果在返回通知中进行访问

@After("execution(com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.(int,int))")

publicvoidafterMethod(JoinPointjoinPoint){

StringmethodName=joinPoint.getSignature().getName();

System.out.println("Themethod"+methodName+"end");

}



/

返回通知:在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的

/

@AfterReturning(value="execution(publicintcom.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int,int))",

returning="result")

publicvoidafterReturning(JoinPointjoinPoint,Objectresult){

StringmethodName=joinPoint.getSignature().getName();

System.out.println("Themethod"+methodName+"endswith"+result);

}



/

在目标方法出现异常时会执行的代码

可以访问到异常对象,且可以指定在出现特定异常时再执行通知代码

例如Exceptionex,NullPointerExceptionex,可以指定不同种类的异常,当是指定的异常种类时执行

@paramjoinPoint

@paramex

/

@AfterThrowing(value="execution(publicintcom.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.div(int,int))",

throwing="ex")

publicvoidafterThrowing(JoinPointjoinPoint,Exceptionex){

StringmethodName=joinPoint.gewww.tt951.comtSignature().getName();

System.out.println("Themethod"+methodName+"occursexcetion"+ex);

}



/

环绕通知需要携带ProceedingJoinPoint类型的参数

环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法

并且环绕通知必须有返回值,返回值即为目标方法的返回值(类似于动态代理)

最强的,前置、后置、返回与异常通知都可以,但是并不代表是最常用的

@parampjd

/

@Around("execution(com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int,int))")

publicObjectaroundMethod(ProceedingJoinPointpjd){



Objectresult=null;

StringmethodName=pjd.getSignature().getName();

//执行目标方法

try{

//前置通知

System.out.println("Themethod"+methodName+"beginswith"+Arrays.asList(pjd.getArgs()));

result=pjd.proceed();

//后置通知

System.out.println("Themethod"+methodName+"endswith"+result);

}catch(Throwablee){

//异常通知

System.out.println("Themethod"+methodName+"occursexcetion"+e);

}

//后置通知

System.out.println("Themethod"+methodName+"ends");

returnresult;

}

Bean的配置:













验证方法:



publicclassMain{



publicstaticvoidmain(String[]args){



//1.创建IOC容器

ClassPathXmlApplicationContextctx=newClassPathXmlApplicationContext("applicationContext.xml");



//2.从IOC容器中获取bean实例

ArithmeticCalculatorarithmeticCalculator=(ArithmeticCalculator)ctx.getBean(ArithmeticCalculator.class);

//3.调用bean的方法



intresult1=arithmeticCalculator.add(3,6);

System.out.println("result1:"+result1);



//intresult2=arithmeticCalculator.div(1000,0);//通过异常通知显示

//System.out.println("result2:"+result2);

//4.关闭容器

ctx.close();

}

指定切面的优先级及重用切入点



指定切面的优先级



实例可以参照上诉代码的@Order



在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的.

切面的优先级可以通过实现Ordered接口或利用@Order注解指定.

实现Ordered接口,getOrder()方法的返回值越小,优先级越高.

若使用@Order注解,序号出现在注解中

@Aspect

@Order(0)

publicclassCalculaotorValidationAspect{}

@Aspect

@Order(1)

publicclassCalculaotorLoggingAspect{}

重用切入点



在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式.但同一个切点表达式可能会在多个通知中重复出现.

在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法.切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的.

切入点方法的访问控制符同时也控制着这个切入点的可见性.如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中.在这种情况下,它们必须被声明为public.在引入这个切入点时,必须将类名也包括在内.如果类没有与这个切面放在同一个包中,还必须包含包名.

其他通知可以通过方法名称引入该切入点.

(总结就是一句话:将切入点的表达式“封装”为一个方法,通过方法调用来实现)

同类中使用:

/

实现重用切面表达式

定义一个方法,用于声明切入点表达式。一般该方法中不需要再填入其他的代码

使用@Pointcut来声明切入点表达式

/

@Pointcut("execution(publicintcom.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int,int))")

publicvoiddeclareJointPointExpression(){



}





@Before("declareJointPointExpression()")

publicvoidbeforeMethod1(JoinPointjoinPoint){

StringmethodName=joinPoint.getSignature().getName();

Listargs=Arrays.asList(joinPoint.getArgs());

System.out.println("Themethod"+methodName+"beginswith"+args);

}

在外部类中使用:



//在类的外部使用重用切面表达式,再指定其所在类即可,若不同包下,则再引入包名

@Before("LoggingAspect.declareJointPointExpression()")

publicvoidvalidateArgs1(JoinPointjoinPoint){

System.out.println("-->validate:"+Arrays.asList(joinPoint.getArgs()));

}

在外部包使用同理。



用基于XML的配置声明切面



除了使用AspectJ注解声明切面,Spring也支持在Bean配置文件中声明切面.这种声明是通过aopschema中的XML元素完成的.

正常情况下,基于注解的声明要优先于基于XML的声明.通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的.由于AspectJ得到越来越多的AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会.

在Bean配置文件中,所有的SpringAOP配置都必须定义在元素内部.对于每个切面而言,都要创建一个元素来为具体的切面实现引用后端Bean实例.

切面Bean必须有一个标示符,供元素引用

切入点使用元素声明

切入点必须定义在元素下,或者直接定义在元素下.

定义在元素下:只对当前切面有效

定义在元素下:对所有切面都有效

基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点.

通知元素需要使用来引用切入点,或用直接嵌入切入点表达式.method属性指定切面类中通知方法的名称.

具体的一个实例如下(注:此处bean代码的实现不再具体列举):

















































献花(0)
+1
(本文系thedust79首藏)