JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

Spring remoting i HttpInvoker

Saša Slavnić
Sadržaj:
4 Apr 2009

U sadašnje vreme veoma često postoji potreba za povezivanjem različitih komponenti sistema koje su na neki način razdvojene. Skoro svako je imao priliku da na neki način pristupi udaljenim objektima kroz web servise. Web servisi su svakako odlično rešenje za povezivanje udaljenih ili raznorodnih aplikacija, a i samo njihovo korišćenje je uz postojeće alate postalo veoma jednostavno.

S druge strane, web servisi nikako nisu odgovor za sve potrebe – sama tehnologija je u nekim slučajevima previše spora i komplikovana, pa je moguće zameniti bolje prilagođenim rešenjima. Jedno od takvih rešenja je i HttpInvoker, kao deo Spring remotinga.

Problem

HttpInvoker je kao metod za remoting pogodan za klijent-server aplikacije gde su obe komponente, i klijent i server, pisane u Javi. Odmah da napomenemo da HttpInvoker nije univerzalna tehnologija, te da će raditi samo u slučaju da su sve komponente koje ga koriste pisane u Javi i da koriste Spring framework.

Tipična klijent-server aplikacija mogla bi se generalno predstaviti slikom:

Dakle, na strani klijenta je neka Java aplikacija koja komunicira s bazom na udaljenom serveru. Ovakav koncept je naravno uveliko prevaziđen, a uz to ima i očigledno ograničenje da DBMS mora biti vidljiv klijentima što je nemoguće ako se klijenti nalaze na udaljenoj lokaciji, iz sigurnosnih razloga. Rešenje je svakako u uvođenju dodatnog sloja – serverske aplikacije.

U ovom scenariju Java klijenti komuniciraju sa udaljenim serverom takođe pisanim u Javi. Ovaj tekst pokazuje kako se ovaj scenario može lako implementirati upotrebom HttpInvokera.

Test aplikacija

Za ilustraciju ovog rešenja poslužiće nam veoma jednostavna aplikacija, kao što je prikazano na slici:

Rešenje iz primera je podeljeno u tri komponente - Common, Server i Client. Common komponenta sadrži klase koje su zajedničke za Server i Client komponentu.

Klase koje koristimo su:

Common

Za početak ćemo definisati Book klasu i BookService interfejs. Kao što vidimo, ne postoji ništa specifično u ovom kodu. Book klasa čije instance se prenose između klijenta i severa mora naravno implementirati Serializable interfejs.

public class Book implements Serializable {
  
  private static final long serialVersionUID = -2829008532319730497L;
  
  private String title;
  
  public String getTitle() {
    return title;
  }
  
  public Book(String title) {
    this.title = title;
  }
  
}

public interface BookService {

  List<Book> listBooks();
  
  List<Book> findByTitle(String title);
  
}

Server

Ova komponenta je realizovana kao web aplikacija. Unutar ove web aplikacije postoji samo jedna klasa koja implementira BookService interfejs deklarisan u Common komponenti. Ponovo nema ništa specifično u implementaciji, još uvek se ponašamo kao da klasa nikada neće biti pozivana s udaljenih klijenata.

public class BookServiceImpl implements BookService {
  
  private List<Book> books;
  
  public BookServiceImpl() {
    books = new ArrayList<Book>();
    books.add(new Book("The Hitchhiker's Guide to the Galaxy"));
    books.add(new Book("Count Zero"));
    books.add(new Book("Groovy in Action"));
  }

  public List<Book> listBooks() {
    return books;
  }

  public List<Book> findByTitle(String title) {
    List<Book> result = new ArrayList<Book>();
    for (Book book : books) {
      if (book.getTitle().contains(title)) {
        result.add(book);
      }
    }
    return result;    
  }
}

Klijent

Na klijentu postoji klasa Library, koja predstavlja korisnički interfejs “biblioteke”. Korisnički interfejs je realizovan kao konzolna aplikacija. Kao što se vidi, Library klasa nudi jednostavan način da se pozovu metode BookService interfejsa. U klasi se koriste pomoćne metode readInput i printMenu koje su izdvojene u posebnu klasu, da bi se kod pojednostavio. Ponovo, nemamo nikakav kod koji bi bio specifičan za remoting.

public class Library {

  private BookService bookService;

  public void setBookService(BookService bookService) {
    this.bookService = bookService;
  }

  private void listBooks() {
    for (Book book : bookService.listBooks()) {
      System.out.println(book.getTitle());
    }
  }

