понедельник, 7 июня 2010 г.

Google Web Toolkit — современное средство создания Rich Internet Applikation (часть 2)




В первой части этой статьи мы рассмотрели  возможности фрэймворка Google Web Toolkit, а примере создания клиентской части небольшого веб приложения, теперь займёмся реализацией его взаимодействия с сервером.
Итак, нам предстоит создать то, без чего удобный и функциональный паользовательский интерфейс веб-приложения не имеет никакого смысла. Я говорю о серверной его части взаимодействии между клиентом и сервером.
Реализацию этих механизмов мы будем писать на Java, но напоминаю, её можно сделать на любом языке для этого предназначенном. Просто на Java это несколько проще.

Сначала позаботимся о механизме взаимодействия.

GWT RPC

Сначала немного теории. Взаимодействие с сервером в GWT происходит посредством фрэймворка (мне кажется, более уместен термин API, но с официальной документацией не поспоришь)  GWT RPC. Сам по себе термин RPC (Remote Procedure Call — удалённый вызов процедур), предполагает вызов, в варианте с Java, методов из другого адресного пространства. Более конкретно вызов серверных методов на стороне клиента. Ели еще больше конкретизировать, то речь идет о HTTP сервере, а программы  обслуживающее запросы веб-клиентов, в терминах  GWT RPC называются сервисами (service). Как правило (но вовсе необязательно) они организованы как Java-сервлеты.

Первая и очевидная задача — загрузка и списка клиентов. Пусть пока он  также жестко задан в коде, но код этот будет  расположен на стороне сервера.

Сначала в файл web.xml, который описывает конфигурацюи нашего веб-приложения, добавим декларативное описание будущего сервлета:

<servlet>
  <servlet-name>userListServiceImplservlet-name>
  <servlet-class>com.samag.server.UserListServiceImplservlet-class>
servlet>
<servlet-mapping>
  <servlet-name>userListServiceImplservlet-name>
  <url-pattern>/mailbox/usersurl-pattern>
servlet-mapping>

Теперь приступаем к его реализации.
Создадим в клиентской части сайта два файла (если точнее два интерфейса) отвечающие за взаимодействие с сервером. Первый — UserListService.java:

package com.samag.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("users")
public interface UserListService extends RemoteService {
            String usersServer();  
}

Как видите, единственный (пока), метод интерфейса возвращает строку символов.
Инструкция @RemoteServiceRelativePath("users"), ассоциирует сервис с URL, который мы прописали в разделе servlet-mapping файла web.xml.
Второй интерфейс — асинхронная версия первого, имя которой получается путём добавления окончания «Async». Методы этого интерфейса должны совпадать по сигнатуре, но не могут иметь возвращаемого значения. Для обработки результата вызова, методу передаётся callback параметр (объект AsyncCallback), посредство которого  возвращаемое значение будет передано в метод onSucces(), на стороне клиента. Но об этом чуть ниже.
 UserListServiceAsync.java:

package com.samag.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface UserListServiceAsync {
            void usersServer(AsyncCallback callback);
}

Теперь пишем серверный код. Для этого в пакете com.samag.server создаём кллас (сервлет), наследующий RemoteServiceServlet и реализующий только что созданный интерфейс UserListService. Его имя, это имя основного интерфейса, плюс окончание «Impl» (от implements). UserListServiceImpl.java:

package com.samag.server;

import com.samag.client.UserListService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class UserListServiceImpl extends RemoteServiceServlet implements
 UserListService {
            @Override
            public String usersServer() {
                        return "test RPC";
            }
}

Пока этот класс возвращает лишь тестовое сообщение, в дальнейшем мы его доработаем.
Теперь переписываем метод getUsers класса MailBox:

private void getUsers(Grid grid)  {
    if (userListSvc == null) {
            userListSvc = GWT.create(UserListService.class);
    }  
    userListSvc.usersServer(new AsyncCallback() {   
            public void onFailure( Throwable caught) {   
       System.out.println(SERVER_ERROR);        
      }
    public void onSuccess(String result) {
             Window.alert(result);
      }  
    });
}

Здесь, сначала создаётся прокси-объект класса  UserListService, затем экземпляр класса  AsyncCallback, с двумя методами, назначения которых ясны из названий.
Чуть не забыл! В секции объявления переменных класса Mailbox, пишем строчку:

