O technologii i biznesie naszym zdaniem
Archive for sierpień, 2009
Google Guice, dependency injection framework
sierpnia 18th
Wstęp
Google Guice to framework wspierający wstrzykiwanie zależności (ang. dependency injection) w aplikacjach Java. Można by powiedzieć: kolejny framework wspierający wstrzykiwanie zależności. Faktycznie, trzeba przyznać, że rynek Javowych rozwiązań tego typu jest dość mocno nasycony już od dłuższego czasu i oferuje pełen przekrój złożoności bibliotek, zaczynając od bardzo lekkich, takich jak choćby PicoContainer, a kończąc na pełnych stosach rozwiązań Java Enterprise ze Springiem na czele. Czy warto więc zawracać sobie głowę Guice’m? Moim zdaniem warto, bo zaproponowane przez Google’a rozwiązanie pokazuje nową jakość w implementacji wstrzykiwania zależności w aplikacjach Java, czyniąc ogromny użytek z typów generycznych oraz adnotacji. Guice jest przy tym lekki, ma proste API i bardzo szybko można się go nauczyć. Dlaczego warto to zrobić? Bo jest dobry. Kolesie z Google’a go używają. Musi być dobry. Poza tym, wygrał 18th Jolt Award w kategorii Libraries, Frameworks and Components. Musi być dobry.
Podstawy
W Guice występują dwie podstawowe konstrukcje opisujące wstrzykiwanie zależności. Pierwszą są moduły, które mówią co należy wstrzyknąć, a drugą jest adnotacja @Inject, która mówi gdzie należy wstrzyknąć. Proste. Zobaczmy jak to działa na przykładzie usługi rejestracji użytkowników.
public class RegistrationServiceImpl implements RegistrationService {
private final PersistanceManager persistanceManager;
private final MailService mailService;
@Inject
public RegistrationServiceImpl(PersistanceManager persistanceManager, MailService mailService) {
this.persistanceManager = persistanceManager;
this.mailService = mailService;
}
public void register(User user) {
...
}
}
Jak widać naszą usługę specyfikuje kontrakt RegistrationService. Mamy jego implementację w postaci klasy RegistrationServiceImpl, która wymaga do działania usługi trwałości danych (PersistanceManager) oraz usługi do wysyłania maili (MailService). Chcielibyśmy, aby odpowiednie implementacje tych usług zostały automatycznie wstrzyknięte za pomocą parametrów konstruktora (adnotacja @Inject) już na etapie tworzenia usługi rejestracji. Spróbujmy opisać zależności między interfejsami a ich konkretnymi realizacjami za pomocą modułu.
public class MyModule extends AbstractModule {
@Override
protected void configure() {
bind(RegistrationService.class).to(RegistrationServiceImpl.class);
bind(PersistanceManager.class).to(HibernatePersistanceManager.class);
bind(MailService.class).to(JavaMailService.class);
}
}
Mamy nasz moduł. Jak widać, metoda configure() zawiera definicje kolejnych powiązań. Mamy więc usługę rejestracji określoną kontraktem RegistrationService z przypisaną naszą implementacją RegistrationServiceImpl. Mamy również usługę trwałości danych PersistanceManager zaimplementowaną w klasie HibernatePersistanceManager, która używa Hibernate’a do zapewnienia trwałości encji, oraz usługę MailService wraz z implementacją JavaMailService korzystającą z JavaMail do wysyłania poczty.
OK. Wystarczy już tylko użyć zdefiniowanego zespołu obiektów i przeprowadzić rejestrację użytkownika.
public class UserRegistrationExample {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new MyModule());
RegistrationService registrationService = injector.getInstance(RegistrationService.class);
User user = new User("Michal", "Kalinowski");
registrationService.register(user);
}
}
To wszystko. Działa. “Prawie” jak w Springu, tylko zamiast trochę przydługawego pliku XML mamy elegancką implementację interfejsu Module w Javie, a zamiast identyfikowania obiektów za pomocą jakichś napisów mamy referencje do rzeczywistych klas. Myślę, że nie muszę wspominać jak ułatwia to refactoring. Jedyne do czego można się “przyczepić” to inwazyjność framework’a (odniesienia do API we własnych klasach), ale coś za coś. Wygoda jest niesamowita. Swoją drogą, pomysł na tyle przypadł do gustu kolesiom od Springa, że sami piszą już coś podobnego; nazywa się to Spring JavaConfig i powinno niedługo zostać oficjalnie wydane.
Oczywiście, proste powiązania interfejs <=> klasa i wstrzykiwanie zależności przez konstruktor to nie koniec możliwości Guice.
Powiązania
Oprócz prostych powiązań klasy z interfejsem, jest również kilka bardziej zaawansowanych konstrukcji.
Jedną z nich jest powiązanie adnotacją. Wyobraźmy sobie, że JavaMailService ma zostać użyty przez 2 różne usługi. Jedna z nich, nasz RegistrationService, do rozsyłania poczty powinna używać konta na Gmail’u, a inna – konta na Yahoo. Nic prostszego. Potrzebna nam dodatkowa adnotacja.
@BindingAnnotation
@Target( { FIELD, PARAMETER, METHOD })
@Retention(RUNTIME)
public @interface Gmail {
}
Modyfikujemy nieznacznie konstruktor naszej usługi rejestracji.
public class RegistrationServiceImpl implements RegistrationService {
@Inject
public RegistrationServiceImpl(PersistanceManager persistanceManager, @Gmail MailService mailService) {
this.persistanceManager = persistanceManager;
this.mailService = mailService;
}
...
}
Oczywiście potrzebne jest również dodatkowe powiązanie w module.
bind(MailService.class).annotatedWith(Gmail.class).to(GmailJavaMailService.class);
Widać więc, że w Guice powiązanie jednoznacznie definiuje dopiero para adnotacji oraz typu.
Idźmy dalej. Jest możliwość stworzenia powiązania z konkretną instancją danego typu.
bind(String.class).annotatedWith(JdbcUri.class).toInstance("jdbc:mysql://localhost/application");
public class HibernatePersistanceManager implements PersistanceManager {
public HibernatePersistanceManager(@JdbcUri String jdbcConnectionString) {
...
}
...
}
Cool, huh? Można również zdefiniować w module własną metodę tworzenia obiektu. Używa się do tego adnotacji @Provides.
public class MyModule extends AbstractModule {
@Override
protected void configure() {
...
}
@Provides
PersistanceManager providePersistanceManager() {
HibernatePersistanceManager persistanceManager = new HibernatePersistanceManager("jdbc:mysql://localhost/application");
persistanceManager.initConnectionPool();
return persistanceManager;
}
}
Robi się bałagan w klasie modułu? Napiszmy zewnętrzne providery dla naszych komponentów.
public class DatabasePersistanceManagerProvider implements Provider<persistanceManager> {
private final Connection connection;
@Inject
public DatabasePersistanceManagerProvider(Connection connection) {
this.connection = connection;
}
public PersistanceManager get() {
DatabasePersistanceManager persistanceManager = new DatabasePersistanceManager();
persistanceManager.setConnection(connection);
return persistanceManager;
}
}
I jeszcze odpowiednie powiązanie.
bind(PersistanceManager.class).toProvider(DatabasePersistanceManagerProvider.class);
Można nawet zdefiniować providera bezpośrednio w interfejsie.
@ProvidedBy(DatabasePersistanceManagerProvider.class)
public interface PersistanceManager {
...
}
Warto zwrócić uwagę, że definicje powiązań w Guice “czytają się same”. Jest to ogromna zaleta tej biblioteki. Nie ma tu nawet co tłumaczyć.
Zasięgi
Domyślnie Guice dostarcza nową instancję klasy przy każdym zapytaniu do kontenera. Można jednak zażyczyć sobie za pomocą adnotacji @Singleton, by istniała dokładnie jedna instancja danego komponentu w obrębie całej aplikacji. W aplikacjach webowych, po dołączeniu odpowiedniego dodatku do deskryptora web.xml, można używać również zasięgu sesyjnego (@SessionScoped) oraz zasięgu pojedynczego żądania HTTP (@RequestScoped). Zasięgi można ustawiać na kilka sposobów. Można np. zrobić to na samej klasie.
@Singleton
public class JavaMailService implements MailService {
...
}
Nic nie stoi na przeszkodzie, by ustawić zasięg na metodzie tworzącej obiekt w obrębie modułu.
public class MyModule extends AbstractModule {
@Override
protected void configure() {
...
}
@Provides @Singleton
MailService provideMailService() {
...
}
}
Oczywiście, zawsze można zrobić to po prostu w definicji powiązania.
bind(MailService.class).to(JavaMailService.class).in(Singleton.class);
Wstrzykiwanie
W Guice dostępne są 3 standardowe metody wstrzykiwania zależności. Widzieliśmy już wstrzykiwanie przez konstruktor. Można również użyć do tego regularnej metody (niekoniecznie klasycznego settera).
public class RegistrationServiceImpl implements RegistrationService {
@Inject
public void provideLogger(Logger logger) {
...
}
...
}
Można również wstrzykiwać bezpośrednio do pola klasy, ale jest to zdecydowanie niezalecane.
public class RegistrationServiceImpl implements RegistrationService {
@Inject Logger logger;
...
}
Można w Guice również wstrzykiwać same providery.
public interface Provider<t> {
T get();
}
Zapewniają one dodatkową elastyczność polegającą na tym, że kontener zwraca instancję komponentu-zależności wraz z każdym wywołaniem metody get(), a nie tylko jednorazowo na etapie tworzenia grafu obiektów.
public class DatabasePersistanceManager implements PersistanceManager {
private final Provider<connection> connectionProvider;
@Inject
public DatabasePersistanceManager(Provider<connection> connectionProvider) {
this.connectionProvider = connectionProvider;
}
public void persist(Object entity) {
Connection connection = connectionProvider.get();
... // do something with provided connection
}
}
Jakie to może mieć zastosowanie? Różne, przeróżne. Możemy potrzebować wielu instancji tego samego komponentu. Możemy zechcieć pomieszać komponenty różnych zasięgów (np. wstrzykiwanie danych użytkownika utrzymywanych w sesji HTTP do komponentu z zasięgiem Singleton). Możemy wreszcie użyć późnego ładowania (ang. lazy loading), jeśli konstruowanie komponentu-zależności jest kosztowne, a nie musi być wykonywane za każdym razem.
Czy to wszystko?
Oczywiście, omówiono tylko część możliwości Guice, aczkolwiek prawdopodobnie wystarczy to w 90% zastosowań. Widać więc, jak szybko można tę technologię przyswoić. Więcej informacji można odnaleźć w dokumentacji biblioteki. Znajduje się tam m.in. omówienie wsparcia dla programowania aspektowego oraz opis integracji Guice z rozwiązaniami webowymi. Warto tam zajrzeć.
Podsumowanie
Google Guice wydaje się stanowić bardzo ciekawą ofertę jako framework do wstrzykiwania zależności. Jeśli nie potrzebujemy bardzo kompleksowego rozwiązania takiego jak np. Spring, Guice powinien być w zupełności wystarczający. Jest bardzo lekki, prosty i ma niezwykle szybką krzywą uczenia. Dokładne przeczytanie tego posta powinno pozwolić na rozpoczęcie pracy, a w razie problemów zawsze pozostaje dokumentacja. Guice znacznie uprzyjemnia pracę programisty, pozwalając opisywać zależności i szczegóły ich wstrzykiwania za pomocą kodu Java “ozdobionego” adnotacjami i typami generycznymi, z zachowaniem jednocześnie deklaratywnego charakteru tego opisu. Jest to niewątpliwie znaczny krok do przodu. Uważam, że w dobrym kierunku.