JavaRush /בלוג Java /Random-HE /טיול קצר להזרקת תלות או "מה זה עוד CDI?"
Viacheslav
רָמָה

טיול קצר להזרקת תלות או "מה זה עוד CDI?"

פורסם בקבוצה
הבסיס עליו בנויות כעת ה-frameworks הפופולריות ביותר הוא הזרקת תלות. אני מציע לבדוק מה מפרט ה-CDI אומר על זה, אילו יכולות בסיסיות יש לנו וכיצד אנחנו יכולים להשתמש בהן.
טיול קצר לתוך הזרקת תלות או

מבוא

אני רוצה להקדיש את הסקירה הקצרה הזו לדבר כזה כמו CDI. מה זה? CDI ראשי תיבות של Contexts and Dependency Injection. זהו מפרט Java EE שמתאר הזרקת תלות והקשרים. למידע, אתה יכול להסתכל באתר http://cdi-spec.org . מכיוון ש-CDI הוא מפרט (תיאור של איך זה אמור לעבוד, סט של ממשקים), נצטרך גם מימוש כדי להשתמש בו. אחד מיישומים כאלה הוא Weld - http://weld.cdi-spec.org/ כדי לנהל תלות וליצור פרויקט, נשתמש ב-Maven - https://maven.apache.org אז התקנו את Maven, עכשיו אנחנו יבין זאת בפועל, כדי לא להבין את המופשט. לשם כך, ניצור פרויקט באמצעות Maven. בוא נפתח את שורת הפקודה (ב-Windows, אתה יכול להשתמש ב-Win+R כדי לפתוח את חלון ה-"Run" ולהפעיל cmd) ונבקש מ-Maven לעשות הכל בשבילנו. לשם כך, ל-Maven יש מושג שנקרא ארכיטיפ: Maven Archetype .
טיול קצר לתוך הזרקת תלות או
לאחר מכן, בשאלות " בחר מספר או החל מסנן " ו"בחר org.apache.maven.archetypes:maven-archetype-quickstart version " פשוט הקש על Enter. לאחר מכן, הזן את מזהי הפרויקט, מה שנקרא GAV (ראה מדריך אמנת השמות ).
טיול קצר לתוך הזרקת תלות או
לאחר יצירה מוצלחת של הפרויקט, נראה את הכתובת "בונה הצלחה". עכשיו אנחנו יכולים לפתוח את הפרויקט שלנו ב-IDE האהוב עלינו.

הוספת CDI לפרויקט

בהקדמה ראינו שלCDI יש אתר מעניין - http://www.cdi-spec.org/ . ישנו קטע הורדה, המכיל טבלה המכילה את הנתונים הדרושים לנו:
טיול קצר לתוך הזרקת תלות או
כאן נוכל לראות כיצד מייבן מתארת ​​את העובדה שאנו משתמשים ב-CDI API בפרויקט. API הוא ממשק תכנות יישומים, כלומר ממשק תכנות כלשהו. אנו עובדים עם הממשק מבלי לדאוג מה ואיך הוא עובד מאחורי הממשק הזה. ה-API הוא ארכיון jar שנתחיל להשתמש בו בפרויקט שלנו, כלומר הפרויקט שלנו מתחיל להיות תלוי בצנצנת הזו. לכן, ה-CDI API עבור הפרויקט שלנו הוא תלות. ב-Maven, פרויקט מתואר בקבצי POM.xml ( POM - Project Object Model ). התלות מתוארות בבלוק התלות, שאליו עלינו להוסיף ערך חדש:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
כפי שאולי שמתם לב, איננו מציינים היקף עם הערך שסופק. למה יש הבדל כזה? היקף זה אומר שמישהו יספק לנו את התלות. כאשר אפליקציה פועלת על שרת Java EE, המשמעות היא שהשרת יספק לאפליקציה את כל טכנולוגיות ה-JEE הדרושות. למען הפשטות של סקירה זו, אנו נעבוד בסביבת Java SE, ולכן אף אחד לא יספק לנו את התלות הזו. אתה יכול לקרוא עוד על היקף התלות כאן: " היקף התלות ". אוקיי, כעת יש לנו את היכולת לעבוד עם ממשקים. אבל אנחנו צריכים גם יישום. כזכור, נשתמש בוולד. מעניין שבכל מקום ניתנות תלות שונות. אבל אנחנו נעקוב אחר התיעוד. לכן, בואו נקרא את " 18.4.5. הגדרת נתיב הכיתה " ונעשה ככתוב:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
חשוב שגרסאות השורה השלישית של Weld יתמכו ב-CDI 2.0. לכן, אנו יכולים לסמוך על ה-API של גרסה זו. עכשיו אנחנו מוכנים לכתוב קוד.
טיול קצר לתוך הזרקת תלות או

