สวัสดีทุกคน! หากไม่เข้าใจแนวคิดพื้นฐาน การเจาะลึกกรอบงานและแนวทางในการสร้างฟังก์ชันการทำงานจึงค่อนข้างยาก ดังนั้นวันนี้เราจะพูดถึงหนึ่งในแนวคิดเหล่านี้ - AOP หรือ การเขียน โปรแกรมเชิงแง่มุม
นี่ไม่ใช่หัวข้อง่ายและไม่ได้ใช้บ่อยนัก แต่เฟรมเวิร์กและเทคโนโลยีจำนวนมากใช้มันภายใต้ประทุน และแน่นอนว่า บางครั้งในระหว่างการสัมภาษณ์ คุณอาจถูกขอให้บอกคุณในแง่ทั่วไปว่านี่คือสัตว์ชนิดใดและสามารถนำมาใช้ได้ที่ไหน ดังนั้นเรามาดูแนวคิดพื้นฐานและตัวอย่างง่ายๆของ AOP ใน Javaกัน
ดังนั้นAOP - การเขียนโปรแกรมเชิงแง่มุม - เป็นกระบวนทัศน์ที่มุ่งเป้าไปที่การเพิ่มความเป็นโมดูลของส่วนต่าง ๆ ของแอปพลิเคชันโดยการแยกข้อกังวลแบบตัดขวาง เมื่อต้องการทำเช่นนี้ จะมีการเพิ่มลักษณะการทำงานเพิ่มเติมลงในโค้ดที่มีอยู่ โดยไม่ต้องเปลี่ยนโค้ดต้นฉบับ กล่าวอีกนัยหนึ่ง ดูเหมือนว่าเราจะวางฟังก์ชันเพิ่มเติมไว้เหนือวิธีการและคลาสโดยไม่ต้องแก้ไขโค้ดที่แก้ไข เหตุใดจึงจำเป็น? ไม่ช้าก็เร็วเราก็ได้ข้อสรุปว่าแนวทางเชิงวัตถุตามปกติไม่สามารถแก้ไขปัญหาบางอย่างได้อย่างมีประสิทธิภาพเสมอไป ในขณะนี้ AOPได้เข้ามาช่วยเหลือและให้เครื่องมือเพิ่มเติมแก่เราในการสร้างแอปพลิเคชัน และเครื่องมือเพิ่มเติมหมายถึงความยืดหยุ่นในการพัฒนาที่เพิ่มขึ้น เนื่องจากมีทางเลือกมากขึ้นในการแก้ปัญหาเฉพาะ
AOP ยังใช้สำหรับการจัดการข้อยกเว้น การแคช และการลบฟังก์ชันบางอย่างเพื่อให้สามารถนำมาใช้ซ้ำได้
ฉันขอทราบทันทีว่าในตัวอย่างของเรา เราจะใช้ การ ทอแบบเวลาคอมไพล์ ก่อนอื่นเราต้องเพิ่มการพึ่งพาต่อไปนี้ให้กับpom.xml ของเรา :
สามารถอ่านเกี่ยวกับข้อยกเว้นและการจัดการได้ในบทความเหล่านี้: ข้อยกเว้นใน Javaและข้อยกเว้นและการจัดการ นั่นคือทั้งหมดสำหรับฉันในวันนี้ วันนี้เราได้ทำความคุ้นเคยกับAOPและคุณจะเห็นได้ว่าสัตว์ร้ายตัวนี้ไม่ได้น่ากลัวเท่ากับการทาสี ลาก่อนทุกคน!
การประยุกต์ใช้ AOP
การเขียนโปรแกรมเชิงมุมมองได้รับการออกแบบมาเพื่อแก้ไขปัญหาที่ต่อเนื่องกัน ซึ่งอาจเป็นโค้ดใดๆ ก็ตามที่ถูกทำซ้ำหลายครั้งในรูปแบบที่แตกต่างกัน ซึ่งไม่สามารถจัดโครงสร้างเป็นโมดูลแยกต่างหากได้อย่างสมบูรณ์ ดังนั้น ด้วยAOPเราสามารถปล่อยสิ่งนี้ไว้นอกโค้ดหลักและกำหนดในแนวตั้งได้ ตัวอย่างคือการประยุกต์ใช้นโยบายความปลอดภัยในแอปพลิเคชัน โดยทั่วไปแล้ว การรักษาความปลอดภัยจะตัดองค์ประกอบหลายอย่างของแอปพลิเคชัน นอกจากนี้ นโยบายความปลอดภัยของแอปพลิเคชันจะต้องนำไปใช้กับส่วนที่มีอยู่และส่วนใหม่ของแอปพลิเคชันอย่างเท่าเทียมกัน ในขณะเดียวกัน นโยบายความปลอดภัยที่ใช้ก็สามารถพัฒนาตัวเองได้ นี่คือจุดที่การใช้ AOPมีประโยชน์ อีกตัวอย่างหนึ่งก็คือการบันทึก มีข้อดีหลายประการ ในการใช้ แนวทาง AOPในการบันทึกเมื่อเปรียบเทียบกับการแทรกการบันทึกด้วยตนเอง:- รหัสบันทึกง่ายต่อการใช้งานและลบ: คุณเพียงแค่ต้องเพิ่มหรือลบการกำหนดค่าสองสามรายการในบางแง่มุม
- ซอร์สโค้ดทั้งหมดสำหรับการบันทึกจะถูกจัดเก็บไว้ในที่เดียว และไม่จำเป็นต้องค้นหาตำแหน่งการใช้งานทั้งหมดด้วยตนเอง
- คุณสามารถเพิ่มโค้ดที่มีไว้สำหรับการบันทึกได้ทุกที่ ไม่ว่าจะเป็นวิธีการและคลาสที่เขียนไว้แล้ว หรือฟังก์ชันการทำงานใหม่ ซึ่งจะช่วยลดจำนวนข้อผิดพลาดของนักพัฒนา
นอกจากนี้ เมื่อคุณลบแง่มุมออกจากการกำหนดค่าการออกแบบ คุณจะมั่นใจได้อย่างแน่นอนว่าโค้ดการติดตามทั้งหมดถูกลบออกและไม่มีสิ่งใดขาดหายไป - Aspects เป็นโค้ดแบบสแตนด์อโลนที่สามารถนำมาใช้ซ้ำและปรับปรุงได้ซ้ำแล้วซ้ำอีก