private UserListServiceAsync userListSvc = GWT.create(UserListService.class);

Теперь скомпилируем и получим результат - вместо списка акаунтов у нас теперь выскакивает бесполезное окошко (рис 1). Но нас это должно только радовать — ведь текст в этом окошке формируется на сервере, причём после начала загрузки страницы!

Рис. 1
Весть с сервера!







Теперь, логично перейти к реализации нашей задачи. Что должен возвратить сервер?  Список акаунтов. Точнее массив акаунтов.   Логично для начала создать класс, являющийся моделью аккаунта пользователя.

Примечание: Наверняка опытные Java-разработчики предложат лучшую реализацию. Я, к сожалению, таковым не являюсь, и просто хочу продемонстрировать технологию.

Создаём объект UserData.  Файл  UserData.java:

package com.samag.client;

              public class UserData {
              private int id;
              private String name;
              private String email;
              private int active;
                            public UserData(int id, String name, String email, int active) {
                                 this.id=id;
                            this.name = name;
                            this.email = email;
                            this.active = active;
                          }         
            public int getId() {  return this.id; }
               public String getName() { return this.name; }
             public String getEmail() {return this.email; }
 public int getActive() {return this.active; }

             public void setIde(int id) { this.id = id; }

            /* здесь остальные «сеттеры» */

}

В массиве, который мы должны получить, будут содержаться экземпляры этого класса. По этому изменяем сервлет  UserListServiceImpl.java:


package com.samag.server;

import com.samag.client.UserData;
import com.samag.client.UserListService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class UserListServiceImpl extends RemoteServiceServlet implements
 UserListService {
            UserData User;
            UserData[] UserList;
            private static final long serialVersionUID = 1L;

            @Override
            public UserData[] usersServer() {
                        UserList=new UserData[4];
                        User= new UserData(1, "Иванов", "ivanov@gwt.ru", 1);  
                        UserList[0]=User;
                        User= new UserData(2, "Сидоров", "sidorov@gwt.ru", 1);          
                        ...
                        return UserList;
            }

}

В основном классе изменим вызов метода на сервере:

userListSvc.usersServer(new AsyncCallback() {
      
    public void onFailure( Throwable caught) { 
     System.out.println(SERVER_ERROR);       
             }
    public void onSuccess(UserData[] result) {
     Window.alert(result.toString());  
            }
       });

Аналогичные изменения вносим и в интерфейсы.

MailBox/src/com/samag/client/UserListService.java:

package com.samag.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("users")
public interface UserListService extends RemoteService {
                UserData[] usersServer(); 
}
MailBox/src/com/samag/client/UserListServiceAsync.java:
package com.samag.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
public interface UserListServiceAsync {
                void usersServer(AsyncCallback asyncCallback);
}

Запускаем и … Получаем ошибку (Рис. 2). К счастью встроенные средства отладки позволяют нам быстро понять проблему — наш объект  UserData (последовательно и массив этих объектов) не сериализуем.


Рис. 2
Диагностируем ошибку – объект не сериализуем!

К счастью встроенные средства отладки позволяют нам быстро понять проблему — наш объект  UserData (последовательно и массив этих объектов) не сериализуем.
 
Сериализация

Сериализация, это перевод объекта, или вообще любой структуры данных в последовательность битов. Эта процедура используется для хранения сложных структур в файле, в текстовом поле базы данных, для передачи этих данных по сети. Как раз с последним случаем мы и имеем дело
В Java, изначально сериализуемымии являются примитивные типы их производные (поэтому  с передачей с сервера строки текста проблем и не возникло). С экземплярами классов сложнее. Он должен удовлетворять нескольким требованиям. А именно:

-Реализовывать интерфейс Serializable (или IsSerializable)
-Содержать конструктор без аргументов
-Не являться final или transsient
-Все объектные поля должны быть сериалиализируемыми.

Итак, класс  UserData переделываем следующим образом:

