JDK动态代理以及Spring AOP使用介绍

0.前言


本文主要想阐述的问题如下:

  • 什么动态代理(AOP)以及如何用JDK的Proxy和InvocationHandler实现自己的代理?
  • 什么是Spring动态代理(AOP)?
  • Spring AOP注解实现

1.动态代理(AOP)

1.1 AOP

  • 什么是AOP?
    AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
  • 为什么需要用AOP?
    OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
    AOP技术利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
  • 什么是切面(Aspect)?
    所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
  • 使用切面(Aspect)技术有什么好处?
    使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

1.2 代理模式

代理模式是AOP的基础,也是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。
使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能。


代理模式

如上图所示:
1.委托对象和代理对象都共同实现的了同一个接口。
2.委托对象中存在的方法在代理对象中也同样存在。

代理模式分为两种:

  • 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。
  • 动态代理:代理类是在运行时生成的,也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。

1.2 静态代理实现

//客户端
public class Client {
  public static void main(String args[]) {
      Target subject = new Target();
      Proxy p = new Proxy(subject);
      p.request();
  }
}
//委托对象和代理对象都共同实现的接口
interface Interface {
  void request();
}

//委托类
class Target implements Interface {
  public void request() {
      System.out.println("request");
  }
}

//代理类
class Proxy implements Interface {
  private Interface subject;

  public Proxy(Interface subject) {
      this.subject = subject;
  }

  public void request() {
      System.out.println("PreProcess");
      subject.request();
      System.out.println("PostProcess");
  }
}

1.3 Java 实现动态代理

Java实现动态代理的大致步骤如下:

    1. 定义一个委托类和公共接口
//公共接口
public interface IHello {
  void sayHello();
}

//委托类
class Hello implements IHello {
  public void sayHello() {
      System.out.println("Hello world!!");
  }
}
    1. 通过实现InvocationHandler接口来自定义自己的InvocationHandler,指定运行时将生成的代理类需要完成的具体任务
//自定义InvocationHandler
public class HWInvocationHandler implements InvocationHandler {
  // 目标对象
  private Object target;

  public HWInvocationHandler(Object target) {
      this.target = target;
  }

  public Object invoke(Object proxy, Method method, Object[] args) >throws Throwable {
      System.out.println("------插入前置通知代码-------------");
      // 执行相应的目标方法
      Object rs = method.invoke(target, args);
      System.out.println("------插入后置处理代码-------------");
      return rs;
  }
}
    1. 生成代理对象,这个可以分为四步:
      (1)通过Proxy.getProxyClass获得动态代理类
      (2)通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class)
      (3)通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入
      (4)通过代理对象调用目标方法
public class Client {
  public static void main(String[] args)
          throws NoSuchMethodException, IllegalAccessException, >InvocationTargetException, InstantiationException {
      // 生成Proxy的class文件
      System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
      // 获取动态代理类
      Class<?> proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(), IHello.class);
      // 获得代理类的构造函数,并传入参数类型InvocationHandler.class
      Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
      // 通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入
      IHello iHello = (IHello) constructor.newInstance(new  HWInvocationHandler(new Hello()));
      // 通过代理对象调用目标方法
      iHello.sayHello();
  }
}

Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h),如下例:

public class Client2 {
  public static void main(String[] args) throws NoSuchMethodException, >IllegalAccessException, InvocationTargetException, InstantiationException {
         //生成$Proxy0的class文件
         System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
         IHello  ihello = (IHello) >Proxy.newProxyInstance(IHello.class.getClassLoader(),  //加载接口的类加载器
                 new Class[]{IHello.class},      //一组接口
                 new HWInvocationHandler(new Hello())); //自定义的>InvocationHandler
         ihello.sayHello();
     }
}

这个静态函数的第一个参数是类加载器对象(即哪个类加载器来加载这个代理类到 JVM 的方法区),第二个参数是接口(表明你这个代理类需要实现哪些接口),第三个参数是调用处理器类实例(指定代理类中具体要干什么)

以上就是对代理类如何生成,代理类方法如何被调用的分析!在很多框架都使用了动态代理如Spring,HDFS的RPC调用等等。

