spring框架:AOP动态代理(以注释的方式配置切面)

动态代理背景

动态代理里面有三个对象:原始对象,代理对象,目标对象
这三个对象可以理解成找工作的人(原始对象),帮你找工作的中介(代理对象),找工作这件事(目标对象)
现在以一个例子来理解:
就是你需要实现一个计算器,计算器的功能需要又计算的操作,还需要有日志的记录,比如执行前记录现在执行的是那一个方法,计算的两个数是什么,执行后记录方法名和计算的结果
现在的难点就是在这个日志的功能你需要怎么实现,可能你想到最简单粗暴的方法就是直接输出,比如

package math;

public class MathImpl implements MathI
{
	@Override
	public int add(int x, int y)
	{
		System.out.println("日志:方法名add,参数"+x+"和"+y);
		int result=x+y;
		System.out.println("日志:方法名add,结果"+result);
		return result;
	}
}

那么这样写就是有问题的,
你将日志文件的代码写在事务代码里面,这样做成代码混乱,
还有就是代码的重复利用不高,就比如在计算器里面加一个除法的功能,那么就得也在除法的方法里面增加日志的代码,而且这两个日志的代码完全是相似相同的

现在我们就想创建一个动态代理的类,这个类就是自动地帮我们写日志文件
动态代理类是可以为任何的目标对象创建代理对象,动态代理的方式有两种:

  1. 基于接口实现的动态代理:JDK代理,要求代理的对象必须有实现的接口,

  2. 基于继承实现动态代理:Cglib动态代理
    下面代码演示JDK自动代理

package math;

public interface MathI
{
	//这里只实现计算器的加的方法
	int add(int x,int y);
}

package math;

public class MathImpl implements MathI
{
	@Override
	public int add(int x, int y)
	{
//		System.out.println("日志:方法名add,参数"+x+"和"+y);
		int result=x+y;
//		System.out.println("日志:方法名add,结果"+result);
		return result;
	}
}

package math;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ProxyUtil
{
	private MathImpl mathimpl;
	
	public ProxyUtil(MathImpl mathimpl)
	{
		this.mathimpl = mathimpl;
	}

	public Object getProxy()
	{
		/*
		 * 这里使用的是JDK动态代理,这里需要三个参数
		 * 类加载器:因为这个动态代理对象是这件事需要做的时候
		 * 			而创建的一个动态代理对象来帮你做这件事,
		 * 			那么动态代理对象的创建是需要类的,
		 * 			类的加载需要加载器的
		 * 接口:这个是代理对象想帮你去做事情,
		 * 		但是它得知道你需要做什么事情,所以代理对象
		 * 		就是通过你实现的接口来知道你需要做什么事情的
		 * 		因为你实现的方法就是在接口里面的
		 * 执行处理器:就是动态代理对象知道需要做什么了,
		 * 			  这个执行处理器就是具体的做法
		 * 			  这个执行处理器是一个接口InvocationHandler,
		 * 			  你可以实现这个接口,这个参数就直接写this
		 * 			  或者直接创建一个内部类重写抽象方法
		 */
		ClassLoader loader=this.getClass().getClassLoader();
		Class[] interfaces=mathimpl.getClass().getInterfaces();
		
		return Proxy.newProxyInstance(loader, interfaces, new InvocationHandler()
		{
			/*
			 * 这个方法有三个参数
			 * proxy,
			 * method 这个是动态代理对象需要执行的方法
			 * args  这个是动态代理对象需要执行的方法的参数
			 * 代理对象帮你做事情,但是要保证结果的一致性
			 * 那么执行方法的时候就是调用了目标对象,
			 * 那么就是使用目标对象的方法,结果肯定是一致性的
			 */
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
			{
				Logger.before(method.getName(), Arrays.toString(args));
				Object result = method.invoke(mathimpl, args);
				Logger.after(method.getName(), result);
				return result;
			}
		});
	}
}

package math;

public class Logger
{
	public static void before(String methodName,String args)
	{
		System.out.println("方法:"+methodName+",参数:"+args);
	}
	public static void after(String methodName,Object result)
	{
		System.out.println("方法:"+methodName+",结果:"+result);
	}
}

package math;

