Ako rozbehať Spring HTTP Invoker na vzdialené volanie procedúr?

Načo invoker?

Na remoting v Jave, čiže komunikáciu medzi vzdialenými službami, existuje milión technológií: RMI, SOAP, REST, stačí si vybrať.

Ak chceme obetovať interoperabilitu, bežný zabudovaný RMI, takmer úplne postačuje. Azda jedinou veľkou nevýhodou je réžia okolo transportného protokolu: musíte mať spustený dedikovaný server na špecifickom porte, čo sa nekamaráti s firewallmi.

Spring Framework však dáva k dispozícii svoj vlastný variant, Spring HTTP Invoker, ktorý funguje podobne ako klasické RMI, ibaže miesto binárneho protokolu využíva klasické HTTP. Službu tak možno zavesiť priamo do servletového kontajnera spolu s ostatnými komponentami aplikácie, a možno ju tiež sledovať pomocou bežných trasovacích nástrojov

Ukážme si to všetko na veľkom príklade jednoduchej služby.

Vytvoríme jednoduchú službu, ktorá bude ponúkať jedlo:

public interface FoodService {
    List<Food> list();      
    void add(Food food);
}

Jedlo Food bude jednoduchá trieda:

public class Food implements Serializable {
    protected int energy;

    protected String name;

    /* .. gettre, settre ..*/

}

Objekty typu Food budú lietať po kábli, čiže budú obsiahnuté v posielaných a prijímaných správach. Keďže HTTP Invoker využíva klasickú Java serializáciu, trieda musí implementovať java.io.Serializable.

Tri projekty

Celá “infraštruktúra” bude pozostávať z troch projektov:

  • serverovská časť: obsahuje samotnú implementáciu služby a pomocnú triedu, ktorou pustíme server
  • klientska časť: obsahuje jednoduchú triedu, ktorou zavoláme vzdialenú službu na serveri
  • spoločné triedy: obsahuje interfejs služby a triedu pre objekt správ

Celá architektúra bude vyzerať nasledovne:

Celková architektúra

Spoločné API pre server i klient

Podobne ako v prípade RMI musí mať klient i server k dispozícii interfejs služby. Na strane servera sa za interfejsom vytvorí dynamická proxy trieda (skeleton), ktorá bude spracovávať HTTP požiadavky a preposielať ich implementácii služby.

Na strane klienta sa za interfejsom zase vytvorí klientská dynamická proxy trieda (stub), ktorá bude odosielať HTTP požiadavky, serializovať objekty parametrov a deserializovať odpovede zo servera.

Spoločné API nech je v projekte spring-http-invoker-demo-api, ktorý nemusí mať žiadne závislosti a bude obsahovať len:

  • službu FoodService
  • dátové objekty Food

Časť pre server

Máme dve možnosti ako implementovať serverovskú časť. Ak náš server pobeží na oraclovskej Jave 6 a novšej, môžeme využiť tamojší zabudovaný HTTP server. To nám zjednoduší konfiguráciu, ale obmedzí nasadenie. Tak či onak, budeme potrebovať:

  • implementáciu triedy FoodService — čiže samotnú “biznis logiku”.
  • pomocnú triedu, ktorá spustí server.

Biznis logika

Biznis logiku môžeme implementovať podľa uváženia. V našom projekte bude veľmi jednoduchá. Všetky jedlá budeme držať v jednom konkurentnom zozname, ktorý v metóde list() vrátime a pomocou metódy add() doň môžeme pridávať nové jedlá. V konštruktore do tohto zoznamu nasekáme niekoľko ukážkových jedál.

package sihd.server;

import java.util.*;
import sihd.api.*;

public class MemoryFoodService implements FoodService {
    private List<Food> foodList = new CopyOnWriteArrayList<Food>();

    public MemoryFoodService() {
        add(new Food(350, "Lindt"));
        add(new Food(120, "Poor man's nonchocolate"));
    }

    public List<Food> list() {
        return foodList;
    }

    public void add(Food food) {
        foodList.add(food);
    }

}

Závislosti a server pre Oracle JDK 6+

Ak riadime závislosti pomocou Mavenu, deklarujme ich takto:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>3.2.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.sun.net.httpserver</groupId>
        <artifactId>http</artifactId>
        <version>20070405</version>
    </dependency>
    <dependency>
        <groupId>sk.upjs.ics.novotnyr</groupId>
        <artifactId>spring-http-invoker-demo-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Prvá závislosť predstavuje modul spring-web, v ktorom sa nachádzajú triedy HTTP Invokera, druhá závisí na JARoch HTTP servera (ktoré sa v Maven úložisku nachádzajú aj v samostatnom JARe vyextrahovanom z oraclovskej Javy) a tretia predstavuje spoločné API.

