spring-projects / spring-session

Spring Session
https://spring.io/projects/spring-session
Apache License 2.0
1.86k stars 1.11k forks source link

Update an object stored #648

Open crisarmen opened 7 years ago

crisarmen commented 7 years ago

Hello,

I am trying to use spring-session-data-redis:1.2.2 on an existing project built with Spring/Spring MVC.

At the moment we have java objects stored in the session, and such objects are retrieved a modified in different parts of the code. Just as a simplified example, consider the simple object:

public class TestObject implements Serializable{
    private static final long serialVersionUID = -1283966188531894027L;  
    private Integer val;
    public TestObject(Integer val){
        this.val = val;
    }

    public Integer getVal() {
        return val;
    }

    public void setVal(Integer val) {
        this.val = val;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((val == null) ? 0 : val.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        TestObject other = (TestObject) obj;
        if (val == null) {
            if (other.val != null)
                return false;
        } else if (!val.equals(other.val))
            return false;
        return true;
    }   
}

At a certain point in the flow we retrieve the object an modify the internal value doing something like:

TestObject t = (TestObject)WebUtils.getSessionAttribute(request, "testAttribute");
t.setVal(99);

And after in the flow we retrieve it and read the value doing something like:

TestObject t = (TestObject)WebUtils.getSessionAttribute(request, "testAttribute");
int value = t.getVal();  //here we expect to see 99

Before trying to use spring-session, here value was 99. But now when I retrieve the value, I don't see 99, but I see the original value that the TestObject had when he was initially stored in the session.

Is there something I can configure in order to be able to replicate the behaviour I had before using spring-session?

Few notes:

TestObject t = (TestObject)WebUtils.getSessionAttribute(request, "testAttribute");
t.setVal(99);
WebUtils.setSessionAttribute(request, "testAttribute", t);

but I was curious to know if there was something I could do without modifying the existing code.

Thanks in advance for your help.

candrews commented 6 years ago

I also encountered this issue, here's how I'm working around it:

In an @Configuration class:

    @Bean
    public FilterRegistrationBean<Filter> sessionSetWhenGetAttributesFilter(final @Autowired SessionSetWhenGetAttributesFilter sessionSetWhenGetAttributes){
        final FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setOrder(SessionRepositoryFilter.DEFAULT_ORDER -1);
        filterRegistrationBean.setFilter(sessionSetWhenGetAttributes);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
        return filterRegistrationBean;
    }

Then create SessionSetWhenGetAttributesFilter.java:

import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

/** When {@link HttpSession#getAttribute(String)} is called, also call {@link HttpSession#setAttribute(String, Object)}.
 * This application is written with the assumption that if an object is gotten from the session then changed, then that change is persisted.
 * However, Spring Session does not work that way -it requires {@link HttpSession#setAttribute(String, Object)} to be called to tell it that the object has changed.
 * This approach isn't terribly efficient (it calls {@link HttpSession#setAttribute(String, Object)} more than necessary) but it's much less effort that determining all the places where {@link HttpSession#setAttribute(String, Object)} should be called and adding those missing invocations.
 * @author Craig Andrews
 * @see <a href="https://github.com/spring-projects/spring-session/issues/648">https://github.com/spring-projects/spring-session/issues/648</a>
 */
@Component("sessionSetWhenGetAttributes")
public class SessionSetWhenGetAttributesFilter extends OncePerRequestFilter {

    private static class HttpSessionWrapper implements HttpSession {
        protected final HttpSession delegate;

        public HttpSessionWrapper(HttpSession delegate) {
            this.delegate = delegate;
        }

        public long getCreationTime() {
            return delegate.getCreationTime();
        }

        public String getId() {
            return delegate.getId();
        }

        public long getLastAccessedTime() {
            return delegate.getLastAccessedTime();
        }

        public ServletContext getServletContext() {
            return delegate.getServletContext();
        }

        public void setMaxInactiveInterval(int interval) {
            delegate.setMaxInactiveInterval(interval);
        }

        public int getMaxInactiveInterval() {
            return delegate.getMaxInactiveInterval();
        }

        public HttpSessionContext getSessionContext() {
            return delegate.getSessionContext();
        }

        public Object getAttribute(String name) {
            return delegate.getAttribute(name);
        }

        public Object getValue(String name) {
            return delegate.getValue(name);
        }

        public Enumeration<String> getAttributeNames() {
            return delegate.getAttributeNames();
        }

        public String[] getValueNames() {
            return delegate.getValueNames();
        }

        public void setAttribute(String name, Object value) {
            delegate.setAttribute(name, value);
        }

        public void putValue(String name, Object value) {
            delegate.putValue(name, value);
        }

        public void removeAttribute(String name) {
            delegate.removeAttribute(name);
        }

        public void removeValue(String name) {
            delegate.removeValue(name);
        }

        public void invalidate() {
            delegate.invalidate();
        }

        public boolean isNew() {
            return delegate.isNew();
        }

    }

    private static class SessionSetWhenGetAttributesWrapper extends HttpSessionWrapper {

        public SessionSetWhenGetAttributesWrapper(HttpSession delegate) {
            super(delegate);
        }

        @Override
        public Object getAttribute(String name) {
            Object value = super.getAttribute(name);
            setAttribute(name, value);
            return value;
        }

    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        filterChain.doFilter(new HttpServletRequestWrapper(request) {

            @Override
            public HttpSession getSession(boolean create) {
                final HttpSession session = super.getSession(create);
                return session == null ? null : return new SessionSetWhenGetAttributesWrapper(session);
            }

            @Override
            public HttpSession getSession() {
                return new SessionSetWhenGetAttributesWrapper(super.getSession());
            }

        }, response);
    }

}