  private void findBookByTitle() {    
    String title = readInput("Enter book title: ");
    
    for (Book book : bookService.findByTitle(title)) {
      System.out.println(book.getTitle());
    }    
  }

  public void run() throws IOException {
    
    printMenu();

    while (true) {
      switch (readInput()) {
        case '1': listBooks()break;
        case '2': findBookByTitle()break;
        case 'q': System.out.println("Goodbye!")return;
      }
    }
  }
}

Povezivanje

Kao što se vidi u kodu, ne postoji ništa specifično što bi bilo potrebno dodati da bi remoting radio. Cela magija odigrava se unutar Spring konfiguracije. To bi možda moglo biti značajno timovima koji već imaju veliku količinu napisanog koda. Uz minimalne prepravke taj kod bi se mogao prilagoditi tako da iskoristi prednosti remotinga.

Kod ispod prikazuje sadržaj web.xml konfiguracije na serveru. Pre svega tu je listener koji učitava Spring kontekst na serveru iz fajla /WEB-INF/serverContext.xml. Osim toga definisan je i servlet bookExporter, koji je mapiran na /remoting/BookService url.

<web-app>
  <display-name>Library server</display-name>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/serverContext.xml</param-value>
  </context-param>
  
  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

  <servlet>
    <servlet-name>bookExporter</servlet-name>
    <servlet-class>
      org.springframework.web.context.support.HttpRequestHandlerServlet
    </servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>bookExporter</servlet-name>
    <url-pattern>/remoting/BookService</url-pattern>
  </servlet-mapping>
</web-app>

Kada server dobije zahtev na /remoting/BookService url, kako zna šta u tom trenutku treba preduzeti? U Spring konfiguraciji u fajlu /WEB-INF/serverContext.xml definisan je bean sa istim imenom kao i servlet – bookExporter. Kao što se i vidi u xmlu, deklarisali smo da je bookExporter instanca HttpInvokerServiceExporter klase, da eksportuje BookService interfejs, te da se konkretna implementacija interfejsa nalazi u bookService beanu.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="bookService" class="rs.javasvet.service.BookServiceImpl" />

  <bean name="bookExporter" 
    class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
      <property name="service" ref="bookService"/>
      <property name="serviceInterface" value="rs.javasvet.service.BookService"/>
  </bean>

</beans>

Na klijentu je priča jednako jednostavna. Ispod je prikazan sadržaj datoteke appContext.xml koji predstavlja Spring konfiguraciju klijenta. U Spring kontekstu smo deklarisali bean library, koji je instanca naše Library klase. U Library klasu ubacujemo instancu BookService interfejsa koju smo deklarisali u bookServiceProxy beanu.

Bean bookServiceProxy je kao što se vidi instanca HttpInvokerProxyFactoryBean klase. Za HttpInvokerProxyFactoryBean klasu je obavezno deklarisati koji inerfejs implementira – u našem slučaju to je BookService. Još je potrebno deklarisati gde se nalazi implementacija – kod nas je to http://localhost:8080/server/remoting/BookService url.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="bookServiceProxy"
      class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl"
          value="http://localhost:8080/server/remoting/BookService"/>
        <property name="serviceInterface" value="rs.javasvet.service.BookService"/>
  </bean>
  
  <bean id="library" class="rs.javasvet.Library">
    <property name="bookService" ref="bookServiceProxy" />
  </bean>

</beans>

I na samom kraju potrebno je za klijent aplikaciju napraviti ulaznu tačku, što je urađeno u App klasi. Main metod je veoma jednostavan – učitava Spring kontekst i iz njega uzima instancu Library klase koju zatim pokreće.

public class App {

  public static void mainString[] args throws IOException {
    
      ApplicationContext context = 
        new ClassPathXmlApplicationContext(new String[]{"appContext.xml"});
    
      Library library = (Library)context.getBean("library");
      library.run();
    }
}

To je sve što je potrebno. Kada pokrenete server i klijentsku aplikaciju, te na klijentu izaberete neku opciju koja poziva metod BookService interfejsa Spring remoting će uraditi sve što je potrebno. Poslaće zahtev kroz Http na udaljeni server, gde će se pozvati metod konkretne implementacije. Rezultat će se zatim serijalizovati putem standardne Java serijalizacije i poslati nazad na klijent aplikaciju. Tamo će Spring uraditi deserijalizaciju i aplikaciji proslediti rezultat poziva.

Download

Moguće je preuzeti primerne maven2 projekte common, client, server (Eclipse workspace).