Trieda pre spustenie servera

V rámci triedy pre spustenie servera potrebujeme:

  1. vytvoriť inštanciu našej pamäťovej služby MemoryFoodService.
  2. vytvoriť inštanciu HttpServera,
  3. vytvoriť inštanciu HttpHandlera, ktorý bude obsluhovať požiadavky. Našťastie, Spring ponúka mechanizmy HTTP Invokera presne v tejto podobe, čiže konfigurácia je jednoduchá. Tú reprezentuje SimpleHttpInvokerServiceExporter, ktorej potrebujeme nastaviť dve veci:
    • interfejs služby, za ktorým sa vytvorí skeleton: teda FoodService
    • implementačnú triedu s biznis logikou, teda inštanciu MemoryFoodService.
  4. prepojiť HTTP handlera so serverom.
  5. spustiť HTTP server

Výsledný kód vyzerá nasledovne:

package sihd.server;

import java.io.IOException;
import org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter;
import sihd.api.FoodService;
import com.sun.net.httpserver.HttpServer;

public class JDkServerRunner {
    public static void main(String[] args) throws IOException {
        HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);

        FoodService foodService = new MemoryFoodService();

        SimpleHttpInvokerServiceExporter serviceExporter = new SimpleHttpInvokerServiceExporter();
        serviceExporter.setServiceInterface(FoodService.class);
        serviceExporter.setService(foodService);
        // prebehne inicializácia service exportera
        serviceExporter.afterPropertiesSet();

        httpServer.createContext("/food.service", serviceExporter);
        httpServer.start();
    }
}

Všimnime si, že HTTP server pobeží na porte 8080, a prípona URL adresy pre službu bude /food.service. Znamená to, že služba bude dostupná na:

http://localhost:8080/food.service 

spring-http-invoker-architecture-server1

Časť pre klienta

Klienta časť bude jednoduchá: pozostáva z jedinej triedy, kde otestujeme klienta. Projekt spring-http-invoker-demo-api bude mať dve závislosti: * na spring-web z projektu Spring Framework * na spoločnom API.

Mavenovský pom.xml teda môže obsahovať:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>3.2.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>sk.upjs.ics.novotnyr</groupId>
        <artifactId>spring-http-invoker-demo-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

Použitie klienta bude jednoduché: jadrom je trieda HttpInvokerProxyFactoryBean zo Springu, ktorej nastavíme URL adresu koncového bodu podľa servera. Ďalej potrebujeme nastaviť interfejs so servicom a to presne taký istý ako je na strane servera (čiže FoodService).

Pomocou metódy getObject() získame stub, teda objekt typu FoodService, za ktorým sa skrýva dynamické proxy zodpovedné za sieťovú komunikáciu a môžeme veselo volať vzdialené metódy.

package sihd.client;

import java.util.List;

import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;

import sihd.api.Food;
import sihd.api.FoodService;

public class FoodClientRunner {
    public static void main(String[] args) {
        HttpInvokerProxyFactoryBean stub = new HttpInvokerProxyFactoryBean();
        stub.setServiceUrl("http://localhost:8080/food.service");
        stub.setServiceInterface(FoodService.class);
        stub.afterPropertiesSet();

        FoodService foodService = (FoodService) stub.getObject();
        List<Food> foodList = foodService.list();
        for (Food food : foodList) {
            System.out.println(food);
        }
    }
}

Časť pre server [skrz DispatcherServlet]

Ak chceme použiť HTTP Invoker v plnoprávnom servletovom kontajneri, je to nielen možné, ale je to dokonca preferovaná možnosť.

Závislosti

Budeme potrebovať tri závislosti:

  • na module spring-webmvc zo Springu. (Budeme potrebovať nielen veci okolo remotingu, ale aj niektoré triedy zo Spring MVC.)
  • na API pre servlety 3.0
  • a na spoločnom API

Definícia v Mavene môže vyzerať nasledovne:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>3.2.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>sk.upjs.ics.novotnyr</groupId>
    <artifactId>spring-http-invoker-demo-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

Triedy