אתחול מיכל CDI

CDI הוא מנגנון. מישהו חייב לשלוט במנגנון הזה. כפי שכבר קראנו לעיל, מנהל כזה הוא מיכל. לכן, אנחנו צריכים ליצור אותו; הוא עצמו לא יופיע בסביבת SE. בואו נוסיף את הדברים הבאים לשיטה העיקרית שלנו:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
יצרנו את מיכל ה-CDI באופן ידני כי... אנו עובדים בסביבת SE. בפרויקטים קרביים טיפוסיים, הקוד פועל על שרת, המספק טכנולוגיות שונות לקוד. בהתאם לכך, אם השרת מספק CDI, זה אומר שלשרת יש כבר קונטיינר CDI ולא נצטרך להוסיף כלום. אבל למטרות המדריך הזה, ניקח את סביבת SE. בנוסף, המיכל נמצא כאן, ברור ומובן. למה אנחנו צריכים מיכל? המיכל שבתוכו מכיל שעועית (שעועית CDI).
טיול קצר לתוך הזרקת תלות או

שעועית CDI

אז, שעועית. מהו פח CDI? זוהי מחלקת Java שעוקבת אחר כללים מסוימים. כללים אלה מתוארים במפרט, בפרק " 2.2. אילו סוגי מחלקות הם שעועית? ". בואו נוסיף שעועית CDI לאותה חבילה כמו מחלקת האפליקציה:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
עכשיו אנחנו יכולים לקרוא לשעועית הזו מהשיטה שלנו main:
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
כפי שאתה יכול לראות, לא יצרנו את השעועית באמצעות מילת המפתח החדשה. שאלנו את מיכל ה-CDI: "מיכל CDI. אני באמת צריך מופע של המחלקה לוגר, תן לי אותו בבקשה." שיטה זו נקראת " בדיקת תלות ", כלומר חיפוש תלות. עכשיו בואו ניצור מחלקה חדשה:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
מחלקה פרימיטיבית המחזירה ייצוג טקסט של תאריך. כעת נוסיף את פלט התאריך להודעה:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
הופיעה הערת @Inject מעניינת. כפי שנאמר בפרק " 4.1. נקודות הזרקה " בתיעוד הריתוך של cdi, באמצעות הערה זו אנו מגדירים את נקודת ההזרקה. ברוסית ניתן לקרוא את זה כ"נקודות יישום". הם משמשים את מיכל CDI להזרקת תלות בעת יצירת שעועית. כפי שאתה יכול לראות, אנחנו לא מקצים שום ערכים לשדה dateSource. הסיבה לכך היא העובדה שמיכל ה-CDI מאפשר לשעועית ה-CDI (רק השעועית שהוא עצמו יצר, כלומר שהוא מצליח) להשתמש ב"הזרקת תלות ". זוהי דרך נוספת של Inversion of Control , גישה שבה התלות נשלטת על ידי מישהו אחר ולא אנחנו יוצרים במפורש את האובייקטים. הזרקת תלות יכולה להתבצע באמצעות שיטה, בנאי או שדה. לפרטים נוספים, ראה פרק מפרט CDI " 5.5. הזרקת תלות ". ההליך לקביעת מה צריך להיות מיושם נקרא רזולוציית סוג בטוחה, וזה מה שאנחנו צריכים לדבר עליו.
טיול קצר לתוך הזרקת תלות או

רזולוציית שם או רזולוציית Typesafe

