Введение
Спешу поделиться результатами небольшого исследования, оказавшегося необходимым для текущего проекта. Рассматривается возможность связки Java и JSON, её преимущества и недостатки. Я расскажу о практической части, о теории больше поведают нижеприведённые ссылки (англ.).
- Mastering JSON – самая приятная статья по JSON, описывающая все основные принципы и дополнительные возможности
- Использование JSON для передачи данных посредством AJAX в статье от IBM
- Описание связи JSON и JavaScript на официальном сайте
- Краткое описание скрещивания JSON и Java на официальном сайте
- Java-классы на официальном сайте
- Способы сериализации Java-объектов в статье от IBM
- И ещё о сериализации JSON-объектов в строку
- JSONTools для Java
Если кратко – JSON (JavaScript Object Notation) не является ничем более сложным, чем описано в его названии. Если вы можете описать сложно-структурированный объект на JavaScript – то о клиентской стороне JSON вы знаете практически всё. Серверная часть JSON занимается тем, что принимает каким-либо способом объект, записанный в нотации JavaScript и разворачивает данные таким образом (наверное можно сказать, десериализует), чтобы они стали доступны (или хотя бы понятны :) ) остальной части кода.
Не скажу корректно о других языках, но для Java код приёма объекта вам придётся написать самим (если только я не пропустил что-то очевидное) – ну и это не так сложно, поскольку всё необходимое для разворачивания объекта доступно на сайте JSON. Ммм, я сказал только “разворачивания”? Простите, и сворачивания тоже. Засчёт приведённого кода вы можете, например, создать Java-проекцию объекта в JavaScript-нотации (далее — JSON-объект) из JavaBean‘а (с некоторыми оговорками, о которых ниже), из java.util.Map, или, собственно, из строки в этой нотации.
Я не буду приводить примеров объектов на JSON, их доступно изрядно по ссылкам выше, да и зная JavaScript, как я уже говорил – вы знаете JSON. Обращу ваше внимание на то, что с передачей JSON-объектов следует быть осторожными и всё время помнить, что недоброжелатель, знающий серверные технологии и JavaScript, если захочет – эту лазейку, наверное, найдёт в первую очередь. Вспоминайте ваши любимые методы JavaScript и Java-секьюрности – это несколько сторонняя тема, об этом я здесь рассказывать не буду. И, кстати, передача JSON-объектов — это ещё и протокол.
Описание
Итак, практика. Я выбрал путь общего Java-сервлета для принятия (или раздачи) всех JSON-объектов в приложении и менеджера, ему их подготавливающего (или от него их принимающего). Для того, чтобы было легче создавать JSON-объекты на основе Java-объектов я выбрал путь JavaBean‘ов, JSON для Java умеет на их основе (засчет геттеров) создавать JSON-объект, а нам достаточно написать код для обратного действия. Для объектов со сложной структурой наверняка понадобится эти методы переопределять, поэтому я выделил их в отдельный абстрактный класс, который должен стать отцом для всех объектов, которые будут передаваться между сервером и клиентом. В качестве бонуса, в конце статьи представлены несколько кривенькие, но читабельные, диаграмма классов и диаграмма последовательностей этой небольшой конструкции.
Процесс
В первую очередь соберём jar-библиотеку JSON, поскольку для Java этот пакет поставляется в исходниках (описание сборки позаимствовано отсюда и вы можете спокойно пропустить эту часть, если свободно собираете jar‘ы из исходников или вам это не требуется):
- Сохраните пакет в какой-либо каталог, который будет далее называться
%DOWNLOAD_HOME% - Распакуйте его и убедитесь, что структура каталогов (
/org/json/) не изменилась. - Перейдите в каталог
%DOWNLOAD_HOME%/org/json/ - Скомпилируйте классы командой
javac *.java - Вернитесь в каталог
%DOWNLOAD_HOME% - Командой
jar -cvf json.jar org\json\*.classсоздайтеjar-архив. - Добавьте библиотеку в ваш проект
Теперь приведу код интерфейса IJSONSerializable (объекта, который может быть свёрнут в JSON и развернут обратно – думаю, это довольно корректно) и абстрактного класса JSONBean, который его имплементирует.
1 2 3 4 5 6 7 8 9 10 11 | package com.acme.json;
import org.json.JSONObject;
public interface IJSONSerializable {
public boolean fromJSONObj(JSONObject object);
public JSONObject toJSONObject();
}
|
Обратите внимание, что в стандартной версии JSON по геттерам считает за пары ключ-свойство значения некоторых недозволенных методов, например, getClass и getInstance — нижеприведённый класс этот недостаток (в случае указанных методов) обходит и, собственно, добавляет функциональность конструирования (а в данном случае правильнее – инициализации) Bean‘а из JSON-объекта. Да, здесь, иcпользуется reflection, и если вас не устраивает этот факт – вы вольны поменять концепцию :) – JSON выстраивает свой объект из Bean‘а точно таким же способом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | package com.acme.json;
import java.lang.reflect.Method;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class JSONBean implements IJSONSerializable {
public boolean fromJSONObj(JSONObject jsonObj) {
Class beanClass = this.getClass();
Method[] methods = beanClass.getMethods();
for (int i = 0; i < methods.length; i += 1) {
try {
Method method = methods[i];
String name = method.getName();
String key = "";
if (name.startsWith("set")) {
key = name.substring(3);
}
if (key.length() > 0 &&
Character.isUpperCase(key.charAt(0)) &&
method.getParameterTypes().length == 1) {
if (key.length() == 1) {
key = key.toLowerCase();
} else if (!Character.isUpperCase(key.charAt(1))) {
key = key.substring(0, 1).toLowerCase() +
key.substring(1);
}
if (isAllowedKey(key))
method.invoke(this, jsonObj.get(key));
}
} catch (Exception e) {
return false;
}
}
return true;
}
public JSONObject toJSONObject() {
return new JSONObject(this) {
@Override
public Object get(String key) throws JSONException {
return isAllowedKey(key) ? super.get(key) : null;
}
};
}
protected static boolean isAllowedKey(String key) {
return ((key != "class") && (key != "instance"));
}
}
|
Ну, и простенький пример Bean‘а, с которым мы будем работать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package com.acme.json.beans;
import com.acme.json.JSONBean;
public class PersonBean extends JSONBean {
private String personFirstName = "Homer";
private String personLastName = "Simpson";
private int personAge = 46;
public String getPersonFirstName() {
return personFirstName;
}
public void setPersonFirstName(String personFirstName) {
this.personFirstName = personFirstName;
}
public String getPersonLastName() {
return personLastName;
}
public void setPersonLastName(String personLastName) {
this.personLastName = personLastName;
}
public int getPersonAge() {
return personAge;
}
public void setPersonAge(int personAge) {
this.personAge = personAge;
}
}
|
JSONBeanManager управляет подготовкой Bean‘ов для отправки и принятия их на основе параметров запроса. Думаю, концентрация этого кода в одном месте оправдана, поскольку вы вряд ли захотите, чтобы отвечающий за пересылку Bean‘ов код был разбросан по проекту. В худших случаях паттерны проектирования придут вам на помощь. Кстати, возможно вы захотите сделать некоторые ваши Bean‘ы Singleton‘ами, тогда здесь вы можете возвращать их единственные инстансы (не забудьте только, что в связи с этим их нужно аккуратнее готовить :) ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package com.acme.json;
import java.util.Map;
import com.acme.json.beans.PersonBean;
public class JSONBeanManager {
protected JSONBean prepareBeanForReceiving(Map parametersMap) {
if (parametersMap.containsKey("source") &&
(parametersMap.get("source") == "sampleBean")) {
return new PersonBean();
}
return null;
}
protected JSONBean prepareBeanForSending(Map parametersMap) {
if (parametersMap.containsKey("source") &&
(parametersMap.get("source") == "sampleBean")) {
return new PersonBean();
}
return null;
}
protected void onBeanReceived(JSONBean bean) { }
protected void onBeanSent(JSONBean bean) { }
protected void onBeanTransferError() { }
}
|
Ну и наконец – сервлет. Ядро пересылки. Запрос GET на сервер отправляет клиенту Bean, отданный менеджером на основе анализа параметров запроса, а затем сконвертированный в JSON-объект, а POST – принимает и заполняет предоставленный тем же менеджером Bean полученными из JSON-объекта данными.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | package com.acme.json;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONException;
import org.json.JSONObject;
public class JSONBeanServlet extends HttpServlet {
protected static final String JSON_OBJ_PARAM = "jsonBean";
private JSONBeanManager beanManager = null;
public JSONBeanServlet(/*Class beanManagerClass*/) {
super();
this.beanManager = new JSONBeanManager();
}
@Override
public void doGet(HttpServletRequest req,
HttpServletResponse resp)
throws java.io.IOException, ServletException {
JSONBean activeBean =
beanManager.prepareBeanForSending(req.getParameterMap());
if (activeBean != null) {
resp.setContentType("application/x-json");
resp.getWriter().print(activeBean.toJSONObject());
beanManager.onBeanSent(activeBean);
} else {
beanManager.onBeanTransferError();
// throw new ServletException("JSONBeanServlet got no bean for sending");
}
}
@Override
protected void doPost(HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException {
JSONBean activeBean =
beanManager.prepareBeanForReceiving(req.getParameterMap());
if (activeBean != null) {
String jsonText = req.getParameter(JSON_OBJ_PARAM);
JSONObject jsonObj = null;
try {
jsonObj = new JSONObject(jsonText);
} catch (JSONException e) {
e.printStackTrace();
}
activeBean.fromJSONObj(jsonObj);
beanManager.onBeanReceived(activeBean);
} else {
beanManager.onBeanTransferError();
// throw new ServletException("JSONBeanServlet got no bean for receiving");
}
}
}
|
Для завершения описания серверной части следует напомнить о добавлении сервлета в web.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>SomeAplication</display-name>
. . .
<servlet>
<description>JSON Beans Manage Servlet</description>
<display-name>JSON Beans Servlet</display-name>
<servlet-name>JSON Beans Servlet</servlet-name>
<servlet-class>
com.acme.json.FNJSONBeanServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>JSON Beans Servlet</servlet-name>
<url-pattern>/jsonBean/*</url-pattern>
</servlet-mapping>
. . .
</web-app>
|
Клиентская часть состоит, собственно из JSON-парсера-конструктора (да, всё это можно сделать через eval(), но предоставленный разработчиками код делает это, по их обещаниям, аккуратнее) и, в моём случае, класса, облегчающего работу с сервлетом. Класс использует немного модифицированную функцию makeRequest из статьи о решениях JavaScript (которую я обновлю до этой версии там сразу же после написания статьи) и обеспечивающие ООП функции Class [1] и createMethodReference [2] оттуда же.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | var JSONManager = Class.extend({
JSON_BEAN_SERVLET_PATH: "./jsonBean",
JSON_BEAN_PARAM_NAME: "jsonBean",
construct:
function() {
this._handlerFuncRef =
createMethodReference(this, "_responseHandler");
},
requestJSONBean: function(handlerFunc, addParams) {
makeRequest(this.JSON_BEAN_SERVLET_PATH, addParams,
this._handlerFuncRef, handlerFunc);
},
sendJSONBean: function(jsonBean, addParams) {
makeRequest(this.JSON_BEAN_SERVLET_PATH,
this.JSON_BEAN_PARAM_NAME + "=" +
JSON.stringify(jsonBean) + (addParams ?
("&" + addParams) : ""), null, true);
},
_responseHandler: function(http_request, handlerFunc) {
handlerFunc(JSON.parse(http_request.responseText));
}
});
|
Ну и в завершение – пример использующего всё вышеприведённое кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var alexanderJSON =
{"personFirstName": "Alexander",
"personLastName": "Makedonsky",
"personAge": 35,
};
var jsonManager = new JSONManager();
jsonManager.sendJSONBean(alexanderJSON, "source=SampleBean");
var homerJSON = null;
function onGotObject(http_request) {
homerJSON = JSON.parse(http_request.responseText);
}
jsonManager.requestJSONBean(onGotObject, "source=SampleBean");
|
В качестве альтернативных идей – методы JSONBeanManager‘а можно сделать статическими, а JSONBean научить приготавливать самого себя к отправке (инициировать данными) – но при сложной структуре менеджера и требовании комплексной подготовки, когда Bean не может подготовить сам себя – придётся от них отказаться. Однако, поскольку выбор Bean‘а по параметрам будет общим и для передачи и для приёма – код выбора можно вынести и в отдельный метод.
Заключение
Кажется, задача ознакомления выполнена и я со спокойной совестью, надеюсь, могу идти делать другие дела (я помню про обновление функции :) ). Если совесть должна быть неспокойна – обязательно сообщайте, я стараюсь исправлять ошибки в своих статьях – и даже те, которые, изредка, сам нахожу со временем. Приятной вам разработки.