แนวคิดพื้นฐานของ AOP
หากต้องการวิเคราะห์หัวข้อเพิ่มเติม ก่อนอื่นมาทำความรู้จักกับแนวคิดหลักของ AOP กันก่อน คำแนะนำคือตรรกะเพิ่มเติม รหัส ที่ถูกเรียกจากจุดเชื่อมต่อ คำแนะนำสามารถทำได้ก่อน หลัง หรือแทนจุดเชื่อมต่อ (ดูข้อมูลเพิ่มเติมด้านล่าง) ประเภทของคำแนะนำที่เป็นไปได้:- ก่อน - คำแนะนำประเภทนี้จะเปิดตัวก่อนที่จะดำเนินการตามวิธีเป้าหมาย - จุดเชื่อมต่อ เมื่อใช้แง่มุมต่างๆ เป็นคลาส เราจะใช้ คำอธิบาย ประกอบ @Beforeเพื่อทำเครื่องหมายประเภทคำแนะนำว่ามาก่อน เมื่อใช้แง่มุมต่างๆ เป็น ไฟล์ .ajนี่จะเป็น เมธอด before()
- After (หลัง) - คำแนะนำที่ดำเนินการหลังจากเสร็จสิ้นการดำเนินการตามวิธีการ - จุดเชื่อมต่อทั้งในกรณีปกติและเมื่อมีการส่งข้อยกเว้น
เมื่อใช้แง่มุมต่างๆ เป็นคลาส เราสามารถใช้ คำอธิบาย ประกอบ @Afterเพื่อระบุว่านี่คือคำแนะนำที่มาภายหลัง
เมื่อใช้แง่มุมต่างๆ เป็น ไฟล์ .ajนี่จะเป็น เมธอด after() - หลังจากการส่งคืน - เคล็ดลับเหล่านี้จะดำเนินการเฉพาะในกรณีที่วิธีการเป้าหมายทำงานได้ตามปกติโดยไม่มีข้อผิดพลาด
เมื่อแง่มุมต่างๆ ถูกแสดงเป็นคลาส เราสามารถใช้ คำอธิบาย ประกอบ @AfterReturningเพื่อทำเครื่องหมายคำแนะนำว่ากำลังดำเนินการเมื่อดำเนินการเสร็จสิ้น
เมื่อใช้แง่มุมต่างๆ เป็นไฟล์ .aj นี่จะเป็น เมธอดafter() ที่ส่งคืน (Object obj ) - หลังจากการโยน - คำแนะนำประเภทนี้มีไว้สำหรับกรณีที่เมธอดซึ่งก็คือจุดเชื่อมต่อส่งข้อยกเว้น เราสามารถใช้คำแนะนำนี้สำหรับการจัดการบางอย่างกับการดำเนินการที่ล้มเหลว (เช่น การย้อนกลับธุรกรรมทั้งหมด หรือการบันทึกด้วยระดับการติดตามที่ต้องการ)
สำหรับคลาสแง่มุม คำอธิบายประกอบ @AfterThrowingใช้เพื่อระบุว่าคำแนะนำนี้ถูกใช้หลังจากมีข้อยกเว้นเกิดขึ้น
เมื่อใช้ลักษณะต่างๆ ในรูปแบบของ ไฟล์ .ajนี่จะเป็นวิธีการ - after() การขว้างปา (ข้อยกเว้น e) - รอบๆอาจเป็นหนึ่งในประเภทคำแนะนำที่สำคัญที่สุดที่เกี่ยวข้องกับวิธีการ นั่นคือ จุดเชื่อมต่อ ซึ่งเราสามารถ เช่น เลือกว่าจะดำเนินการวิธีจุดเชื่อมต่อที่กำหนดหรือไม่
คุณสามารถเขียนโค้ดคำแนะนำที่ทำงานก่อนและหลังการดำเนินการเมธอด join point
ความรับผิดชอบของคำแนะนำได้แก่ การเรียกเมธอด join point และการส่งคืนค่าหากเมธอดส่งคืนบางสิ่ง นั่นคือในเคล็ดลับนี้คุณสามารถเลียนแบบการทำงานของวิธีการเป้าหมายโดยไม่ต้องเรียกมันและส่งคืนบางสิ่งของคุณเอง
สำหรับแง่มุมต่างๆ ในรูปแบบของคลาส เราใช้ คำอธิบายประกอบ @Aroundเพื่อสร้างเคล็ดลับที่ล้อมรอบจุดเชื่อมต่อ เมื่อใช้แง่มุมต่างๆ เป็น ไฟล์ .ajนี่จะเป็น เมธอด around()
- การทอผ้าตามเวลาคอมไพล์ - หากคุณมีซอร์สโค้ดของแง่มุมและโค้ดที่คุณใช้แง่มุมต่างๆ คุณสามารถคอมไพล์ซอร์สโค้ดและแง่มุมได้โดยตรงโดยใช้คอมไพเลอร์ AspectJ
- การทอหลังการคอมไพล์ (การทอแบบไบนารี) - หากคุณไม่สามารถหรือไม่ต้องการใช้การแปลงซอร์สโค้ดเพื่อสานแง่มุมต่างๆ ลงในโค้ดของคุณ คุณสามารถใช้คลาสหรือขวดที่คอมไพล์แล้วและฉีดลักษณะต่างๆ ได้
- การทอแบบเวลาโหลดเป็นเพียงการทอแบบไบนารีที่ถูกเลื่อนออกไปจนกว่าตัวโหลดคลาสจะโหลดไฟล์คลาสและกำหนดคลาสสำหรับ JVM
เพื่อสนับสนุนสิ่งนี้ จำเป็นต้องมี "ตัวโหลดคลาสสาน" อย่างน้อยหนึ่งตัว สิ่งเหล่านี้มีให้อย่างชัดเจนโดยรันไทม์หรือเปิดใช้งานโดย "ตัวแทนการทอผ้า"
ตัวอย่างในภาษาจาวา
ต่อไป เพื่อทำความเข้าใจAOP ให้ดียิ่งขึ้น เราจะมาดูตัวอย่างเล็กๆ น้อยๆ ของระดับ Hello World
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
ตามกฎแล้ว คอมไพ เลอ ร์ Ajs พิเศษ จะใช้เพื่อใช้ลักษณะต่างๆ IntelliJ IDEAไม่มีค่าเริ่มต้น ดังนั้นเมื่อเลือกให้เป็นแอปพลิเคชันคอมไพเลอร์ คุณจะต้องระบุเส้นทางไปยังการ กระจาย AspectJ คุณสามารถอ่าน เพิ่มเติมเกี่ยวกับวิธีการเลือกAjsเป็นคอมไพเลอร์ได้ในหน้านี้ นี่เป็นวิธีแรก และวิธีที่สอง (ซึ่งฉันใช้) คือการเพิ่มปลั๊กอินต่อไปนี้ในpom.xml :
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
หลังจากนี้ ขอแนะนำให้นำเข้าอีกครั้งจากMavenและรันmvn cleanคอมไพล์ ตอนนี้เรามาดูตัวอย่างกันดีกว่า
ตัวอย่างหมายเลข 1
มาสร้าง คลาส Main กันดี กว่า ในนั้นเราจะมีจุดเริ่มต้นและวิธีการพิมพ์ชื่อที่ส่งผ่านไปยังคอนโซล:public class Main {
public static void main(String[] args) {
printName("Толя");
printName("Вова");
printName("Sasha");
}
public static void printName(String name) {
System.out.println(name);
}
}
ไม่มีอะไรซับซ้อน: พวกเขาส่งชื่อและแสดงในคอนโซล ถ้าเรารันตอนนี้ คอนโซลจะแสดง:
โทลียา โววา ซาชา
ถึงเวลาใช้ประโยชน์จากพลังของ AOP แล้ว ตอน นี้เราจำเป็นต้องสร้างไฟล์ - แง่มุม มีสองประเภท: ประเภทแรกคือไฟล์ที่มี นามสกุล .ajส่วนประเภทที่สองเป็นคลาสปกติที่ใช้ ความสามารถ AOPโดยใช้คำอธิบายประกอบ ก่อนอื่น มาดูไฟล์ที่มี นามสกุล .aj :
public aspect GreetingAspect {
pointcut greeting() : execution(* Main.printName(..));
before() : greeting() {
System.out.print("Привет ");
}
}
ไฟล์นี้ค่อนข้างคล้ายกับคลาส เรามาดูกันว่าเกิดอะไรขึ้นที่นี่: pointcut - การตัดหรือชุดของจุดเชื่อมต่อ Greeting() — ชื่อของสไลซ์นี้ : การดำเนินการ - เมื่อดำเนินการ* - ทั้งหมด โทร - Main.printName(..) - เมธอดนี้ ถัดมาเป็นคำแนะนำเฉพาะ - before() - ซึ่งจะถูกดำเนินการก่อนที่จะเรียกเมธอดเป้าหมาย: Greeting() - ส่วนที่คำแนะนำนี้ตอบสนอง และด้านล่างเราจะเห็นเนื้อหาของเมธอดเอง ซึ่งเขียนด้วยภาษา Java ภาษาที่เราเข้าใจ เมื่อเรารันmainโดยมีลักษณะนี้ เราจะได้ผลลัพธ์ต่อไปนี้ไปยังคอนโซล:
สวัสดี Tolya สวัสดี Vova สวัสดี Sasha
เราจะเห็นว่าการเรียกเมธอดprintName ทุกครั้ง ได้รับการแก้ไขตามแง่มุม ตอนนี้เรามาดูกันว่าแง่มุมจะเป็นอย่างไร แต่เป็นคลาส Java ที่มีคำอธิบายประกอบ:
@Aspect
public class GreetingAspect{
@Pointcut("execution(* Main.printName(String))")
public void greeting() {
}
@Before("greeting()")
public void beforeAdvice() {
System.out.print("Привет ");
}
}
หลังจาก ไฟล์ .aj แง่มุม ทุกอย่างจะชัดเจนยิ่งขึ้น:
- @Aspectแสดงว่าคลาสที่กำหนดนั้นเป็นแง่มุม @Pointcut("execution(* Main.printName(String))")เป็นจุดตัดที่เรียกใช้ในการเรียกMain.printName ทั้งหมด ด้วยอาร์กิวเมนต์ประเภทขาเข้าString ;
- @Before("greeting()") - คำ แนะนำที่ใช้ก่อนที่จะเรียกโค้ดที่อธิบายไว้ที่ จุดตัด Greeting()
สวัสดี Tolya สวัสดี Vova สวัสดี Sasha
ตัวอย่างหมายเลข 2
สมมติว่าเรามีวิธีการบางอย่างที่ดำเนินการบางอย่างสำหรับลูกค้าและเรียกวิธีนี้จากmain :public class Main {
public static void main(String[] args) {
makeSomeOperation("Толя");
}
public static void makeSomeOperation(String clientName) {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
}
}
การใช้ คำอธิบาย ประกอบ @Aroundลองทำบางอย่างเช่น "ธุรกรรมหลอก":
@Aspect
public class TransactionAspect{
@Pointcut("execution(* Main.makeSomeOperation(String))")
public void executeOperation() {
}
@Around(value = "executeOperation()")
public void beforeAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("Открытие транзакции...");
try {
joinPoint.proceed();
System.out.println("Закрытие транзакции....");
}
catch (Throwable throwable) {
System.out.println("Операция не удалась, откат транзакции...");
}
}
}
เมื่อใช้ วิธี การดำเนินการ ของ วัตถุProceedingJoinPointเราจะเรียกวิธีการของ wrapper เพื่อกำหนดตำแหน่งในบอร์ด และตามด้วยโค้ดในวิธีการด้านบนjoinPoint.proceed(); - นี่คือBeforeซึ่งอยู่ด้านล่าง- After ถ้าเรารันmainเราจะเข้าสู่คอนโซล:
การเปิดธุรกรรม... กำลังดำเนินการบางอย่างให้กับลูกค้า - Tolya กำลังปิดธุรกรรม....
หากเราเพิ่มการโยนข้อยกเว้นให้กับวิธีการของเรา (การดำเนินการล้มเหลวกะทันหัน):
public static void makeSomeOperation(String clientName)throws Exception {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
throw new Exception();
}
จากนั้นเราจะได้ผลลัพธ์ในคอนโซล:
กำลังเปิดธุรกรรม... กำลังดำเนินการบางอย่างให้กับลูกค้า - Tolya การดำเนินการล้มเหลว ธุรกรรมถูกย้อนกลับ...
มันกลับกลายเป็นการประมวลผลความล้มเหลวแบบหลอกๆ
ตัวอย่างหมายเลข 3
ดังตัวอย่างถัดไป เรามาทำบางอย่างเช่นการเข้าสู่ระบบคอนโซลกัน ก่อนอื่น มาดูที่Mainซึ่งตรรกะทางธุรกิจหลอกของเราเกิดขึ้น:public class Main {
private String value;
public static void main(String[] args) throws Exception {
Main main = new Main();
main.setValue("<некоторое meaning>");
String valueForCheck = main.getValue();
main.checkValue(valueForCheck);
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void checkValue(String value) throws Exception {
if (value.length() > 10) {
throw new Exception();
}
}
}
ในmainให้ใช้setValueเราจะตั้งค่าของตัวแปรภายใน - valueจากนั้นใช้getValueเราจะรับค่านี้และในcheckValueเราจะตรวจสอบว่าค่านี้ยาวเกิน 10 ตัวอักษรหรือไม่ หากใช่ จะมีการยกเว้นข้อยกเว้น ตอนนี้เรามาดูแง่มุมที่เราจะบันทึกการทำงานของวิธีการต่างๆ:
@Aspect
public class LogAspect {
@Pointcut("execution(* *(..))")
public void methodExecuting() {
}
@AfterReturning(value = "methodExecuting()", returning = "returningValue")
public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
if (returningValue != null) {
System.out.printf("Успешно выполнен метод - %s, класса- %s, с результатом выполнения - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
returningValue);
}
else {
System.out.printf("Успешно выполнен метод - %s, класса- %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName());
}
}
@AfterThrowing(value = "methodExecuting()", throwing = "exception")
public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
System.out.printf("Метод - %s, класса- %s, был аварийно завершен с исключением - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
exception);
}
}
เกิดอะไรขึ้นที่นี่? @Pointcut("execution(* *(..))") - จะเชื่อมต่อกับการโทรทั้งหมดไปยังวิธีการทั้งหมด @AfterReturning(value = "methodExecuting()", return = "returningValue") - คำแนะนำที่จะดำเนินการหลังจากเมธอดเป้าหมายเสร็จสมบูรณ์แล้ว เรามีสองกรณีที่นี่:
- เมื่อเมธอดมีค่าส่งคืนif (returningValue != null) {
- เมื่อไม่มีค่าตอบแทนelse {
ดำเนินการเมธอด - setValue ของคลาส - Main สำเร็จแล้ว เมธอด - getValue ของคลาส - Main ดำเนินการสำเร็จพร้อมผลลัพธ์ของการดำเนินการ - <ค่าบางส่วน> วิธีการ - checkValue ของคลาส - Main ถูกยกเลิกอย่างผิดปกติโดยมีข้อยกเว้น - วิธี java.lang.Exception - main, class-Main, ขัดข้องโดยมีข้อยกเว้น - java.lang.Exception
เนื่องจากเราไม่ได้จัดการข้อยกเว้น เราจึงจะได้รับสแต็กเทรซด้วย: คุณ

GO TO FULL VERSION