Giới thiệu Google Guice – Injection, Scope

Trong bài trước, tôi đã giới thiệu với các bạn các loại binding được hỗ trợ bởi Google Guice. Trong bài này, chúng ta sẽ cùng tìm hiểu về các loại Injection và Scope được hỗ trợ bởi Guice.

1. Injection

Dependency Injection Pattern tách hành vi khỏi các phụ thuộc (dependency). Thay vì tìm kiếm các phụ thuộc trực tiếp hoặc từ các factory, mẫu này khuyến nghị rằng các phụ thuộc được truyền vào từ bên ngoài. Quá trình thiết lập các phụ thuộc từ bên ngoài vào một đối tượng được gọi là tiêm (injection).

Trong Guice để inject một đối tượng chúng ta sẽ sử dụng @Inject annotation.

1.1. Các loại Injection được hỗ trợ bởi Google Guice

  • Constructor Injection : đối tượng được inject thông qua constructor.
  • Method Injection : đối tượng được inject thông qua method.
  • Field Injection : đối tượng được inject thông qua field.
  • Optional Injection : đối tượng được inject được truyền từ bên ngoài thông qua constructor, method, field hoặc sử dụng một giá trị mặc định.
  • On-demand Injection : các method hoặc field có thể được khởi tạo bằng cách sử dụng một instance đã tồn tại thông qua phương thức injector.injectMembers().
  • Static Injection : đối tượng được inject thông qua static field, loại injection này thích hợp khi migrate ứng dụng sử dụng static factory sang Guice.
  • Injecting Provider : thông thường mỗi loại (type) sẽ nhận được chính xác một thể hiện của từng loại phụ thuộc. Đôi khi chúng ta muốn có nhiều hơn một instance của type phụ thuộc. Guice cúng cấp một Provider với phương thức get(). Một instance mới được tạo ra khi phương thức get() được gọi.

1.1.1. Constructor Injection

class UserController {
    private MessageService messageService;
  
    @Inject
    public UserController(MessageService messageService) {
        this.messageService = messageService;
    }
  
    public void send() {
        messageService.sendMessage("Linked Binding example");
    }
}

1.1.2. Method Injection

class UserController {
     
    private MessageService messageService;
 
    @Inject
    public void setMessageService(MessageService messageService) {
        this.messageService = messageService;
    }
 
    public void send() {
        messageService.sendMessage("Linked Binding example");
    }
}

1.1.3. Field Injection

class UserController {
    @Inject
    private MessageService messageService;
  
    public void send() {
        messageService.sendMessage("Linked Binding example");
    }
}

1.1.4. Optional Injection

package com.maixuanviet.patterns.creational.googleguice.injection.optional;
 
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 BaseModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(MessageService.class).to(Customer1EmailService.class);
    }
}
class UserController {
    @Inject(optional=true)
    private MessageService messageService = new EmailService();
  
    public void send() {
        messageService.sendMessage("Optional injection example");
    }
}
public class OptionalInjectionExample {
    public static void main(String[] args) {
        // Inject by default
        Injector injector = Guice.createInjector();
        UserController userController = injector.getInstance(UserController.class);
        userController.send(); // Email message: Optional injection example
         
        // Inject by config
        injector = Guice.createInjector(new BaseModule());
        userController = injector.getInstance(UserController.class);
        userController.send(); // Customer 1 email message: Optional injection example
    }
}

1.1.5. On-demand Injection

package com.maixuanviet.patterns.creational.googleguice.injection.on_demand;
 
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.ImplementedBy;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
 
@ImplementedBy(EmailService.class)
interface MessageService {
    void sendMessage(String message);
}
class EmailService implements MessageService {
 
    @Inject
    @Named("signature")
    private String signature;
     
    @Override
    public void sendMessage(String message) {
        System.out.println("Email message: " + message + " by " + signature);
    }
}
 
class BaseModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(String.class).annotatedWith(Names.named("signature")).toInstance("maixuanviet.com");
    }
}
class UserController {
     
    private MessageService messageService;
  
    public UserController(MessageService messageService) {
        this.messageService = messageService;
    }
 