בדרך כלל, ממשק משמש כסוג האובייקט ליישום, ומיכל ה-CDI עצמו קובע באיזה מימוש לבחור. זה שימושי מסיבות רבות, בהן נדון. אז יש לנו ממשק לוגר:
public interface Logger {
    void print(String message);
}
הוא אומר שאם יש לנו איזה לוגר, נוכל לשלוח אליו הודעה והוא ישלים את המשימה שלו - יומן. איך ואיפה לא יעניין במקרה זה. כעת ניצור יישום עבור לוגר:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
כפי שאתה יכול לראות, זהו לוגר שכותב ל-System.out. נִפלָא. כעת, השיטה העיקרית שלנו תעבוד כמו קודם. Logger logger = container.select(Logger.class).get(); קו זה עדיין יתקבל על ידי לוגר. והיופי הוא שאנחנו רק צריכים להכיר את הממשק, והמיכל CDI כבר חושב על היישום עבורנו. נניח שיש לנו יישום שני שאמור לשלוח את היומן למקום אחסון מרוחק:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
אם נריץ כעת את הקוד שלנו ללא שינויים, נקבל שגיאה, כי קונטיינר CDI רואה שני יישומים של הממשק ולא יכול לבחור ביניהם: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger מה לעשות? קיימות מספר וריאציות זמינות. הפשוטה שבהן היא הערת @Vetoed עבור שעועית CDI כך שמיכל ה-CDI אינו תופס את המעמד הזה כשעועית CDI. אבל יש גישה הרבה יותר מעניינת. שעועית CDI יכולה להיות מסומנת כ"חלופה" באמצעות ההערה @Alternativeהמתוארת בפרק " 4.7. חלופות " בתיעוד Weld CDI. מה זה אומר? זה אומר שאם לא נאמר במפורש להשתמש בו, הוא לא ייבחר. זוהי גרסה חלופית של השעועית. בואו נסמן את שעועית NetworkLogger כ-@Alternative ונוכל לראות שהקוד מבוצע שוב ומשמש את SystemOutLogger. כדי להפעיל את האלטרנטיבה, עלינו להיות בעל קובץ beans.xml . עלולה להתעורר השאלה: " beans.xml, איפה אני שם אותך? " לכן, בואו נמקם את הקובץ בצורה נכונה:
טיול קצר לתוך הזרקת תלות או
ברגע שיש לנו את הקובץ הזה, החפץ עם הקוד שלנו ייקרא " ארכיון שעועית מפורשת ". כעת יש לנו 2 תצורות נפרדות: תוכנה ו-xml. הבעיה היא שהם יטענו את אותם נתונים. לדוגמה, הגדרת שעועית DataSource תיטען 2 פעמים והתוכנית שלנו תתרסק בעת ביצועה, מכיוון מיכל ה-CDI יחשוב עליהם כעל 2 שעועית נפרדת (למרות שלמעשה מדובר באותה כיתה, שמיכל ה-CDI למד עליה פעמיים). כדי להימנע מכך ישנן 2 אפשרויות:
  • הסר את השורה initializer.addPackages(App.class.getPackage())והוסף אינדיקציה של החלופה לקובץ ה-xml:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • הוסף תכונה bean-discovery-modeעם הערך " none " לאלמנט שורש השעועית וציין חלופה באופן פרוגרמטי:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
לפיכך, באמצעות חלופת CDI, המיכל יכול לקבוע איזו שעועית לבחור. מעניין שאם מיכל ה-CDI מכיר מספר חלופות לאותו ממשק, אז נוכל לדעת זאת על ידי ציון העדיפות באמצעות הערה @Priority(מאז CDI 1.1).
טיול קצר לתוך הזרקת תלות או

מוקדמות

