• Java 动态代理与AOP


     动态代理与AOP

    代理模式 

    代理模式给某一个目标对象(target)提供代理对象(proxy),并由代理对象控制对target对象的引用。

    模式图:

    代理模式中的角色有:

    • 抽象对象角色(AbstractObject):声明了目标对象和代理对象的共同接口,这样依赖在任何可以使用目标对象的地方都可以使用代理对象。
    • 目标对象角色(RealObject):定义了代理对象所代表的目标对象。
    • 代理对象角色(ProxyObject):代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或者之后,执行某个操作,而不是单纯的将调用传递给目标对象。
    示例:
    抽象对象角色
    public abstract class AbstractObject {
        /**
         * 定义操作
         */
        public abstract void operation();
    }

    目标对象角色

    public class RealObject extends AbstractObject {
        public void operation() {
            System.out.println("Do Something!");
        }
    }

    代理对象角色

    public class ProxyObject extends AbstractObject {
        RealObject realObject = new RealObject();
        public void operation() {
            //在调用目标对象之前,完成一些操作
            System.out.println("Before Do Something");
            realObject.operation();
            //在调用目标对象之后,完成一些操作
            System.out.println("After Do Something");
        }
    }
    客户端
    public class Client {
        public static void main(String[] args) {
            AbstractObject abstractObject = new ProxyObject();
            abstractObject.operation();
        }
    }
     
     
     
     

    按照代理类的创建时期,可分为静态代理和动态代理:

    • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。
    • 动态:在程序运行时运用反射机制动态创建而成。

    静态代理

     如下面的例子:

     1 public interface Flyable {
     2     void fly(long ms);
     3 }
     4 
     5 
     6 public class Bird implements Flyable {
     7 
     8     @Override
     9     public void fly(long ms) {
    10         System.out.println("bird is flying!");
    11         try {
    12             Thread.sleep(ms);
    13         } catch (Exception e) {
    14 
    15         }
    16     }
    17 }
    18 
    19 public class Kite implements Flyable {
    20 
    21     @Override
    22     public void fly(long ms) {
    23         System.out.println("kite is flying!");
    24         try {
    25             Thread.sleep(ms);
    26         } catch (Exception e) {
    27 
    28         }
    29     }
    30 }
    31 
    32 public class StaticProxy implements Flyable {
    33     private Flyable flyable;
    34 
    35     public StaticProxy(Flyable flyable) {
    36         this.flyable = flyable;
    37     }
    38 
    39     @Override
    40     public void fly(long ms) {
    41         try {
    42             System.out.println("before invoke ");
    43             long begin = System.currentTimeMillis();
    44             flyable.fly(ms);
    45             long end = System.currentTimeMillis();
    46             System.out.println("after invoke elpased " + (end - begin));
    47         } catch (Exception e) {
    48             e.printStackTrace();
    49             System.out.println("invoke failed!");
    50             throw e;
    51         }
    52     }
    53 }
    54 public static void main(String[] args) {
    55     StaticProxy staticProxyBird = new StaticProxy(new Bird()); 
    56     staticProxyBird.fly(100); 
    57 
    58     StaticProxy staticProxyKite = new StaticProxy(new Kite()); 
    59     staticProxyKite.fly(200);
    60 }

    可见,静态代理可以做到在不修改目标对象的前提下,拓展目标对象的功能。但静态代理有2个缺点:

    1)代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

    2)代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。如上的代码是只为Flyable类的访问提供了代理,但是如果还要为其他类提供代理的话,就需要我们再次添加代理类。

    JDK代理

    在动态代理中,Proxy代理类在编译期是不存在的,而是在程序运行时被动态生成的,因为有了反射,可以根据传入的参数,生成你想要的代理(如你想代理A就代理A,想代理B就代理B),实现原理就是在生成Proxy的时候你需要传入被代理类的所有接口(如果没有接口是另一种方式,下文会提),反射机制会根据你传入的所有接口,帮你生成一个也实现这些接口的代理类出来。之后,代理对象每调用一个方法,都会把这个请求转交给InvocationHandler来执行,而在InvocationHandler里则通过反射机制,继续转发请求给真正的目标对象,最后由目标对象来返回结果。

    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强。

     1 public class DynamicProxy implements InvocationHandler {
     2 
     3     private Object targetObject;
     4 
     5     public Object newProxyInstance(Object targetObject) {
     6         this.targetObject = targetObject;
     7         return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
     8     }
     9 
    10     @Override
    11     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    12         try {
    13             System.out.println("before invoke ");
    14             long begin = System.currentTimeMillis();
    15             proxy = method.invoke(targetObject, args);
    16             long end = System.currentTimeMillis();
    17             System.out.println("after invoke elpased " + (end - begin));
    18         } catch (Exception e) {
    19             e.printStackTrace();
    20             System.out.println("invoke failed!");
    21             throw e;
    22         }
    23 
    24         return proxy;
    25     }
    26 }
    27 
    28 public static void main() {
    29 
    30     DynamicProxy dynamicProxy = new DynamicProxy();
    31     Flyable bird = (Flyable) dynamicProxy.newProxyInstance(new Bird());
    32     bird.fly(100);
    33 
    34     Flyable kite = (Flyable) dynamicProxy.newProxyInstance(new Kite());
    35     kite.fly(200);
    36 }

    上面的例子中,动态代理除了接受Flyable类型的目标对象,还可以接受任何其他类型的对象;也不管目标对象实现的接口有多少方法,都可以被代理。

    从上面的代码可以看出,动态代理对象不需要实现目标对象接口,但是目标对象一定要实现接口,否则不能使用动态代理。 

    CGLIB代理

    上面的静态代理和JDK代理模式都需要目标对象是一个实现了接口的目标对象,但是有的时候,目标对象可能只是一个单独的对象,并没有实现任何的接口,这个时候,我们就可以使用目标对象子类的方式实现代理,这种代理方式就是:Cglib代理,也叫做子类代理,它是在内存中构件一个子类对象,从而实现对目标对象的功能拓展。

    Cglib是强大的高性能的代码生成包,它可以在运行期间拓展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)。

    Cglib包的底层是通过使用一个小而快的字节码处理框架ASM来转换字节码并生成新的类,不鼓励直接只使用ASM,因为它要求你必须对JVM内部结构,包括class文件的格式和指令集都很熟悉。

     1 public class Plane {
     2 
     3     public void fly(long ms) {
     4         System.out.println("plane is flying!");
     5         try {
     6             Thread.sleep(ms);
     7         } catch (Exception e) {
     8 
     9         }
    10     }
    11 }
    12 
    13 public class CglibProxy implements MethodInterceptor {
    14     private Object target;
    15 
    16     public CglibProxy(Object target) {
    17         this.target = target;
    18     }
    19 
    20     public Object getProxyInstance() {
    21         //1. 实例化工具类
    22         Enhancer en = new Enhancer();
    23         //2. 设置父类对象
    24         en.setSuperclass(this.target.getClass());
    25         //3. 设置回调函数
    26         en.setCallback(this);
    27         //4. 创建子类,也就是代理对象
    28         return en.create();
    29     }
    30 
    31     public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    32         System.out.println("before invoke ");
    33         long begin = System.currentTimeMillis();
    34 
    35         //执行目标对象的方法
    36         Object returnValue = method.invoke(target, objects);
    37 
    38         long end = System.currentTimeMillis();
    39         System.out.println("after invoke elpased " + (end - begin));
    40 
    41         return returnValue;
    42     }
    43 
    44 }
    45 
    46 public static void main() {
    47     CglibProxy cglibProxy = new CglibProxy(new Plane()); 
    48     Plane plane = (Plane) cglibProxy.getProxyInstance();             
    49     plane.fly(150);
    50 }

    AOP

    AOP(Aspect Oriented Programming,面向切面编程),像日志、安全、缓存、事务 等与业务逻辑分离的功能,可能会散布于各个业务bean,这样的称为 横切关注点(cross-cutting concern)。AOP有助于横切关注点与它们所影响的对象之间解耦。

     

    • 切面(Aspect):通知+切点,即它是什么,在何时、何处完成其功能;
    • 切点(Pointcut):匹配通知所要织入的一个或多个连接点(where),通常使用明确的(或正则匹配的)类和方法名称定义切点。
    1. 静态方法切点,
    2. 动态方法切点,
    3. 注解切点,
    4. 表达式切点,
    5. 流程切点,
    6. 复合切点,
    • 通知(Advice):定义了切面的工作和时机,也就是要做什么(what),什么时候做(when)。
    1. 前置通知(@Before):在目标方法被调用之前调用通知;
    2. 后置通知(@After):在目标方法被调用之后调用通知;
    3. 返回通知(@After-returning):在目标方法成功执行之后调用通知;
    4. 异常通知(@After-throwing):在目标方法抛出异常后调用通知;
    5. 环绕通知(@Around):目标方法调用之前、之后执行自定义的行为;
     
    • 连接点(joint point):允许使用通知的地方,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。
    • 引入(Introduction):向现有的类添加新方法或属性;
    • 织入(Weaving):把切面应用到目标对象,并创建新的代理对象的过程。

    AOP的实现原理是基于动态代理。在Spring的AOP编程中:

    • 如果加入容器的目标对象有实现接口,就使用JDK代理
    • 如果目标对象没有实现接口,就使用Cglib代理。

    AOP除了有Spring AOP实现外,还有著名的AOP实现者:AspectJ。

    • AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件;
    • Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器的;

    Spring AOP

    Spring在新版本中对AOP功能进行了增强,体现在这么几个方面:

    • 在XML配置文件中为AOP提供了aop命名空间
    • 增加了AspectJ切点表达式语言的支持
    • 可以无缝地集成AspectJ

    先看一个例子, 如何使用 引介切面(Introduction Advisor)为一个现有对象添加任何接口的实现:

    public interface Waiter {
    
        // 向客人打招呼
        void greetTo(String clientName);
    
        // 服务
        void serveTo(String clientName);
    }
    
    public class NaiveWaiter implements Waiter {
    
        public void greetTo(String clientName) {
            System.out.println("NaiveWaiter:greet to " + clientName + "...");
        }
    
        public void serveTo(String clientName) {
            System.out.println("NaiveWaiter:serving " + clientName + "...");
        }
    }
    public interface Seller { // 卖东西 int sell(String goods, String clientName); } public class SmartSeller implements Seller { // 卖东西 public int sell(String goods, String clientName) { System.out.println("SmartSeller: sell " + goods + " to " + clientName + "..."); return 100; } }

    如上示例代码,有一个服务员的接口,还有一个售货员的接口,现在想做的就是:想这个服务员可以充当售货员的角色,可以卖东西!

    我们的引介切面具体是这样干的:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.DeclareParents;
    
    @Aspect
    public class EnableSellerAspect {
    
        @DeclareParents(value = "com.example.demo.NaiveWaiter",  // 切点(目标类)
                defaultImpl = SmartSeller.class)                 // 增强类
    
        public Seller seller;                                   // 增强类接口
    }

    切面技术将SmartSeller融合到NaiveWaiter中,这样NaiveWaiter就实现了Seller接口!!!

    PS:上面使用@Aspect注解需要引入如下依赖

         <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>1.9.4</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.4</version>
            </dependency>
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>3.3.0</version>
            </dependency>

    beans.xml 也比较简单

    <?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"
    > <aop:aspectj-autoproxy/> <bean id="waiter" class="com.example.demo.NaiveWaiter"/> <bean class="com.example.demo.EnableSellerAspect"/> </beans>

    测试一下:

    public class Test {
        public static void main(String[] args) {
    
            ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
            Waiter waiter = (Waiter) ctx.getBean("waiter");
    
            // 调用服务员原有的方法
            waiter.greetTo("Java3y");
            waiter.serveTo("Java3y");
    
            // 通过引介/引入切面已经将waiter服务员实现了Seller接口,所以可以强制转换
            Seller seller = (Seller) waiter;
            seller.sell("水军", "Java3y");
    
        }
    }

    当引入接口方法被调用时,代理对象会把此调用委托给实现了新接口的某个其他对象。实际上,一个Bean的实现被拆分到多个类中

     引介切面用代理的方式为某个对象实现接口,从而能够使用该接口下的方法。这种方式是非侵入式的。

    上面是使用注解方式,再看看下使用XML配置的方式,例如:

       <bean id="testBeforeAdvice" class="com.example.demo.TestBeforeAdvice"/>
        <bean id="waiter" class="com.example.demo.NaiveWaiter"/>
        <bean class="com.example.demo.EnableSellerAspect"/>
        <aop:config proxy-target-class="true">
            <aop:advisor advice-ref="testBeforeAdvice" pointcut="execution(* com..*.Waiter.greetTo(..))"/>
        </aop:config>

    前置增强方法实现

    public class TestBeforeAdvice implements MethodBeforeAdvice {
    
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("before prepared...");
            System.out.println("args[0]: " + args[0]);
            System.out.println("before finish.");
        }
    }

    如此,当调用 waiter.greetTo("Java3y") 方法时,会先调用before通知。

    更多AOP配置元素:

    配置元素   用途
    <aop:advisor> 定义AOP通知器
    <aop:after> 定义AOP后置通知
    <aop:after-returning> 定义AOP返回通知
    <aop:after-throwing> 定义AOP异常通知
    <aop:around> 定义AOP环绕通知
    <aop:aspect> 定义一个切面
    <aop:before> 定义AOP前置通知
    <aop:config> 顶层AOP配置元素,大多数的<aop:*>元素必须包含在<aop:config>元素内
    <aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
    <aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口
    <aop:pointcut> 定义一个切点

  • 相关阅读:
    关于类的继承与初始化顺序
    大数据协作框架
    关于委托和线程
    Hive高级
    聚集索引和非聚集索引
    Hadoop生态优秀文章集锦
    hive深入使用
    hive常规配置及常用命令使用
    HBase核心技术点
    zipkin环境搭建
  • 原文地址:https://www.cnblogs.com/chenny7/p/11201010.html
Copyright © 2020-2023  润新知