public class Test
{
	public static void main(String[] args)
	{
//		MathI math=new MathImpl();
//		int add = math.add(2, 3);
//		System.out.println(add);
		ProxyUtil proxy = new ProxyUtil(new MathImpl());
		MathI proxy2 = (MathI)proxy.getProxy();
		int add=proxy2.add(1, 3);
		System.out.println(add);
	}
}

结果
方法:add,参数:[1, 3]
方法:add,结果:4
4

AOP概述

  1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充。
    面向对象是纵向继承机制,比如上面的背景中,如果需要写日志功能使用的是面向对象的方法来写,就是可能是写一个MathI的子类,继承了MathI的所有方法,然后在代码里面增加日志的代码,结果还是代码混乱,代码重复率不高等问题
    面向切面编程是横向抽取机制,比如上面的背景中的动态代理的写法,将日志功能的代码抽取出来写在一个日志的类里面,然后通过一些方法让他作用在MathI类里面,这样就不像面向对象的方法一样纵向增加代码,而是横向增加代码,而且这样可以将业务代码和非业务代码进行分离
  2. AOP编程操作的主要对象是切面(aspect),
    切面就是上面背景的Logger的类,
    而切面用于模块化横切关注点(公共功能),
    横切关注点就是上面背景的日志功能的方法,
    模块化就是将非业务代码全部放在一块。
  3. 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。
    这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
  4. AOP的好处:
    每个事物逻辑位于一个位置,代码不分散,便于维护和升级
    业务模块更简洁,只包含核心业务代码
    AOP和代理对象是一样的
    在这里插入图片描述

AOP术语

横切关注点
从每个方法中抽取出来的同一类非核心业务的代码,比如上面背景的日志方法。
切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。比如上面背景的Logger类。
通知(Advice)
切面必须要完成的各个具体工作,和横切关注点是同一个东西,放在切面里面就叫通知,在业务代码就叫横切关注点
目标(Target)
被通知的对象,比如上面的MathI类
代理(Proxy)
向目标对象应用通知之后创建的代理对象
连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。
例如:类某个方法调用前、调用后、方法捕获到异常后等。比如上面的背景中的日志信息,执行前有一个日志信息,那么抽取出来的日志信息的代码的位置就是一个连接点
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:
横坐标:各个模块中的可执行方法
纵坐标:每个方法中的各个关注点
在这里插入图片描述
切入点(pointcut)
切入点其实就是一个判断表达式,通知作用于连接点的条件,定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

图解
在这里插入图片描述

AspectJ

概述
AspectJ:Java社区里最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
在Spring中启用AspectJ注解支持

  1. 导入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
    spring-aop-4.0.0.RELEASE.jar
    spring-aspects-4.0.0.RELEASE.jar
  2. 引入aop的命名空间
  3. 配置
    配置文件写上< aop:aspectj-autoproxy>
    当Spring IOC容器侦测到bean配置文件中的< aop:aspectj-autoproxy>元素时,会自动为与AspectJ切面匹配的bean创建代理

用AspectJ注解声明切面

  1. 要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。
  2. 当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。
  3. 在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。
  4. 通知是标注有某种注解的简单的Java方法。
  5. AspectJ支持5种类型的通知注解:
    ① @Before:前置通知,在方法执行之前执行
    ② @After:后置通知,在方法执行之后执行
    ③ @AfterRunning:返回通知,在方法返回结果之后执行
    ④ @AfterThrowing:异常通知,在方法抛出异常之后执行
    ⑥ @Around:环绕通知,围绕着方法执行

AOP使用

切入点表达式
作用:通过表达式的方式定位一个或多个具体的连接点。
切入点表达式的语法格式:
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表])) ,
在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
具体看例子

当前连接点
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中。
在这里插入图片描述

通知
概述

  1. 通知是在具体的连接点上要执行的操作。
  2. 一个切面可以包括一个或者多个通知。
  3. 通知所使用的注解的值往往是切入点表达式。

分类

  1. 前置通知:在方法执行之前执行的通知,使用@Before注解
  2. 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候,使用@After注解
  3. 返回通知:方法执行完执行的通知,方法有异常就不执行
  4. 异常通知:只在连接点抛出异常时才执行异常通知
    1)将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
    2)如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
  5. 环绕通知:所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
    1)对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
    2)在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
    3)注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

