Archiwum

Archive for Lipiec 2012

Jak szybko stworzyć aplikacje webową, która zwraca JSON-a

16/07/2012 Dodaj komentarz

Ostatnio sprawdzałem możliwości JavaScriptu w aplikacjach webowych. Tutaj udział część serwerowej sprowadzał się do obsługi wywołań AJAX-owych (zwracania wyników w JSON-e). W skrócie nie chciałem się zajmować serwerem, wystarczało że zwracał mi jakiś kawałek JSON-a, tak abym mógł skupić się tylko na HTML, CSS i JS.

Bez Javy, ale tak bym chciał
Tak więc na początek trafiłem na takie całkiem fajne rozwiązanie w pythonie.

python -m SimpleHTTPServer 8080

Jak ma się zainstalowanego pythona, to jest to najprostszy sposób na postawienie serwera HTTP. Gdzie wystawioną zawartością, poprzez URL, jest to co znajduje się w katalogu, w którym z linii poleceń uruchomiło się to. Tak więc jeśli mamy tam katalog
dane, a w nim plik test.json, to możemy do tego pliku odwołać się poprzez taki url:

http://localhost:8080/dane/test.json

Zatem statyczne kawałki JSON-a leżą sobie w plikach na dysku, obok może znajdować się plik app.js, który odwołuję się do nich, i który to jest głównym przedmiotem pracy. Całość mogę edytować po dowolnym narzędziem, i każda zmiana od razu jest widoczna w przeglądarce (wystarczy F5). Piękne 🙂 nie trzeba się martwić synchronizacją z serwerem, można się skupić na pracy i od razu widzieć efekty w przeglądarce.
Rozwiązanie to daje jednak tylko statycznego JSON-a. Aby wprowadzić więcej dynamizmu trzeba jednak napisać trochę po stronie serwera. Nie patrzyłam już czy SimpleHTTPServer oferuję jakieś większe możliwość przeszedłem od razu do javy, w końcu jak na razie w tym głównie piszę.

Z Javą, próba odtworzenia
Zacząłem od stworzenia prostego projektu mavenowego, tak aby móc łatwo zarządzać zależnościami, po za tym i tak zwykle jest to projekt tego typu. Idą za wskazówkami zawartymi w tym poście wybrałem SpringMVC. Cała moja praca po stronie serwera ograniczyła się do stworzenia jednej klasy, która produkowała mi JSON-a jakiego chciałem.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/app")
public class AppController {
	
	private static List<String> names = new ArrayList<String>();
	
	@RequestMapping(value = "/add", method = RequestMethod.GET)
	public @ResponseBody void add(@RequestParam(value="name") String name){
		names.add(name);
	}
	
	@RequestMapping(value = "/listAll", method = RequestMethod.GET)
	public @ResponseBody List<String> listAll(){
		return names;
	}
	
	@RequestMapping(value = "/remove", method = RequestMethod.GET)
	public @ResponseBody void remove(@RequestParam(value="name") String name){
		names.remove(name);
	}

}

Dzięki temu dostałem trzy url-e, z których mogłem korzystać po stornie JavaScript:

http://localhost:8080/RestTest/rest/app/add?name=dwa
http://localhost:8080/RestTest/rest/app/remove?name=dwa
http://localhost:8080/RestTest/rest/app/listAll

Ostatni może zwracać takiego JSON-a:

["jeden","dwa","trzy"]

Takie coś w zupełności wystarcza do symulowania logiki po stronie serwera, a jak widać w przedstawionej klasie, rozbudowanie tego nie powinno być trudne.
Oprócz tej klasy potrzebne były jeszcze techniczne pliki, wszytko wziąłem z projektu w przytoczonym poście.
Tak wiec w web.xml potrzebne są takie wpisy aby postawić kontekst Springa.

	<servlet>
		<servlet-name>mvc-dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>mvc-dispatcher</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

Najistotniejsze jest zwrócenie skąd się bierze rest w URL (RestTest to nazwa mego wara), a plik mvc-dispatcher-servlet.xml opisujący co Spring ma robić jest trywialny:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc" 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-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

	<context:component-scan base-package="test.controller" />

	<mvc:annotation-driven />

</beans>

jak widać jedyne co trzeba dodać to wpis włączający interpretacje adnotacji w napisanej klasie. Do dopełnienie wszystkiego jest jeszcze plik mavena, który zaprezentuje na końcu.

Jest Java, ale jakby krok wstecz
Tak więc miałem już dynamicznego JSON-a, i sposób jak go dostosowywać. Mogłem się zabrać za JavaScript. Jednak utraciłem tutaj szybką pętle zwrotną, w weryfikacji wprowadzanych zmian. Teraz pomiędzy zapisaniem zmiany w pliku, a odświeżeniem widoku w przeglądarce, doszedł krok budowy wara i wgrania go na Tomcata lub inny WebSerwer. A ja chciałem tylko zmieniać pliki JS lub HTML, część serwerowa nie zmieniałaby się w ogóle.

