Guice jest frameworkiem dostarczającym tzn. wstrzykiwania zależności w naszej części serwerowej aplikacji. Gdybyśmy rozważali aplikację typu desktop poniższa konfiguracja w zupełności by wystarczyła:
package testingguice;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class Main {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new MyModule());
PersonDao personDao = injector.getInstance(PersonDao.class);
for (Person person : personDao.getAll()) {
System.out.println("Person: "+person);
}
}
}
Klasa modułu w rozumieniu IoC:
public class MyModule extends AbstractModule {
@Override
protected void configure() {
bind(PersonDao.class).to(PersonDaoImpl.class);
}
}
Interfejs przykładowego DAO dla klasy Person:
public interface PersonDao {
public List<Person> getAll();
}
Implementacja naszego interfejsu, którą dzięki IoC możemy w łatwy sposób podmienić na inną implementującą ten sam interfejs:
public class PersonDaoImpl implements PersonDao{
public List<Person> getAll() {
ArrayList<Person> list = new ArrayList<Person>();
list.add(new Person(1, "Jacek"));
list.add(new Person(2, "Zbyszek"));
list.add(new Person(3, "Piotrek"));
return list;
}
}
Przykładowe POJO wykorzystywane w DAO:
public class Person {
private int id;
private String name;
public Person() {
}
public Person(int id, String name) {
this.id = id;
this.name = name;
}
...
@Override
public String toString() {
return "Person{" + "id=" + id + ",name=" + name + '}';
}
}
My jednak korzystamy z aplikacji JEE, z którą nie jest aż tak łatwo. Aplikacja, która jest deployowana na serwer przed pierwszym wywołaniem strony ładującej powinna zostać zainicjonowana, co zostało zrobione w przypadku aplikacji desktopowej tu:
Injector injector = Guice.createInjector(new MyModule());
W aplikacji typu web za pomocą małego wpisu w web.xml:
<listener>
<listener-class>pl.testing.server.GuiceServletConfig</listener-class>
</listener>
sprawiamy, że zaraz po zdeployowaniu aplikacji zostanie od razu wywołana wskazana klasa dziedzicząca po ContextListener. Wykorzystamy ten fakt do zainicjonowania naszego injectora. W tym przypadku będziemy posiłkować się dodatkowo klasą GuiceServletContextListener, która wspomaga nas i odciąża tak, że wszystko co musimy zrobić to zwrócić injector:
public class MainGuiceServletContextListener extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
return Guice.createInjector(new MainGuiceModule());
}
}
Resztą tzn. pobieraniem instancji z injectora zajmie się specjalnie przygotowany przez nas w web.xml filtr:
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Jest to następna sztuczka wykorzystywana przez Guice’a. Gdybyśmy nie wykorzystali filtra każdorazowo gdy żądamy zawartości serwleta zostałaby stworzona instancja serwleta uruchomiona i zwrócony efekt jego przetwarzania. Natomiast wykorzystując filtr jesteśmy w stanie do wybranych (najczęściej dla wszystkich) serwletów wstrzyknąć odpowiednie zależności konfiguracyjne.
Konfigurując moduł w następujący sposób mapujemy wywołanie adresu: /examplexmlrpc z ExampleRpcImpl.class:
public class GuiceModule extends ServletModule{
@Override
protected void configureServlets() {
super.configureServlets();
serve("/examplerpc").with(ExampleRpcImpl.class);
}
}
Jest to dokładnie odpowiednik wpisu w web.xml:
<servlet>
<servlet-name>ExampleRpc</servlet-name>
<servlet-class>pl.testing.server.ExampleRpcImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ExampleRpc</servlet-name>
<url-pattern>/pl.testing.Main/examplerpc</url-pattern>
</servlet-mapping>
Dzięki powyższemu zastąpieniu uzyskujemy zdecydowanie bardziej przyjazną konfigurację programiście. Dzięki temu rozwiązaniu możemy wykorzystać pełen potencjał naszego IDE i zapisywać bezbłędnie konfigurację przy pomocy autosugestii. W podobny sposób możemy nie tylko mapować serwlety, ale również i filtry. W praktyce większość konfiguracji może zostać przeniesiona z web.xml-a do odpowiednich modułów Guice-a. Na tym jednak nie koniec. Obecny stan wymaga podczas tworzenia połączenia GwtRpc następującego zapisu:
Część kliencka:
@RemoteServiceRelativePath("../examplerpc")
public interface ExampleRpc extends RemoteService {
public String myMethod(String s);
}
public interface ExampleRpcAsync {
public void myMethod(String s, AsyncCallback<String> callback);
}
I część serwerowa:
@Singleton
public class ExampleRpcImpl extends RemoteServiceServlet implements ExampleRpc {
public String myMethod(String s) {
return "Server says: " + s;
}
}
Jak widać w synchronicznym interfejsie nadal występuje niebezpieczeństwo pomyłki i konieczności zapamiętywania mapowania:
@RemoteServiceRelativePath(“../examplerpc”)
Dzięki kolejnej sztuczce wyeliminujemy i ten “boilerplate code” – kod narażony/narażający na pomyłki. Dodajemy w części serwerowej klasę:
@Singleton
public class GuiceRemoteServiceServlet extends RemoteServiceServlet {
@Inject
private Injector injector;
@Override
public String processCall(String payload) throws SerializationException {
try {
RPCRequest req = RPC.decodeRequest(payload, null, this);
RemoteService service = (RemoteService) injector.getInstance(req.getMethod().getDeclaringClass());
return RPC.invokeAndEncodeResponse(service, req.getMethod(), req.getParameters(), req.getSerializationPolicy());
} catch (IncompatibleRemoteServiceException ex) {
log("IncompatibleRemoteServiceException in the processCall(String) method.", ex);
return RPC.encodeResponseForFailure(null, ex);
}
}
}
Jest to pośredniczący serwlet, dzięki któremu mapowanie urla na klasę serwleta zostanie zastąpione na bardzo dobrze znane i lubiane mapowanie konfiguracyjne tj. interfejsu na jego implementację. Rozwiązanie to zaproponował Eric Burke na łamach swojego bloga, które zdobyło ogromne uznanie wśród developerów GWT. W naszym przypadku osiągnęliśmy bardzo ciekawy efekt, mianowicie mapujemy interfej będący elementem części klienckiej na implementację serwerową.
Nasz plik konfiguracyjny w tym momencie wygląda następująco:
public class GuiceModule extends ServletModule{
@Override
protected void configureServlets() {
super.configureServlets();
serve("/GWT.rpc").with(GuiceRemoteServiceServlet.class);
bind(ExampleRpc.class).to(ExampleRpcImpl.class);
}
}
Jak widać obsługujemy “/GWT.rpc” za pomocą GuiceRemoteServiceServlet.class, który jest tak naprawdę naszym dispatcherem, czyli klasą delegującą. Oznacza to, że GuiceRemoteServiceServlet.class kontroluje ruch. Gdy po stronie klienckiej zostanie wywołany serwlet za pomocą urla: “/GWT.rpc” zostanie wywołany właśnie nasz dispatcher. Wywołanie jest dekodowane:
RPCRequest req = RPC.decodeRequest(payload, null, this);
a następnie pobierana jest klasa wywołująca servlet:
req.getMethod().getDeclaringClass()
na podstawie której poprzez Injector otrzymujemy RemoteService, czyli klasę, z której musi dziedziczyć każdy servlet obsługujący GwtRPC. W naszym przypadku będzie to klasa, którą zbindowaliśmy z klasą wywołującą request. Dalsze linijki polegają na obsłudze samego wywołania oraz na zwróceniu odpowiedzi… ot i cała magia. Należy pamiętać, że w części klienckiej będziemy używać teraz za każdym razem adnotacji aby wszystko chodziło jak należy:
@RemoteServiceRelativePath("../GWT.rpc")
Dzięki zastosowaniu powyższej metody otrzymaliśmy mechanizm, który pozwoli na klarowne bo nie w xml-u oraz bezpieczne pisanie konfiguracji w oparciu o autosugestie. Ponadto jest on idealnym wstępem do stworzenia modularnej części serwerowej aplikacji wykorzystującej GWT, ale o tym dowiecie się z przyszłych postów.
Podsumowując konfigurację Guice-a powinniśmy otrzymać przykładowy zestaw klas po stronie klienckiej:
@RemoteServiceRelativePath("../GWT.rpc")
public interface ExampleRpc extends RemoteService {
public String myMethod(String s);
}
public interface ExampleRpcAsync {
public void myMethod(String s, AsyncCallback<String> callback);
}
oraz zestaw plików po stronie serwerowej:
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<listener>
<listener-class>pl.testing.server.GuiceConfig</listener-class>
</listener>
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>welcomeGWT.html</welcome-file>
</welcome-file-list>
</web-app>
public class GuiceConfig extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
return Guice.createInjector(new GuiceModule());
}
}
public class GuiceModule extends ServletModule{
@Override
protected void configureServlets() {
super.configureServlets();
serve("/GWT.rpc").with(GuiceRemoteServiceServlet.class);
bind(ExampleRpc.class).to(ExampleRpcImpl.class);
}
}
public class ExampleRpcImpl extends RemoteServiceServlet implements ExampleRpc {
public String myMethod(String s) {
return "Server says: " + s;
}
}
@Singleton
public class GuiceRemoteServiceServlet extends RemoteServiceServlet {
@Inject
private Injector injector;
@Override
public String processCall(String payload) throws SerializationException {
try {
RPCRequest req = RPC.decodeRequest(payload, null, this);
RemoteService service = (RemoteService) injector.getInstance(req.getMethod().getDeclaringClass());
return RPC.invokeAndEncodeResponse(service, req.getMethod(), req.getParameters(), req.getSerializationPolicy());
} catch (IncompatibleRemoteServiceException ex) {
log("IncompatibleRemoteServiceException in the processCall(String) method.", ex);
return RPC.encodeResponseForFailure(null, ex);
}
}
}
W tym miejscu zakończyliśmy konfigurację Guice-a wraz z GWT. W następnej części zajmiemy się obsługą Hibernate.