JavaRush /Java 博客 /Random-ZH /Java 中的动态代理

Java 中的动态代理

已在 Random-ZH 群组中发布
你好!今天我们将讨论一个相当重要且有趣的主题——在Java中创建动态代理类。这不太简单,所以让我们尝试通过示例来弄清楚:) 那么,最重要的问题是:什么是动态代理以及它们的用途是什么?代理类是原始类的一种“上层建筑”,它允许我们在必要时更改其行为。改变行为意味着什么?它是如何运作的?让我们看一个简单的例子。假设我们有一个接口Person和一个Man实现该接口的 简单类
public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("Меня зовут " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("Мне " + this.age + " years");
   }

   @Override
   public void sayFrom(String city, String country) {

       System.out.println("Я из города " + this.city + ", " + this.country);
   }

   //..геттеры, сеттеры, и т.д.
}
我们班Man有3种方法:自我介绍,说你的年龄,说你来自哪里。假设我们收到此类作为现成 JAR 库的一部分,并且不能简单地获取和重写其代码。然而,我们需要改变他的行为。例如,我们不知道将在我们的对象上调用哪个方法,因此我们希望该人在调用其中任何一个方法时首先说“Hello!”。(没有人喜欢不礼貌的人)。 动态代理 - 1遇到这样的情况我们该怎么办呢?我们需要一些东西:
  1. InvocationHandler

这是什么?可以直译为“调用拦截器”。这非常准确地描述了其目的。 InvocationHandler是一个特殊的接口,允许我们拦截对对象的任何方法调用并添加我们需要的附加行为。我们需要制作自己的拦截器——即创建一个类并实现这个接口。这很简单:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hello!");
       return null;
   }
}
我们只需要实现一个接口方法 - invoke()。事实上,它做了我们需要的事情——它拦截对我们对象的所有方法调用并添加必要的行为(这里我们invoke()在方法内的控制台上打印“Hello!”)。
  1. 原始对象及其代理。
让我们创建一个原始对象Man和它的“上层结构”(代理):
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());

   }
}
看起来很不简单啊!我专门为每一行代码写了一条注释:让我们仔细看看那里发生了什么。

在第一行中,我们只需创建要为其创建代理的原始对象。以下两行可能会让您感到困惑:
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
但这里确实没有什么特别的:)要创建代理,我们需要ClassLoader原始对象的(类加载器)和原始类(即)实现的所有接口的列表Man。如果您不知道它是什么ClassLoader,您可以阅读这篇关于将类加载到 JVM 的文章或Habré 上的这篇文章,但先不要太在意它。请记住,我们将获得一些额外的信息,然后我们将需要这些信息来创建代理对象。在第四行,我们使用一个特殊的类Proxy及其静态方法newProxyInstance()
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
这个方法只是创建我们的代理对象。我们向该方法传递有关我们在上一步中收到的原始类的信息(它ClassLoader及其接口列表),以及我们之前创建的拦截器的对象 - InvocationHandler'a. 最重要的是不要忘记将我们的原始对象传递给拦截器vasia,否则它将没有任何东西可以“拦截”:)我们最终得到了什么?我们现在有了一个代理对象vasiaProxy。它可以调用任何接口方法Person。为什么?因为我们向它传递了所有接口的列表 - 这里:
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();

//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
现在他已经“认识”了所有的接口方法Person。此外,我们还向代理传递了一个配置PersonInvocationHandler为与该对象一起使用的对象vasia
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
现在,如果我们调用代理对象上的任何接口方法Person,我们的拦截器将“捕获”此调用并执行其自己的方法invoke()。让我们尝试运行该方法main()控制台输出: 你好! 伟大的!我们看到我们的方法不是真正的方法,而是Person.introduce()被调用: invoke()PersonInvocationHandler()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hello!");
   return null;
}
控制台显示“Hello!” 但这并不是我们想要得到的行为:/ 根据我们的想法,应该首先显示“Hello!”,然后我们调用的方法本身应该起作用。换句话说,这个方法调用:
proxyVasia.introduce(vasia.getName());
应该输出到控制台“Hello!我的名字是瓦夏”,而不仅仅是“你好!” 我们怎样才能做到这一点?没什么复杂的:你只需要稍微修改一下我们的拦截器和方法invoke():)注意传递给这个方法的参数:
public Object invoke(Object proxy, Method method, Object[] args)
方法invoke()可以访问它所调用的方法及其所有参数(方法方法、Object[] args)。换句话说,如果我们调用一个方法,并且调用的proxyVasia.introduce(vasia.getName())不是方法,而是方法,那么在该方法中我们可以访问原始方法及其参数!结果,我们可以这样做: introduce()invoke()introduce()
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hello!");
       return method.invoke(person, args);
   }
}
现在我们已经invoke()在方法中添加了对原始方法的调用。如果我们现在尝试运行前面示例中的代码:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());
   }
}
然后我们会看到现在一切正常:) 控制台输出: 你好!我叫 Vasya。 您在哪里需要它?其实很多地方。“动态代理”设计模式在流行技术中被积极使用......顺便说一句,我忘了告诉你,Dynamic Proxy它是一种模式!恭喜你,你又学会了一个!:) 动态代理 - 2因此,它被积极地应用于与安全相关的流行技术和框架中。想象一下,您有 20 个方法只能由程序的登录用户执行。使用您学到的技术,您可以轻松地向这 20 个方法添加检查,以查看用户是否输入了登录名和密码,而无需在每个方法中单独复制验证码。或者,例如,如果您想创建一个记录所有用户操作的日志,使用代理也可以轻松完成。即使现在也是可能的:只需在示例中添加代码,以便在调用时方法的名称显示在控制台中invoke(),您将获得我们程序的简单日志:) 在讲座结束时,请注意一个重要的内容限制。创建代理对象发生在接口级别,而不是类级别。为该接口创建代理。看一下这段代码:
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
这里我们专门为该接口创建一个代理Person。如果我们尝试为该类创建代理,也就是说,我们更改链接的类型并尝试强制转换为该类Man,则什么都不会起作用。
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

proxyVasia.introduce(vasia.getName());
线程“main”中出现异常 java.lang.ClassCastException: com.sun.proxy.$Proxy0 无法转换为 Man 接口的存在是强制性要求。代理工作在接口级别。这就是今天的全部内容:) 作为有关代理主题的附加材料,我可以向您推荐一个精彩的视频和一篇好文章。好吧,现在解决一些问题就好了!:) 再见!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION