我们学过面向对象编程,比如Java语言,围绕着类和对象展开。
那么在面向切面编程,切面是什么?面向切面来编程又是怎么一回事呢?
先来看一个例子:
做一个计算器,来算两个数的加减乘除,同是还需要在完成计算前后写日志;
按照需求,先这么写:
package com.csu.aop.helloworld;
public class CaculatorImpl implements Caculator{
@Override
public int add(int i, int j) {
// TODO Auto-generated method stub
sysout(*********);
int result = i + j;
sysout(*********);
return result;
}
@Override
public int sub(int i, int j) {
// TODO Auto-generated method stub
sysout(*********);
int result = i - j;
sysout(*********);
return result;
}
@Override
public int mul(int i, int j) {
// TODO Auto-generated method stub
sysout(*********);
int result = i * j;
sysout(*********);
return result;
}
@Override
public int div(int i, int j) {
// TODO Auto-generated method stub
sysout(*****准备开始****);
int result = i / j;
sysout(****结束计算*****);
return result;
}
}
可以看到,每个计算方法中都需要在核心业务代码前后加上一串代码,看起来代码重复的相当多,而且当非业务代码加进来,代码变的庞大难维护;
现在,我们另有一个需求:让计算器只负责做运算,这个意思就是说要把写日志的任务给剥离开。
AOP可以做到,不过先用动态代理来模拟一下AOP的工作:
修改上面的代码:
package com.csu.aop.helloworld;
public class CaculatorImpl implements Caculator{
@Override
public int add(int i, int j) {
// TODO Auto-generated method stub
return i+j;
}
@Override
public int sub(int i, int j) {
// TODO Auto-generated method stub
return i-j;
}
@Override
public int mul(int i, int j) {
// TODO Auto-generated method stub
return i*j;
}
@Override
public int div(int i, int j) {
// TODO Auto-generated method stub
return i/j;
}
}
创建CalculatorLoggingProxy类:
package com.csu.aop.helloworld;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class CaculatorLoggingProxy {
//要为谁代理事务
private Caculator target;
public CaculatorLoggingProxy(Caculator target)
{
this.target=target;
}
//获取代理类
public Caculator getLoggingProxy() {
Caculator proxy=null;
//目标加载器,代理对象由哪个加载器负责
ClassLoader loader=target.getClass().getClassLoader();
//代理类是什么类型
Class [] interfaces=new Class[]{Caculator.class};
//当调用代理对象其中的方法时 该执行什么handler
InvocationHandler handler = new InvocationHandler() {
//proxy要返回的proxy,在invoke方法里不用,会造成死循环
//method是正在被调用的方法
//args调用方法时传进来的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// TODO Auto-generated method stub
System.out.println("The method"+method.getName()+"begin with"+Arrays.asList(args));
Object result=method.invoke(target, args);
System.out.println("The method result"+method.getName()+result);
return result;
}
};
proxy=(Caculator) Proxy.newProxyInstance(loader, interfaces, handler);
return proxy;
}
}
再来测试一下:
package com.csu.aop.helloworld;
public class MainAop {
public static void main(String[] args) {
// TODO Auto-generated method stub
Caculator target=new CaculatorImpl();
CaculatorLoggingProxy proxy=new CaculatorLoggingProxy(target);
// TODO Auto-generated method stub
Caculator target=new CaculatorImpl();
CaculatorLoggingProxy proxy=new CaculatorLoggingProxy(target);
Caculator caculator=proxy.getLoggingProxy();
int result=caculator.add(1, 2);
System.out.println("result->"+result);
}
}
控制台打印出:
The methodaddbegin with[1, 2]
The method resultadd3
result->3
目的达到。
但是用动态代理,步骤繁杂,代码也难看懂,如果有AOP帮助,变得很简单。
AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充.
AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点.
在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里.
AOP 的好处:
每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级
业务模块更简洁, 只包含核心业务代码.
切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
通知(Advice): 切面必须要完成的工作(验证,日志,切面里的每个方法)
目标(Target): 被通知的对象()
代理(Proxy): 向目标对象应用通知之后创建的对象()
连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
切点(pointcut):每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
接下来就用AOP来实现:
1.加jar包(主要是AspectJ框架)
* 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-aspects-4.0.0.RELEASE.jar
2.xml文件配置
* 2.1 在配置文件中配置自动扫描的包: <context:component-scan base-package="com.atguigu.spring.aop"></context:component-scan>
* 2.2 加入使 AspjectJ 注解起作用的配置: <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
3.创建切面类:LoggingAspect
package com.csu.aop.helloworld;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Aspect
@Component
//切面类
public class LoggingAspect {
//拼错单词execution 也是够了
@Before("execution(public int com.csu.aop.helloworld.Caculator.*(int, int))")
public void beforeMethod(JoinPoint joinpoint)
{//横切关注点
String methodName=joinpoint.getSignature().getName();
List<Object> args=Arrays.asList(joinpoint.getArgs());
System.out.println(methodName+" Method begin:"+args);
}
}
配置切面
* 1 切面必须是 IOC 中的 bean: 实际添加了 @Component 注解
* 2 声明是一个切面: 添加 @Aspect
* 3 声明通知: 即额外加入功能对应的方法.
* 3.1 前置通知: @Before(“execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(int, int))”)
* @Before 表示在目标方法执行之前执行 @Before 标记的方法的方法体.
* @Before 里面的是切入点表达式:
*
4. 在通知中访问连接细节: 可以在通知方法中添加 JoinPoint 类型的参数, 从中可以访问到方法的签名和方法的参数.
控制台打印出:
add Method begin:[1, 2]
result->3
出了在方法执行前的前置通知,还有后置通知(@After),返回通知(@AfterReturing),异常通知(@AfterThrowing):
package com.csu.aop.helloworld;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Aspect
@Component
//切面类
public class LoggingAspect {
/**
* 定义一个方法, 用于声明切入点表达式. 一般地, 该方法中再不需要添入其他的代码.
* 使用 @Pointcut 来声明切入点表达式.
* 后面的其他通知直接使用方法名来引用当前的切入点表达式.
*/
@Pointcut("execution(public int com.csu.aop.helloworld.Caculator.*(..))")
public void declareJointPointExpression(){}
//拼错单词 也是够了
@Before("execution(public int com.csu.aop.helloworld.Caculator.*(int, int))")
//前置通知
/**
* 在 com.csu.aop.helloworld.Caculator 接口的每一个实现类的每一个方法开始之前执行一段代码
*/
public void beforeMethod(JoinPoint joinpoint)
{//横切关注点
String methodName=joinpoint.getSignature().getName();
List<Object> args=Arrays.asList(joinpoint.getArgs());
System.out.println(methodName+" Method begin:"+args);
}
//后置通知
/**
* 无论方法有没有异常都会执行
* 取不到方法执行的结果
*/
@After("execution(public int com.csu.aop.helloworld.Caculator.*(int, int))")
public void afterMethod(JoinPoint joinpoint) {
String methodName=joinpoint.getSignature().getName();
List<Object> args=Arrays.asList(joinpoint.getArgs());
System.out.println(methodName+" Method end:"+args);
}
/**
* 在方法法正常结束受执行的代码
* 返回通知是可以访问到方法的返回值的!
*/
@AfterReturning(value="declareJointPointExpression()",
returning="result")
public void afterReturning(JoinPoint joinpoint,Object result) {
String methodName=joinpoint.getSignature().getName();
List<Object> args=Arrays.asList(joinpoint.getArgs());
System.out.println(methodName+" Method afterreturning:"+result);
}
/**
* 在目标方法出现异常时会执行的代码.
* 可以访问到异常对象; 且可以指定在出现特定异常时在执行通知代码
*/
@AfterThrowing(value="declareJointPointExpression()",
throwing="ex")
public void afterThrowing(JoinPoint joinpoint,ArithmeticException ex) {
String methodName=joinpoint.getSignature().getName();
List<Object> args=Arrays.asList(joinpoint.getArgs());
System.out.println(methodName+" Method afterThrowing:"+ex);
}
}
切面的优先级问题:
如果同时有多个切面作用于相同的目标,那么用@Order(x)注解来解决谁先执行的问题,x越小,优先级越高:
@Order(1)
@Aspect
@Component