Giới thiệu về CDI (Contexts and Dependency Injection) trong Java

1. Khái quát chung

CDI (Contexts and Dependency Injection) là một khung tiêm phụ thuộc tiêu chuẩn có trong Java EE 6 trở lên.

Nó cho phép chúng tôi quản lý vòng đời của các thành phần trạng thái thông qua các ngữ cảnh vòng đời dành riêng cho miền và đưa các thành phần (dịch vụ) vào các đối tượng khách theo cách an toàn về kiểu.

Trong hướng dẫn này, chúng ta sẽ xem xét sâu hơn các tính năng có liên quan nhất của CDI và triển khai các cách tiếp cận khác nhau để đưa các phụ thuộc vào các lớp khách hàng.

2. DYDI (Do-it-You-Yourself Dependency Injection)

Tóm lại, có thể triển khai DI mà không cần sử dụng đến bất kỳ khuôn khổ nào.

Cách tiếp cận này được gọi phổ biến là DYDI (Do-it-Yourself Dependency Injection).

Với DYDI, chúng tôi giữ cho mã ứng dụng được tách biệt khỏi việc tạo đối tượng bằng cách chuyển các phụ thuộc bắt buộc vào các lớp máy khách thông qua các nhà máy / nhà xây dựng cũ đơn giản.

Đây là cách triển khai DYDI cơ bản có thể trông như thế nào:

public interface TextService { String doSomethingWithText(String text); String doSomethingElseWithText(String text); }
public class SpecializedTextService implements TextService { ... }
public class TextClass { private TextService textService; // constructor }
public class TextClassFactory { public TextClass getTextClass() { return new TextClass(new SpecializedTextService(); } }

Tất nhiên, DYDI phù hợp với một số trường hợp sử dụng tương đối đơn giản.

Nếu ứng dụng mẫu của chúng tôi ngày càng tăng về quy mô và độ phức tạp, triển khai một mạng lưới lớn hơn các đối tượng được kết nối với nhau, chúng tôi sẽ làm ô nhiễm nó với hàng tấn nhà máy biểu đồ đối tượng.

Điều này sẽ yêu cầu rất nhiều mã soạn sẵn chỉ để tạo đồ thị đối tượng. Đây không phải là một giải pháp có thể mở rộng hoàn toàn.

Chúng ta có thể làm DI tốt hơn không? Tất nhiên, chúng tôi có thể. Đây chính xác là nơi CDI xuất hiện trong bức tranh.

3. Một ví dụ đơn giản

CDI biến DI thành một quy trình không cần trí tuệ, chỉ đơn giản là trang trí các lớp dịch vụ với một vài chú thích đơn giản và xác định các điểm tiêm tương ứng trong các lớp khách hàng.

Để giới thiệu cách CDI triển khai DI ở mức cơ bản nhất, giả sử rằng chúng ta muốn phát triển một ứng dụng chỉnh sửa tệp hình ảnh đơn giản. Có khả năng mở, chỉnh sửa, ghi, lưu tệp hình ảnh, v.v.

3.1. Các “beans.xml” file

Đầu tiên, chúng ta phải đặt tệp “bean.xml” trong thư mục “src / main / resources / META-INF /” . Ngay cả khi tệp này không chứa bất kỳ chỉ thị DI cụ thể nào, nó vẫn cần thiết để thiết lập và chạy CDI :

3.2. Các lớp dịch vụ

Tiếp theo, hãy tạo các lớp dịch vụ thực hiện các thao tác trên tệp được đề cập ở trên trên các tệp GIF, JPG và PNG:

public interface ImageFileEditor { String openFile(String fileName); String editFile(String fileName); String writeFile(String fileName); String saveFile(String fileName); }
public class GifFileEditor implements ImageFileEditor { @Override public String openFile(String fileName) { return "Opening GIF file " + fileName; } @Override public String editFile(String fileName) { return "Editing GIF file " + fileName; } @Override public String writeFile(String fileName) { return "Writing GIF file " + fileName; } @Override public String saveFile(String fileName) { return "Saving GIF file " + fileName; } }
public class JpgFileEditor implements ImageFileEditor { // JPG-specific implementations for openFile() / editFile() / writeFile() / saveFile() ... }
public class PngFileEditor implements ImageFileEditor { // PNG-specific implementations for openFile() / editFile() / writeFile() / saveFile() ... }

3.3. Lớp khách hàng

Cuối cùng, hãy triển khai một lớp khách hàng có triển khai ImageFileEditor trong phương thức khởi tạo và chúng ta hãy xác định một điểm tiêm với chú thích @Inject :

public class ImageFileProcessor { private ImageFileEditor imageFileEditor; @Inject public ImageFileProcessor(ImageFileEditor imageFileEditor) { this.imageFileEditor = imageFileEditor; } }

Nói một cách đơn giản, chú thích @Inject là workhorse thực sự của CDI. Nó cho phép chúng ta xác định các điểm tiêm trong các lớp khách hàng.

Trong trường hợp này, @Inject hướng dẫn CDI đưa vào thực thi ImageFileEditor trong hàm tạo.

Hơn nữa, cũng có thể đưa một dịch vụ vào bằng cách sử dụng chú thích @Inject trong các trường (chèn trường) và bộ định tuyến (chèn bộ định tuyến). Chúng ta sẽ xem xét các tùy chọn này sau.

3.4. Xây dựng Đồ thị Đối tượng ImageFileProcessor Với Weld

Tất nhiên, chúng ta cần đảm bảo rằng CDI sẽ đưa việc triển khai ImageFileEditor phù hợp vào phương thức khởi tạo lớp ImageFileProcessor .

Để làm như vậy, trước tiên, chúng ta nên lấy một thể hiện của lớp.

Vì chúng tôi sẽ không dựa vào bất kỳ máy chủ ứng dụng Java EE nào để sử dụng CDI, chúng tôi sẽ thực hiện điều này với Weld, triển khai tham chiếu CDI trong Java SE :

public static void main(String[] args) { Weld weld = new Weld(); WeldContainer container = weld.initialize(); ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get(); System.out.println(imageFileProcessor.openFile("file1.png")); container.shutdown(); } 

Ở đây, chúng ta đang tạo một đối tượng WeldContainer , sau đó lấy một đối tượng ImageFileProcessor và cuối cùng gọi phương thức openFile () của nó .

Như mong đợi, nếu chúng tôi chạy ứng dụng, CDI sẽ phàn nàn rất lớn bằng cách ném DeploymentException:

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...

Chúng tôi nhận được ngoại lệ này vì CDI không biết triển khai ImageFileEditor nào để đưa vào hàm tạo ImageFileProcessor .

Trong thuật ngữ của CDI , điều này được gọi là một ngoại lệ tiêm không rõ ràng .

3.5. Các @Default@Alternative Chú thích

Solving this ambiguity is easy. CDI, by default, annotates all the implementations of an interface with the @Default annotation.

So, we should explicitly tell it which implementation should be injected into the client class:

@Alternative public class GifFileEditor implements ImageFileEditor { ... }
@Alternative public class JpgFileEditor implements ImageFileEditor { ... } 
public class PngFileEditor implements ImageFileEditor { ... }

In this case, we've annotated GifFileEditor and JpgFileEditor with the @Alternative annotation, so CDI now knows that PngFileEditor (annotated by default with the @Default annotation) is the implementation that we want to inject.

If we rerun the application, this time it'll be executed as expected:

Opening PNG file file1.png 

Furthermore, annotating PngFileEditor with the @Default annotation and keeping the other implementations as alternatives will produce the same above result.

This shows, in a nutshell, how we can very easily swap the run-time injection of implementations by simply switching the @Alternative annotations in the service classes.

4. Field Injection

CDI supports both field and setter injection out of the box.

Here's how to perform field injection (the rules for qualifying services with the @Default and @Alternative annotations remain the same):

@Inject private final ImageFileEditor imageFileEditor;

5. Setter Injection

Similarly, here's how to do setter injection:

@Inject public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. The @Named Annotation

So far, we've learned how to define injection points in client classes and inject services with the @Inject, @Default , and @Alternative annotations, which cover most of the use cases.

Nevertheless, CDI also allows us to perform service injection with the @Named annotation.

This method provides a more semantic way of injecting services, by binding a meaningful name to an implementation:

@Named("GifFileEditor") public class GifFileEditor implements ImageFileEditor { ... } @Named("JpgFileEditor") public class JpgFileEditor implements ImageFileEditor { ... } @Named("PngFileEditor") public class PngFileEditor implements ImageFileEditor { ... }

Now, we should refactor the injection point in the ImageFileProcessor class to match a named implementation:

@Inject public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

It's also possible to perform field and setter injection with named implementations, which looks very similar to using the @Default and @Alternative annotations:

@Inject private final @Named("PngFileEditor") ImageFileEditor imageFileEditor; @Inject public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. The @Produces Annotation

Sometimes, a service requires some configuration to be fully-initialized before it gets injected to handle additional dependencies.

CDI provides support for these situations, through the @Produces annotation.

@Produces allows us to implement factory classes, whose responsibility is the creation of fully-initialized services.

To understand how the @Produces annotation works, let's refactor the ImageFileProcessor class, so it can take an additional TimeLogger service in the constructor.

The service will be used for logging the time at which a certain image file operation is performed:

@Inject public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... } public String openFile(String fileName) { return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime(); } // additional image file methods 

In this case, the TimeLogger class takes two additional services, SimpleDateFormat and Calendar:

public class TimeLogger { private SimpleDateFormat dateFormat; private Calendar calendar; // constructors public String getTime() { return dateFormat.format(calendar.getTime()); } }

How do we tell CDI where to look at for getting a fully-initialized TimeLogger object?

We just create a TimeLogger factory class and annotate its factory method with the @Produces annotation:

public class TimeLoggerFactory { @Produces public TimeLogger getTimeLogger() { return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance()); } }

Whenever we get an ImageFileProcessor instance, CDI will scan the TimeLoggerFactory class, then call the getTimeLogger() method (as it's annotated with the @Produces annotation), and finally inject the Time Logger service.

If we run the refactored sample application with Weld, it'll output the following:

Opening PNG file file1.png at: 17:46

8. Custom Qualifiers

CDI supports the use of custom qualifiers for qualifying dependencies and solving ambiguous injection points.

Custom qualifiers are a very powerful feature. They not only bind a semantic name to a service, but they bind injection metadata too. Metadata such as the RetentionPolicy and the legal annotation targets (ElementType).

Let's see how to use custom qualifiers in our application:

@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface GifFileEditorQualifier {} 
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface JpgFileEditorQualifier {} 
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface PngFileEditorQualifier {} 

Now, let's bind the custom qualifiers to the ImageFileEditor implementations:

@GifFileEditorQualifier public class GifFileEditor implements ImageFileEditor { ... } 
@JpgFileEditorQualifier public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier public class PngFileEditor implements ImageFileEditor { ... } 

Lastly, let's refactor the injection point in the ImageFileProcessor class:

@Inject public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... } 

If we run our application once again, it should generate the same output shown above.

Custom qualifiers provide a neat semantic approach for binding names and annotation metadata to implementations.

In addition, custom qualifiers allow us to define more restrictive type-safe injection points (outperforming the functionality of the @Default and @Alternative annotations).

If only a subtype is qualified in a type hierarchy, then CDI will only inject the subtype, not the base type.

9. Conclusion

Không nghi ngờ gì nữa, CDI làm cho việc tiêm phụ thuộc trở nên không cần bàn cãi , chi phí của các chú thích thêm là rất ít nỗ lực để đạt được lợi ích của việc tiêm phụ thuộc có tổ chức.

Đôi khi DYDI vẫn có vị trí của nó trên CDI. Giống như khi phát triển các ứng dụng khá đơn giản chỉ chứa các đồ thị đối tượng đơn giản.

Như mọi khi, tất cả các mẫu mã hiển thị trong bài viết này đều có sẵn trên GitHub.