为什么要使用AOP
问题:
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其它多个关注点。
代码分散:以日志需求为例,只是为了满足这个单一的需求,就不得不在多个模块里重复相同的日志代码。如果日志需求发生变化必须修改所有的模块。
使用动态代理解决上述问题:
代理设计模式的原理:使用一个代理将对象包装起来,然后调用该代理对象取代原始对象。任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调到原始对象上。
举例说明:
比如我们要写一个计算器相关的功能。首先我们需要创建一个接口。
public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
之后我们写在实现类
public class ArithmaticCalculatorLoggingImpl implements ArithmeticCalculator {
@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;
}
}
此时我们需要加入日志的功能。在每个计算方法前后加上日志。此时我们是这样写的。
public class ArithmaticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
System.out.println("The method add begins with " + i + ", " + j);
int result = i + j;
System.out.println("The method add ends with " + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("The method sub begins with " + i + ", " + j);
int result = i - j;
System.out.println("The method sub ends with " + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("The method mul begins with " + i + ", " + j);
int result = i * j;
System.out.println("The method mul ends with " + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("The method div begins with " + i + ", " + j);
int result = i / j;
System.out.println("The method div ends with " + result);
return result;
}
}
可以看到这样写会很繁琐。如果有一天我们不需要日志记录的功能时,需要一个一个的删除。此时AOP的作用就体现出来了。
这里我们需要把日志的功能抽象出来作为一个代理类。
public class ArithmeticCalculatorLoggingProperty {
private ArithmeticCalculator target;
public ArithmeticCalculator getLoggingProxy(){
ArithmeticCalculator proxy = null;
//代理对象由那一个类加载器负责加载
ClassLoader loader = target.getClass().getClassLoader();
//代理对象的类型,即其中有哪些方法
Class [] interfaces = new Class[]{ArithmeticCalculator.class};
//调用代理对象中的方法时,该执行的代码
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Invoke...");
return 0;
}
};
proxy = (ArithmeticCalculator)Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}
之后我们按照需求来改写代理类。
public ArithmeticCalculator getLoggingProxy(){
ArithmeticCalculator proxy = null;
ClassLoader loader = target.getClass().getClassLoader();
Class [] interfaces = new Class[]{ArithmeticCalculator.class};
InvocationHandler h = new InvocationHandler() {
@Override
/**
* @param
* proxy: 正在返回的代理对象,一般情况下在invoke方法中不使用该对象。
* method: 正在被调用的方法。
* args: 调用方法时传入的参数。
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//日志
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
//执行方法
Object result = method.invoke(target, args);
System.out.println("The method " + methodName + " ends with " + result);
return result;
}
};
proxy = (ArithmeticCalculator)Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
之后写测试类
public static void main(String[] args) {
ArithmeticCalculator calculator = new ArithmaticCalculatorLoggingImpl();
ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProperty(calculator).getLoggingProxy();
System.out.println("Add");
System.out.println(proxy.add(1, 2));
System.out.println("Subtract");
System.out.println(proxy.sub(2, 1));
}
此时,我们如果要更改日志只需要修改一个地方不需要更改多个地方。
AOP简介
AOP(Aspect-oriented programming)面向切面编程,是对OOP(Object-oriented programming)的补充。
AOP的主要编程对象是切面(Aspect),而切面模块化横切关注点。
在应用AOP编程时,仍然需要定义公共功能。但可以明确的定义这个功能在哪里,以什么方式使用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里。
AOP的好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级。
- 业务模块更简洁,只包含核心业务代码。
如何使用AOP处理上述业务
AOP相关术语
切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。
通知(Advice):切面必须要完成的工作。
目标(Target):被通知的对象。
代理(Proxy):向目标对象应用通知之后创建的对象。
连接点(jointpoint):程序执行的某个特定位置。如类某个方法调用前,调用后,方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点。相对点表示的方位。例如ArithmeticCalculator的add方法执行前的连接点ArithmeticCalculator的add方法之前的位置。
切点(pointcut):每个类都拥有多个连接点。例如ArithmeticCalculator的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比,连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一一对应的关系,一个切点匹配多个连接点。切点通过org.springframework.app.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
Spring AOP
Aspect J:Java社区里最完整最流行的AOP框架。在Spring 2.0以上版本中,可以使用基于Aspect J注解或基于XML配置的AOP。
使用Spring AOP的过程:
- Spring的AOP需要在配置文件中加入aop的命名空间
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
- 基于注解的方式,我们需要在配置文件中加入如下配置。它会自动匹配Aspect注解标注的类生成代理对象。
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
把横切关注点的代码抽象到切面的类中。
- 切面首先是一个IOC中的bean,即加入@Component注解。
- 切面还需要加上@Aspect注解。
- 在类中声明各种通知。
AspectJ支持五种通知类型:
- @Before: 前置通知,在方法执行前执行。
- @After:后置通知,在方法执行后执行。
- @AfterReturning:返回通知,在方法返回结果之后执行。
- @AfterThrowing:异常通知,在方法抛出异常之后执行。
- @Around:环绕通知,围绕着方法执行。
@Component
@Aspect
public class LoggingAspect {
//声明该方法是一个前置通知
@Before("execution(public int com.spring.impl.ArithmaticCalculatorLoggingImpl.*(int, int))")
public void beforeMethod(){
System.out.println("The method " + methodName + " begins with args " + args);
}
}
- 可以在通知方法中声明一个类型为JointPoint的参数,然后就能访问链接细节,如方法名称和参数值。
@Before("execution(public int com.spring.impl.ArithmaticCalculatorLoggingImpl.*(int, int))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object>args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " begins with args " + args);
}
后置通知
在目标方法执行之后(无论是否发生异常)执行的通知。
示例:
@After("execution(public int com.spring.impl.ArithmaticCalculatorLoggingImpl.*(int, int))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
注意:在后置通知中不能访问目标方法执行的结果。
网友评论