AOP 面向切面编程,和IOC/DI 是spring 的两大基石
PART ONE
场景:
一个计算类(方法包括:加减乘除),要求在方法调用的时候打印此刻执行的方法名和参数,也就是打印一些方法执行过程中的细节
一: 动态代理
在开头的场景中如果,我们用最原始的方法完成需求,那么在每一个算术方法中都要写相应的打印语句,并且当要求的日志输出变多时,打印语句(日志语句)将会远远多于函数的核心代码,这样的代码维护何修改都十分麻烦
public int add(int a, int b) {
int result = a+b;
System.out.println("args are :"+a+","+b);
System.out.println("the result is : "+result);
return result;
}
比如这样一个add 方法变得就很复杂,多个方法需要这样的日志输出,代码就会重复,更改也不方便, 每个方法在处理核心逻辑
时还要兼顾其他多个关注点
这样的实现方法导致:代码混乱,代码分散
动态代理 :
使用一个代理对象将原始对象包装起来,然后用代理对象取代原始对象。当使用动态代理类调用方法时,动态代理类决定是否将该调用传递给原始类。
以上场景使用JDK 接口动态代理的方法。有接口那么方法就是对于公共方法的抽取
代理类执行任何方法下一步执行的就是 代理类中的invoke 方法
接口:
public interface Calculator {
int add(int a,int b);
int sub(int a,int b);
}
实现类:
public class CalculatorImp implements Calculator {
@Override
public int add(int a, int b) {
System.out.println("add");
return a+b;
}
@Override
public int sub(int a, int b) {
return 0;
}
}
动态代理类:
public class CalculatorLogProxy {
//要代理的对象
private Calculator calculator;
public CalculatorLogProxy(Calculator calculator){
this.calculator = calculator;
}
//获取代理对象
public Calculator getCalculator(){
Calculator proxy =null;
//代理对象的类加载器
ClassLoader classLoader = calculator.getClass().getClassLoader();
//动态代理方法调用之后要做的事
InvocationHandler invocationHandler= new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*参数
*proxy : 要返回的代理对象
method : 代理对象正在调用的那个方法
args :方法的参数
*/
String methodName = method.getName();
//日志
System.out.println("method is:"+methodName+" And the args are:"+ Arrays.asList(args));
//方法执行,执行那个对象的方法,和参数
Object result = method.invoke(calculator,args);
return result;
}
};
//代理对象
proxy =(Calculator) Proxy.newProxyInstance(classLoader,calculator.getClass().getInterfaces(),invocationHandler);
/**Proxy 中三个参数的意义
*classLoard : 代理对象的类加载器
*calculator.getclass().getInterfaces() : 代理类所实现的接口(原始类所实现的接口)
*invocationHandler : 代理对象调用方法之后要进行的工作
*/
return proxy;
}
}
测试:
public static void main(String[] args){
CalculatorImp calculatorImp = new CalculatorImp();
Calculator calculator = new CalculatorLogProxy(calculatorImp).getCalculator();
int result =calculator.add(4,5);
System.out.println(result);
}
CalculatorLogProxy 这个类将Calculator 接口下的实现代理过来,在CalculatorLogProxy 中通过反射创建一个代理对象。proxy 代理了 Calculator 的实现 calculatorImp :
Calculator calculator = new CalculatorLogProxy(calculatorImp).getCalculator();
当语句执行顺序
//1.
calculator.add(4,5);
//2. invoke 函数,此时invoke 函数,的参数值:method :add,args: 4,5
String methodName = method.getName();
也就是说 代理对象调用方法执行的是动态代理类中的invoke 方法
可以理解为代理类是外圈,原始类被包含在里面(被代理类代理),当代理类调用方法是由代理类中的invoke 函数进行处理,甚至决定这个调用是否传递给原始类
PART TWO: AOP
一: 相关概念和基本理解
AOP(面向切面编程)核心的就是动态代理
spring本身也有AOP的框架 ,只是AspectJ 更加推荐使用,要导入相关的jar 包
关于面向切面编程中的专业名词
1.
二:基于AspectJ 注解的方式
//将这个类声明为一个切面
@Aspect
@Component
public class LoggingAspect {
//声明一个前置通知
@Before("execution(public int blogwebsite.A_Spring_Test.Calculator.add(int,int))")
public void BeforeMethod(){
System.out.println("方法执行之前");
}
}
进行xml 配置,使AspectJ 注解起作用:自动为匹配aspectJ 注解的java 类生成对象
/*AspectJ 的注解包括:
@Before @After @AfterRunning @AfterTrowing @Around
*/
<aop:aspectJ-autoproxy></aop:aspectJ-autoproxy>
测试:
public static void main(String[] args){
//创建IOC 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring_AOP.xml");
//从IOC容器中获取Bean实例
Calculator calculator = (Calculator) ctx.getBean("calculatorImp");
int result=calculatorImp.add(4,5);
}
在spring中使用AspectJ 切面,只需要在IOC容器中将切面声明为Bean.
当IOC容器中初始化AspectJ切面之后,SpringIOC 容器就会为那些与AspectJ切面匹配的Bean创建代理.
在Aspect J 注解中,切面只是一个带有@Aspect 注解的java 类.
1.基本使用
- 将横切关注点的代码写入一个切面类中
1. 首先切面类是一个IOC的Bean,因此加上注解 @Component
2. 切面还需要加上注解 @Aspect - 在切面类中声明各种通知
AspectJ 支持五种类型的通知注解
3. @Before : 牵制通知,在方法执行之前
4. @After : 后置通知,在方法执行之后
5. @AfterRunning : 返回通知,在方法执行之后
6. @AfterTrowing : 异常通知,在方法抛出异常之后
7. @Around : 环绕通知,围绕着方法执行
通知就是方法,在声明通知的时候要指明 切点所在的位置
切点的写法:
例1:
public int blogwebsite.A_Spring_Test.Calculator.add(int,int)
public 修饰的;返回值为int ;blogwebsite.A_Spring_Test.Calculator 中的有两个int 型参数的add 方法
例2:
* blogwebsite.controller.*.*(..)
任意修饰的;任意返回值的;blogwebsite.controller 包下的,任意类的任意方法(名字,参数的类型数量都任意)
JoinPoint:
在通知方法中声明一个JoinPoint 参数,然后就能访问连接细节,比如:方法名称和参数值等
@Before("execution(public int blogwebsite.A_Spring_Test.Calculator.add(int,int))")
public void BeforeMethod(JoinPoint joinPoint){
//获得方法的名称
String methodName = joinPoint.getSignature().getName();
System.out.println("Method name : "+methodName);
//获得参数
Object[] args = joinPoint.getArgs();
System.out.println("args : "+ Arrays.asList(args));
}
2.五种通知详解:
@Aspect
@Component
public class LoggingAspect {
//声明一个前置通知
@Before("execution(public int blogwebsite.A_Spring_Test.Calculator.add(int,int))")
public void BeforeMethod(JoinPoint joinPoint){
//获得方法的名称
String methodName = joinPoint.getSignature().getName();
System.out.println("Method name : "+methodName);
//获得参数
Object[] args = joinPoint.getArgs();
System.out.println("args : "+ Arrays.asList(args));
}
//后置通知,在方法执行结束之后(无论是否发生异常),进行通知。这个通知一定会执行
//在后置通知中之无法访问目标方法的执行结果(如果方法执行过程中发生异常,是没有返回结果的),执行结果在返回通知中可以访问
@After("execution(public int blogwebsite.A_Spring_Test.Calculator.div(int,int))")
public void after(JoinPoint joinPoint){
System.out.println("MethodName : "+joinPoint.getSignature().getName()+" ends");
}
//返回通知
@AfterReturning(value = "execution(public int blogwebsite.A_Spring_Test.Calculator.div(int,int))",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
System.out.println("the returning is :"+result);
}
/**
* 异常通知:目标方法出现异常,并且出现的异常和通知参数的异常类型一致的时候才会执行这个通知
* @param joinPoint
* @param ex
*/
@AfterThrowing(value = "execution(public int blogwebsite.A_Spring_Test.Calculator.div(int,int))",throwing = "ex")
public void afterThrowingException(JoinPoint joinPoint,Exception ex){
}
/** 全面,但是不常用
* 环绕通知需要携带ProceedingJoinPoint 类型的参数
* 环绕通知类似于动态代理:ProceedingJoinPoint 类型的参数可以据欸的那个是否执行目标方法;也可以获取链接细节(类似JoinPoint)
* 环绕通知必须有返回值,及返回值就是目标方法的返回值
* @param pdj
* @return
*/
@Around("execution(public int blogwebsite.A_Spring_Test.Calculator.div(int,int))")
public Object around(ProceedingJoinPoint pdj) {
Object result = null;
try {
//前置通知
String methodName = pdj.getSignature().getName();
pdj.proceed();
//返回通知
} catch (Throwable throwable) {
throwable.printStackTrace();
//异常通知
}
//后置通知
return result;
}
}
这五种通知底层的实现时动态代理,所以在动态代理中可以找到着五种通知的位置,大概位置和环绕通知差不多
动态代理类中的invoke 方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*参数
*proxy : 要返回的代理对象
method : 代理对象正在调用的那个方法
args :方法的参数
这些是为了获取链接的细节
*/
String methodName = method.getName();
//日志
System.out.println("method is:"+methodName+" And the args are:"+ Arrays.asList(args));
//方法执行,执行那个对象的方法,和参数
try{
//前置通知
Object result = method.invoke(calculator,args);
//返回通知
}catch(Exception ex){
throw ex
//异常通知
}
//后置通知
return result;
}
3.使用细节
- 切面的优先级:
使用注解 @Order(n) n 为整数,值越小优先级越高 - 切点表达式的重用
在每个通知上都标注了切点的位置,这样的代码是不符合代码的重用性的
//统一定义切点
@Pointcut("execution(public int blogwebsite.A_Spring_Test.Calculator.add(int,int))")
public void point(){}
//在通知中的使用
@Before("Point()")
/*
*如果事在同一个包下的不同类中使用,使用的时候只需要注明,这个切点定义的类名就行了: @Before("类名.Point()")
*如果是在不同的包中使用,那么注明这个切点定义的包结构:@Before("包名.包名.包名.类名.Point()")
AOP面向切面编程的理解:
程序的执行过程,可以看成一段有起点和终点的线段,选择一个点,切入一个面,在个面上完成你想在这个点前后完成的事,这就是面向切面编程,在切面上一般完成日志和数据校验