    public void send() {
        messageService.sendMessage("On-Demand injection example");
    }
}
public class OnDemandInjectionExample {
    public static void main(String[] args) {
        // Use exiting object
        EmailService emailService = new EmailService();
 
        // Create injector
        Injector injector = Guice.createInjector(new BaseModule());
         
        // initialize methods and fields
        injector.injectMembers(emailService);
         
        // Use emailService as a parameter of constructor  
        UserController userController = new UserController(emailService);
        userController.send(); // Email message: On-Demand injection example by maixuanviet.com
    }
}

Như bạn thấy, chúng ta sử dụng phương thức injectMembers() để inject các field và method có đánh dấu là @Inject của EmailService đã được khởi tạo. Sau khi đã inject đầy đủ các giá trị, chúng ta có thể sử dụng trong UserController như một cách thông thường, không cần sử dụng injector cho UserController.

1.1.6. Static Injection

Trong ví dụ bên dưới tôi sẽ giới thiệu với các bạn cách Migrate một Static Factory Class sang Guice.

Giả sử chúng ta có một class đang sử sụng Static Factory Pattern như bên dưới:

package com.maixuanviet.patterns.creational.googleguice.injection.static_injection.factory_pattern;
 
interface Bank {
    String getBankName();
}
 
class TPBank implements Bank {
    @Override
    public String getBankName() {
        return "TPBank";
    }
}
 
class VietcomBank implements Bank {
    @Override
    public String getBankName() {
        return "VietcomBank";
    }
}
 
enum BankType {
    VIETCOMBANK, TPBANK;
}
 
class BankFactory {
      
    private static Bank tpBank = new TPBank();
    private static Bank vietcomBank = new VietcomBank();
     
    private BankFactory() {
        throw new UnsupportedOperationException();
    }
  
    public static final Bank getBank(BankType bankType) {
        switch (bankType) {
  
        case TPBANK:
            return tpBank;
  
        case VIETCOMBANK:
            return vietcomBank;
  
        default:
            throw new IllegalArgumentException("This bank type is unsupported");
        }
    }
}
 
public class StaticInjectionExample {
    public static void main(String[] args) {
        Bank bank = BankFactory.getBank(BankType.TPBANK);
        System.out.println(bank.getBankName()); // TPBank
    }
}

Chuyển sang Guice như sau:

package com.maixuanviet.patterns.creational.googleguice.injection.static_injection.guice;
 
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
 
interface Bank {
    String getBankName();
}
 
class TPBank implements Bank {
    @Override
    public String getBankName() {
        return "TPBank";
    }
}
 
class VietcomBank implements Bank {
    @Override
    public String getBankName() {
        return "VietcomBank";
    }
}
 
enum BankType {
    VIETCOMBANK, TPBANK;
}
 
class BankFactory {
     
    @Inject
    @Named("tpBank")
    private static Bank tpBank ;
     
    @Inject
    @Named("vietcomBank")
    private static Bank vietcomBank;
     
    private BankFactory() {
        throw new UnsupportedOperationException();
    }
  
    public static final Bank getBank(BankType bankType) {
        switch (bankType) {
  
        case TPBANK:
            return tpBank;
  
        case VIETCOMBANK:
            return vietcomBank;
  
        default:
            throw new IllegalArgumentException("This bank type is unsupported");
        }
    }
}
 
class BaseModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Bank.class).annotatedWith(Names.named("tpBank")).toInstance(new TPBank());
        bind(Bank.class).annotatedWith(Names.named("vietcomBank")).toInstance(new VietcomBank());
        requestStaticInjection(BankFactory.class);
    }
}
 
public class StaticInjectionExample {
    public static void main(String[] args) {
        Guice.createInjector(new BaseModule());
        Bank bank = BankFactory.getBank(BankType.TPBANK);
        System.out.println(bank.getBankName()); // TPBank
    }
}

Lưu ý: một phương thức quan trọng để hỗ trợ Injection cho static field là phương thức requestStaticInjection() trong class Module.

1.1.7. Injecting Provider

Các bạn hãy xem lại ví dụ @Provides Annotation và Provider Class ở bài viết trước.

1.2. @AssistedInject

@AssistedInject cho phép chúng ta cấu hình một dynamic factory để gán giá trị các parameter của một class thay vì code chúng một cách thủ công. Nó thường được sử dụng khi chúng ta có một object mà có các dependency được inject bởi Guice và các parameter được xác định trong quá trình khởi tạo object.

Để sử dụng @AssistedInject chúng ta cần thêm thư viện com.google.inject.extensions vào:

<!-- https://mvnrepository.com/artifact/com.google.inject.extensions/guice-assistedinject -->
<dependency>
    <groupId>com.google.inject.extensions</groupId>
    <artifactId>guice-assistedinject</artifactId>
    <version>4.2.2</version>
</dependency>

Ví dụ chúng ta có một class UserController. Class này có dependency với MessageService và có 2 giá trị callerId, signature được xác định trong quá trình sử dụng UserController.

Chúng ta có thể sử dụng cách inject thông thường được hỗ trợ bởi Guice và cung cấp thêm các setter để gán các giá trị cho các tham số cho UserController. Tuy nhiên, cách này không được tốt, do phía client có thể quên gán các giá trị cần thiết.

Chúng ta sẽ sử dụng @AssistedInject – một extension hỗ trợ Guice để giải quyết vấn đề này.

Các bước cần thiết để sử dụng @AssistedInject:

  • Tạo một Factory với phương thức có thể gán các parameter cần được xác định trong quá trình sử dụng với kiểu trả về là một class hoặc interface của client sử dụng (target). Lưu ý: thứ tự các parameter phải đúng với thứ tự parameter của constructor trong target.
  • Khai báo Factory trong Module.
  • Đánh dấu @Assisted cho các parameter (có thể xác định name cho param nếu cần thiết).
  • Khởi tạo Factory từ Injector, gán giá trị cho các paramter và sử dụng target nhận được.

Chương trình của chúng ta như sau:

package com.maixuanviet.patterns.creational.googleguice.injection.assisted_inject;
 
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.FactoryModuleBuilder;
 
// 01. Defining a factory
// Create an interface whose methods return the constructed type, or any of its supertypes. 
// The method's parameters are the arguments required to build the constructed type.
interface CallerFactory {
    UserController create(Integer callerId, String signature);
}
 
interface MessageService {
    void sendMessage(String message);
}
 
class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}
 
class BasicModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(MessageService.class).to(EmailService.class);
        // 02. Configuring simple factories
        install(new FactoryModuleBuilder().build(CallerFactory.class));
    }
}
 
class UserController {
    private MessageService messageService;
    private Integer callerId;
    private String signature;
 
    // 03. @Assisted: Annotates an injected parameter or field whose value comes
    // from an argument to a factory method
    @Inject
    public UserController(@Assisted Integer callerId, @Assisted String signature, MessageService messageService) {
        this.callerId = callerId;
        this.signature = signature;
        this.messageService = messageService;
    }
 
    public void send() {
        messageService.sendMessage("CallerId " + callerId + ": Assisted Inject example by " + signature);
    }
}
 
public class AssistedInjectExample {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new BasicModule());
 
        // 04. Inject a factory that combines the caller's arguments with
        // injector-supplied values to construct objects
        CallerFactory callerFactory = injector.getInstance(CallerFactory.class);
        UserController userController = callerFactory.create(1, "maixuanviet.com");
        userController.send(); // CallerId 1: Assisted Inject example by maixuanviet.com
    }
}

Khi chúng ta thêm cấu hình install(new FactoryModuleBuilder().build(CallerFactory.class)) trong class Module, Guice sẽ tự thêm implement cho CallerFactory và handle các parameter cần thiết.

Chi tiết về @AssistedInject các bạn tham khảo thêm trên Wiki của Guice.

1.3. Guice quyết định inject như thế nào?

Đầu tiên là lựa chọn Constructor. Khi Guice khởi tạo một kiểu (Type) bằng cách sử dụng constructor của nó, nó sẽ quyết định constructor nào được gọi dựa theo các quy tắc sau:

  • Tìm constructor được đánh dấu với @Inject. Nếu có nhiều @Inject cho constructor hoặc constructor được đánh dấu là @Inject(optional=true) hoặc @Named thì sẽ throw một exception.
  • Nếu không có constructor nào được đánh dấu vơi @Inject. Nó sẽ  tìm constructor không có tham số và không phải là private. Nếu constructor được đánh dấu là @Named thì sẽ throw một exception.
  • Nếu không thõa mãn bất kỳ quy tắc nào ở trên sẽ throw một exception.