重用切入点定义

  1. 在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
  2. 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
  3. 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
  4. 其他通知可以通过方法名称引入该切入点

指定切面的优先级

  1. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。
  2. 切面的优先级可以通过实现Ordered接口或利用@Order注解指定。
  3. 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。
  4. 若使用@Order注解,序号出现在注解中

上面的AOP使用的具体,都看下面的例子,这样好理解

package aop;

public interface MathI
{
	//这里只实现计算器的加的方法
	int add(int x,int y);
}

package aop;

import org.springframework.stereotype.Component;

@Component
public class MathImpl implements MathI
{
	@Override
	public int add(int x, int y)
	{
//		System.out.println("日志:方法名add,参数"+x+"和"+y);
		int result=x/y;
//		System.out.println("日志:方法名add,结果"+result);
		return result;
	}
}

<?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: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.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<context:component-scan base-package="aop"/>
	<!-- 这里说明里面有动态代理的注解的类都自动创建动态代理对象 -->
	<aop:aspectj-autoproxy/>
</beans>

package aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
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.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect //标注当前类是切面类
public class MyloggerAspect
{
	/*
	 * @Pointcut(value="execution( * aop.*.*(..))")
	 * 这个是定义一个重用的的切入点,下面的切入点表达式想使用这个切入点的话,可以这样写
	 * @Before(value = "jane()"),这样写的效果和
	 * @Before(value = "execution( * aop.*.*(..))")的效果是一样的
	 */
	@Pointcut(value="execution( * aop.*.*(..))")
	public void jane() {}
	
	/*
	 * @Before(value = "")
	 * 是将方法设置为前置通知,方法执行前进行调用
	 * 而且注解必须设置value参数,value是切入点表达式
	 * 使用execution()这个方法进行解析切入点表达式
	 * 注意:
	 * @Before(value = "execution(public int aop.MathImpl.add(int, int))")
	 * 这样写这个方法就只能作用于add的方法,但是我们需要作用的是还有加减乘除的方法
	 * 我们还可以像写过滤器一样就行占位符的写法
	 * @Before(value = "execution( * aop.*.*(..))")
	 * 前面的一个*是代表任意的修饰符和返回值
	 * 第二个*代表的是任意的接口
	 * 第三个是这个类的任意方法
	 * 后面..代表的是任意参数数量和类型
	 * 例如:
	 * @Before(value = "execution(public * aop.*.*(..))")
	 * 这里表示aop包下所有的接口的所有公共方法
	 * 
	 * @Before(value = "execution(public int aop.*.*(..))")
	 * 这里表示aop包下所有的公共接口中返回值是int的方法
	 * 
	 * @Before(value = "execution(public int aop.*.*(int,..))")
	 * 这里表示aop包下所有的公共接口中返回值是int并且第一个参数是int的方法
	 * 
	 * execution (* *.add(int,..)) || execution(* *.sub(int,..))
	 * 任意类中第一个参数为int类型的add方法或sub方法
	 * 
	 * !execution (* *.add(int,..)) 
	 * 匹配不是任意类中第一个参数为int类型的add方法
	 * 
	 * @Order(1)是设置切面的优先级
	 * 如果有多个切面作用于同一个连接点,那么切面的优先级是可以设置的
	 * 就是使用@Order(1)进行设置,里面的值越小优先级越高,默认值是int的最大值
	 * 如果你是写负数的的话就是没有效果的
	 */
//	@Before(value = "execution(public int aop.MathImpl.add(int, int))")
//	@Before(value = "execution( * aop.*.*(..))")
	@Before(value = "jane()")
	@Order(1)
	public void beforeMethod(JoinPoint jionpoint)
	{
		Object[] args = jionpoint.getArgs();//获取参数
		String name = jionpoint.getSignature().getName();//获取方法名
		System.out.println("前置通知:名称"+name+"参数"+Arrays.toString(args));
	}
	