בנפרד, כדאי לדון בדבר כזה כמו מוקדמות. המוקדמות מסומנות על ידי ביאור מעל השעועית ומצמצמת את החיפוש אחר השעועית. ועכשיו פרטים נוספים. מעניין שלכל שעועית CDI בכל מקרה יש לפחות מוקדמות אחד - @Any. אם אנחנו לא מציינים שום qualifier מעל ה-bean, אבל אז מיכל ה-CDI עצמו מוסיף @Anyqualifier נוסף ל- qualifier - @Default. אם נציין משהו (לדוגמה, נציין במפורש @Any), אזי ה-@Default qualifier לא יתווסף אוטומטית. אבל היופי של מוקדמות הוא שאתה יכול לעשות מוקדמות משלך. המוקדמות כמעט לא שונה מהביאורים, כי בעצם, זו רק הערה שנכתבה בצורה מיוחדת. לדוגמה, אתה יכול להזין Enum עבור סוג הפרוטוקול:
public enum ProtocolType {
    HTTP, HTTPS
}
בשלב הבא נוכל לעשות מוקדמות שייקח בחשבון את הסוג הזה:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
ראוי לציין ששדות המסומנים כשדות @Nonbindingאינם משפיעים על קביעת המוקדמות. כעת עליך לציין את המוקדמות. זה מצוין מעל סוג השעועית (כדי ש-CDI ידע להגדיר אותו) ומעל נקודת ה-Injection (עם הערת @Inject, כדי שתבינו איזה שעועית לחפש להזרקה במקום הזה). לדוגמה, אנו יכולים להוסיף מחלקה כלשהי עם מוקדמות. לשם הפשטות, עבור מאמר זה נעשה אותם בתוך ה-NetworkLogger:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
ואז כשאנחנו מבצעים Inject, נציין מוקד שישפיע באיזו מחלקה ישמש:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
נהדר, לא?) זה נראה יפה, אבל לא ברור למה. כעת דמיינו את הדברים הבאים:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
כך נוכל לעקוף את קבלת הערך כך שניתן יהיה לחשב אותו באופן דינמי. לדוגמה, זה יכול להילקח מכמה הגדרות. לאחר מכן נוכל לשנות את היישום אפילו תוך כדי תנועה, מבלי לבצע קומפילציה או הפעלה מחדש של התוכנית/שרת. זה נהיה הרבה יותר מעניין, לא? )
טיול קצר לתוך הזרקת תלות או

מפיקים

תכונה שימושית נוספת של CDI היא מפיקים. אלו שיטות מיוחדות (הן מסומנות בביאור מיוחד) שנקראות כאשר שעועית כלשהי ביקשה הזרקת תלות. פרטים נוספים מתוארים בתיעוד, בסעיף " 2.2.3. שיטות מפיק ". הדוגמה הפשוטה ביותר:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
כעת, בעת הזרקה לשדות מסוג Integer, שיטה זו תיקרא וממנה יתקבל ערך. כאן עלינו להבין מיד שכאשר אנו רואים את מילת המפתח new, עלינו להבין מיד שלא מדובר בשעועית CDI. כלומר, מופע של המחלקה Random לא יהפוך לשעועית CDI רק בגלל שהוא נגזר ממשהו ששולט במיכל ה-CDI (במקרה הזה, המפיק).
טיול קצר לתוך הזרקת תלות או

מיירטים

