Spring-AOP

Posted jerzy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring-AOP相关的知识,希望对你有一定的参考价值。

概念

AOP是什么

AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)

但是OOP有它的一些弊端,比如当我们需要为多个不具有继承关系的对象引入一个公共行为,例如日志,权限验证,事务等功能时,只能在在每个对象里引用公共行为,这样做不便于维护,而且有大量重复代码。AOP的出现弥补了OOP的这点不足。AOP提供在不改变原代码的前提下对方法进行功能增强 .

AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。

①选择spring的AOP还是AspectJ?

spring确实有自己的AOP。功能已经基本够用了,除非你的要在接口上动态代理或者方法拦截精确到getter和setter。这些都是写奇葩的需求,一般不使用。

②在使用AOP的时候,你是用xml还是注解的方式(@Aspect)?

1)如果使用xml方式,不需要任何额外的jar包。

2)如果使用@Aspect方式,你就可以在类上直接一个@Aspect就搞定,不用费事在xml里配了。但是这需要额外的jar包( aspectjweaver.jar)。因为spring直接使用AspectJ的注解功能,注意只是使用了它的注解功能而已,并不是核心功能 。

aspect 切面

aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

  1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
  2. 如何在 advice 中编写切面代码.

可以简单地认为, 使用 @Aspect 注解的类就是切面.

advice 通知

由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.
许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.
例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.

advisor 通知器

其实就是point cut和advice的结合 。

join point 连接点

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.

a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

point cut 切点

匹配 join point 的谓词(a predicate that matches join points).
Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.

join point 和 point cut 的区别

在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice

introduction

为一个类型添加额外的方法或字段. Spring AOP 允许我们为 目标对象 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现.

target 目标对象

织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

AOP proxy

一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类.
在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象.

weaving 织入

将 aspect 和其他对象连接起来, 并创建 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:

  • 编译器织入, 这要求有特殊的Java编译器.
  • 类装载期织入, 这需要有特殊的类装载器.
  • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
    Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

关于 aspect, join point, point cut, advice 关系的比喻

让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.

来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.

  • join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
  • point cut --> 男性, 身高约七尺五寸: pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
  • advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸爪哇的小县城里的百姓.
  • aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: "根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问" 这一整个动作可以被认为是一个 aspect.

代理模式

为其他对象提供一种代理以控制对这个对象的访问

比如A对象要做一件事情,在没有代理前,自己来做,在对A代理后,由A的代理类B来做。代理其实是在原实例前后加了一层处理,这也是AOP的初级轮廓。

静态代理

//接口
public interface IUserDao {
     void save();
     void find();
}
//目标对象
public class UserDaoImpl implements IUserDao {
    @Override
     public void save() {
       System.out.println("模拟:保存用户!");
     }
     @Override
     public void find() {
       System.out.println("模拟:查询用户");
     }
}
/**
 * 静态代理 
 * 特点: 
 * 1. 目标对象必须要实现接口 
 * 2. 代理对象,要实现与目标对象一样的接口
 */
class UserDaoProxy implements IUserDao {
    // 代理对象,需要维护一个目标对象
    private IUserDao target = new UserDaoImpl();

    @Override
    public void save() {
        System.out.println("代理操作: 开启事务...");
        target.save(); // 执行目标对象的方法
        System.out.println("代理操作:提交事务...");
    }

    @Override
    public void find() {
        target.find();
    }
}

测试结果:

技术分享图片

静态代理虽然保证了业务类只需关注逻辑本身,代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理。再者,如果增加一个方法,除了实现类需要实现这个方法外,所有的代理类也要实现此方法。增加了代码的维护成本。那么要如何解决呢?答案是使用动态代理。

动态代理

动态代理类的源码是在程序运行期间通过JVM反射等机制动态生成(动态编译),代理类和委托类的关系是运行时才确定的。

原理:

  1. 动态编译 JavaCompiler.CompilationTask
  2. 反射被代理类 使用Method.invoke(Object o,Object... args);对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。  
  3. 类的加载 URLClassLoader可以加载硬盘任意位置的.java文件。class.getClassLoader只能加载classPath目录下的类。

这里的 接口 和 目标对象 延用上面静态代理的代码

写法一 匿名内部类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* 动态代理:
*    代理工厂,给多个目标对象生成代理对象!
*/
class ProxyFactory {
 // 接收一个目标对象
 private Object target;
 public ProxyFactory(Object target) {
   this.target = target;
 }
 // 返回对目标对象(target)代理后的对象(proxy)
 public Object getProxyInstance() {
   Object proxy = Proxy.newProxyInstance(
     target.getClass().getClassLoader(),  // 目标对象使用的类加载器
     target.getClass().getInterfaces(),   // 目标对象实现的所有接口
     new InvocationHandler() {      //匿名内部类-- 执行代理对象方法时候触发
   
       @Override
       public Object invoke(Object proxy, Method method, Object[] args)
           throws Throwable {
           
           //可以获取当前执行的方法的方法名,针对不同方法采取不同事务
           //String methodName = method.getName();
           
           System.out.println("开启事务...");
           // 执行目标对象方法
           Object result = method.invoke(target, args);
           System.out.println("提交事务...");         
         return result;
       }
     }
   );
   return proxy;
 }
}
public class Test {
    public static void main(String[] args) {
        //创建目标对象
        IUserDao target = new UserDaoImpl();
        System.out.println("目标对象:"+target.getClass());
        //创建代理对象
        IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
        System.out.println("代理对象:"+proxy.getClass());
        //执行代理对象方法
        proxy.save();
    }
}

写法二 实现接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class MyInvocationHandler implements InvocationHandler {

    private Object target;
    public Object bind(Object target) {
        //绑定委托对象
        this.target = target;
        //返回代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    /**
     * proxy : 表示代理对象
     * method : 表示真实要执行的方法对象
     * args : 表示方法在执行的时候需要传入的参数 
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开始事务..");
        Object result = method.invoke(target, args);
        System.out.println("提交事务..");
        return result;
    }

}
public class Test {
    public static void main(String[] args) {

        IUserDao target = new UserDaoImpl();
        System.out.println("目标对象:"+target.getClass());
        IUserDao handler = (IUserDao) new MyInvocationHandler().bind(target);
         System.out.println("代理对象:"+handler.getClass());
        handler.save();
    }
}

测试结果:

技术分享图片

在运行测试类中创建测试类对象代码中

IUserDao proxy = (IUserDao)new ProxyFactory(target).getProxyInstance();

其实是JDK动态生成了一个类去实现接口,隐藏了这个过程:

class $jdkProxy implements IUserDao{}

使用jdk生成的动态代理的前提是目标类必须有实现的接口。但这里又引入一个问题,如果某个类没有实现接口,就不能使用JDK动态代理,所以CGLIB代理就是解决这个问题的。

CGLIB是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类,如下:

public class UserDaoImpl{}
//Cglib是以动态生成的子类继承目标的方式实现,程序执行时,隐藏了下面的过程
public class $Cglib_Proxy_class  extends UserDaoImpl{}

Cglib使用的前提是目标类不能为final修饰。因为final修饰的类不能被继承。

现在,我们可以看看AOP的定义:面向切面编程,核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。

通过定义和前面代码我们可以发现3点:

1.AOP是基于动态代理模式。

2.AOP是方法级别的(要测试的方法不能为static修饰,因为接口中不能存在静态方法,编译就会报错)。

3.AOP可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。

技术分享图片

Spring AOP

前文提到JDK代理和Cglib代理两种动态代理,优秀的Spring框架把两种方式在底层都集成了进去,我们无需担心自己去实现动态生成代理。那么,Spring是如何生成代理对象的?:

1.创建容器对象的时候,根据切入点表达式拦截的类,生成代理对象。

2.如果目标对象有实现接口,使用jdk代理。如果目标对象没有实现接口,则使用Cglib代理。

然后从容器获取代理后的对象,在运行期植入"切面"类的方法。通过查看Spring源码,我们在DefaultAopProxyFactory类中,找到这样一段话。

技术分享图片

Spring AOP综合两种代理方式的使用前提有会如下结论:如果目标类没有实现接口,且class为final修饰的,则不能进行Spring AOP编程!

实现

1.注解方式

  • @Aspect

2.命名空间方式

  • aop:advisor+aop:pointcut
  • aop:aspect+aop:pointcut

作用

1.Spring声明式事务管理配置。

2.Controller层的参数校验。

3.使用Spring AOP实现mysql数据库读写分离案例分析

4.在执行方法前,判断是否具有权限。

5.对部分函数的调用进行日志记录。监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员。

6.信息过滤,页面转发等等功能

参考文献

http://www.cnblogs.com/solverpeng/p/5628100.html

https://segmentfault.com/a/1190000007469968

https://github.com/ameizi/DevArticles/issues/152

http://www.importnew.com/24305.html

http://blog.jobbole.com/28791/

https://mp.weixin.qq.com/s/E4orrW90GedXoKKgw-yVEQ

http://www.cnblogs.com/stay-9527/p/3689266.html

https://www.jianshu.com/p/9a61af393e41?from=timeline&isappinstalled=0

以上是关于Spring-AOP的主要内容,如果未能解决你的问题,请参考以下文章

Spring-AOP

spring-AOP原理

spring-AOP(面向切面编程)

Spring-AOP

spring-AOP概念

Spring-AOP