	/*
	 * @After(value = "execution( * aop.*.*(..))")
	 * 后置通知,作用于方法执行后,这里的后是指finally语句块里面
	 * 所以这里的后置通知无论方法有没有异常都会执行
	 */
	@After(value = "execution( * aop.*.*(..))")
	public void after()
	{
		//这里一般用来关闭资源
		System.out.println("后置通知");
	}
	
	/*
	 * @AfterReturning(value = "execution( * aop.*.*(..))",returning = "result")
	 * 返回通知:作用于方法执行后,这里是指方法执行后的语句块,如果有异常的话就不会执行这个方法了
	 * 如果想得到方法返回的结果,那么就需要配置两个地方
	 * 1. 在注解里面写上returning设置接收方法返回值的变量名
	 * 2. 在方法里面写上一个和上面的变量名相同名字的形参
	 */
	@AfterReturning(value = "execution( * aop.*.*(..))",returning = "result")
	public void afterreturning(JoinPoint joinpoint,Object result)
	{
		String name = joinpoint.getSignature().getName();
		System.out.println("返回通知:名称:"+name+"方法"+result);
	}
	
	/*
	 * @AfterThrowing:将这个方法标注为异常通知(也叫例外通知)
	 * 异常通知:作用于方法执行时如果有异常就执行这个方法,如果没有就不执行,和try-catch语句一样的
	 * 如果想得到异常信息也得配置两个信息:
	 * 1. 在注解里面写上throwing设置接收方法返回值的变量名
	 * 2. 在方法里面写上一个和上面的变量名相同名字的形参,而变量的类型自己定义
	 * 注意形参的类型:
	 * 如果是NullPointerException,那么方法执行的时候有空指针异常的时候才执行这个方法
	 * 如果是其他的异常就不会执行这个方法的,因为spring赋值的时候会找形参对应类型的方法进行赋值的
	 * 所以根据这个可以根据异常类型处理特定的异常
	 */
	@AfterThrowing(value = "execution( * aop.*.*(..))",throwing = "ex")
	public void afterthrowing(NullPointerException ex)
	{
		System.out.println("异常信息:"+ex);
	}
	
	/*
	 * @Around
	 * 环绕通知,和直接写动态代理类是一样的
	 */
	@Around(value = "execution( * aop.*.*(..))")
	public Object around(ProceedingJoinPoint joinpoint)
	{
		Object result=null;
		try
		{
			System.out.println("环绕前置");
			//执行这个方法
			result = joinpoint.proceed();
			System.out.println("环绕返回");
		} catch (Throwable e)
		{
			System.out.println("环绕异常");
			e.printStackTrace();
		}finally
		{
			System.out.println("环绕后置");
		}
		return result;
	}
}

package aop;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test
{
	public static void main(String[] args)
	{
		ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("aop.xml");
		MathI mathI = ac.getBean("mathImpl", MathI.class);
		System.out.println(mathI.add(1, 3));
	}
}

以XML方式配置切面

除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
配置
在bean配置文件中,所有的Spring AOP配置都必须定义在< aop:config>元素内部。对于每个切面而言,都要创建一个< aop:aspect>元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供< aop:aspect>元素引用。
声明切入点

  1. 切入点使用< aop:pointcut>元素声明。
  2. 切入点必须定义在< aop:aspect>元素下,或者直接定义在< aop:config>元素下。
    ① 定义在< aop:aspect>元素下:只对当前切面有效
    ② 定义在< aop:config>元素下:对所有切面都有效
  3. 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。

声明通知

  1. 在aop名称空间中,每种通知类型都对应一个特定的XML元素。
  2. 通知元素需要使用来引用切入点,或用直接嵌入切入点表达式。
  3. method属性指定切面类中通知方法的名称
<?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:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<context:component-scan base-package="aopxml"></context:component-scan>
	<aop:config>
		<aop:aspect ref="mylogger">
			<aop:pointcut expression="execution(* aopxml.MathI.*(..))" id="cut"/>
			<aop:before method="before" pointcut-ref="cut"/>
			<!-- 或者写成这样,都是一样的,上面是切入点引用 -->
			<!-- <aop:before method="before" pointcut="execution(* aopxml.MathI.*(..))"/> -->
			
		</aop:aspect>
	</aop:config>
</beans>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReflectMirroring

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值