PA165/Cvičení Remoting
Obsah
JavaRMI
Nejdřív si předvedeme JavaRMI.
Na tabule.jar je JAR soubor s následující definicí:
package cz.muni.fi.pa165.rmi; import java.rmi.Remote; import java.rmi.RemoteException; public interface Tabule extends Remote { void zverejni(String text) throws RemoteException; }
Klient
Vytvořte si projekt, přidejte k němu JAR s rozhraním a vytvořte klienta:
package cz.muni.fi.pa165.rmi; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; import java.rmi.RemoteException; import java.rmi.NotBoundException; public class Pisalek { public static void main(String[] args) throws RemoteException, NotBoundException { if(args.length!=2) { System.out.println("Usage: java "+Pisalek.class.getName()+" host text"); System.exit(1); } String host = args[0]; String text = args[1]; Registry remregistry = LocateRegistry.getRegistry(host); Tabule tabule = (Tabule) remregistry.lookup("tabule"); tabule.zverejni(text); } }
Klienta spusťte vůči serveru cvičícího na nymfe31.fi.muni.cz.
(Pro ty, kdo neumí zkompilovat a spustit javovou třídu, tak z příkazového řádku se to udělá takto:
mkdir rmicviceni cd rmicviceni wget http://acrab.ics.muni.cz/~makub/rmi/tabule.jar mkdir -p cz/muni/fi/pa165/rmi vi cz/muni/fi/pa165/rmi/Pisalek.java javac -cp tabule.jar:. cz/muni/fi/pa165/rmi/Pisalek.java java -cp tabule.jar:. cz.muni.fi.pa165.rmi.Pisalek nymfe31 'neco chytreho'
nebo v NetBeans si vytvoříte nový projekt pomocí File - New Project - Categories: Java - Projects: Java Application, v dialogu 'Create Main Class zadat cz.muni.fi.pa165.rmi.Pisalek, pak v Libraries zvolit Add JAR/Folder, přidat soubor tabule.jar, přepsat třídu Pisalek, na projektu pak zvolit Properties - Run - Arguments a do políčka napsat nymfe31 'neco chytreho' . Pak spustit projekt. Z příkazové řádky je to jednodušší, že. )
Server
Utvořte dvojice se sousedem a vyzkoušejte komunikaci s jeho serverem.
Na straně serveru je třeba spustit na pozadí program
rmiregistry &
mít implementaci serveru:
package cz.muni.fi.pa165.rmi; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.ServerNotActiveException; import java.rmi.server.UnicastRemoteObject; public class TabuleImpl implements Tabule { synchronized public void zverejni(String text) throws RemoteException { try { System.out.println(UnicastRemoteObject.getClientHost() + ": " + text); } catch (ServerNotActiveException e) { e.printStackTrace(); } } public static void main(String[] args) throws RemoteException { System.out.println("startuji Tabuli"); Tabule stub = (Tabule) UnicastRemoteObject.exportObject(new TabuleImpl(), 0); LocateRegistry.getRegistry().rebind("tabule", stub); } }
která musí být zkompilována
javac -cp tabule.jar:. cz/muni/fi/pa165/rmi/TabuleImpl.java
a spuštěna se správnými parametry:
java -Djava.rmi.server.codebase=http://acrab.ics.muni.cz/~makub/rmi/tabule.jar -cp tabule.jar:. cz.muni.fi.pa165.rmi.TabuleImpl
Pozor, v B130 v listopadu 2012 je nesmyslně zkonfigurovaný /etc/hosts, váže název stroje na lokální adresu, takže nelze kontaktovat jiné stroje přes RMI. Proto je potřeba použít následující:
MYADDR=`ifconfig | awk -F':' '/inet addr/&&!/127.0.0.1/{split($2,_," ");print _[1]}'` echo $MYADDR java -Djava.rmi.server.codebase=http://acrab.ics.muni.cz/~makub/rmi/tabule.jar \ -Djava.rmi.server.hostname=$MYADDR -cp tabule.jar:. cz.muni.fi.pa165.rmi.TabuleImpl
Metoda v rozhraní Tabule nepřijímá žádné třídy, které by JVM na straně serveru neznal. Pokud by tomu tak bylo, i klient by musel vystavit takové třídy někde na webu a musel by být spuštěn s parametrem -Djava.rmi.server.codebase
, aby si server mohl třídy stáhnout.
JSON-RPC
Ukážeme si vzdálené volání procedur (RPC) pomocí protokolu JSON-RPC 2.0. K implementaci použijeme knihovnu jsonrpc4j.
Rozhraní služby
Stáhněte si Mavenový projekt tabule-json-rpc.zip, rozzipujte, a nainstalujte, bud pomocí mvn install nebo otevřením v NetBeans a zvolením Build.
Projekt obsahuje definici rozhraní a vyjímky:
Tabule.java
package cz.muni.fi.pa165.jsonrpc; public interface Tabule { void zverejni(String text) throws Vyjimka; }
Vyjimka.java
package cz.muni.fi.pa165.jsonrpc; public class Vyjimka extends Exception { public Vyjimka(String message) { super(message); } }
JSON-RPC server
Vytvořte si nový projekt typu Maven s názvem tabule-json-rpc-server a do pom.xml připište závislost na projektu tabule-json-rpc a knihovnách pro JSON-RPC:
<name>tabule-json-rpc-server</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <repositories> <repository> <id>jsonrpc4j-webdav-maven-repo</id> <name>jsonrpc4j maven repository</name> <url>http://jsonrpc4j.googlecode.com/svn/maven/repo/</url> <layout>default</layout> </repository> </repositories> <dependencies> <dependency> <groupId>cz.muni.fi.pa165</groupId> <artifactId>tabule-json-rpc</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.googlecode</groupId> <artifactId>jsonrpc4j</artifactId> <version>0.25</version> </dependency> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId> <version>7.0.27</version> </dependency> <dependency> <groupId>org.simpleframework</groupId> <artifactId>simple</artifactId> <version>4.1.21</version> </dependency> </dependencies>
Vytvořte následující třídy v balíku cz.muni.fi.pa165.jsonrpc:
TabuleImpl.java
package cz.muni.fi.pa165.jsonrpc; public class TabuleImpl implements Tabule { @Override public void zverejni(String text) throws Vyjimka { if (text == null || text.isEmpty()) throw new Vyjimka("text je prázdný"); System.out.println(SimpleServer.getClient()+": " + text); } }
SimpleServer.java
package cz.muni.fi.pa165.jsonrpc; import com.googlecode.jsonrpc4j.JsonRpcServer; import java.io.IOException; import java.net.InetSocketAddress; import java.util.logging.*; import org.simpleframework.http.Request; import org.simpleframework.http.Response; import org.simpleframework.http.core.Container; import org.simpleframework.transport.connect.Connection; import org.simpleframework.transport.connect.SocketConnection; public class SimpleServer implements Container { private JsonRpcServer jsonRpcServer; public SimpleServer(JsonRpcServer jsonRpcServer) { this.jsonRpcServer = jsonRpcServer; } private static ThreadLocal<String> client = new ThreadLocal<String>(); static String getClient() { return client.get(); } @Override public void handle(Request request, Response response) { try { if ("POST".equals(request.getMethod())) { response.set("Content-Type", "application/json"); client.set(request.getClientAddress().getHostName()); jsonRpcServer.handle(request.getInputStream(), response.getOutputStream()); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { //enable logging Logger log = LogManager.getLogManager().getLogger(""); log.setLevel(Level.ALL); for (Handler h : log.getHandlers()) h.setLevel(Level.FINEST); //start server Logger.getLogger(SimpleServer.class.getName()).info("starting server"); JsonRpcServer tabule = new JsonRpcServer(new TabuleImpl(), Tabule.class); Connection connection = new SocketConnection(new SimpleServer(tabule)); connection.connect(new InetSocketAddress(1420)); } }
Spusťte server.
JSON-RPC klient
Vytvořte nový projekt typu Maven jménem tabule-json-rpc-klient a do pom.xml připište:
<repositories> <repository> <id>jsonrpc4j-webdav-maven-repo</id> <name>jsonrpc4j maven repository</name> <url>http://jsonrpc4j.googlecode.com/svn/maven/repo/</url> <layout>default</layout> </repository> </repositories> <dependencies> <dependency> <groupId>cz.muni.fi.pa165</groupId> <artifactId>tabule-json-rpc</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.googlecode</groupId> <artifactId>jsonrpc4j</artifactId> <version>0.25</version> </dependency> </dependencies>
Vytvořte třídu
SimpleClient.java
package cz.muni.fi.pa165.jsonrpc; import com.googlecode.jsonrpc4j.JsonRpcHttpClient; import com.googlecode.jsonrpc4j.ProxyUtil; import java.net.MalformedURLException; import java.net.URL; public class SimpleClient { public static void main(String[] args) throws MalformedURLException, Vyjimka { JsonRpcHttpClient client = new JsonRpcHttpClient(new URL("http://localhost:1420/")); Tabule tabule = ProxyUtil.createClientProxy(SimpleClient.class.getClassLoader(), Tabule.class, client); tabule.zverejni("ahoj"); //tabule.zverejni(null); } }
Spusťte klienta. Pak zkuste odkomentovat řádek s tabule.zverejni(null); a spusťte ho znovu.
Samostatný úkol - změna rozhraní
V původním projektu tabule-json-rpc přidejte do rozhraní Tabule nějakou další metodu, dejte Build.
V projektu tabule-json-rpc-server implementujte metodu ve třídě TabuleImpl a znovu spusťte server.
V projektu tabule-json-rpc-klient zavolejte novou metodu.
Pozorujte, jak vypadají JSON-RPC zprávy v logu serveru.
SOAP/WSDL Web Services
Ukážeme si web services pomocí Apache CXF.
SOAP server
Vytvořte si nový projekt typu Maven zvaný třeba soap-cxf-server. Do pom.xml připište:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <cxf.version>2.7.0</cxf.version> </properties> <dependencies> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>${cxf.version}</version> </dependency> <!-- Jetty is needed if you're are not using the CXFServlet --> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http-jetty</artifactId> <version>${cxf.version}</version> </dependency> <!-- SLF4J binding --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.2</version> </dependency> </dependencies>
Vytvořte package cz.muni.fi.pa165.soap a v něm tyto třídy:
package cz.muni.fi.pa165.soap; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.ws.WebFault; @WebFault(name = "Vyjimka") @XmlAccessorType(XmlAccessType.FIELD) public class Vyjimka extends RuntimeException { String duvod; public Vyjimka(String message,String duvod) { super(message); this.duvod = duvod; } }
package cz.muni.fi.pa165.soap; import javax.jws.WebService; import javax.jws.WebParam; @WebService public interface Tabule { void zverejni(@WebParam(name = "text")String text) throws Vyjimka; }
package cz.muni.fi.pa165.soap; import javax.annotation.Resource; import javax.jws.WebService; import javax.servlet.http.HttpServletRequest; import javax.xml.ws.WebServiceContext; import javax.xml.ws.handler.MessageContext; @WebService(endpointInterface = "cz.muni.fi.pa165.soap.Tabule", serviceName = "Tabule") public class TabuleImpl implements Tabule { @Resource WebServiceContext wsCtx; public void zverejni(String text) throws Vyjimka { if (text == null || text.isEmpty()) throw new Vyjimka("empty text", "retezec prazdny"); System.out.println(getClient() + ":" + text); } private String getClient() { MessageContext msgCtx = wsCtx.getMessageContext(); HttpServletRequest req = (HttpServletRequest) msgCtx.get(MessageContext.SERVLET_REQUEST); return req.getRemoteHost(); } }
package cz.muni.fi.pa165.soap; import javax.xml.ws.Endpoint; public class Server { public static void main(String[] args) throws InterruptedException { String address = "http://localhost:9000/tabule"; Endpoint.publish(address, new TabuleImpl()); System.out.println("Server ready..."); } }
Spusťte třídu cz.muni.fi.pa165.soap.Server a navštivte URL http://localhost:9000/tabule?wsdl.
SOAP klient
Aniž uzavřete předchozí projekt, vytvořte nový projekt zvaný třeba soap-cxf-klient. Děláme to tak proto, abychom viděli, že jediným pojítkem mezi serverem a klientem je WSDL popis služby.
Do pom.xml připište
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <cxf.version>2.7.0</cxf.version> </properties> <dependencies> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http-jetty</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-common-utilities</artifactId> <version>2.5.6</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.cxf</groupId> <artifactId>cxf-codegen-plugin</artifactId> <version>${cxf.version}</version> <executions> <execution> <id>generate-sources</id> <configuration> <wsdlOptions> <wsdlOption> <wsdl>http://localhost:9000/tabule?wsdl</wsdl> </wsdlOption> </wsdlOptions> </configuration> <goals> <goal>wsdl2java</goal> </goals> </execution> </executions> </plugin> <!-- nastaveni verze zdrojaku --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> <!-- zavislosti na JARech --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <configuration> <outputDirectory> ${project.build.directory} </outputDirectory> </configuration> </plugin> </plugins> </build>
Přidejte třídu:
package cz.muni.fi.pa165.soap; public class Klient { public static void main(String[] args) { Tabule tabule = new Tabule_Service().getTabuleImplPort(); try { tabule.zverejni("ahoj"); System.out.println("zaslano"); //tabule.zverejni(""); } catch (Vyjimka_Exception e) { e.printStackTrace(); System.out.println(e.getFaultInfo().getDuvod()); } } }
V NetBeans dejte Build. Alternativně můžete z příkazového řádku spustit mvn generate-sources a mvn package.
Spusťte třídu Klient.
Pro kopii všech knihoven potřebných pro spuštění z příkazového řádku zadejte příkaz mvn dependency:copy-dependencies.
URL na službu se bere z WSDL popisu služby. Pokud ho potřebujete změnit, lze to udělat takto:
((BindingProvider) tabule).getRequestContext() .put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, "http://nymfe31.fi.muni.cz:9000/tabule");
REST
REST server
Vytvořte si nový projekt typu Maven - Web Application, jménem třeba rest-tabule-server.
Do pom.xml připište:
<packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <!-- Jackson JSON processor --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.1</version> </dependency> <!-- servlet API 3.0 --> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <!-- JSTL , just for escaping HTML --> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> </dependencies> <build> <plugins> <!-- Java language version --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> <!-- Servlet 3.0 without web.xml --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.1.1</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build>
Vytvořte si nový servlet
package cz.muni.fi.pa165.mavenproject2; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.taglibs.standard.functions.Functions; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.*; @WebServlet(urlPatterns = "/tabule/*") public class TabuleServlet extends HttpServlet { final List<Map<String, String>> zaznamy = Collections.synchronizedList(new ArrayList<Map<String, String>>()); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pathInfo = request.getPathInfo(); if (pathInfo != null && pathInfo.matches("/\\d+")) { response.setContentType("application/json"); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), zaznamy.get(Integer.parseInt(pathInfo.substring(1)))); } else { if ("html".equals(request.getParameter("type"))) { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); response.setHeader("Refresh", "5"); out.println("<h1>Tabule na " + request.getContextPath() + "/tabule</h2>"); out.println("<table border=\"1\">"); synchronized (zaznamy) { for (int i = zaznamy.size(); --i >= 0;) { Map<String, String> m = zaznamy.get(i); out.println("<tr><td><a href=\""+request.getContextPath()+"/tabule/"+i+"\">" + i + "</a>" + "<td>" + m.get("kdy") + "<td>" + m.get("kdo") + "<td>" + Functions.escapeXml(m.get("co"))); } } out.println("</table>"); } else { response.setContentType("application/json"); ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), zaznamy); } } } @Override protected synchronized void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String zverejni = request.getParameter("zverejni"); if (zverejni != null) { Map<String, String> m = new LinkedHashMap<String, String>(); m.put("kdy", sdf.format(new Date())); m.put("kdo", request.getRemoteHost()); m.put("co", zverejni); zaznamy.add(m); response.sendRedirect(request.getContextPath() + "/tabule/" + (zaznamy.size() - 1)); } else if (request.getContentType().startsWith("application/json")) { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readValue(request.getInputStream(), JsonNode.class); JsonNode jsonZverejni = jsonNode.get("zverejni"); if (!jsonNode.isMissingNode()) { Map<String, String> m = new LinkedHashMap<String, String>(); m.put("kdy", sdf.format(new Date())); m.put("kdo", request.getRemoteHost()); m.put("co", jsonZverejni.textValue()); zaznamy.add(m); response.sendRedirect(request.getContextPath() + "/tabule/" + (zaznamy.size() - 1)); } else { System.err.println("Missing key"); response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required data key."); } } else { System.err.println("Unknown request type " + request.getContentType()); response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unknown request type."); } } }
Po spuštění by na URL http://localhost:8080/tabule/?type=html měla být viditelná HTMl verze tabule, zatím prázdná. Na http://localhost:8080/tabule/ pak JSON data, zatím také prázdná.
REST klient
Vytvořte si nový projekt typu Maven - Java Application, jménem třeba rest-tabule-klient.
Do pom.xml připište
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <build> <plugins> <!-- Java language version --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>6</source> <target>6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>3.1.3.RELEASE</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.1</version> </dependency> </dependencies>
Vyvtořte si REST klienta, který přidá záznam a získá všechny záznamy:
package cz.muni.fi.pa165.rest.klient; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.web.client.RestTemplate; import java.net.URI; import java.util.HashMap; import java.util.Map; public class TabuleRestKlient { static String STROJ = "localhost"; static int PORT = 8080; static String webapp = "rest-tabule-server"; public static void main(String[] args) { RestTemplate rt = new RestTemplate(); String url = "http://"+STROJ+":"+PORT+"/"+webapp+"/tabule"; //POST pro přidání URI uri = rt.postForLocation(url+"?zverejni={text}", null, "příliš žluťoučký kůň"); System.out.println("uri nového záznamu = " + uri); //GET pro získání všech záznamů JsonNode jsonNode = rt.getForObject(url, JsonNode.class); for(JsonNode m : jsonNode) { System.out.println("záznam = " + m); } } }
Používáme Spring 3.x RestTemplate (javadoc pro RestTemplate).
Po spuštění na výše uvedeném URL můžete pozorovat přibývající záznamy.
Všimněte si, že kódování češtiny moc nefunguje, zkusíme to opravit.
Změníme kód klienta tak, aby místo URL parametru používal tělo POST requestu ve formátu JSON:
//POST pro přidání Map<String,String> map = new HashMap<String, String>(); map.put("zverejni","příliš žlutoučká kráva"); URI uri = rt.postForLocation(url, map);