V rámci projektu použijeme bezXMLový prístup v duchu Spring 3.0 a Servlet API 3.0. V projekte budeme mať tri triedy:

  • Namiesto web.xml využijeme WebInitializer.
  • Namiesto XML aplikačného kontextu využijeme triedu anotovanú pomocou `@Configuration@.
  • Potrebujeme tiež implementáciu biznis logiky pre FoodService, na čo zrecyklujeme MemoryFoodService.

Vlastný WebInitializer

V nových Springoch už nemusíme používať web.xml. Miesto neho stačí prekryť metódu triedy AbstractAnnotationConfigDispatcherServletInitializer.

V nich deklarujeme, ktorá trieda obsahuje konfiguráciu springovského kontextu a tiež predpis pre URL adresy, ktoré bude obsluhovať centrálny dispatcher servlet.

Prvé dosiahneme prekrytím metódy getServletConfigClasses(), kde vrátime názov našej triedy s konfiguráciou (zmienime sa o nej o chvíľu, bude ňou trieda sihd.server.ApplicationContextConfiguration), druhé zase cez prekrytie metódy getServletMappings(), kde vrátime predpis *.service. Ten znamená, že cez springácky dispatcher servlet pôjdu všetky URL adresy končiace sa na .service.

package sihd.server;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { ApplicationContextConfiguration.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "*.service" };
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

}

Konfigurácia aplikačného kontextu.

Konfiguráciu aplikačného kontextu vyriešime tiež v kóde. Bude ním bežná trieda anotovaná cez @Configuration.

@Configuration
@EnableWebMvc
public class ApplicationContextConfiguration {
    ...
}

Anotácia @Configuration indikuje triedu s konfiguráciou springácky beanov. Druhá anotácia @EnableWebMvc zase automaticky ponastavuje veci súvisiace s webovou časťou: nahodí preddefinovaný dispatcher servlet s viacerými magickými vecami pre mapovanie URL adries na obslužné triedy.

Budeme potrebovať dva beany:

  • bean s biznis logikou
  • HttpInvokerServiceExporter, ktorý je jadrom HTTP invokera

Celý kód je nasledovný:

package sihd.server;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import sihd.api.FoodService;

@Configuration
@EnableWebMvc
public class ApplicationContextConfiguration {
    @Bean(name="/food.service")
    public HttpInvokerServiceExporter httpInvokerServiceExporter() {
        HttpInvokerServiceExporter exporter  = new HttpInvokerServiceExporter();
        exporter.setServiceInterface(FoodService.class);
        exporter.setService(foodService());
        return exporter;
    }

    @Bean
    public FoodService foodService() {
        return new MemoryFoodService();
    }
}

V beane HttpInvokerServiceExporter deklarujeme dve tradičné veci: interfejs s triedou FoodService a jeho implementáciou. Exportér sa automaticky postará o vytvorenie stubu a jeho zverejnenie v serveri. URL prípona sa odvodí z parametra name v anotácii @Bean, čiže na http://názovServera:port/food.service.

spring-http-invoker-architecture-server2

Nasadenie v Jetty

Pokiaľ máte Maven, môžete server spustiť veľmi jednoducho. Dodajte plug-in pre Jetty:

<plugins>
    <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>9.0.4.v20130625</version>
    </plugin>
</plugins>

A spustite server cez

mvn jetty:run

Služba sa automaticky spustí na

http://localhost:8080/food.service

Následne môžete projekt vyskúšať zo strany klienta.

Poznámky k architektúre

Je veľmi dôležité nezabudnúť na pravidlo, že všetky objekty lietajúce po kábli musia byť serializovateľné! To platí aj pre komplexné triedy a interfejsy.

Ak by trieda FoodService vracala Food, ktorý by nebol triedou, ale interfejsom, za ktorým by sa skrývala napríklad čokoláda Chocolate či zelenina Vegetable, obe triedy musia byť serializovateľné a prítomné v projekte spoločného API.

Inak povedané, klientská strana nemôže automaticky prijať definície tried zo servera, alebo očakávať objekty ľubovoľného typu. Ak sa deserializácia údajov, ktoré prídu z kábla, nepodarí, komunikácia bude hádzať výnimky.

Ukážkové projekty

Na GitHube možno nájsť ukážkové projekty demonštrujúce funkcionalitu. Všetky projekty sú založené na Mavene.

  • spring-http-invoker-demo-api: spoločné API s interfejsom a objektami lietajúcami po drôte.
  • spring-http-invoker-demo-server: ukážka servera založeného na Springu a Dispatcher Servlete nasaditeľná do Jetty
  • spring-http-invoker-demo-server-jvm: ukážka servera založeného na oraclovskej JDK 6 bez nutnosti servletovského kontajnera
  • spring-http-invoker-demo-client: ukážka klientského modulu

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *