Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Примитивный вызов удалённых сервисных способов в одностраничных приложениях

Anna | 2.06.2014 | нет комментариев
В этой статье, я хочу поделиться своим подходом в организации
заказчик-серверного взаимодействия, в одностраничных браузерных приложениях
с серверной частью на Java.

Сокращённо, я называю данный подход «Json Remote Service Procedure Call» — JRSPC.
(Не дюже благозвучно, безусловно, но из песни слова не выбросишь.)

Использование jrspc — разрешает отказаться от применения слоёв
определений интерфейсов сервисов на заказчике и сервере, что уменьшает число кода,
упрощает его рефакторинг, и снижает вероятность возникновения ошибок.

Цена за это — замена комплекта параметров в сервисных способах,
на один параметр — объект Json, что немножко усложняет код в сервисных способах.

Т.е, на сервере, взамен: int plus(int a, int, b){return a b;};,
мы обязаны будем написать: int plus(JSONObject p){return p.optInt("a") p.optInt("b", "4");};,

а на заказчике, взамен: PlusService.plus(1, 2, callbacks);,
обязаны будем написать: Server.call("plusService", "plus", {b: 2, a: 1}, callbacks);.

Впрочем, уплатив эту цену, мы получаем вероятность исключить из процесса разработки
конфигурирование сервисов на сервере и подключение их на заказчике,
а также, сумеем избежать ошибок, связанных с изменением мест параметров,
и сумеем добавлять в параметры значения по умолчанию ( p.optInt(«b», «4») ).

Как это работает

На транспортном ярусе, jrspc — использует json-rpc, с вероятностью указывать
в вызове не только способ, но и сервис.
Следственно, такой json-rpc дозволено было бы назвать json-rspc (s-service).

Если бы на него существовала спецификация, то она была бы схожа на
спецификацию json-rpc 2.0, за исключением того, что в объекте запроса
было бы добавлено поле «service», а поле «id» — было бы не непременным, и в результате — необязателен errorCode.

Для демонстрации, я написал примитивное демо-приложение, в котором реализуются
функциональности регистрации, логина, и метаморфозы данных и прав пользователя.

Клиентская часть

Клиентская часть этого приложения — написана на фреймворке AngularJS.

(Считаю своим длинном — предупредить тех, кто ещё не пробовал писать на нём:
{{user.name}}, Ангуляр — тяжёлый наркотик!
Для попадения в связанность от него — достатчно словить кайф каждого один раз.)

Для оформления применяется Bootstrap.

В серверной части — Spring

В качестве реализации объекта json, применяется JSONObject
из библиотеки json-lib.

Клиентская часть состоит из трёх файлов:

ajax-connector.js

var Server = {url: "http://"  document.location.host  "/jrspc/ajax-request"};

(function() {

	function getXMLHttpRequest() {
		if (window.XMLHttpRequest) {
			return new XMLHttpRequest();
		} else if (window.ActiveXObject) {
			return new ActiveXObject("Microsoft.XMLHTTP");
		}
		if(confirm("This browser not support AJAX!\nDownload modern browser?")){
			document.location = "http://www.mozilla.org/ru/firefox/new/";
		}else{
			alert("Download modern browser for work with this application!");
		    throw "This browser not suport Ajax!";
		}
	}

	Server.call = function(service, method, params, successCallback, errorCallback, control) {
		var data = {
			service : service,
			method : method,
			params : params ? params : {}
		};
		if (control) {control.disabled = true;}
		var requestData = JSON.stringify(data);
		var request = getXMLHttpRequest();

		request.onreadystatechange = function() {			
			//log("request.status=" request.status ", request.readyState=" request.readyState);
			if ((request.readyState == 4 && request.status != 200)) {
				processError("network error!", errorCallback);
				if (control) {control.disabled = false;}
				return;
			}	
			if (!(request.readyState == 4 && request.status == 200)) {return;}			
			//log("request.responseText=" request.responseText);
			try {
				var response = JSON.parse(request.responseText);
				if (response.error) {
					processError(response.error, errorCallback);
				} else {
					if (successCallback) {
						try {
							//log("response=" JSON.stringify(response));							
							successCallback(response.result);
						} catch (ex) {
							error("in ajax successCallback: "   ex   ", data="   data);
						}
					}
				}
			} catch (conectionError) {
				error("in process ajax request: "   conectionError);
			}			
			if (control) {control.disabled = false;}
		}
		request.open("POST", Server.url, true);
		request.send(requestData);
	}

	function processError(error, errorCallback){
		if (errorCallback) {
			try {
				errorCallback(error);
			} catch (ex) {
				error("in ajax errorCallback: "   ex);
			}
		} else {
			alert(error);
		}

	}	
})();

function error(s){if(window.console){console.error(s);}};
function log(s){if(window.console){console.log(s);}};

Реализация механизма запросов к серверу, инкапсулированная в объекте Server.
(Префикс ajax — применяется, Дабы отличать его от вебсокетного ws-connector.js,
которым он может быть заменён, без метаморфозы кода user-controller.js.)

user-controller.js

function userController($scope){

	var self = $scope;	

	self.user = {login: "", password: ""};

	self.error = "";
	self.result = "Для входа либо регистрации - введите логин и пароль.";
	self.loged = false;

	/** This method will called at application initialization (see last string in this file). */

	self.trySetSessionUser = function(control){
		Server.call("testUserService", "getSessionUser", null, 
		   function(user){
			log("checkUser: user=" JSON.stringify(user));
			if(!user.id){return;}
			self.user = user;
			self.loged = true;
			self.$digest();			
		}, self.onError, control);		
	}	

	/** common user methods */

	self.registerUser = function(control){
		Server.call("testUserService", "registerUser", self.user, 
		   function(id){
			self.user.id = id;			
			self.onSuccess("you registered with id: " id);		
			setTimeout(function(){control.disabled = true;}, 20);
		}, self.onError, control);		
	}

	self.logIn = function(control){
		self.loginControl = control;
		Server.call("testUserService", "logIn", self.user, function(user){
			self.user = user;
			self.loged = true;
			self.onSuccess("you loged in with role: " user.role);	
			setTimeout(function(){control.disabled = true;}, 20);
		}, self.onError, control);		
	}

	self.logOut = function(control){		
		Server.call("testUserService", "logOut", {}, function(){
			self.user.role = "";
			self.user.city = "";
			self.loged = false;
			self.onSuccess("you loged out");
			setTimeout(function(){
				control.disabled = true;
				if(self.loginControl){self.loginControl.disabled = false;}
			}, 20);
		}, self.onError, control);	
	}		

	self.getUsersCount = function(control){
		Server.call("testAdminService", "getUsersCount", null, function(count){
			self.onSuccess("users count: " count);			
		}, self.onError, control);			
	}	

	self.changeCity = function(control){
		Server.call("testUserService", "changeCity", {city: self.user.city}, function(){
			self.onSuccess("users city changed to: " self.user.city);			
		}, self.onError, control);			
	}		

	/** admin methods */

	self.grantRole = function(control){		
		Server.call("testAdminService", "grantRole", {role: self.role, userId: self.userId}, function(result){
     		self.onSuccess(result);		
		}, self.onError, control);		
	}	

	self.removeUser = function(control){
		Server.call("testAdminService", "removeUser", {userId: self.userId}, self.onSuccess, self.onError, control);		
	}		

	/** common callbacks */

	self.onError = function(error){
		self.error = error;		
		self.$digest();		
	}

	self.onSuccess = function(result){	
		self.result = result;
		self.error = "";
		self.$digest();		
	}		

	/** initialization */
	self.trySetSessionUser();
}

Тут находится бизнес-логика приложения, инкапсулированная в функции userController.

application.html

<html x-ng-app><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>JRSPC Demo application</title>
<link href="http://getbootstrap.com/dist/css/bootstrap.css" rel="stylesheet">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
<script src="ajax-connector.js"></script>
<script src="user-controller.js"></script>
</head><body style="padding-left: 42px; padding-top: 12px; padding-right: 12px;" 
             x-ng-app="jrspcTest">
<table><tr><td>
<h1 title="JSON RemoteService Procedure Call" style="cursor: help;">
JRSPC Demo application </h1></td><td style="padding-left: 120px;" valign="middle">
     <a href="aga">related article on habrahabr.ru</a></td></tr></table>
<div  x-ng-controller="userPanelController">
<pre>

 User:
        id: {{user.id}}

     login: <input type="text" x-ng-model="user.login" x-ng-disabled="loged"/>         

  password: <input type="password" x-ng-model="user.password" x-ng-disabled="loged"/>

 from city: <input type="text" x-ng-model="user.city"/>  <input value="save" 
                   x-ng-disabled="!loged" type="button" 
                   x-ng-click="changeCity($event.target)"/>             

      role: {{user.role}}         

      <input value="register" x-ng-disabled="loged || user.id > 0  || user.login=='' || user.password==''"
             type="button" x-ng-click="registerUser($event.target)"/> <input              

             value="log in" x-ng-disabled="loged || user.login=='' || user.password==''"
             type="button" x-ng-click="logIn($event.target)"/> <input 

             value="log out" x-ng-disabled="!loged"
             type="button" x-ng-click="logOut($event.target)"/> 

     If you are is admin, you also can:

      <input value="grant role:" type="button" x-ng-click="grantRole($event.target)"
            /> <input type="text" style="width: 50px;"
             x-ng-model="role"/> to user: <input type="text" x-ng-model="userId" 
             style="width: 40px;"/> or <input value="remove this user" 
             type="button" x-ng-click="removeUser($event.target)"/>                          

</pre>
<div>{{error == '' ? result : error}}</div>              
      <input value="get users count" 
             type="button" x-ng-click="getUsersCount($event.target)"/>   
</div>
</body></html>

Графический интерфейс приложения с логикой блокировки элементов.

Как видим, в представлении скриптового кода, удалённый сервер — выглядит как
объект Server, тот, что должен быть проинициализирован url’ом.

Через данный объект, мы можем обращаться к любому компоненту на сервере
и вызывать всякие его способы, таким методом:

Server.call(serviceName, mathodName, params, successCallBack, errorCallback, control);

Результаты либо ошибки — приходят в соответствующие коллбэки.

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

Безусловно, сказав «любому и всякие» — я немножко отошёл от истины.
На самом деле, как удалённые сервисы, вызываться могут только классы, производные от
AbstractService, а вызываемые удалённо способы, обязаны быть аннотированы @Remote.

Для ограничения прав доступа к способам — применяется аннотация @Secured(roleName).
Так, скажем, способ, аннотированный @Secured("Admin") — не может быть вызван пользователем
с ролью «User».

Cерверная часть

Каждый серверный «фреймворк», если дозволено так выразиться, занимает поменьше 9 кб.,
и состоит из шести классов, два из которых — теснее знакомые нам аннотации

Remote

package habr.metalfire.jrspc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** If method NOT annotated as Remote MethodInvoker throw exception, 
 *  when user try to call this method from browser
 **/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Remote {}
Secured

package habr.metalfire.jrspc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** If method annotated as Secured  MethodInvoker throw exception,
 *  if User not in declared role.
 **/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Secured {
    String[] value();    
}

а также

AbstractService

package habr.metalfire.jrspc;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/** parent class for all services */

public abstract class AbstractService {

    protected  Log log = LogFactory.getLog(this.getClass());

    private User user;

    public void setUser(User user) {          
         this.user = user;
    } 

    public User getUser() {          
        return user;
    }  

}

отвлеченный класс, от которого обязаны наследоваться все сервисы, и

CommonServiceController(controller)

.

package habr.metalfire.jrspc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.servlet.http.HttpSession;

import net.sf.json.JSONObject;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class CommonServiceController {

    final static Log log = LogFactory.getLog(CommonServiceController.class);

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private HttpSession session;

    @RequestMapping(value = "/ajax-request", method = RequestMethod.POST)
    @ResponseBody
    private String processAjaxRequest(@RequestBody String requestJson) {
        //log.debug("requestJson=" requestJson);    
        JSONObject request = JSONObject.fromObject(requestJson);
        String serviceName = request.optString("service");
        String methodName = request.optString("method");
        JSONObject params = request.optJSONObject("params");  
        log.debug("request =" request);
        JSONObject response = callServiceMethod(serviceName, methodName, params);
        log.debug("response=" response);
        return response.toString();       
    }    

    private JSONObject callServiceMethod(String serviceName, String methodName, JSONObject params) {
        JSONObject response =  new JSONObject(); 
        try {
            Object serviceObject = applicationContext.getBean(serviceName);
            if (serviceObject == null) {
                throw new RuntimeException("AbstractService bean with name "   serviceName   " not found!");
            }
            if (!(serviceObject instanceof AbstractService)) {
                throw new RuntimeException("Collable service \"" serviceName "\" MUST be instance of AbstractService, but not of: "
                          serviceObject.getClass().getName());
            }
            AbstractService service = (AbstractService) serviceObject;             
            User user = (User) session.getAttribute("user");
            service.setUser(user);
            Object result = invokeMethod(service, methodName, params);          
            if(result != null){
                response.put("result", result);
            } else{
                response.put("result", new JSONObject());
            }                            
        } catch (Throwable th) {       
            response.put("error", th.getMessage());       
        }
        return response;
    }  

    private Object invokeMethod(AbstractService service, String methodName, JSONObject methodParams) throws Throwable {
        try {                                
            User user = service.getUser();
            log.debug("user="  JSONObject.fromObject(user));            
            Class<?> ownerClass = service.getClass();
            Class<?>[] parameterTypes = new Class[] { JSONObject.class };
            Object[] arguments = new Object[] { methodParams };
            Method actionMethod = ownerClass.getMethod(methodName, parameterTypes);
            checkAccess(actionMethod, methodParams, user);                   
            Object result = actionMethod.invoke(service, arguments);          
            return result == null ? new Object() : result;            
        } catch (Throwable th) {
            if (th instanceof InvocationTargetException) {
                th = ((InvocationTargetException) th).getTargetException();
            } 
            if (th instanceof NoSuchMethodException) {
                th = new RuntimeException("Method \"" methodName "\" not found on class \"" service.getClass().getName() "\"!");
            }         
            throw th;
        }
    }

    private void checkAccess(Method method, Object methodParams, User user) {
        if (!method.isAnnotationPresent(Remote.class)) {
            throw new RuntimeException("Remotely invoked method MUST be annotated as Remote!");
        }                
        if (method.isAnnotationPresent(Secured.class)) {
            String[] roles = method.getAnnotation(Secured.class).value();            
            if ( user == null || ( !Arrays.asList(roles).contains(user.getRole()) && !"Admin".equals(user.getRole()) ) ) {
                String message = "User not in role: "
                              StringUtils.arrayToDelimitedString(roles, " or ")                     
", required for invocation of \""
                              method.getName()   "\" method !";               
                throw new RuntimeException(message);
            }
        }         
    }      
}

В его способ processAjaxRequest приходят запросы из скриптового объекта Service.
Дальше, запрос преобразуются в JSONObject, находится компонент, по имени обслуживания,
и на нём, позже проверки прав доступа, рефлективно, вызвается указанный способ.
В вызываемом удалённо способе — неизменно должен быть только один параметр, типа JSONObject.

User (entity)

package habr.metalfire.jrspc;

public class User{    

    public static enum Role { User, Admin, Supervisor }

    private Long id;
    private String login;    
    private String password;
    private String city;      
    private String role;

    public User() { }

    public Long getId() {return id;}
    public void setId(Long id) {this.id = id;}

    public String getLogin() {return login;}
    public void setLogin(String login) {this.login = login;}

    public String getPassword() { return password;}
    public void setPassword(String password) {this.password = password; }

    public String getRole() {return role;}
    public void setRole(String role) {this.role = role;}

    public String getCity() { return city;}
    public void setCity(String city) {this.city = city;}   

}

для хранения данных о пользователе, и

UserManager(component)

package habr.metalfire.jrspc;

import java.util.HashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

@Component
public class UserManager {

    private static HashMap<Long, User> idUsersMap = new HashMap<Long, User>();

    private static HashMap<String, Long> loginIdMap = new HashMap<String, Long>();

    private AtomicLong nextId = new AtomicLong(0);

    public User findById(Long id) {       
        return idUsersMap.get(id);
    }

    public User findByLogin(String login) {
        Long id = loginIdMap.get(login);
        if(id == null){return null;}
        return  findById(id);
    }  

    public boolean saveUser(User user) {
        user.setId(nextId.addAndGet(1));
        idUsersMap.put(user.getId(), user);
        loginIdMap.put(user.getLogin(), user.getId());
        return false;
    }

    public void updateUser(User user) {
       idUsersMap.put(user.getId(), user);       
    }

    public void deleteUser(User user) {
       idUsersMap.remove(user.getId());       
       loginIdMap.remove(user.getLogin());           
    }

    public Integer getUsersCount() {
       return idUsersMap.size();  
    }    

}

для операций с объектом User (тестовая реализация с эмуляцией персистентности).

Бизнес-логика реализована в 2-х сервисах:

TestUserService(component)

package habr.metalfire.jrspc;

import javax.servlet.http.HttpSession;

import net.sf.json.JSONObject;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("session")
public class TestUserService extends AbstractService{

    @Autowired
    UserManager userManager;    

    @Autowired
    private HttpSession session;

    @Remote
    public Long registerUser(JSONObject userJson){        
        User user = (User) JSONObject.toBean(userJson, User.class);        
        if(userManager.findByLogin(user.getLogin()) != null){
          throw new RuntimeException("User with login " user.getLogin() " already registered!");
        }           
        if(userManager.getUsersCount() == 0){
          user.setRole(User.Role.Admin.name());
        }else{
          user.setRole(User.Role.User.name());
        } 
        userManager.saveUser(user); 
        return user.getId();
    }    

    @Remote
    public User logIn(JSONObject params){      
         String error = "Unknown combination of login and password!";
         User user = userManager.findByLogin(params.optString("login"));
         if(user == null){ throw new RuntimeException(error);}
         if(!user.getPassword().equals(params.optString("password"))){ throw new RuntimeException(error);} 
         session.setAttribute("user", user);
         return user;
    }     

    @Secured("User") 
    @Remote
    public void logOut(JSONObject params){       
         session.removeAttribute("user");
    }           

    @Secured("User")   
    @Remote
    public void changeCity(JSONObject params){   
        String city = params.optString("city");                
        User user = getUser();
        user.setCity(city);                
        userManager.updateUser(user);
    }           

    @Remote
    public User getSessionUser(JSONObject params){           
        try{
           return (User) session.getAttribute("user");
        }catch(Throwable th){log.debug("in checkUser: " th);}
        return null;
    }    

}

сервис с способами для регистрации, логина, и редактирования данных, и

TestAdminService(component)

package habr.metalfire.jrspc;

import net.sf.json.JSONObject;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("session")
public class TestAdminService extends AbstractService{

    @Autowired
    UserManager userManager;    

    private User checkUser(Long userId){
        User user = userManager.findById(userId);
        if(user == null){throw new RuntimeException("User with id " userId " not found!");}
        return user;        
    }

    @Secured("Admin")   
    @Remote
    public String grantRole(JSONObject params){    
        Long userId = params.optLong("userId");  
        User user = userManager.findById(userId);
        String role = params.optString("role");             
        if(user.getId().equals(getUser().getId())){throw new RuntimeException("Admin role cannot be revoked!");}
        user.setRole(role); 
        userManager.updateUser(user);
        return "role " role " granted to user " userId;        
    }     

    @Secured("Admin")   
    @Remote
    public String removeUser(JSONObject params){ 
        User user = checkUser(params.optLong("userId"));
        if("Admin".equals(user.getRole())){throw new RuntimeException("Admin cannot be removed!");}
        userManager.deleteUser(user);
        return "User " user.getId() " removed.";        
    }     

    @Remote
    public Integer getUsersCount(JSONObject params){        
        return userManager.getUsersCount();
    }        
}

сервис с способами для удаления юзера, и метаморфозы его роли.

Код написан максимально self-explanatory, следственно верю, что разобраться в нём будет легко.

Код демо-приложения на Гитхабе

Что дальше?

В дальнейшей статье, я планирую написать, как, на базе данного подхода,
дозволено организовать заказчик-серверное взаимодействие через вебсокеты,
и как, на сервере, из вебсокетного контекста, достать сессию http.

 Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB