Tuesday, September 21, 2010

Аналог cookeis в spring remoting

В браузере есть способ хранить и передавать некоторую дополнительную информацию, которая не присутствует в параметрах при вызове GET или не хранится в теле запроса POST. Данная функциональность реализована с помощью кук. 
Для браузера это позволяет создать некоторое постоянное хранилище данных на стороне клиента, с помощью которых сервер может идентифицировать клиента. 
Spring Remoting можно реализовать поверх протокола http, однако ни одного упоминания про куки я не нашел. Попробуем реализовать их самостоятельно.




Пусть есть некий сервис, опубликованный с помощью spring remoting:

package foo;

public interface FooService {
   public String hello(String name);
}


package foo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class FooServiceImpl {


   private Log logger = LogFactory.getLog(getClass());
   public String hello(String name) {
      logger.info("Greeting "+name);
      return "Hello "+name;
   }
}

<bean id="fooService" class="foo.FooServiceImpl">

<bean name="/FooService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
   <property name="service" ref="fooSearcher"/>
   <property name="serviceInterface" value="foo.FooService"/>
</bean>













На клиенте мы получаем ссылку на сервис:


<bean id="fooService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
   <property name="serviceUrl" value="http://${server.address}:${server.port}/server/remoting/FooService"/>
   <property name="serviceInterface" value="foo.FooService"/>
</bean>

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



package foo;


import java.util.*;
import java.io.Serializable;


public class CookiesHolder {


    private Map<String, Serializable> cookies;


    public void addCookie(String name, Serializable o) {
        if (cookies == null) {
            cookies =new HashMap<String, Serializable>();
        }


        cookies.put(name, o);
    }


    public Serializable getCookie(String name) {
        return cookies!=null ? cookies.get(name) : null;
    }




    public Set<String> getCookiesNames() {
        if (cookies==null) {
            return Collections.emptySet();
        }
        else {
            return cookies.keySet();
        }
    }
}



<bean id="cookieHolder" class="core.service.remote.CookiesHolder" />

Далее необходим способ передать куки на сервер при вызове любого метода. Изучая класс org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean, я наткнулся на метод 



/**
 * Set the RemoteInvocationFactory to use for this accessor.
 * Default is a {@link DefaultRemoteInvocationFactory}.
 * <p>A custom invocation factory can add further context information
 * to the invocation, for example user credentials.
 */
public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
   this.remoteInvocationFactory =
(remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory());
}


По описанию выглядит как то, что нам нужно. Если изучить код 
RemoteInvocationFactory, мы увидим единственный метод: 

RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation);

Очевидно, что RemoteInvocation содержит данные о том, какой метод вызывается, какие параметры в него передаются. Посмотрим на код RemoteInvocation. Сразу же бросается в глаза поле

private Map attributes;

смотрим описание:
/**  
* Add an additional invocation attribute. Useful to add additional  
* invocation context without having to subclass RemoteInvocation
* <p>Attribute keys have to be unique, and no overriding of existing  
* attributes is allowed.  
* <p>The implementation avoids to unnecessarily create the attributes  
* Map, to minimize serialization size.  
* @param key the attribute key  
* @param value the attribute value  
* @throws IllegalStateException if the key is already bound  
*/ 
public void addAttribute(String key, Serializable value) throws IllegalStateException {
...

Итак, при вызове метода сервиса, есть возможность, переопределив RemoteInvocationFactory, поместить в RemoteInvocation дополнительные атрибуты, в частности, наши куки:


package foo;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
import org.springframework.remoting.support.RemoteInvocation;

import java.io.Serializable;

public class CookiesBasedRemoteInvocationFactory extends DefaultRemoteInvocationFactory {
   
    private CookiesHolder cookiesHolder;
    @Override
    public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
        final RemoteInvocation invocation = super.createRemoteInvocation(methodInvocation);
        final Application instance = Application.getInstance();
        CookiesHolder holder;
        if (instance!=null) {
            holder = instance.getCookiesHolder();
        }
        else {
            holder = new CookiesHolder();            
        }
        for (String name : holder.getCookiesNames()) {
            final Object value = holder.getCookie(name);
            if (value instanceof Serializable) {
                invocation.addAttribute(name, (Serializable) value);
            }
        }
        return invocation;
    }

    public void setCookiesHolder(CookiesHolder cookiesHolder) {
        this.cookiesHolder = cookiesHolder;
    }

}


теперь изменим конфигурацию ссылки на сервис:
<bean id='cookiesBasedRemoteInvocationFactory' class="foo.CookiesBasedRemoteInvocationFactory"/>

<bean id="fooService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">


   <property name="serviceUrl" value="http://${server.address}:${server.port}/server/remoting/FooService"/>
   <property name="serviceInterface" value="foo.FooService"/>
   <property name="remoteInvocationFactory" ref="cookiesBasedRemoteInvocationFactory"/>
</bean>

Таким образом, любой вызов метода сервиса (в частности hello()) будет передавать на сервер наши куки. Осталось их получить на сервере. Будем действовать аналогичным способом.

Создаем структуру для хранения кук:
package foo;

import org.springframework.util.Assert;

public class ServerCookiesHolder {

    private static ThreadLocal contextHolder = new ThreadLocal();

    public static void clearContext() {
        contextHolder.set(null);
    }

    public static CookiesHolder getContext() {
        if (contextHolder.get() == null) {
            contextHolder.set(new CookiesHolder());
        }

        return (CookiesHolder) contextHolder.get();
    }

    public static void setContext(CookiesHolder context) {
        Assert.notNull(context, "Only non-null CookiesHolder instances are permitted");
        contextHolder.set(context);
    }

}


Изучаем HttpInvokerServiceExporter, находим метод setRemoteInvocationExecutor, который устанавливает обработчика вызова метода. Переопределяем обработчик:


package foo;

import org.springframework.remoting.support.DefaultRemoteInvocationExecutor;
import org.springframework.remoting.support.RemoteInvocation;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;

public class CookiesBasedRemoteInvocationExecutor extends DefaultRemoteInvocationExecutor {

    @Override
    public Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        final Map map = invocation.getAttributes();
        if (map!=null) {

            final Set<String> set = map.keySet();
            final CookiesHolder cookiesContext = ServerCookiesHolder.getContext();
            for (String name : set) {
                cookiesContext.addCookie(name, invocation.getAttribute(name));
            }
        }
        return super.invoke(invocation, targetObject);
    }
}

Изменяем конфигурацию сервиса:





<bean id="cookiesBasedRemoteInvocationExecutor" class="foo.CookiesBasedRemoteInvocationExecutor"/>





<bean name="/FooService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
   <property name="service" ref="fooSearcher"/>
   <property name="serviceInterface" value="foo.FooService"/>
   <property name="remoteInvocationExecutor" ref="cookiesBasedRemoteInvocationExecutor"/>
</bean>

Вуаля. Проверяем. Измененный код сервиса:

   public String hello(String name) {


      logger.info("Greeting "+ServerCookiesHolder.getContext().getCookie("realName"));
      return "Hello "+name;
   }
...

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

public void test() {
   CookiesHolder.setCookie("realName", "Foo");
   fooService.hello("Fred");
}


No comments :

Post a Comment