Z Javą, już prawie idealnie
Rozwiązań tego pewnie jest parę. Ja zwróciłem się w kierunku wbudowanego kontenera aplikacje webowych, czyli użyłem jetty.
W pom.xml pojawia się taki wpis jak niżej. Jetty uruchamia się taka komendą mvn jetty:run.

			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>maven-jetty-plugin</artifactId>
				<version>6.1.24</version>
				<configuration>
					<stopPort>9966</stopPort>
					<stopKey>RestTest</stopKey>
					<!-- Redeploy every x seconds if changes are detected, 0 for no automatic redeployment -->
					<scanIntervalSeconds>0</scanIntervalSeconds>
					<!-- make sure Jetty also finds the widgetset -->
					<webAppConfig>
						<contextPath>/RestTest</contextPath>
						<!-- -->
						<baseResource implementation="org.mortbay.resource.ResourceCollection">
							<resources>src/main/webapp</resources>
						</baseResource>
					</webAppConfig>
				</configuration>
			</plugin>

Ważny jest parametr scanIntervalSeconds, mówi on o tym czy zmiany w naszych klasach będą automatycznie redeployowane. Trwa to trochę, ale można z tym żyć. To i tak nie dotyczy naszego HTML-a i JS, to co powoduję że zmiany w tych plikach są widoczne od razu w przeglądarce, to parametr baseResource. Wskazanie na katalog webapp, gdzie trzymane są te pliki powoduje, że zmiany są propagowane (wykomentowałem ten parametr i funkcjonalność ta nadal działało co pewnie świadczy o tym że jest to wartość domyśla). Nie wczytywałem się za bardzo w szczegóły ustawień i może trochę podorabiałem teorii do praktyki, ale działa tak jak chciałem. Mam co chciałem i mogę spokojnie pracować nad plikami JS, i zmiany w nich widzieć od razu w przeglądarce.

Taki mały zgrzyt
Pojawia się zgrzyt, podobno tylko dla Windowsa, pliki uaktualniają się tylko za pierwszym razem, kolejny raz nie można już zapisać, bo są blokowane. Ten wpis opisuje problem i możliwe rozwiązania. Ja użyłem dodatkowego wpisu w web.xml, na razie nie martwię się tym że brudzi to ten plik. Dodatkowy element to:

	<servlet>
	    <servlet-name>default</servlet-name>
	    <servlet-class>org.mortbay.jetty.servlet.DefaultServlet</servlet-class>
	    <init-param>
	      <param-name>useFileMappedBuffer</param-name>
	      <param-value>false</param-value>
	    </init-param>
	    <load-on-startup>0</load-on-startup>
	  </servlet>

I to wszytko, można się zabrać za JavaScript. Teraz mam szybką odpowiedź zwrotną w przeglądarce, wiec jak dla mnie bardzo dobre warunki do pracy z tak krnąbrny językiem 😉 , o czym może napisze następnym razem.

A całość pom.xml prezentuję się tak:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>test</groupId>
	<artifactId>RestTest</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<spring.version>3.0.5.RELEASE</spring.version>
	</properties>

	<dependencies>

		<!-- Jackson JSON Mapper -->
		<dependency>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
			<version>1.7.1</version>
		</dependency>
		<!-- Spring 3 dependencies -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>

	</dependencies>

	<build>
		<finalName>RestTest</finalName>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>maven-jetty-plugin</artifactId>
				<version>6.1.24</version>
				<configuration>
					<stopPort>9966</stopPort>
					<stopKey>RestTest</stopKey>
					<!-- Redeploy every x seconds if changes are detected, 0 for no automatic 
						redeployment -->
					<scanIntervalSeconds>0</scanIntervalSeconds>
					<!-- make sure Jetty also finds the widgetset -->
					<webAppConfig>
						<contextPath>/RestTest</contextPath>
						<!-- -->
						<baseResource implementation="org.mortbay.resource.ResourceCollection">
							<resources>src/main/webapp</resources>
						</baseResource>
					</webAppConfig>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Małe uaktualnienie: taki sam efekt z wbudowanym kontenerem aplikacji webowych można uzyskać z tomcat-maven-plugin. Nie ma się tylko przeładowania klas (w każdym razie nie znalazłem tego). Ale nie trzeba brudzić swego pliku web.xml i nie ma problemu z blokowaniem edytowanych plików. Tak więc w pomie zamiast jetty należy dodać:

			
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>tomcat-maven-plugin</artifactId>
				<version>1.1</version>
			</plugin>

i uruchomić z linii poleceń komendą: mvn tomcat:run