美文网首页
JDK动态代理以及Spring AOP使用介绍

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

作者: SYFHEHE | 来源:发表于2017-10-08 23:45 被阅读0次

    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地址

    相关文章

      网友评论

          本文标题:JDK动态代理以及Spring AOP使用介绍

          本文链接:https://www.haomeiwen.com/subject/ioityxtx.html