מיירטים הם מיירטים ש"מתערבים" בעבודה. ב-CDI זה נעשה די ברור. בואו נראה איך אנחנו יכולים לבצע רישום באמצעות מתורגמנים (או מיירטים). ראשית, עלינו לתאר את הקישור למיירט. כמו הרבה דברים, זה נעשה באמצעות הערות:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
העיקר כאן הוא שזוהי כריכה עבור המיירט ( @InterceptorBinding), שיורש על ידי מרחיב ( @InterceptorBinding). כעת נכתוב את המיירט עצמו:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
אתה יכול לקרוא עוד על איך מיירטים נכתבים בדוגמה מתוך המפרט: " 1.3.6. דוגמה מיירט ". ובכן, כל מה שאנחנו צריכים לעשות זה להפעיל את הסרפט. לשם כך, ציין את ההערה המחייבת מעל השיטה המתבצעת:
@ConsoleLog
public void print(String message) {
ועכשיו עוד פרט חשוב מאוד. מיירטים מושבתים כברירת מחדל ויש להפעיל אותם באותו אופן כמו האלטרנטיבות. לדוגמה, בקובץ beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
כפי שאתה יכול לראות, זה די פשוט.
טיול קצר לתוך הזרקת תלות או

אירועים ומשקיפים

CDI מספקת גם מודל של אירועים ומשקיפים. כאן הכל לא ברור כמו עם מיירטים. אז, האירוע במקרה זה יכול להיות לחלוטין כל שיעור; שום דבר מיוחד לא נדרש לתיאור. לדוגמה:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
עכשיו מישהו צריך לחכות לאירוע:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
העיקר כאן הוא לציין את ההערה @Observes, המעידה על כך שלא מדובר רק בשיטה, אלא בשיטה שצריך לקרוא לה כתוצאה מצפייה באירועים מסוג LogEvent. ובכן, עכשיו אנחנו צריכים מישהו שיראה:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
יש לנו שיטה יחידה שתגיד לקונטיינר שאירוע Event התרחש עבור סוג האירוע LogEvent. כעת כל שנותר הוא להשתמש בצופה. לדוגמה, ב-NetworkLogger נוכל להוסיף זריקה של הצופה שלנו:
@Inject
private LogObserver observer;
ובשיטת ההדפסה נוכל להודיע ​​למתבונן שיש לנו אירוע חדש:
public void print(String message) {
	observer.observe(new LogEvent());
חשוב לדעת שניתן לעבד אירועים בשרשור אחד או במספר. לעיבוד אסינכרוני, השתמש בשיטה .fireAsync(במקום .fire) ובביאור @ObservesAsync(במקום ב-@Observes). לדוגמה, אם כל האירועים מבוצעים בשרשורים שונים, אז אם שרשור אחד זורק חריגה, אז האחרים יוכלו לעשות את עבודתם עבור אירועים אחרים. ניתן לקרוא עוד על אירועים ב-CDI, כרגיל, במפרט, בפרק " 10. אירועים ".
טיול קצר לתוך הזרקת תלות או

מעצבים

כפי שראינו לעיל, דפוסי עיצוב שונים נאספים מתחת לכנף CDI. והנה עוד אחד - דקורטור. זה דבר מאוד מעניין. בואו נסתכל על השיעור הזה:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
בהכרזתו כ-Decorator, אנו אומרים שכאשר נעשה שימוש בכל מימוש של לוגר, ייעשה שימוש ב"תוסף" זה, שיודע את המימוש האמיתי, המאוחסן בשדה הנציג (שכן הוא מסומן עם ההערה @Delegate). מעצבים יכולים להיות קשורים רק לשעועית CDI, שהיא בעצמה לא מיירט או דקורטור. ניתן לראות דוגמה גם במפרט: " 1.3.7. דוגמה לקשט ". הדקורטור, כמו המיירט, חייב להיות מופעל. לדוגמה, ב- beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
לפרטים נוספים, ראה התייחסות לריתוך: " פרק 10. דקורטורים ".

מעגל החיים

לשעועית יש מחזור חיים משלה. זה נראה בערך כך:
טיול קצר לתוך הזרקת תלות או
כפי שניתן לראות מהתמונה, יש לנו מה שנקרא התקשרות חוזרת במחזור החיים. אלו הן הערות שיאמרו למיכל ה-CDI לקרוא לשיטות מסוימות בשלב מסוים במחזור החיים של השעועית. לדוגמה:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
שיטה זו תיקרא כאשר שעועית CDI מופעלת על ידי מיכל. אותו הדבר יקרה עם @PreDestroy כאשר השעועית תהרוס כשאין צורך בה יותר. לא בכדי ראשי התיבות CDI מכילים את האות C - Context. שעועית ב-CDI היא הקשרית, כלומר מחזור החיים שלהן תלוי בהקשר שבו הן קיימות בתוך מיכל ה-CDI. כדי להבין זאת טוב יותר, עליך לקרוא את סעיף המפרט " 7. מחזור חיים של מופעים הקשריים ". כדאי גם לדעת שלמכולה עצמה יש מחזור חיים, עליו תוכלו לקרוא ב"אירועי מחזור חיים של מיכל ".
טיול קצר לתוך הזרקת תלות או

סה"כ

למעלה הסתכלנו על קצה הקרחון שנקרא CDI. CDI הוא חלק ממפרט JEE ומשמש בסביבת JavaEE. מי שמשתמש ב-Spring לא משתמש ב-CDI, אלא ב-DI, כלומר מדובר במפרט מעט שונה. אבל לדעת ולהבין את האמור לעיל, אתה יכול בקלות לשנות את דעתך. בהתחשב בכך ש-Spring תומך בביאורים מעולם ה-CDI (אותו Inject). חומרים נוספים: #ויאצ'סלב
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION