สวัสดี! วันนี้เราจะมาดูหัวข้อที่ค่อนข้างสำคัญและน่าสนใจ - การสร้างคลาสพร็อกซีแบบไดนามิกใน 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 สำเร็จรูป และไม่สามารถรับและเขียนโค้ดของคลาสนี้ใหม่ได้ อย่างไรก็ตามเราจำเป็นต้องเปลี่ยนพฤติกรรมของเขา ตัวอย่างเช่น เราไม่รู้ว่าจะมีการเรียกใช้เมธอดใดบนอ็อบเจ็กต์ของเรา ดังนั้นเราจึงต้องการให้บุคคลนั้นกล่าว “สวัสดี!” ก่อนเมื่อทำการเรียกอ็อบเจ็กต์ใดๆ (ไม่มีใครชอบคนไม่สุภาพ) เราควรทำอย่างไรในสถานการณ์เช่นนี้? เราจะต้องมีบางสิ่ง:
-
InvocationHandler
InvocationHandler
เป็นอินเทอร์เฟซพิเศษที่ช่วยให้เราสามารถสกัดกั้นการเรียกเมธอดใดๆ ไปยังอ็อบเจ็กต์ของเรา และเพิ่มพฤติกรรมเพิ่มเติมที่เราต้องการ เราจำเป็นต้องสร้าง interceptor ของเราเอง - นั่นคือสร้างคลาสและใช้งานอินเทอร์เฟซนี้ มันค่อนข้างง่าย:
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!" ไปยังคอนโซลภายในเมธอด)
- วัตถุต้นฉบับและพร็อกซีของมัน
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
และรายการอินเทอร์เฟซ) รวมถึงวัตถุของ interceptor ที่เราสร้างไว้ก่อนหน้านี้ - InvocationHandler
'a สิ่งสำคัญคืออย่าลืมส่งวัตถุดั้งเดิมของเราไปยัง interceptor 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
interceptor ของเราจะ "จับ" การโทรนี้และดำเนินการตามวิธีการของตัวเอง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!" ก่อน จากนั้นวิธีการที่เราเรียกใช้ก็ควรจะได้ผล กล่าวอีกนัยหนึ่ง วิธีการนี้เรียกว่า:
proxyVasia.introduce(vasia.getName());
ควรส่งออกไปยังคอนโซล “Hello! ฉันชื่อวาสยา” และไม่ใช่แค่ “สวัสดี!” เราจะบรรลุเป้าหมายนี้ได้อย่างไร? ไม่มีอะไรซับซ้อน: คุณเพียงแค่ต้องปรับแต่งเล็กน้อยด้วย interceptor และวิธีการของเรา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());
}
}
แล้วเราจะเห็นว่าตอนนี้ทุกอย่างทำงานได้ตามปกติ :) เอาต์พุตคอนโซล: สวัสดี! ฉันชื่อวาสยา คุณต้องการมันที่ไหน? จริงๆแล้วมีหลายสถานที่ รูปแบบการออกแบบ “พร็อกซีแบบไดนามิก” ถูกนำมาใช้อย่างแข็งขันในเทคโนโลยียอดนิยม... และอีกอย่าง ฉันลืมบอกคุณว่าDynamic Proxy
มันเป็นรูปแบบ ! ยินดีด้วย คุณได้เรียนรู้อีกหนึ่งอย่างแล้ว! :) ดังนั้นจึงมีการใช้อย่างแข็งขันในเทคโนโลยีและกรอบการทำงานยอดนิยมที่เกี่ยวข้องกับความปลอดภัย ลองจินตนาการว่าคุณมี 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 ได้ การมีอินเทอร์เฟซเป็นข้อกำหนดบังคับ พร็อกซีทำงานในระดับอินเทอร์เฟซ นั่นคือทั้งหมดสำหรับวันนี้ :) เพื่อเป็นเนื้อหาเพิ่มเติมในหัวข้อผู้รับมอบฉันทะ ฉันสามารถแนะนำวิดีโอ ที่ยอดเยี่ยม และบทความ ที่ดีให้กับคุณ ได้ ตอนนี้คงจะดีถ้าได้แก้ไขปัญหาบางอย่าง! :) พบกันใหม่!
GO TO FULL VERSION