一、AOP简介
1、AOP概念
AOP(Aspect Oreriented Programming)面向切面编程
OOP(Object Oreriented Programming)面向对象编程
面向切面编程:基于OOP新的编程思想,指在程序运行期间,将某段代码动态切入到指定方法的指定位置进行运行的编程方式
2、场景
计算器在计算的时候进行日志记录
2.1、创建一个AOP工程
2.2、实验
2.2.1、创建一个Calculator接口
创建加减乘除四个运算方法
package com.fxp;
public interface Calculator {
//定义加减乘除四个方法
public int add(int i,int j);
public int sub(int i,int j);
public int mul(int i,int j);
public int div(int i,int j);
}
2.2.2、定义一个实现类
package com.fxp.impl;
import com.fxp.inter.Calculator;
public class MyCalculator implements Calculator {
@Override
public int add(int i, int j) {
int result = i+j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i-j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i*j;
return result;
}
@Override
public int div(int i, int j) {
int result = i/j;
return result;
}
}
继承calculator接口,并重写它的方法。
2.2.3、定义一个测试类
package com.fxp.test;
import com.fxp.impl.MyCalculator;
import com.fxp.inter.Calculator;
import org.junit.Test;
import static org.junit.Assert.*;
public class AopTest {
@Test
public void test(){
Calculator calculator = new MyCalculator();
calculator.add(1,2);
calculator.sub(2,1);
calculator.mul(2,3);
calculator.div(4,2);
}
}
运行结果什么都没有显示,我们的目的是加一个日志记录
2.2.4、加日志记录
2.2.4.1、第一种:直接编写在方法内部,不推荐,修改维护很麻烦
在每一个方法的内部输出一句话:
package com.fxp.impl;
import com.fxp.inter.Calculator;
public class MyCalculator implements Calculator {
//在每个方法开始之前和结束之后都加入日志信息
@Override
public int add(int i, int j) {
System.out.println("add方法开始了.....它使用的参数是:"+i+","+j+"");
int result = i+j;
System.out.println("add方法结束了.....运行结果是"+result+"");
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("sub方法开始了.....它使用的参数是:"+i+","+j+"");
int result = i-j;
System.out.println("sub方法结束了.....运行结果是"+result+"");
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("mul方法开始了.....它使用的参数是:"+i+","+j+"");
int result = i*j;
System.out.println("mul方法结束了.....运行结果是"+result+"");
return result;
}
@Override
public int div(int i, int j) {
System.out.println("div方法开始了.....它使用的参数是:"+i+","+j+"");
int result = i/j;
System.out.println("div方法结束了.....运行结果是"+result+"");
return result;
}
}
运行结果:
这种方式很麻烦,现在只是为一个计算器添加日志记录,如果以后为更加庞大的代码量的工程去添加日志的话,显然是不合适的。
而且,日志记录只是系统的辅助功能,是可有可无的,而加减乘除才是真正的业务逻辑功能,如果把代码全都写到一起,就形成了耦合,这是我们要极力避免的
3、动态代理(第二种加日志的方式)
我们希望的是,业务逻辑是核心功能,日志模块在核心功能运行期间,自己动态的加上。
3.1、创建一个帮Calculoator生成代理对象的类
package com.fxp.proxy;
import com.fxp.inter.Calculator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 这个类就是帮Calculator生成代理对象的类
* static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
* 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
*/
public class CalculatorProxy {
/**
* 为传入的参数创建一个代理对象
* 传入的Calculator calculator:被代理对象
* 返回:代理对象:return proxy;
*/
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {
//proxy为目标对象创建代理对象
/**
* newProxy有三个参数
* InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
* Class<?>[] interfaces:目标对象实现了的接口
* ClassLoader loader:目标对象的类加载器
*/
Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {
/**
* new一个InvocaotionHandler,这是一个匿名内部类,有三个参数
* Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
* Method method:当前将要执行的目标对象的方法
* Object[] args:这个方法调用时,外界传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
//利用反射使用method.invoke,执行目标方法
//method.invoke里面有两个参数:
//object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
//内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
//args:目标方法里面的参数
//返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
Object result = method.invoke(calculator, args);
//加入日志信息(方法开始后)
System.out.println("["+method.getName()+"]执行结束.....它的结果是:"+result+"");
//这个返回值必须返回出去,外界才能拿到真正执行后的返回值
return result;
}
});
return (Calculator) proxy;
}
}
把原来加减乘除方法中的日志信息全部删除
AOPtest源码:
package com.fxp.test;
import com.fxp.impl.MyCalculator;
import com.fxp.inter.Calculator;
import com.fxp.proxy.CalculatorProxy;
import org.junit.Test;
import static org.junit.Assert.*;
public class AopTest {
@Test
public void test(){
Calculator calculator = new MyCalculator();
calculator.add(1,2);
calculator.sub(2,1);
calculator.mul(2,3);
calculator.div(4,2);
//分隔符
System.out.println("===============================================");
//以前我们都是直接new了一个MyCalculator()的对象,然后用这个对象去调用方法
//现在我们可以创建一个MyCalculator的代理对象,用这个代理对象去执行加减乘除方法
Calculator proxy = CalculatorProxy.getProxy(calculator);
proxy.add(1,2);
}
}
运行结果:
3.2、动态代理流程整理
1、创建一个用于生成目标对象(Calculator)的代理对象的类(CalculatorProxy),在这个类里面写上一个方法(getProxy),此方法的返回值是目标对象,然后在这个方法传入目标对象(getProxy(final Calculator calculator)),参数要加一个final的值,原因见下
2、在此方法里面直接用Proxy.newProxyInstance得到一个代理对象,里面有三个参数
·InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
·Class<?>[] interfaces:目标对象实现了的接口
·ClassLoader loader:目标对象的类加载器
其中InvocationHandler是一个匿名内部类,传参的时候,直接new一个InvocationHandler即可,它会重写一个invoke方法,这个invoke方法也有三个参数:
·Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
·Method method:当前将要执行的目标对象的方法
·Object[] args:这个方法调用时,外界传入的参数
3、利用反射menthod.invoke执行目标方法,invoke方法要传入两个参数:
·object:这里是calculator,就是目标对象,为哪个对象创建代理对象,就填哪个对象。
但是有一个规定:如果要在内部类里面要用一个声明的参数,要加final,因为calculator在最开始的getProxy方法里已经声明过了,InvocationHandler内部类里的invoke方法要 用到这个参数,所以要在getProxy方法里面的calculator参数前面加final
·args:目标方法里面的参数
method.invoke返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值(这是invoke方法的返回值,并不是里面的参数)
4、将result返回出去,这个result就是真正的方法的返回值,也就是加减乘除方法的返回 值,至此,匿名内部类实现完毕
5、最后,将Proxy.newProxyInstance返回的proxy对象返回出去,这个proxy就是为目标对 象(Calculator)生成的代理对象
6、在测试端调用:
Calculator proxy = CalculatorProxy.getProxy(calculator);
proxy.add(1,2);
3.3、改进
例如这个计算器的例子,可能除法运算的时候会出现异常,所以我们还可以用try-catch
package com.fxp.proxy;
import com.fxp.inter.Calculator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 这个类就是帮Calculator生成代理对象的类
* static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
* 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
*/
public class CalculatorProxy {
/**
* 为传入的参数创建一个代理对象
* 传入的Calculator calculator:被代理对象
* 返回:代理对象:return proxy;
*/
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {
//proxy为目标对象创建代理对象
/**
* newProxyInstance有三个参数
* InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
* Class<?>[] interfaces:目标对象实现了的接口
* ClassLoader loader:目标对象的类加载器
*/
Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {
/**
* new一个InvocaotionHandler,这是一个匿名内部类,有三个参数
* Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
* Method method:当前将要执行的目标对象的方法
* Object[] args:这个方法调用时,外界传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//利用反射使用method.invoke,执行目标方法
//menthod.invoke里面有两个参数:
//object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
//内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
//args:目标方法里面的参数
//返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
Object result = null;
try {
//加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
result = method.invoke(calculator, args);
//加入日志信息(方法开始后)
System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");
}
catch(Exception e){
System.out.println("["+method.getName()+"]出现异常:"+e.toString()+"");
}
finally {
System.out.println("方法最终执行完毕");
}
//这个返回值必须返回出去,外界才能拿到真正执行后的返回值
return result;
}
});
return (Calculator) proxy;
}
}
然后测试一下除法:
Calculator proxy = CalculatorProxy.getProxy(calculator);
proxy.add(1,2);
proxy.div(2,0);
运行结果:
3.4、继续改进,解耦
创建一个LogUtils类,用于记录日志信息,把原来代理对象类的方法里面的日志信息提取出来封装成一个新的LogUtils类,进行解耦
CalculatorProxy:
package com.fxp.proxy;
import com.fxp.inter.Calculator;
import com.fxp.util.LogUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 这个类就是帮Calculator生成代理对象的类
* static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
* 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
*/
public class CalculatorProxy {
/**
* 为传入的参数创建一个代理对象
* 传入的Calculator calculator:被代理对象
* 返回:代理对象:return proxy;
*/
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {
//proxy为目标对象创建代理对象
/**
* newProxyInstance有三个参数
* InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
* Class<?>[] interfaces:目标对象实现了的接口
* ClassLoader loader:目标对象的类加载器
*/
Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {
/**
* new一个InvocationHandler,这是一个匿名内部类,有三个参数
* Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
* Method method:当前将要执行的目标对象的方法
* Object[] args:这个方法调用时,外界传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//利用反射使用method.invoke,执行目标方法
//menthod.invoke里面有两个参数:
//object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
//内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
//args:目标方法里面的参数
//返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
Object result = null;
try {
//加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
//System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
//上面这句输出语句的日志信息还能用LogUtils来封装,把上面这条语句封装到LogUtils类的静态LogStart方法里面,然后调用
LogUtils.logStart(method,args);
result = method.invoke(calculator, args);
//加入日志信息(方法开始后)
//System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");
//上面这句输出语句的日志信息还能用LogUtils来封装,把上面这条语句封装到LogUtils类的静态LogStart方法里面,然后调用
LogUtils.LogReturn(method,args);
}
catch(Exception e){
//System.out.println("["+method.getName()+"]出现异常:"+e.toString()+"");
//上面这条语句也可以放在Loutils中
LogUtils.LogErr(method,e);
}
finally {
//System.out.println("方法最终执行完毕");
//在Logutils中的表示方法
LogUtils.LogEnd();
}
//这个返回值必须返回出去,外界才能拿到真正执行后的返回值
return result;
}
});
return (Calculator) proxy;
}
}
Logutils源码:
package com.fxp.util;
import java.lang.reflect.Method;
import java.util.Arrays;
public class LogUtils {
public static void logStart(Method method,Object... args){
System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
}
//方法正常执行,正常返回的方法
public static void LogReturn(Method method,Object... result){
System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");
}
public static void LogErr(Method method,Exception e) {
System.out.println("["+method.getName()+"]出现异常:"+e.getCause()+"");
}
public static void LogEnd() {
System.out.println("方法最终执行完毕");
}
}
运行结果
至此,我们达到了我们刚开始的期望,使用了动态代理,在目标方法执行的前后进行执行
3.5、动态代理的缺点
3.5.1、写起来太难
现在这个例子,只是给计算器加了个动态代理,如果还有其他类要写动态代理,就要写很多遍,很麻烦。
3.5.2、目标类必须实现接口
1、在创建动态代理的时候,Proxy.newProxyInstance里面有三个参数,其中一个是Class<?> [ ]interfaces,这个参数是目标类所实现的所有接口,如果说目标类并没有实现任何接口,就意味着根本创建不了动态代理。
2、我们可以看一下Proxy真正的类型
System.out.println(proxy.getClass());
3、代理对象和被代理对象唯一能产生的关联,就是实现了同一个接口
4、我们可以看一下proxy实现的接口:
//proxy实现的接口
System.out.println(Arrays.asList(proxy.getClass().getInterfaces()));
运行结果:
可以看到proxy也是实现了Calculator这个接口。
5、结论:
所以如果目标类没有实现任何接口,是不能为目标类创建代理对象的。
3.6、解决办法
由于动态代理太难写,所以Spring实现了AOP功能,AOP的底层就是动态代理
可以利用Spring一句代码都不写的去创建动态代理,实现简单,而且没有强制要求目标类必须实现接口。
3.7、AOP的专业术语
以Calculator为例:
它有四个方法:add、sub、mul、div,
横切关注点:每个方法都能在方法的开始、返回、异常、结束的地方记录日志,这些地方就叫做横切关注点
通知方法:其实就是记录日志信息的方法,比如在方法执行前输出的信息等等.....
切面类:横切关注点和通知方法所在的类
连接点:每一个方法的每一个位置(方法开始、返回、异常、结束)都是一个连接点
切入点:这些连接点中,我们真正需要记录日志的地方
切入点表达式:在众多连接点中选出我们真正需要的点的表达式