Trong bài trước, tôi đã giới thiệu với các bạn cơ bản về Google Guice. Trong bài này, chúng ta sẽ cùng tìm hiểu chi tiết hơn về các loại Binding được hỗ trợ bởi Google Guice.
Nội dung
Linked Bindings
Linked bindings (ràng buộc) ánh xạ một type với một implementation của nó. Cú pháp:
bind(Type.class).to(Implementation.class);
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.linked; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println("Email message: " + message); } } class Customer1EmailService extends EmailService { @Override public void sendMessage(String message) { System.out.println("Customer 1 email message: " + message); } } class FirstModule extends AbstractModule { @Override protected void configure() { bind(MessageService.class).to(EmailService.class); bind(EmailService.class).to(Customer1EmailService.class); } } class UserController { private MessageService messageService; @Inject public UserController(MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Linked Binding example"); } } public class LinkedBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); UserController userController = injector.getInstance(UserController.class); userController.send(); // Customer 1 email message: Linked Binding example } }
Binding Annotations
Trong trường hợp, chúng ta muốn map một Type với nhiều implementation. Chúng ta có tạo một custom Annotation để sử dụng. Cú pháp:
@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) @interface CustomAnnotation {} bind(Type.class).annotatedWith(CustomAnnotation.class).to(Implementation.class);
Trong đó:
- @BindingAnnotation : đánh dấu đây là một binding annotation.
- @Target : cho biết phạm vi sử dụng của Annotation này là FIELD, PARAMETER, METHOD.
- @Retention : để chú thích mức độ tồn tại của annotation này là tại thời điểm runtime.
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.custom_annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import com.google.inject.AbstractModule; import com.google.inject.BindingAnnotation; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; @BindingAnnotation @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @interface SmsMessageService {} interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println("Email message: " + message); } } class SmsService implements MessageService { @Override public void sendMessage(String message) { System.out.println("Sms message: " + message); } } class FirstModule extends AbstractModule { @Override protected void configure() { bind(MessageService.class).to(EmailService.class); bind(MessageService.class).annotatedWith(SmsMessageService.class).to(SmsService.class); } } class User1Controller { private MessageService messageService; @Inject public User1Controller(MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Default annotation binding example"); } } class User2Controller { private MessageService messageService; @Inject public User2Controller(@SmsMessageService MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Sms annotation binding example"); } } public class CustomAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); User1Controller user1Controller = injector.getInstance(User1Controller.class); user1Controller.send(); // Email message: Default annotation binding example User2Controller user2Controller = injector.getInstance(User2Controller.class); user2Controller.send(); // Sms message: Sms annotation binding example } }
@Named Binding
Guice cung cấp một cách khác để map binding mà không cần tạo custom annotation là sử dụng @Named annotation. Cú pháp:
bind(Type.class).annotatedWith(Names.named("your_name")).to(Implementation.class);
Ví dụ: tương tự như ví dụ Binding Annotations trên, chúng ta chỉ việc thay thế @SmsMessageService bằng @Named(“SmsMessageService”)
import com.google.inject.name.Named; import com.google.inject.name.Names; class FirstModule extends AbstractModule { @Override protected void configure() { bind(MessageService.class).to(EmailService.class); bind(MessageService.class).annotatedWith(Names.named("SmsMessageService")).to(SmsService.class); } } class User2Controller { private MessageService messageService; @Inject public User2Controller(@Named("SmsMessageService") MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Sms annotation binding example"); } }
Constant Bindings
Ngoài cách binding với các class/ interface, Guice còn cung cấp một cách để tạo binding với một giá trị của một object hay constant. Cú pháp:
bind(Type.class).annotatedWith(Names.named("your_name")).toInstance(your_value);
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.constant; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.name.Named; import com.google.inject.name.Names; class FirstModule extends AbstractModule { @Override protected void configure() { bind(String.class).annotatedWith(Names.named("message")).toInstance("Hello constant bindings"); } } class UserController { private String message; @Inject public UserController(@Named("message") String message) { this.message = message; } public void send() { System.out.println(message); } } public class ConstantAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); UserController userController = injector.getInstance(UserController.class); userController.send(); // Hello constant bindings } }
Constant Bindings với Generic Type
Đối với constant với giá trị là một Generic type, chẳng hạn List<String>. Chúng ta có thể sử dụng một trong các cách sau:
- Sử dụng TypeLiteral.
- Sử dụng phương thức @Provides.
- Sử dụng Provide class.
Ví dụ sử dụng TypeLiteral:
package com.gpcoder.patterns.creational.googleguice.binding.constant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provides; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; class SecondModule extends AbstractModule { private final List<String> emails = Arrays.asList("email1", "email2"); @Override protected void configure() { bind(new TypeLiteral<List<String>>() {}).annotatedWith(Names.named("messages")).toInstance(emails); } } class EmailController { private List<String> messages; @Inject public EmailController(@Named("messages") List<String> messages) { this.messages = messages; } public void send() { System.out.println(messages); } } public class GenericTypeConstantAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new SecondModule()); EmailController emailController = injector.getInstance(EmailController.class); emailController.send(); // [email1, email2] } }
Đối với @Provides Annotation, chỉ đơn giản như sau:
class SecondModule extends AbstractModule { private final List<String> emails = Arrays.asList("email1", "email2"); @Override protected void configure() { // Do nothing } @Provides @Named("messages") public List<String> providesListOfString() { return emails; } }
@Provides Annotation
Guice cung cấp cho chúng ta để khởi tạo một binding value phức tạp thông qua @provides annotation.
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.provide_annotation; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provides; interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println("Email message: " + message); } } class FirstModule extends AbstractModule { @Override protected void configure() { // Do nothing } @Provides public MessageService provideMessageService() { // Do some complicated task return new EmailService(); } } class UserController { private MessageService messageService; @Inject public UserController(MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Provides Annotation Binding example"); } } public class ProvidesAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); UserController userController = injector.getInstance(UserController.class); userController.send(); // Email message: Provides Annotation Binding example } }
Provider Class
Trong trường hợp có nhiều @Provide method được định nghĩa trong một module, làm cho class này quá phức tạp, chúng ta có thể move nó sang một class độc lập và implement một Provider interface.
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.provide_annotation; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println("Email message: " + message); } } class MessageServiceProvider implements Provider<MessageService> { @Override public MessageService get() { // Do some complicated task return new EmailService(); } } class FirstModule extends AbstractModule { @Override protected void configure() { bind(MessageService.class).toProvider(MessageServiceProvider.class); } } class UserController { private MessageService messageService; @Inject public UserController(MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Provider Class Binding example"); } } public class ProviderClassBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); UserController userController = injector.getInstance(UserController.class); userController.send(); // Email message: Provider Class Binding example } }
Untargeted Bindings
Chúng ta có thể tạo một binding mà không có target, nghĩa là không cần gọi phương thức to(). Nó hữu ích cho các type cụ thể được chú thích bởi @ImplementedBy hoặc @ProvidedBy. Hai annotation này sẽ được giới thiệu ở phần tiếp theo của bài viết (Just-in-time Bindings).
Một untargeted binding thông báo cho injector biết về Type có thể được inject, vì vậy nó chuẩn bị các dependency một cách eager, không cần đến Just-in-time Bindings.
Ví dụ:
@ImplementedBy(EmailService.class) interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println(message); } } class BaseModule extends AbstractModule { @Override protected void configure() { bind(MessageService.class); } }
Khi xác định binding một Type với một annotation, chúng ta bắt buộc phải xác định target ngay cả khi type và target class là giống nhau.
Ví dụ:
class BaseModule extends AbstractModule { @Override protected void configure() { bind(EmailService.class).annotatedWith(Names.named("emailService")).to(EmailService.class); } }
Constructor Bindings
Trong trường hợp một class có nhiều constructor, Guice cung cấp cho chúng ta một cách để tạo binding với constructor cụ thể của object thông qua phương thức toConstructor().
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.constructor; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.name.Named; import com.google.inject.name.Names; interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { private String email; public EmailService() { this.email = "Default email"; } public EmailService(@Named("email") String email) { this.email = email; } @Override public void sendMessage(String message) { System.out.println(email + ": " + message); } } class FirstModule extends AbstractModule { @Override protected void configure() { bind(String.class).annotatedWith(Names.named("email")).toInstance("gpcodervn@gmail.com"); // bind(MessageService.class).to(EmailService.class); try { bind(MessageService.class).toConstructor(EmailService.class.getConstructor(String.class)); } catch (NoSuchMethodException | SecurityException e) { e.printStackTrace(); } } } class UserController { private MessageService messageService; @Inject public UserController(MessageService messageService) { this.messageService = messageService; } public void send() { messageService.sendMessage("Linked Binding example"); } } public class ConstructorBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new FirstModule()); UserController userController = injector.getInstance(UserController.class); userController.send(); // Hello constant bindings } }
Inbuilt Bindings
Guice cung cấp built-in (được xây dựng bên trong Guice) binding cho java.util.logging.Logger class. Tên của Logger được tự động đặt thành tên của lớp mà Logger được đưa vào.
Ví dụ:
package com.gpcoder.patterns.creational.googleguice.binding.builtin; import java.util.logging.Logger; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; class UserController { private Logger logger; @Inject public UserController(Logger logger) { this.logger = logger; } public void log(String message) { logger.info(message); } } public class BuiltInBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(); UserController userController = injector.getInstance(UserController.class); userController.log("Hello built-in bindings"); } }
Just-in-time Bindings
Các binding được định nghĩa trong Module, Guice sử dụng chúng bất cứ khi nào cần tiêm phụ thuộc (inject). Trong trường hợp các binding không tồn tại, nó có thể cố gắng tạo ra các binding chỉ khi cần thiết (JIT – just-in-time). Các binding được định nghĩa trong Module được gọi là các ràng buộc rõ ràng (explicit binding) và có độ ưu tiên cao hơn. Trong khi các JIT binding được gọi là các ràng buộc ngầm (implicit binding). Nếu cả hai loại binding đều tồn tại, explicit binding sẽ được ưu tiên để sử dụng.
Có 3 loại JIT binding:
- Injectable Constructors : các constructor với phạm vi truy cập không phải là private, không đối số có đủ điều kiện cho các JIT binding. Một cách khác là đánh dấu @Inject annotation với constructor.
- @ImplementatedBy annotation : cho biết thông tin về class implementation cụ thể, không cần khai báo trong Module.
- @ProvidedBy annotation : cho biết thông tin về provider của class implementation cụ thể, không cần khai báo trong Module.
Ví dụ Injectable Constructors
Ví dụ Inbuilt Bindings là một trường hợp Injectable Constructor sử dụng @Inject annotation.
Trong ví dụ bên dưới, chúng ta sẽ sử dụng Injectable Constructor với constructor với phạm vi truy cập không phải là private, không đối số.
package com.gpcoder.patterns.creational.googleguice.binding.builtin; import java.util.logging.Logger; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; class UserController { @Inject private Logger logger; public void log(String message) { logger.info(message); } } public class BuiltInBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(); UserController userController = injector.getInstance(UserController.class); userController.log("Hello built-in bindings"); } }
Nếu trong class trên, bạn cố gắng tạo constructor với phạm vi truy cập private hoặc không có hàm khởi tạo không đối số và không sử dụng @Inject, thì bạn sẽ gặp exception sau:
Exception in thread "main" com.google.inject.ConfigurationException: Guice configuration errors: 1) Could not find a suitable constructor in com.gpcoder... . Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
Ví dụ @ImplementatedBy annotation
package com.gpcoder.patterns.creational.googleguice.binding.implementatedby_annotation; import com.google.inject.Guice; import com.google.inject.ImplementedBy; import com.google.inject.Inject; import com.google.inject.Injector; @ImplementedBy(EmailService.class) interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println(message); } } class UserController { @Inject private MessageService messageService; public void send() { messageService.sendMessage("@ImplementatedBy annotation binding example"); } } public class ImplementatedByAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(); UserController userController = injector.getInstance(UserController.class); userController.send(); // @ImplementatedBy annotation binding example } }
Ví dụ @ProvidedBy annotation
package com.gpcoder.patterns.creational.googleguice.binding.providedby_annotation; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.ProvidedBy; import com.google.inject.Provider; @ProvidedBy(MessageServiceProvider.class) interface MessageService { void sendMessage(String message); } class EmailService implements MessageService { @Override public void sendMessage(String message) { System.out.println(message); } } class MessageServiceProvider implements Provider<MessageService> { @Override public MessageService get() { // Do some complicated task return new EmailService(); } } class UserController { @Inject private MessageService messageService; public void send() { messageService.sendMessage("@ProvidedBy annotation binding example"); } } public class ProvidedByAnnotationBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(); UserController userController = injector.getInstance(UserController.class); userController.send(); // @ProvidedBy annotation binding example } }
Overriding Binding trong Guice
Trong Guice, một service đã được binding chúng ta không thể binding một lần thứ 2. Ví dụ:
Các service và module ở project cha (base). Trong project này chúng ta có các ServiceA, ServiceB và các implement tương ứng của nó (ConcreteA, ConcreteB). Chúng ta có một BaseLogic có dependency với ServiceA và ServiceB.
public interface ServiceA { void log(); } public class ConcreteA implements ServiceA { @Override public void log() { System.out.println("ConcreteA"); } } public interface ServiceB { void sendMail(); } public class ConcreteB implements ServiceB { @Override public void sendMail() { System.out.println("ConcreteB"); } } public class BaseLogic { @Inject private ServiceA serviceA; @Inject private ServiceB serviceB; public void execute() { serviceA.log(); serviceB.sendMail(); } } public class BaseModule extends AbstractModule { @Override protected void configure() { bind(ServiceA.class).to(ConcreteA.class); bind(ServiceB.class).to(ConcreteB.class); } }
Các service và module ở project con (customer1). Trong project con này, chúng ta kế thừa lại tất cả các service ở project base, nhưng có thêm một serviceC và ở ServiceB cần custom lại đôi chút (Customer1ConcreteB). Logic ở project con có khác biệt đôi chút, nó cần sử dụng thêm ServiceC và Customer1ConcreteB.
public interface ServiceC { void save(); } public class ConcreteC implements ServiceC { @Override public void save() { System.out.println("ConcreteC"); } } public class Customer1ConcreteB extends ConcreteB { @Override public void sendMail() { System.out.println("Customer1ConcreteB"); } public void doAnythingElse() { // Do any thing else } } public class Customer1Logic extends BaseLogic { @Inject private ServiceC serviceC; @Override public void execute() { super.execute(); serviceC.save(); } } public class Customer1Module extends BaseModule { @Override protected void configure() { super.configure(); bind(ServiceC.class).to(ConcreteC.class); bind(ServiceB.class).to(Customer1ConcreteB.class); // ServiceB was already configured } }
Chương trình chính chúng ta như sau:
public class OverrideBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new Customer1Module()); BaseLogic customer1Logic = injector.getInstance(Customer1Logic.class); customer1Logic.execute(); } }
Chương trình trên sẽ throw một ngoại lệ CreationException như sau:
Exception in thread "main" com.google.inject.CreationException: Unable to create injector, see the following errors: 1) A binding to com.gpcoder....ServiceB was already configured at com.gpcoder...BaseModule.configure(OverrideBindingExample.java:34). at com.gpcoder...Customer1Module.configure(OverrideBindingExample.java:65)
Ngoại lệ trên xảy ra do ServiceB đã được binding ở BaseModule, trong Customer1Module chúng ta gọi binding lại lần nữa cho ServiceB nên xảy ra lỗi.
Để giải quyết vấn đề trên, chúng ta có thể sử dụng một trong những cách sau:
- Sử dụng Linked binding: cách này có hạn chế là service class ở project con phải extend từ service cha.
- Thiết kế các module theo cách mà chúng ta không cần override. Có nghĩa là chia nhỏ module, các service có thể override tách ra một module khác, trong project con chúng ta sẽ binding các module chung và binding một module khác ở con.
- Sử dụng phương thức override module được hỗ trợ bởi Guice: Modules.override(basic_module).with(override_module)
Sử dụng Linked binding
Đơn giản chỉ việc sửa lại Customer1Module như sau:
package com.gpcoder.patterns.creational.googleguice.binding.override.module_v2; import com.gpcoder.patterns.creational.googleguice.binding.override.base_service.ConcreteB; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.ConcreteC; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.Customer1ConcreteB; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.ServiceC; public class Customer1Module extends BaseModule { @Override protected void configure() { super.configure(); bind(ServiceC.class).to(ConcreteC.class); bind(ConcreteB.class).to(Customer1ConcreteB.class); // ServiceB -> ConcreteB -> Customer1ConcreteB } }
Thực thi chương trình trên chúng ta có kết quả như sau:
ConcreteA Customer1ConcreteB ConcreteC
Chia nhỏ module
Chúng ta cố gắng gom nhóm các service ra thành từng Module riêng lẻ (trường hợp lý tưởng là ở project con không cần override lại module), ở project con chỉ việc tạo injector với các Module cần thiết.
Chia nhỏ BaseModule thành BaseModule1 và BaseModule2:
public class BaseModule1 extends AbstractModule { @Override protected void configure() { bind(ServiceA.class).to(ConcreteA.class); } } public class BaseModule2 extends AbstractModule { @Override protected void configure() { bind(ServiceB.class).to(ConcreteB.class); } }
Ở project con chỉ khai báo các binding ứng với các service mới của nó.
public class Customer1Module extends AbstractModule { @Override protected void configure() { bind(ServiceC.class).to(ConcreteC.class); bind(ServiceB.class).to(Customer1ConcreteB.class); } }
Chương trình chính của chúng ta bây giờ như sau:
package com.gpcoder.patterns.creational.googleguice.binding.override.module_v3; import com.google.inject.Guice; import com.google.inject.Injector; import com.gpcoder.patterns.creational.googleguice.binding.override.base_service.BaseLogic; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.Customer1Logic; public class OverrideBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector(new BaseModule1(), new Customer1Module()); BaseLogic customer1Logic = injector.getInstance(Customer1Logic.class); customer1Logic.execute(); } }
Sử dụng Modules.override()
Giữ nguyên khai báo Module ở lớp cha. Ở project con chỉ khai báo các binding ứng với các service mới của nó.
import com.google.inject.AbstractModule; import com.gpcoder.patterns.creational.googleguice.binding.override.base_service.ServiceB; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.ConcreteC; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.Customer1ConcreteB; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.ServiceC; public class Customer1Module extends AbstractModule { @Override protected void configure() { bind(ServiceC.class).to(ConcreteC.class); bind(ServiceB.class).to(Customer1ConcreteB.class); } }
Sử dụng phương thức Modules.override() để override các binding.
package com.gpcoder.patterns.creational.googleguice.binding.override.module_v4; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.util.Modules; import com.gpcoder.patterns.creational.googleguice.binding.override.base_service.BaseLogic; import com.gpcoder.patterns.creational.googleguice.binding.override.customer1_service.Customer1Logic; public class OverrideBindingExample { public static void main(String[] args) { Injector injector = Guice.createInjector( Modules.override(new BaseModule()).with(new Customer1Module())); BaseLogic customer1Logic = injector.getInstance(Customer1Logic.class); customer1Logic.execute(); } }
Trên đây là các kiến thức cơ bản về các loại Binding được hỗ trợ bởi Google Guice. Trong bài viết tiếp theo chúng ta sẽ cùng tìm hiểu chi tiết về các loại Injection, Scope trong Guice.
Tài liệu tham khảo:
- https://github.com/google/guice
- https://www.tutorialspoint.com/guice/index.htm
- https://www.baeldung.com/guice