Sau khi đã có được thể hiện, nó sẽ tiến hành inject cho các field và method theo thứ tự. Tất cả field được inject trước, sau đó đến các method. Các field và method ở class cha sẽ được inject trước class con.

1.4. Nhận được gì sau khi Guice đã inject?

Guice inject các instance cho field và method:

  • Tất cả các value được binding với toInstance(), được inject tại thời điểm tạo injector.
  • Tất cả các provider được binding với toProvider(Provider), được inject tại thời điểm tạo injector.
  • Tất cả các instance được đăng ký để inject sử dụng requestInjection, được inject tại thời điểm tạo injector.
  • Tất cả các argument của Injector.injectMembers khi phương thức được gọi.
  • Tất cả các value được khởi tạo bởi Guice thông qua constructor được khởi tạo ngay sau khi đối tượng được tạo. Nó chỉ được inject 1 lần.

Guice inject các instance cho các static method và field:

  • Tất cả các class được đăng ký static injection thông qua requestStaticInjection.

2. Scope

Theo mặc định, Guice trả về một instance mới mỗi lần nó cần cung cấp một giá trị. Hành vi này có thể cấu hình thông qua phạm vi (scope). Scope cho phép chúng ta tái sử dụng instance.

Các loại Scope được hỗ trợ bởi Google Guice:

  • @Singleton : một instance duy nhất được sử dụng trong toàn bộ ứng dụng.
  • @SessionScoped : mỗi session sẽ có một instance khác nhau.
  • @RequestScoped : mỗi request sẽ có một instance khác nhau.
  • Custom scopes: ngoài các scope được hỗ trợ sẵn bởi Guice như trên, chúng ta có thể tự xây dựng một Scope cho riêng mình.

2.1. Các cách để khai báo scope

Chúng ta có thể khai báo scope bằng một trong các cách sau:

  • Class level
  • Configuration level
  • Method level
package com.maixuanviet.patterns.creational.googleguice.scope.level;
 
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
 
interface MessageService {
    void sendMessage(String message);
}
 
// Class level
// @Singleton
class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}
 
class BasicModule extends AbstractModule {
    // Configuration level
    @Override
    protected void configure() {
        bind(MessageService.class).to(EmailService.class); // .in(Singleton.class);
    }
 
    // Method level
    // @Singleton
    @Provides
    public MessageService provideMessageService() {
        return new EmailService();
    }
}

2.2. Ví dụ @Singleton

Trước hết chúng ta sẽ xem kết quả của chương trình dưới đây khi không sử dụng @Singleton scope

package com.maixuanviet.patterns.creational.googleguice.scope.withoutsingleton;
 
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(message + ": " + System.identityHashCode(this));
    }
}
 
class FirstModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(MessageService.class).to(EmailService.class);
    }
}
 
class UserController {
    private MessageService messageService;
 
    @Inject
    public UserController(MessageService messageService) {
        this.messageService = messageService;
    }
 
    public void send() {
        messageService.sendMessage("Singleton Scoped example");
    }
}
 
public class GuiceScopedExample {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new FirstModule());
 
        UserController userController = injector.getInstance(UserController.class);
        userController.send();
 
        userController = injector.getInstance(UserController.class);
        userController.send();
    }
}

Output của chương trình:

Singleton Scoped example: 861842890
Singleton Scoped example: 553871028

Như bạn thấy, chúng ta có 2 giá trị hash code khác nhau cho instance của EmailService. Điều này có nghĩa là EmailService sẽ được tạo mới mỗi khi nó cần được inject.

Bây giờ chúng ta sẽ thêm cấu hình Singleton vào chương trình trên:

class FirstModule extends AbstractModule {
    @Override
    protected void configure() {
        // Singleton: one instance (per Injector) to be reused for all injections for
        // that binding
        bind(MessageService.class).to(EmailService.class).in(Singleton.class);
    }
}

Chạy lại chương trình trên, chúng ta có kết quả như sau:

Singleton Scoped example: 817406040
Singleton Scoped example: 817406040

Như bạn thấy, bây giờ class EmailService chỉ được khởi tạo một lần duy nhất.

Mặc định, Guice sẽ sử dụng cơ chế lazy cho Singleton, chúng ta có thể chỉ định nó sử dụng cơ chế eager một cách tường minh với asEagerSingleton().

bind(Type.class).to(Implementation.class).asEagerSingleton();