Prywatne moduły w Guice – kto zacz…
Na początek małe profanum muzyczne…
Już doszliście do siebie… to dobrze…
Słowo wstępne
Na spotkaniu WJUGa, na którym wraz z Wojtkiem Ebertowskim omawialiśmy Guice Paweł Szklarz zarzucił temat „Guice to ma a Spring nie” na przykładzie modułów prywatnych. Rzecz jest to o tyle ciekawa, że świetnie obrazuje podstawową różnicę pomiędzy Guice, a Springiem. Sposób konfiguracji.
Ogólnie rzecz ujmując. W springu wszystko co zostanie użyte w ramach taga bean jest przetwarzane tylko w kontekście tego taga. Jeżeli zatem macie dwa beany springowe implementujące ten sam interfejs i zależne od jakiegoś tam, ale identycznego, interfejsu w dwóch różnych implementacjach to możecie skonfigurować wszystko w osobnych tagach bean. Zależności uczynić anonimowymi (nie ustawiać id) i generalnie pozbyć się problemu. W Guice nie za bardzo idzie tak zrobić, ponieważ chcąc uzyskać jak najogólniejsze łączenie interfejsu z implementacją musimy takie mapowanie upublicznić. Powoduje to, że nie można dodać kolejnego identycznego mapowania.
Przykład z życia
Potrzebujemy w ramach aplikacji odwoływać się do dwóch identycznych baz danych. Te same mapowania, te same operacje, itd. tylko inne adresy. W Springu stworzylibyśmy klasę i skonfigurowali ja jako dwa niezależne beany, każdy w własną konfiguracją. W Guice taka sztuczka nie przejedzie ponieważ konfiguracja JpaPersistModule nie pozwala na wykorzystanie wielu jednostek utrwalania. Dodatkowo nie idzie tego skonfigurować. Z pomocą przychodzą tu moduły prywatne.
Moduł prywatny kto zacz
Moduł prywatny jest to specjalna implementacja modułu Guice, która w trakcie tworzenia w danym injectorze tworzy injector pochodny i do niego dowiązuje swoją konfigurację. W ten sposób konfiguracja jest niewidoczna w nadrzędnym injectorze, a tym samym można stworzyć wiele identycznych konfiguracji, które nie będą się wzajemnie widzieć. Można oczywiście pewne elementy konfiguracji upublicznić co pozwala na np. użycie dwóch identycznych jednostek utrwalania w ramach jednego projektu.
Przykładzik
Zadanie przepisać tabelę z jednej bazy do drugiej.
Na początek konfiguracja baz danych. Wykorzystuję tu dwie instancje HSQLDB umieszczone w pamięci. Jedyny problem z tym związany to konieczność dodania danych do źródła za pomocą DAO. Jak chcesz to przerzuć się na postgresa i wykorzystaj DBUnit.
Listing 1. konfiguracja baz – persistence.xml
<?xml version="1.0" encoding="UTF-8"??><persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"><persistence-unit name="source-pu" transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.ejb.HibernatePersistence</provider><class>pl.koziolekweb.guice.SomeEntity</class><properties><property name="hibernate.connection.url" value="jdbc:hsqldb:mem:source"></property><property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"></property><property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"></property><property name="hibernate.hbm2ddl.auto" value="create-drop"></property><property name="hibernate.show_sql" value="true"></property><property name="hibernate.format_sql" value="true"></property><property name="hibernate.connection.username" value="sa"></property><property name="hibernate.connection.password" value=""></property></properties></persistence-unit><persistence-unit name="target-pu" transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.ejb.HibernatePersistence</provider><class>pl.koziolekweb.guice.SomeEntity</class><properties><property name="hibernate.connection.url" value="jdbc:hsqldb:mem:target"></property><property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"></property><property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"></property><property name="hibernate.hbm2ddl.auto" value="create-drop"></property><property name="hibernate.show_sql" value="true"></property><property name="hibernate.format_sql" value="true"></property><property name="hibernate.connection.username" value="sa"></property><property name="hibernate.connection.password" value=""></property></properties></persistence-unit></persistence>
Następnie interfejs DAO:
Listing 2. DAO
package pl.koziolekweb.guice;
import java.util.Collection;
public interface SomeEntityDao {
void saveAll(Collection<SomeEntity> someEntities);
Collection<SomeEntity> getAll();
}
Implementacja
Listing 3. implementacja DAO
package pl.koziolekweb.guice;
import java.util.Collection;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import com.google.inject.persist.Transactional;
public class SomeEntityDaoImpl implements SomeEntityDao {
@Inject
private EntityManager em;
@Transactional
@Override
public void saveAll(Collection<SomeEntity> someEntities) {
for (SomeEntity se : someEntities)
em.merge(se);
em.flush();
}
@Transactional
@Override
public Collection<SomeEntity> getAll() {
return em.createQuery("Select se From SomeEntity se", SomeEntity.class).getResultList();
}
}
oraz nasza encja
Listing 4. SomeEntity
package pl.koziolekweb.guice;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
@Entity
public class SomeEntity {
@SequenceGenerator(name = "Gen", sequenceName = "Seq")
@GeneratedValue(generator = "Gen")
@Id
public Long id;
public String name;
public SomeEntity() {
}
public SomeEntity(String name) {
this.name = name;
}
}
Jak widać nie ma tu nic niezwykłego.
Aplikacja
Kolejnym elementem jest aplikacja przepisująca dane. Klasa posiada ona dwa pola reprezentujące dao powiązane z różnymi PU.
Listing 5. Wybitnie skomplikowana klasaApp
package pl.koziolekweb.guice;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class App {
@Inject
@Named("source")
private SomeEntityDao source;
@Inject
@Named("target")
private SomeEntityDao target;
public static void main(String[] args) {
Injector injector = Guice.createInjector(new MainModule());
App instance = injector.getInstance(App.class);
instance.prepare();
instance.verify();
instance.copy();
instance.verify();
}
public void verify() {
System.out.println(source.getAll().size());
System.out.println(target.getAll().size());
}
public void copy() {
target.saveAll(source.getAll());
}
public void prepare() {
List<SomeEntity> data = new LinkedList<SomeEntity>();
data.add(new SomeEntity("A"));
data.add(new SomeEntity("B"));
data.add(new SomeEntity("C"));
source.saveAll(data);
}
}
Jak widać instalujemy tylko jeden moduł – MainModule. Przyjrzyjmy się mu 🙂
Listing 6. główny moduł aplikacji
package pl.koziolekweb.guice;
import com.google.inject.AbstractModule;
public class MainModule extends AbstractModule {
@Override
protected void configure() {
install(new DaoModule("source-pu", "source"));
install(new DaoModule("target-pu", "target"));
}
}
Nie ma tu nic nadzwyczajnego. Instalujemy dwa moduły DaoModule. Przyjrzyjmy się tej klasie (zaczyna się robić wpis ginekologiczno-pornograficzny wnikamy głębiej).
Listing 7. Moduł zarządzający DAO
package pl.koziolekweb.guice;
import com.google.inject.PrivateModule;
import com.google.inject.name.Names;
import com.google.inject.persist.jpa.JpaPersistModule;
public class DaoModule extends PrivateModule {
private String jpaUnit;
private String emMarker;
public DaoModule(String jpaUnit, String emMarker) {
super();
this.jpaUnit = jpaUnit;
this.emMarker = emMarker ;
}
@Override
protected void configure() {
install(new JpaPersistModule(jpaUnit));
bind(DaoInitializer.class).asEagerSingleton();
bind(SomeEntityDao.class).annotatedWith(Names.named(emMarker)).to(SomeEntityDaoImpl.class);
expose(SomeEntityDao.class).annotatedWith(Names.named(emMarker));
}
}
Tu zaczyna być ciekawie. Zanim omówię co i jak to dla porządku klasa DaoInitializer:
Listing 8. Klasa DaoInitializer
package pl.koziolekweb.guice;
import javax.inject.Inject;
import com.google.inject.persist.PersistService;
public class DaoInitializer {
@Inject
public DaoInitializer(PersistService service) {
System.out.println(service.toString());
service.start();
}
}
ale co się dzieje
Na listingu 8 mamy sobie klasę – moduł guice, która zamiast AbstractModule rozszerza PrivateModule. W konstruktorze przyjmuje ona sobie dwa parametry. Pierwszy to nazwa PU z pliku konfiguracyjnego, a drugi to nazwa pod jaką będziemy udostępniać DAO. Generalnie mamy tu dość dużą swobodę. Można przyjąć, że pola trzymające DAO muszą mieć taką samą nazwę (w sensie adnotacji @Named) jak używany PU. Można dać opcję na własną adnotację. Można przyjąć, że nazwa podana w kodzie ma odpowiednik w xmlu z jakimś przedrostkiem, przyrostkiem, wrostkiem, wyrostkiem czy innym tego typu tworem (brak ci inwencji w nazewnictwie – sięgnij po podręcznik do dermatologii, na pewno coś znajdziesz).
Następnie jest sobie metoda configure, a w niej instalujemy w pierwszej kolejności moduł do obsługi JPA. Następnie tworzymy i inicjujemy (użycie asEagerSingleton) obiekt DaoInitializer, który uruchamia nam usługę JPA. Teraz możemy już powiązać interfejs i implementację DAO koniecznie wykorzystując tu informację, że pole jest adnotowane. Ostatnim akordem tej zabawy jest wywołanie metody expose, która udostępnia definicję naszej konfiguracji innym modułom (umieszcza ją w nadrzędnym injectorze). Co ważne wszystko co znajduje się w tym module jest odseparowane od świata. Zatem nie ma możliwości by nadpisać sobie konfigurację w innym module.
Uruchamiamy…
Listing 9. uruchomienie aplikacji
com.google.inject.persist.jpa.JpaPersistService@77158a
com.google.inject.persist.jpa.JpaPersistService@a7c45e
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
select
someentity0_.id as id0_,
someentity0_.name as name0_
from
SomeEntity someentity0_
3
Hibernate:
select
someentity0_.id as id2_,
someentity0_.name as name2_
from
SomeEntity someentity0_
0
Hibernate:
select
someentity0_.id as id0_,
someentity0_.name as name0_
from
SomeEntity someentity0_
Hibernate:
select
someentity0_.id as id2_0_,
someentity0_.name as name2_0_
from
SomeEntity someentity0_
where
someentity0_.id=?
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
select
someentity0_.id as id2_0_,
someentity0_.name as name2_0_
from
SomeEntity someentity0_
where
someentity0_.id=?
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
select
someentity0_.id as id2_0_,
someentity0_.name as name2_0_
from
SomeEntity someentity0_
where
someentity0_.id=?
Hibernate:
insert
into
SomeEntity
(id, name)
values
(null, ?)
Hibernate:
call identity()
Hibernate:
select
someentity0_.id as id0_,
someentity0_.name as name0_
from
SomeEntity someentity0_
3
Hibernate:
select
someentity0_.id as id2_,
someentity0_.name as name2_
from
SomeEntity someentity0_
3
…i się cieszymy.
Podsumowanie
Prywatne moduły wprowadzają nam funkcjonalność, która nie jest spotykana w Springu. Jednocześnie należy zaznaczyć, że Spring reprezentuje zupełnie inne podejście do problemu konfiguracji i zakresów widoczności poszczególnych beanów. Tym samym nie można mówić o tym, które rozwiązanie jest lepsze. One są po prostu diametralnie różne.