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:
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:
- vytvoriť inštanciu našej pamäťovej služby
MemoryFoodService
. - vytvoriť inštanciu
HttpServer
a, - vytvoriť inštanciu
HttpHandler
a, ktorý bude obsluhovať požiadavky. Našťastie, Spring ponúka mechanizmy HTTP Invokera presne v tejto podobe, čiže konfigurácia je jednoduchá. Tú reprezentujeSimpleHttpInvokerServiceExporter
, 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
.
- interfejs služby, za ktorým sa vytvorí skeleton: teda
- prepojiť HTTP handlera so serverom.
- 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
Č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žijemeWebInitializer
. - Namiesto XML aplikačného kontextu využijeme triedu anotovanú pomocou `@Configuration@.
- Potrebujeme tiež implementáciu biznis logiky pre
FoodService
, na čo zrecyklujemeMemoryFoodService
.
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.
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 Jettyspring-http-invoker-demo-server-jvm
: ukážka servera založeného na oraclovskej JDK 6 bez nutnosti servletovského kontajneraspring-http-invoker-demo-client
: ukážka klientského modulu