11 05Spring之AOP编程

Spring中两大核心组成:IOC&DI、AOP。AOP最大的用处在于事务处理上。

1 AOP编程简介

如果你使用MyEclipse开发项目,对于所有的AOP几乎不需要做太多的处理。由开发工具帮助用户完成了。在学习的时候,掌握AOP的编程的核心理念要比实现更为重要。

在整个的开发之中,业务层在项目之中要负责以下的操作:
(1)调用数据层进行处理;
(2)进行事务的处理;
(3)关闭数据库的操作连接。
如果说现在以一个数据的增加操作为例。

那么这样的结构就相当于所有的操作都使用了硬编码的形式完成。类似于如下形式:
范例:增加处理

package org.lks.service.impl;

public class MemberServiceImpl {
	public boolean insert(){
		//1、执行日志处理
		//2、调用数据层操作
		//3、进行事务的提交处理
		//4、数据库的关闭操作
		return false;
	}
}

在之前的代码都是这样完成处理的,但实际上这样并不好,因为整个的业务层只关心处理的核心操作。

就比如一个去外面吃饭,只关注吃饭的本身,但是让这个顾客又买菜、又做饭、又吃饭、又自己收拾,基本上这样的操作就属于非常糟糕的操作。

如果现在你的开发之中遇见了许多这样要编写非业务处理功能的时候,那么该怎么解决呢?于是我们首先可以想到的是代理设计模式,让代理设计模式完成所有的辅助性操作,而后静态代理会有类型限制,应该使用动态代理机制完成,但是动态代理机制麻烦,你什么时候去引用动态代理呢?或者说在整个动态代理的操作过程里面依然需要用户自己去处理对象的产生等操作。

而且同时在整个的设计过程之中并不只是一个简单的代理类就可以完成所有的操作功能,需要考虑如下几种情况:
(1)在数据层调用之前如何执行(执行数据层之前进行记录);
(2)在数据层调用之后如何执行(执行数据层之后手工进行事务的提交);
(3)在业务层操作完成之后返回结果上进行处理;
(4)还有可能出现异常之后需要进行记录。
这么一看,要想准确的实现代理设计操作,至少需要四个处理类,那么这四个类的关系如何处理呢?于是最早的时候(在JDK 1.5出现之前)为了可以处理这样的问题,引入了一系列的Advice操作接口形式。

所有的类都写完之后利用Spring的配置文件将这些操作完整的融合在一起,这样当业务层调用时,会自动触发以上的操作子类进行依次的执行。

但是如果这样去编写代码会有哪些缺点呢?
(1)Spring本身就是一个容器,而以上的操作需要使用明确的接口以及明确的实现子类来明确的定义,这样一来结构又固定了,希望可以让定义这些代理操作的时候不受到接口的限制,于是在Spring接下来的版本之中开始使用Annotation的配置模式解决了此类问题,也就是说利用特定的注解来描述具体的代理操作方法的作用。

任何的开发都应该避免强烈的耦合性问题,尤其是在Spring之中,最大的特点在于String类的操作上,所以为了解决硬编码的方式配置代理结构是不建议使用的,那么在Spring之中引入了Apache推出的AspectJ操作语法形式,以字符串的形式定义代理操作的切入点。并且使用一系列的通配符来找到所需要进行切入的操作程序。

那么现在实际上就可以得出一个结论:如果要想实现一个优秀的代理设计模式,那么必须要使用面向切面的编程,即:将不同的切入点代码单独定义,而后组织在一个程序网上。所以这就构成了整个AOP的理论来源,而在整个AOP之中包含有如下几个重要的组成概念:
(1)切入点:可以理解为所有要操作的方法定义,要求业务层的方法必须统一风格;
(2)分离点:将那些不可再分的组件单独提取出去定义为单独的操作功能;
(3)横切关注点:指的是将所有与开发无关的程序组成类单独提取而后组织运行。
(4)织入:将所有的切入点、关注点的代码组成在一张完整的程序结构之中,而Spring就完成了这样的组织操作。

但是并不是意味着所有的操作都可以由用户任意放肆的去定义切入点,在整个Spring里面依然采用通知的形式完成,也就是说当触发到了某些操作之后那么就自然进行一些固定的处理,所以在整个SpringAOP之中包含有如下的几类通知形式:
(1)前置通知(BeforeAdvice):在某一操作执行之前负责处理;
(2)后置通知(AfterAdvice):在某一操作执行之后负责处理,但是后置通知需要考虑以下几种子类;
|————后置返回通知(After Returning Advice):负责处理返回结果的时候进行拦截;
|————后置异常通知(After Throwing Advice):当出现异常之后进行拦截;
|————后置最终通知(After Finally Advice):不管执行到最后是否出现异常,都会执行拦截;
(3)环绕通知(Around Advice):可以在具体的执行操作之前、之后、异常出现处理等地方任意编写,可以说是包含了如上的几种通知的综合体。

2 AOP基本操作

AOP是面向切面的编程,在实际开发之中,AOP都会工作在业务层;因为业务层要调用数据层,而业务层也要完成所有辅助性的数据层操作。
范例:定义业务层操作接口

package org.lks.service;

import org.lks.vo.Member;

public interface IMemberService {

	public boolean insert(Member vo);
}
package org.lks.service.impl;

import org.lks.service.IMemberService;
import org.lks.vo.Member;

@Service
public class MemberServiceImpl implements IMemberService{

	@Override
	public boolean insert(Member vo) {
		System.out.println("【数据层调用】member = " + vo);
		return false;
	}
	
}

此时的业务层里面只关心有核心的业务功能,核心的功能就是调用了业务层的代码。

随后所有的辅助性操作功能都通过Spring容器动态配置。

范例:为项目配置Annotation的扫描支持

<context:annotation-config/>
<context:component-scan base-package="org.lks"/>

但是这种操作并不严格,还需要准备出相应的辅助性功能,那么这些辅助性的功能可以单独定义在一个切面的处理类之中。

范例:定义切面处理类

package org.lks.aop;

import org.springframework.stereotype.Component;

@Component
public class ServiceAspect {

	public void serviceBefore(){
		System.out.println("[AOP切面]: 执行日志记录操作!");
	}
	
	public void serviceAfter(){
		System.out.println("[AOP切面]: 执行事务处理操作!");
	}
}

默认情况下Spring之中并不会去开启切面的操作模式,所以如果要想开启切面必须引入aop的命名空间。
范例:修改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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">

	<context:annotation-config/>
	<context:component-scan base-package="org.lks"/>
	
</beans>

于是下面的任务就是再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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">

	<context:annotation-config/>
	<context:component-scan base-package="org.lks"/>
	<aop:config>
		<!-- 首先需要定义程序的切入点 -->
		<aop:pointcut expression="execution(* org.lks..*.*(..)))" id="pointcut"/>
		<!-- 定义要使用的面向方面的处理类 -->
		<aop:aspect ref="serviceAspect">
			<aop:before method="serviceBefore" pointcut-ref="pointcut"/>
			<aop:after method="serviceAfter" pointcut="execution(* org.lks..*.*(..)))"/>
		</aop:aspect>
	</aop:config>
</beans>

此时的业务层里面没有出现任何的与业务层的核心操作有关的功能代码。而所有的辅助功能都会以切面的形式出现在项目的执行之中,那么这些切面就相当于最早的代理类。
现在整个配置过程之中,其基本操作意义都可以一一看明白,但是最为重要的是切入点的设置:

execution(* org.lks..*.*(..)))

这种语法就是AspectJ定义的切入点操作语法。此语法结构如下:

execution(修饰符匹配? 返回值类型 操作类型匹配? 名称匹配(参数匹配) 抛出异常匹配)

以上的语法详细的解释如下:
(1)修饰符:public、private,只能够出现0次或1次(本次没有出现);
(2)返回值(*):如果使用的是*表示返回任意类型。
(3)名称匹配(org.lks..*.*):表示的是具体要使用此切面的程序类,如果写上的是org.lks则表示在这个包中,而如果后面出现了..表示在任意子包,随后*.*表示的是任意类的任意方法;
(4)方法参数((..)):使用..表示匹配任意多个参数,如果使用的是*表示匹配任意一个参数。

3 AOP深入操作

在之前只是进行最为简单的AOP拦截处理,只在操作之前与操作之后进行的拦截,但是在整个AOP处理中有一个问题需要解决,那么就是参数问题。
范例:定义参数拦截

package org.lks.aop;

import org.springframework.stereotype.Component;

@Component
public class ServiceAspect {

	public void serviceBefore(){
		System.out.println("[AOP切面]: 执行日志记录操作!");
	}
	
	public void serviceBefore2(Object arg){
		System.out.println("[AOP切面]: 执行增加前的操作,参数= " + arg);
	}
	
	public void serviceAfter(){
		System.out.println("[AOP切面]: 执行事务处理操作!");
	}
}

此时对于serviceBefore2()这个方法上由于存在有参数的定义了,那么就必须修改切入点表达式。
范例:定义切入点表达式

<aop:config>
	<!-- 首先需要定义程序的切入点 -->
	<aop:pointcut expression="execution(* org.lks..*.*(..)) and args(vo))" id="pointcut"/>
	<!-- 定义要使用的面向方面的处理类 -->
	<aop:aspect ref="serviceAspect">
		<aop:before method="serviceBefore2" pointcut-ref="pointcut" arg-names="vo"/>
		<aop:after method="serviceAfter" pointcut="execution(* org.lks..*.*(..)))"/>
	</aop:aspect>
</aop:config>

除了在操作之前的拦截,也可以针对于操作的返回结果进行拦截。
范例:针对于返回结果拦截

package org.lks.aop;

import org.springframework.stereotype.Component;

@Component
public class ServiceAspect {

	public void serviceBefore(){
		System.out.println("[AOP切面]: 执行日志记录操作!");
	}
	
	public void serviceBefore2(Object arg){
		System.out.println("[AOP切面]执行增加前的操作,参数= " + arg);
	}
	
	public void serviceAfter(){
		System.out.println("[AOP切面]: 执行事务处理操作!");
	}
	
	public void serviceAfterReturning(Object val){  //表示操作的结果
		System.out.println("[AOP切面]操作完成,返回结果: " + val);
	}
}

但是此时依然需要在applicationContext.xml文件里面配置操作形式。
范例:修改applicationContext.xml文件

<aop:config>
	<!-- 首先需要定义程序的切入点 -->
	<aop:pointcut expression="execution(* org.lks..*.*(..)) and args(vo))" id="pointcut"/>
	<!-- 定义要使用的面向方面的处理类 -->
	<aop:aspect ref="serviceAspect">
		<aop:before method="serviceBefore2" pointcut-ref="pointcut" arg-names="vo"/>
		<aop:after method="serviceAfter" pointcut="execution(* org.lks..*.*(..)))"/>
		<aop:after-returning method="serviceAfterReturning" pointcut="execution(* org.lks..*.*(..)))" returning="hhybigfool" arg-names="hhybigfool"/>
	</aop:aspect>
</aop:config>

除了返回结果的拦截之外,还可以进行异常处理的拦截操作。
范例:修改一下MemberServiceImpl方法

@Service
public class MemberServiceImpl implements IMemberService{

	@Override
	public boolean insert(Member vo) {
		throw new NullPointerException("hhy big fool!");
	}
}

那么随后需要增加新的拦截处理方法操作。
范例:修改ServiceAspect程序类

public void serviceAfterThrowing(Exception exp){
	System.out.println("[AOP切面]操作出现异常: " + exp);
}
<aop:after-throwing method="serviceAfterThrowing" pointcut="execution(* org.lks..*.*(..)))" arg-names="hhy" throwing="bigfool"/>

理论上而言,以上的几个拦截器已经可以成功的覆盖掉了所有AOP可以处理的范畴,但是为了考虑到简化问题,在整个的AOP处理之中,又提供有一种环绕通知的,即:可以一个方法处理所有的AOP操作,这种操作更像代理结构。

范例:修改ServiceAspect程序类,增加环绕处理
(1)但是在进行环绕处理的时候需要注意一点,它必须考虑到接收参数的情况,而接收的参数类型只能够是:org.aspectj.lang.ProceedingJoinPoint,通过此类型可以取得全部的提交参数信息。

public Object serviceAround(ProceedingJoinPoint point) throws Throwable{
	System.out.println("[AOP切面]数据层方法调用之前,参数: " + Arrays.toString(point.getArgs()));
	Member vo = new Member();
	vo.setMid("lks");
	vo.setMname("big fool");
	Object retval = point.proceed(new Object[]{vo}); //调用具体的真实操作
	System.out.println("[AOP切面]数据层方法调用之后,返回值: " + retval);
	return true;  //可以自己修改返回值
}

在整个环绕拦截之中,用户可以任意的修改传递的参数数据,也可以修改返回的结果。
范例:在applicationContext.xml文件之中配置环绕拦截

<aop:around method="serviceAround" pointcut="execution(* org.lks..*.*(..)))"/>

在整个给出的AOP操作之中,环绕通知的操作应该是功能最强大的,其它的拦截只能做一些基础的信息记录,而环绕甚至可以对传入参数和返回结果进行控制。

【以下的操作不建议在实际开发中使用,讲解它的主要原因是因为有这样的配置,不过此类配置无法基于Annotation实现】
在Spring的AOP编程里面还有一个更加神奇的技术,可以实现接口类型的动态配置。

假设现在已经有一个IMemberService接口和一个MemberServiceImpl子类,但是现在希望有一个IAction接口,并且通过配置(不是真实实现)让MemberServiceImpl也成为IAction接口子类。
范例:定义IAction接口

package org.lks.demo;

public interface IAction {

	public void fun();
}
package org.lks.demo;

import org.springframework.stereotype.Service;

@Service
public class ActionImpl implements IAction{

	@Override
	public void fun() {
		System.out.println("[Action自己的子类] 这个是正宗子类");
	}
	
}

而后此时IAction和MemberServiceImpl类没有任何直接的关系,但是却可以通过配置,让其有直接关系。
范例:修改applicationContext.xml文件

<aop:config>
	<!-- 首先需要定义程序的切入点 -->
	<aop:pointcut expression="execution(* org.lks..*.*(..)) and args(vo))" id="pointcut"/>
	<!-- 定义要使用的面向方面的处理类 -->
	<aop:aspect ref="serviceAspect">
		<aop:declare-parents types-matching="org.lks.service.IMemberService+" 
		implement-interface="org.lks.demo.IAction" 
		default-impl="org.lks.demo.ActionImpl"/>
	</aop:aspect>
</aop:config>

随后的代码之中要取得IAction的接口对象。
范例:取得IAction接口对象

package org.lks.test;

import org.lks.demo.IAction;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAction {

	public static void main(String[] args) {
		ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
		IAction service = ctx.getBean("memberServiceImpl", IAction.class);
		service.fun();
	}
}

这个代码就相当于告诉用户,一个接口不一定非用自己的子类对象取得后才可以实例化,也可以通过其它对象取得后进行实例化,但是问题是调用的时候还是找到自己的子类。

4 利用Annotation配置AOP

以上的AOP操作如果在以后的开发之中基本上都是固定的,但是如果是你自己写的代码,如果通过配置文件编写就太麻烦了,所以可以使用基于Annotation的配置完成。
范例:打开AOP的Annotation支持

<?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:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">

	<context:annotation-config/>
	<context:component-scan base-package="org.lks"/>
	<aop:aspectj-autoproxy/>
</beans>

那么随后的事情就是需要在ServiceAspect类中编写所需要使用的Annotation。
范例:修改ServiceAspect程序类

package org.lks.aop;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.lks.vo.Member;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class ServiceAspect {

	@Before(value="execution(* org.lks..*.*(..)))")
	public void serviceBefore(){
		System.out.println("[AOP切面]: 执行日志记录操作!");
	}
	
	@Before(value="execution(* org.lks..*.*(..)) and args(param)", argNames="param")
	public void serviceBefore2(Object arg){
		System.out.println("[AOP切面]执行增加前的操作,参数= " + arg);
	}
	
	@After(value="execution(* org.lks..*.*(..)))")
	public void serviceAfter(){
		System.out.println("[AOP切面]: 执行事务处理操作!");
	}
	
	@AfterReturning(value="execution(* org.lks..*.*(..)))", argNames="ret", returning="ret")
	public void serviceAfterReturning(Object val){  //表示操作的结果
		System.out.println("[AOP切面]操作完成,返回结果: " + val);
	}
	
	@AfterReturning(value="execution(* org.lks..*.*(..)))",argNames="e",returning="e")
	public void serviceAfterThrowing(Exception exp){
		System.out.println("[AOP切面]操作出现异常: " + exp);
	}
	
	@Around(value="execution(* org.lks..*.*(..)))")
	public Object serviceAround(ProceedingJoinPoint point) throws Throwable{
		System.out.println("[AOP切面]数据层方法调用之前,参数: " + Arrays.toString(point.getArgs()));
		Member vo = new Member();
		vo.setMid("3171301102");
		vo.setMname("hhy");
		Object retval = point.proceed(new Object[]{vo}); //调用具体的真实操作
		System.out.println("[AOP切面]数据层方法调用之后,返回值: " + retval);
		return true;  //可以自己修改返回值
	}
}

如果在实际的开发之中需要进行一些辅助功能编写的时候,建议使用Annotation的配置操作,这样的代码是最简化,也是最直观的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值