动态代理、AOP?
一、什么是AOP
1、概述
AOP为Aspect Oriented Programming的缩写,是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
AOP的出现弥补了OOP的这点不足,AOP 是一个概念,一个规范,本身并没有设定具体语言的实现,AOP是基于动态代理模式。AOP是方法级别的,要测试的方法不能为static修饰,因为接口中不能存在静态方法,编译就会报错。
AOP可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口
2、一句话解释
AOP的全称是 Aspect-Oriented Programming 面向切面编程,就是在程序的运行期间,动态的将某段代码插入到源程序代码的某个位置,这就叫切面编程
二、动态代理
1、什么是动态代理
动态代理技术就是用来产生一个对象的代理对象的。在开发中为什么需要为一个对象产生代理对象呢?
举一个现实生活中的例子:歌星或者明星都有一个自己的经纪人,这个经纪人就是他们的代理人,当我们需要找明星表演时,不能直接找到该明星,只能是找明星的代理人。比如刘德华在现实生活中非常有名,会唱歌,会跳舞,会拍戏,刘德华在没有出名之前,我们可以直接找他唱歌,跳舞,拍戏,刘德华出名之后,他干的第一件事就是找一个经纪人,这个经纪人就是刘德华的代理人(代理),当我们需要找刘德华表演时,不能直接找到刘德华了(刘德华说,你找我代理人商谈具体事宜吧!),只能是找刘德华的代理人,因此刘德华这个代理人存在的价值就是拦截我们对刘德华的直接访问!
这个现实中的例子和我们在开发中是一样的,我们在开发中之所以要产生一个对象的代理对象,主要用于拦截对真实业务对象的访问。那么代理对象应该具有什么方法呢?代理对象应该具有和目标对象相同的方法
所以在这里明确代理对象的两个概念: 1、代理对象存在的价值主要用于拦截对真实业务对象的访问。 2、代理对象应该具有和目标对象(真实业务对象)相同的方法。刘德华(真实业务对象)会唱歌,会跳舞,会拍戏,我们现在不能直接找他唱歌,跳舞,拍戏了,只能找他的代理人(代理对象)唱歌,跳舞,拍戏,一个人要想成为刘德华的代理人,那么他必须具有和刘德华一样的行为(会唱歌,会跳舞,会拍戏),刘德华有什么方法,他(代理人)就要有什么方法,我们找刘德华的代理人唱歌,跳舞,拍戏,但是代理人不是真的懂得唱歌,跳舞,拍戏的,真正懂得唱歌,跳舞,拍戏的是刘德华,在现实中的例子就是我们要找刘德华唱歌,跳舞,拍戏,那么只能先找他的经纪人,交钱给他的经纪人,然后经纪人再让刘德华去唱歌,跳舞,拍戏
注明:本段内容引用博客园“孤傲苍狼”博主、笔者感觉这段话准确的解释了什么是动态代理,“孤傲苍狼”是笔者很喜欢的一位大神,大家可以看看他的博客
2、JDK动态代理Demo
作为打印的工具类
public class LogUtil {
public static void logBefore(String method, Object... args) {
System.out.println("当前方法是:【" + method + "】 , 参数是:" + Arrays.asList(args));
}
public static void logAfterReturning(String method, Object result) {
System.out.println("当前方法是:【" + method + "】 , 结果是:" + result);
}
public static void logAfterThrowing(String method, Exception e) {
System.out.println("当前方法是:【" + method + "】 , 抛出的异常是:" + e);
}
}
计算器接口
public interface Calculate {
public int add(int num1, int num2);
public int add(int num1, int num2, int num3);
public int div(int num1, int num2);
}
计算器实现类
public class Calculator implements Calculate {
public int add(int num1, int num2) {
try {
LogUtil.logBefore("add", num1, num2);
int result = num1 + num2;
LogUtil.logAfterReturning("add", result);
return result;
} catch (Exception e) {
LogUtil.logAfterThrowing("add", e);
}
return 0;
}
public int add(int num1, int num2, int num3) {
try {
LogUtil.logBefore("add", num1, num2, num3);
int result = num1 + num2 + num3;
LogUtil.logAfterReturning("add", result);
return result;
} catch (Exception e) {
LogUtil.logAfterThrowing("add", e);
}
return 0;
}
public int div(int num1, int num2) {
try {
LogUtil.logBefore("div", num1, num2);
int result = num1 / num2;
LogUtil.logAfterReturning("div", result);
return result;
} catch (Exception e) {
LogUtil.logAfterThrowing("div", e);
}
return 0;
}
}
public class JdkProxyFactory {
public static Object createJdkProxy(Object target) {
// 增强==就是给现有功能扩展更强大的功能
/**
* newProxyInstance 创建一个代理对象<br/>
* 第一个参数是代理对象的类加载器<br/>
* 第二个参数是代理对象需要实现的接口<br/>
* 第三个参数是用来拦截代理对象的每一个方法的执行。<br/>
*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 代理对象的每个方法的执行,都会被拦截到,然后执行invoke方法<br/>
* 第一个参数是代理对象<br/>
* 第二个参数是执行的方法<br/>
* 第三个参数是方法的参数<br/>
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 前置代码==== 前面的增强部分叫前置增强====前置通知
LogUtil.logBefore(method.getName(), args);
// 代理都是为了增强 目标对象的方法
Object result = method.invoke(target, args); ///// 执行目标方法
// 后置代码===== 目标方法后面增旨的部分叫后置增强 ==== 后置通知
LogUtil.logAfterReturning(method.getName(), result);
return result;
} catch (Exception e) {
// 目标方法抛了异常。记录异常信息的部分,我们叫异常增强 ==== 异常通知
LogUtil.logAfterThrowing(method.getName(), e);
throw e;
}
}
});
}
public static void main(String[] args) {
// 目标对象
Calculate calculate = new Calculator();
// 创建代理对象
Calculate proxy = (Calculate) createJdkProxy(calculate);
System.out.println(proxy.add(100, 100));
System.out.println(proxy.add(100, 100, 100));
System.out.println(proxy.div(100, 0));
}
}
优点:这种方式已经解决我们前面所有日记需要的问题。非常的灵活。而且可以方便的在后期进行维护和升级。
缺点:当然使用jdk动态代理,需要有接口。如果没有接口。就无法使用jdk动态代理。
3、CGLIB动态代理
public class CglibProxyFactory {
public static Object createCglibProxy(Object target) {
// 创建一个增强器对象
Enhancer enhancer = new Enhancer();
// 它可以对一个类,进行增强,产生一个子类(在你提供的类的基础上,生成一个子类)
// 对哪个类进行增强
enhancer.setSuperclass(Calculator.class); // 设置谁是父类
// 增强哪些内容===拦截目标方法
enhancer.setCallback(new MethodInterceptor() {
/**
* MethodInterceptor 接口,是为了拦截目标方法,然后实现增强功能。<br/>
* 第一个参数是代理对象<br/>
* 第二个参数是目标方法(反射对象)<br/>
* 第三个参数是方法执行的参数<br/>
* 第四个参数是代理对象的方法<br/>
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
try {
// 前置增强====前置通知===前置代码
LogUtil.logBefore(method.getName(), args);
// 执行目标方法 === invokeSuprt 执行父类方法
Object result = methodProxy.invokeSuper(proxy, args);
// 返回通知====
LogUtil.logAfterReturning(method.getName(), result);
return result;
} catch (Exception e) {
LogUtil.logAfterThrowing(method.getName(), e);
throw e;
}
}
});
// 生成一个代理对象===这个代理对象就是Calculator的一个子类
return enhancer.create();
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
// 产生一个cglib代理对象==cglib代理对象是目标对象的一个子类
Calculator proxy = (Calculator) createCglibProxy(calculator);
System.out.println( proxy instanceof Calculate );
System.out.println( proxy instanceof Calculator );
// proxy.add(100, 100);
// proxy.add(100, 100,100);
// proxy.div(100, 0);
}
}
优点:在没有接口的情况下,同样可以实现代理的效果。
缺点:同样需要自己编码实现代理全部过程。
三、AOP专业术语
1、专业术语定义
通知
通知就是增强的代码,例如前置增强的代码,后置增强的代码,异常增强的代码,这些代码就叫通知
切面
切面就是包含通知代码的类叫切面
横切关注点
横切关注点就是我们可以添加增强代码的位置,比如前置位置,后置位置,异常位置和返回值位置,这些都叫横切关注点
目标
目标就是目标对象,被代理的对象
代理
为了拦截目标对象方法,而创建出来的那个对象(增强之后的对象),称为代理对象
连接点
连接点就是横切关注点和程序代码的连接,叫连接点
切入点
切入点就是用户真正处理的连接点,叫切入点
在spring中切入点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件
2、专业术语图解
四、Spring中的AOP应用
1、 如果一个类实现了接口, 那么Spring默认使用JDK动态代理, 所以和接口有关系, 只能使用接口来接收
2、 如果一个类没有使用接口, 那么Spring默认使用CGLIB动态代理, 所以和类有关系, 只能使用类来接收
3、Spring中的注解
@Before 前置通知
@After 后置通知
@AfterThrowing 异常通知
@AfterReturning 返回通知
@Around 环绕通知
4、Spring中的切入点表达式
@Before(“execution(权限修饰符 返回值类型 方法全限定名(参数类型列表))”)
五、Spring简单实现面向切面编程
1、注解版
计算器接口
public interface Calculate {
Integer add(Integer num1, Integer num2);
Integer sub(Integer num1, Integer num2);
Integer mul(Integer num1, Integer num2);
Integer div(Integer num1, Integer num2);
}
实现类
@Component
public class Calcutlator implements Calculate {
@Override
public Integer add(Integer num1, Integer num2) {
System.out.println("add 方法执行");
return num1 + num2;
}
@Override
public Integer sub(Integer num1, Integer num2) {
return num1 - num2;
}
@Override
public Integer mul(Integer num1, Integer num2) {
return num1 * num2;
}
@Override
public Integer div(Integer num1, Integer num2) {
return num1 / num2;
}
}
切面
/**
* @Aspect: 标识这个类是一个切面
* @Component: 切面也是一个组件,需要被IOC容器接管
*/
@Aspect
@Component
public class LogUtil {
@Before("execution(public Integer com.atguigu.interfaces.impl.Calcutlator.*(Integer, Integer))")
public static void logBefore() {
System.out.println("前置通知");
}
@After("execution(public Integer com.atguigu.interfaces.impl.Calcutlator.*(Integer, Integer)))")
public static void logAfter() {
System.out.println("后置通知");
}
@AfterThrowing("execution(public Integer com.atguigu.interfaces.impl.Calcutlator.*(Integer, Integer)))")
public static void logAfterThrowing() {
System.out.println("异常通知");
}
@AfterReturning("execution(public Integer com.atguigu.interfaces.impl.Calcutlator.*(Integer, Integer)))")
public static void logAfterReturning() {
System.out.println("返回通知");
}
}
Spring的配置文件
<!-- 开启包扫描功能 -->
<context:component-scan base-package="com.atguigu"/>
<!-- 开启AOP的自动代理功能 -->
<aop:aspectj-autoproxy/>
2、获取连接点的信息
JoinPoint 是连接点的信息。
只需要在通知方法的参数中,加入一个JoinPoint参数。就可以获取到拦截方法的信息
Tips:是org.aspectj.lang.JoinPoint这个类。
public void logBefore(JoinPoint jp) {
// 获取方法的签名
Signature signature = jp.getSignature();
// getArgs() 获取参数列表 , getName()获取方法名字
System.out.println("前置通知, 方法名是: " + signature.getName() + ",参数是: " + Arrays.asList(jp.getArgs())); // 前置增强代码
}
3、获取拦截方法的返回值和异常
获取方法的返回值分为两个步骤:
1、 在方法参数列表上加上参数Object result
2、 在@AfterResurning注解加上属性 returning="result"
@AfterReturning(value="myPointcut()", returning="result")
public void logAfterReturning(JoinPoint jp, Object result) {
System.out.println("返回通知, 方法名是: " + jp.getSignature().getName() + ",方法的返回值是: " + result);
}
获取方法的异常信息分为两个步骤
1、 在方法的参数列表上加上参数Throwable e
2、 在@AfterThrowing注解中加上属性throwing="e"
@AfterThrowing(value = "myPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint jp, Throwable e) {
System.out.println("异常通知, 方法名时:" + jp.getSignature().getName() + ",异常信息是:" + e);
}
4、xml配置版
public interface Calculate {
Integer add(Integer num1, Integer num2);
Integer sub(Integer num1, Integer num2);
Integer div(Integer num1, Integer num2);
}
public class Calculator implements Calculate {
@Override
public Integer add(Integer num1, Integer num2) {
Integer result = num1 + num2;
System.out.println("执行目标方法add(Integer num1, Integer num2)");
return result;
}
@Override
public Integer sub(Integer num1, Integer num2) {
Integer result = num1 - num2;
System.out.println("执行目标方法sub(Integer num1, Integer num2)");
return result;
}
@Override
public Integer div(Integer num1, Integer num2) {
Integer result = num1 / num2;
System.out.println("执行目标方法div(Integer num1, Integer num2)");
return result;
}
}
public class LogUtil {
public void logBefore(JoinPoint jp) {
// 获取方法的签名
Signature signature = jp.getSignature();
System.out.println("前置通知, 方法名是: " + signature.getName() + ",参数是: " + Arrays.asList(jp.getArgs())); // 前置增强代码
}
public void logAfter(JoinPoint jp) {
// 获取方法的签名
Signature signature = jp.getSignature();
System.out.println("后置通知, 方法名是: " + signature.getName() + ", 参数是: " + Arrays.asList(jp.getArgs())); // 后置增强
}
public void logAfterThrowing(JoinPoint jp, Throwable e) {
// 获取方法的签名
Signature signature = jp.getSignature();
System.out.println("异常通知, 方法名是: " + signature.getName() + ",异常是: " + e);
}
public void logAfterReturning(JoinPoint jp, Object result) {
// 获取方法的签名
Signature signature = jp.getSignature();
System.out.println("返回通知方法名是: " + signature.getName() + ", 返回的结果是: " + 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: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.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 配置目标对象 -->
<bean id="calculator" class="com.atguigu.pojo.Calculator"/>
<!-- 配置切面 -->
<bean id="logUtil" class="com.atguigu.aspect.LogUtil"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置一个切面 -->
<aop:aspect ref="logUtil">
<!-- 配置一个切入表达式 -->
<aop:pointcut expression="execution(public Integer com.atguigu.pojo.Calculate.*(Integer, Integer))" id="mypointcut"/>
<!-- 前置通知 -->
<aop:before method="logBefore" pointcut-ref="mypointcut"/>
<!-- 后置通知 -->
<aop:after method="logAfter" pointcut-ref="mypointcut"/>
<!-- 返回置通知 -->
<aop:after-returning method="logAfterReturning" pointcut-ref="mypointcut" returning="result"/>
<!-- 异常通知 -->
<aop:after-throwing method="logAfterThrowing" pointcut-ref="mypointcut" throwing="e" />
</aop:aspect>
</aop:config>
</beans>
5、环绕通知
1、环绕通知使用@Around注解。
2、环绕通知如果和其他通知同时执行。环绕通知会优先于其他通知之前执行。
3、环绕通知一定要有返回值(环绕如果没有返回值。后面的其他通知就无法接收到目标方法执行的结果)。
4、在环绕通知中。如果拦截异常。一定要往外抛。否则其他的异常通知是无法捕获到异常的。
/**
* 环绕通知
* 1、环绕通知接收到异常一定要向外抛,否则普通的异常通知就接受不到<br/>
* 2、环绕通知的方法一定要把目标方法的返回值返回<br/>
* 3、环绕通知优先于普通通知先执行
*/
@Around(value = "myPointcut()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
try {
try {
// 前置通知
System.out.println("环绕前置通知");
// 执行目标方法
result = pjp.proceed();
} finally {
// 后置通知
System.out.println("环绕后置通知");
}
// 返回通知
System.out.println("环绕返回通知 :" + result);
return result;
} catch (Exception e) {
System.out.println("环绕异常通知" + e);
throw e;
}
}
6、通知执行的顺序
7、多个切面多个通知执行的顺序
1、通知的执行顺序默认是由切面类的字母先后顺序决定。
2、在切面类上使用@Order注解决定通知执行的顺序(值越小,越先执行)