2.Spring动态代理

2.1 Spring AOP实现的原理

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spirng的AOP的动态代理实现机制有两种,分别是:

  • JDK动态代理:JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。这个在之前已经介绍过了。
  • CGLib动态代理:cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

2.2 如何选择的使用代理机制

  • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
  • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
  • 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

2.3 AOP基本概念

在写Spring AOP之前先简单介绍下几个概念:

  • 切面(Aspect) :通知和切入点共同组成了切面,时间、地点和要发生的“故事”。
  • 连接点(Joinpoint) :程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法被调用时、异常被抛出时等等。
  • 通知(Advice) :通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。
  • 切入点(Pointcut) :通知定义了切面要发生的“故事”和时间,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称。
  • 目标对象(Target Object) :即被通知的对象。
  • 织入(Weaving):把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
    1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才能做到,例如AspectJ的织入编译器;
    2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;
    3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理是使用了JDK的动态代理。

AOP通知类型:

  • @Before 前置通知(Before advice) :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。
  • @After 后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  • @AfterReturning 返回后通知(After return advice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况。
  • @Around 环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。
  • @AfterThrowing 抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。

3.Spring AOP注解实现

对于AOP编程,我们只需要做三件事:

  • 定义普通业务组件
  • 定义切入点,一个切入点可能横切多个业务组件
  • 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作

首先我们定义一个接口:它只完成增加用户的功能。

public interface UserDao {
  public void add(User user);
}

其次,我们定义一个接口实现类:它实现了用户的添加功能。

@Component("u")
public class UserDaoImpl implements UserDao {
  @Override
  public void add(User user) {
      System.out.println("add user!");
  }
}

然后,定义一个service类,他会调用UserDao的add方法

@Component
public class UserService {
  private UserDao userDao;

  public void add(User user) {
      userDao.add(user);
  }

  public UserDao getUserDao() {
      return userDao;
  }

  @Resource(name = "u")
  public void setUserDao(UserDao userDao) {
      this.userDao = userDao;
  }

}

定义一下横切关注点的类:我们这里列举了各种情况,在方法执行之前,之后,成功等等情况都有涉及

@Aspect
@Component
public class LogInterceptor {

// @Pointcut("execution(public * com.syf.dao.impl..*.*(..))")
  @Pointcut("execution(public * com.syf.service..*.add(..))")
  public void myMethod() {
  };

  // @Before("execution(public void
  // com.syf.dao.impl.UserDaoImpl.add(com.syf.model.User))")
  // @Before("execution(public * com.syf.dao.impl..*.*(..))")
  @Before("myMethod()")
  public void before() {
      System.out.println("method start");
  }

  // @After("execution(public * com.syf.dao.impl..*.*(..))")
  @After("myMethod()")
  public void after() {
      System.out.println("method end");
  }

  // @AfterReturning("execution(public * com.syf.dao.impl..*.*(..))")
  @AfterReturning("myMethod()")
  public void afterReturning() {
      System.out.println("method after returning");
  }
  
  @Around("myMethod()")
  public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
      System.out.println("around start method");
      pjp.proceed();
      System.out.println("around end method");
  }

}

Spring 的配置文件如下。通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:aop="http://www.springframework.org/schema/aop" 
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-4.3.xsd
      http://www.springframework.org/schema/aop
      http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
   <context:annotation-config></context:annotation-config>
  <context:component-scan base-package="com.syf">></context:component-scan>
  <aop:aspectj-autoproxy />
</beans>

编写测试类对其进行测试:

public class UserServiceTest {

  @Test
  public void testAdd() throws Exception{
      @SuppressWarnings("resource")
      ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
      UserService svc = (UserService) applicationContext.getBean("userService");
      User u = new User();
      u.setId(1);
      u.setName("name");
      svc.add(u);
  }

}

打印出的log证明,在add方法执行前后等情况下,切面均有被织入,Spring
AOP代理实现成功:

around start method
method start
add user!   //add 方法实现的内容
around end method
method end
method after returning

所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

4.代码

本文中所涉及的代码在github上都有,可以点击以下链接:
GIthub地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容