package com.samag.client;
import java.io.Serializable;

            public class UserData implements Serializable{
             private static final long serialVersionUID = 1L;
             private int id;
               private int id;
              private String name;
              private String email;
              private int active;
                          public UserData() {
                          }
             …


В сервлет (UserListServiceImpl) добавляем одну строчку:

            public class UserListServiceImpl extends RemoteServiceServlet implements
                        UserListService {
                        UserData User;
                        UserData[] UserList;
                        private static final long serialVersionUID = 1L;
                       

Теперь всё заработало.
Осталось написать метод формирующий список  акаунтов, и добавляем его вызов в  getUsers.
Вот как он будет выглядеть в окончательном виде:

private void getUsers(final Grid mailGrid)  {
    if (userListSvc == null) {
            userListSvc = GWT.create(UserListService.class);
    } 
    userListSvc.usersServer(new AsyncCallback() {
            public void onFailure( Throwable caught) {    
       System.out.println(SERVER_ERROR);    
      }
 
            public void onSuccess(UserData[] result) {
                        updateTable(result);
                       
            }
            public void updateTable( UserData[] users)  {
      int rows, newRow;
            for(UserData user:users){
                        rows=mailGrid.getRowCount();
                        newRow=mailGrid.insertRow(rows);
                        mailGrid.setText(newRow, 0, String.valueOf(user.getId()));
                        mailGrid.setText(newRow, 1, user.getName());
                        mailGrid.setText(newRow, 1, user.getEmail());
                        mailGrid.setWidget(newRow, 3, new ActiveButton(mailGrid,user.getActive()));
                        mailGrid.setWidget(newRow, 4, new delButton(mailGrid,newRow));
                        }
             }
    });
}

Результат на рис 3.


Рис. 3
Всё тот же список. Но сейчас он сформирован на сервере

Теперь можно переписать все остальные операции для выполнения их на сервере. Например, в класс ответственный за удаление записи внесём вызов удалённого метода:

package com.samag.client;
...
import com.google.gwt.user.client.rpc.AsyncCallback;

public class delButton extends Button {
  private static UserListServiceAsync userListSvc = GWT.create(UserListService.class);           
  public delButton(final Grid grid,final int row) {                                      
  super("Delete",    new ClickHandler() {
            public void onClick(ClickEvent event) {                                       
              if (userListSvc == null) {
                        userListSvc = GWT.create(UserListService.class);
                        }
            userListSvc.deleteUser(row, new AsyncCallback() {                                    public void onFailure( Throwable caught) {
                          // обработка ошибки
                        }                                                          
                        public void onSuccess(UserData[] result) {
                          Window.alert("Удаляем ряд"+row);
              grid.removeRow(row);                   
                                   }                                                                        
                        });                                                                                                
                }
            });
   }
}

Добавляем новый метод в сервлет  UserListServiceImpl:

 @Override
 public void deleteUser(int row, UserData[] usr) {
            // код для удаления записи
            }

И вносим изменения в оба интерфейса:

public interface UserListService extends RemoteService {
            UserData[] usersServer();

            UserData[] deleteUser(int row);       
}

public interface UserListServiceAsync {

            void usersServer(AsyncCallback asyncCallback);
            void deleteUser(int row, AsyncCallback callback);
}

Примерно таким же образом расправляемся с остальными действиями. Как видите, за исключением ужасного количества различных скобок. Других проблем нет.

Реализация JONS

Передавать клиентской части веб - приложения массив объектов, это в общем случае выбор не оптимальный. Теряется основная идея применения Ajax технологии — минимизации объёма трафика при клиент-серверном взаимодействии. В Идеале должны передаваться только данные, формализованные каким-нибудь лёгким протоколом. Одним из удобных решений, при этом является использование JONS (JavaScript Object Notation) – текстовом формате обмена данными, основанным на JavaScript. Он отличается компактностью, и вместе с тем простотой и удобочитаемостью  для человека. Данные в формате JSON выглядят так (пример из wikipedia):

{
     "firstName": "John",
     "lastName": "Smith",
     "age": 25,
     "address": {
         "streetAddress": "21 2nd Street",
         "city": "New York",
         "state": "NY",
         "postalCode": "10021"
     },
     "phoneNumber": [
         { "type": "home", "number": "212 555-1234" },
         { "type": "fax", "number": "646 555-4567" }
     ],
     "newSubscription": false,
     "companyName": null
 }

Большинство современных языков ви средств веб-разработки имеют свои инструменты для работы с этим формативом. Google Web Toolkit – не иключение.
Сервлет, отдающий данные в JOSN формате будет выглядеть примерно следующим образом:


public class JsonUserList extends HttpServlet {


  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    PrintWriter out = resp.getWriter();
      out.println('[');
      out.println("  {");
      out.print("    \"ID\": \"");
      out.print(1);
      out.println("\",");
      out.print("    \"Name\": ");
      out.print("Иванов");
      out.println(',');
      out.print("    \"Email\": ");
      out.println("ivanov@gwt.ru");
      out.println("  },");
      out.println(']');
      out.flush();
  }
}

Сохраняем этот код в файле  JsonUserList.java, в серверной части приложения и прописываем маппинг в файле  web.xml:
 
<servlet-name>JsonUserListservlet-name>
    <servlet-class>com.samag.server.JsonUserListservlet-class>
  servlet>

  <servlet-mapping>
    <servlet-name>JsonUserListservlet-name>
    <url-pattern>/mailbox/jonsusersurl-pattern>
 servlet-mapping>

Теперь, Чтобы увидеть генерируемый JSON вывод, мы можем, запустив приложение, прописать в браузере следующий адрес:http://localhost:8888/mailbox/jonsusers (Рис 4).

Рис. 4
Наш объект в формате JSON

Как видите, несмотря на некоторые проблемы с кодировкой, результат получен.
Как обработать этот вывод на стороне клиента? Ведь мы имеем дело с уже готовыми объектами JavaScript (а не get)?
Для получения объектов, в этом случае используется JSNI (JavaScript Native Interface) -технология позволяющая встроить JavaScript код в gwt приложение. С пощью него можно, например встраивать в новый проект старые не gwt решения на Ajax/javaScript. Для нашего случая JSNI тоже вполне походит.
Методы для обработки JSON, при этом, будут выглядеть следующим образом:

 private final native JsArray<JsonUserList> asArrayOfJsonUserList String json) /*-{
    return eval(json);
  }-*/;
  public final native int getId() /*-{ return this.ID; }-*/;
  public final native String getName() /*-{ return this.name; }-*/;
  public final native double getEmail() /*-{ return this.Email; }-*/;
Первый метод преобразует данные из формата  JSON, используя JavaScript метод eval(). Остальные возвращают данные в приемлемом формате (декларации импорта необходимых классов я опустил).
Обратите внимание на модификатор native и особый формат комментариев — это особенность синтаксиса JSNI.
Теперь, в клиентской части нашего приложения создаём класс JsonUserList:

package com.samag.client;

import com.google.gwt.core.client.JavaScriptObject;

            class JsonUserList  exends JavaScriptObject {          
              protected JsonUserList() {} 
              public final native int getId() /*-{ return this.ID; }-*/;
              public final native String getName() /*-{ return this.name; }-*/;
              public final native double getEmail() /*-{ return this.Email; }-*/;
            }

Для использования соответствующих классов, добавляем модуль inherits в web.xml:

  <inherits name="com.google.gwt.http.HTTP" />

Осталось только сделать HTTP запрос.

Для этого создаём в основном классе метод  getUsersJson:

private void() {
    String url = GWT.getModuleBaseURL() + "jonsusers";
    url = URL.encode(url);
    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url); 
      Request request = builder.sendRequest(null, new RequestCallback() {    
        public void onResponseReceived(Request request, Response response) {
          if (200 == response.getStatusCode()) {
            // здесь пишем код, обновляющий таблицу
          } else {
            // здесь обрабатываем ошибку
          }
        }
                        @Override
                        public void onError(Request request, Throwable exception) {
                                   // TODO Auto-generated method stub                               
                        }
      }); 
  }

Я опускаю подробности, но теперь, я думаю, вы сами сможете написать всё необходимое, для обработки и отображения полученного результата (если нет – значит я вообще зря старался).

Что ещё интересного мы не затронули в технологии Google Web Toolkit? Многое. За четыре года существования технология развивалась хорошими темпами. В декабре прошлого года вышла вторая версия фрэймворка, принеся с собой ряд интересных нововведений.

Комментариев нет:

Отправить комментарий