利用ThreadLocal管理登录用户信息实现随用随取

通常在项目中,用户登录后,我们会将用户的信息存到session,如果想在其它地方获取session中的用户信息,我们需要先获取HttpServletRequest,再通过request.getSession得到HttpSession,从而获取到我们想要的用户信息。通常我们会将以上操作提取一个公共方法,如:

public static User getSessionUser(HttpServletRequest request)
    {
        if(request.getSession().getAttribute( "sessionuser" ) != null)
        {
            return (User)request.getSession().getAttribute( "sessionuser" );
        }
        return null;
    }

但是这样做也比较麻烦,需要传入一个HttpServletRequest,在Servlet和Struts1中我们可以轻松得到request对象,在SpringMVC中,我们只要在一个controller方法参数里显式加上HttpServletRequest参数也可以轻松获取,如:

public String create(HttpServletRequest request,HttpServletResponse response) {}

而在Struts2中,尽管获取request对象有好几种方法,但通常大家会采取在action中实现ServletRequestAware接口并实现setServletRequest方法的方式来获取:

private HttpServletRequest request;

@Override
public void setServletRequest( HttpServletRequest request )
{
    this.request = request;
}

我们在处理请求的时候,很多操作都要获取当前用户的ID等信息,由上可见,我们凡是在action的方法中任何一处想要获取session中的用户信息,则必须要先手动获取到HttpServletRequest,是不是比较麻烦,于是基于此,我们可以想出一种解决方案,就是写一个Action的基类,比如叫BaseAction.java,让这个基类去继承ActionSupport,并实现ServletRequestAware接口,并在此类里写一个获取用户信息的公共方法:

public class BaseAction extends ActionSupport implements ServletRequestAware
{

    private static final long serialVersionUID = -6136249081788565607L;
    
    public HttpServletRequest request;
    
    @Override
    public void setServletRequest( HttpServletRequest request )
    {
        this.request = request;
    }
    
    /**
     * 
     * 获取session中的用户
     * @return
     */
    public User getSessionUser()
    {
        if(request.getSession().getAttribute( "sessionuser" ) != null)
        {
            return (User)request.getSession().getAttribute( "sessionuser" );
        }
        return null;
    }
}

然后所有action都去继承这个BaseAction基类,然后直接调用getSessionUser()方法便可轻松获取用户信息:

public class UserAction extends BaseAction
{

    private static final long serialVersionUID = -394380310251254625L;
    
    public String load()
    {
	//获取session用户信息 
        User user = getSessionUser();
        if(user != null)
        {
            System.out.println("当前用户:"+user.getUsername());
        } else {
            System.out.println("用户为空");
        }
        return "chat";
    }
}

看上去问题似乎得到了解决,可是以上仅能满足在所有action的方法中随用随取,而实际中我们在做一些DAO操作时,往往要记录操作人的ID,也就是当前登录用户的ID,如发表/修改文章,这个时候,在service层和dao层就要用到session中的用户信息,而通常在一个大型项目中,service层和dao层都是和web层分离开来,都是单独的工程,不依赖servlet api,大家也不会为了在service层或者dao层获取登录用户信息而这么做,这样显得会很奇怪,所以我们只能在action中调用service的时候,将用户信息以参数形式传过去,如:

 public interface ArticleService
{

    /**
     * 
     * 保存文章
     * @param article 文章信息
     * @param user 当前用户
     * @return
     */
    boolean saveArticle(Article article, User user);
}

如此一来,代码就不够简洁优雅,所有我们想要获取用户信息的方法都要多加一个参数,增强了依赖,和我们想要的“松耦合”背道而弛。

所以,对于session中的用户信息,我们不仅想要在action中随用随取,还想在其它普通类中取,即使不依赖servlet api, 我们也要在方法里随用随取,anywhere!

为了解决这个问题,我们就要采取一种新的方法来存储用户信息——ThreadLocal。

ThreadLocal,顾名思义,就是本地线程,可是这个名字实在容易让人误解,因为其实它是本地线程局部变量的意思,首先我们要知道,我们每个请求都会对应一个线程,这个ThreadLocal就是这个线程使用过程中的一个变量,该变量为其所属线程所有,各个线程互不影响。这里我们要了解一下ThreadLocal的三个方法:

ThreadLocal.set(T value); //设置值

ThreadLocal.get(); //获取值

ThreadLocal.remove(); //移除值

所以我们可以借助这个ThreadLocal来存储登录用户的信息,在一个请求中,所有调用的方法都在同一个线程中去处理,这样就实现了在任何地方都可以获取到用户信息了,从而摆脱了HttpServletRequest的束缚。具体实现如下:

首先,我们定义一个SessionLocal,在这个类中,我们初始化一个静态的ThreadLocal,其支持泛型,这里类型为用户对应的Bean,因为我们要存储的是用户信息,然后写两个静态方法,setUser用于往ThreadLocal设置用户信息,getUser()用于从ThreadLocal获取用户信息:

public class SessionLocal
{
    private static ThreadLocal local = new ThreadLocal();

    /**
     * 设置用户信息
     * 
     * @param user
     */
    public static void setUser( User user )
    {
        local.set( user );
    }

    /**
     * 获取登录用户信息
     * 
     * @return
     */
    public static User getUser()
    {
        System.out.println( "当前线程:" + Thread.currentThread().getName() );
        return local.get();
    }
}

在用户登录的时候,我们根据登录名和密码查询到用户信息以后,就调用上面的setUser()方法存储我们的用户信息到线程局部变量中,暂时不保存到session中:

SessionLocal.setUser( user );

然后,我们在想要获取用户信息的地方调用getUser()方法即可,比如我在一个action的load方法中:

public String load()
    {
        User user = SessionLocal.getUser();//获取用户信息
        if(user != null)
        {
            System.out.println("当前用户:"+user.getUsername());
        } else {
            System.out.println("用户为空");
        }
        return "chat";
    }

即使在service实现类里也可以正常获取:

public class ArticleServiceImpl implements ArticleService
{

    @Override
    public boolean saveArticle( Article article )
    {
        User user = SessionLocal.getUser();
        System.out.println("[service]当前用户:"+user.getUsername());
        
        //To do save article
        
        return true;
    }
}

可是这时候又出现了问题,在一个线程结束后,我又发起了一次后台请求,这个时候,处理这个请求的线程变成了另外一个线程,线程切换了!!!而这个线程中我们并没有set用户信息到它的ThreadLocal中去,此时我们想要获取用户信息就获取不到了,前面说过,ThreadLocal为各个线程所私有,各线程间不共享,也互不影响,那么问题来了,我们只是在登录的时候,查询用户信息并将其放进当前线程的ThreadLocal,而后续其它请求一旦切换到别的线程,我们的功能就玩不转了,所以我们需要借助一个方法来过滤所有的后台请求(排除非必须登录才能访问的url),给用户信息做个检查,一旦SessionLocal.getUser()为空,那么我们就set进去,so,我们可以借助一个Filter来达到我们的目的。

可是上面我们并没有将用户信息放到session中,此时,我们还是绕回去了,我们依然不得不把用户信息给放到session中,不然我们在Filter中如何获取用户信息并set进ThreadLocal,总不能再查一次数据库,所以,将上面的登录处理做个改动,添加用户信息到session:

public String login()
   {
       User user = new User();
       user.setUsername( username );
       //存储session
       request.getSession().setAttribute( "sessionuser", user );
       //存储ThreadLocal
       SessionLocal.setUser( user );
       System.out.println("用户【"+username+"】登录,存进session,并设置进TheadLocal");
       return "chat";
   }

如此,我们在Filter中就可以先判断SessionLocal.getUser()是否为空,如果为空,则从session中取用户,如果session中有,则将用户放到TheadLocal,否则,跳转到首页或者登录页,这里我们定义一个SessionFilter:

public class SessionFilter implements Filter
{

    @Override
    public void destroy()
    {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void doFilter( ServletRequest req, ServletResponse res,
            FilterChain chain ) throws IOException, ServletException
    {
        HttpServletRequest request = (HttpServletRequest)req;
	//排除登录请求 
        if(request.getRequestURI().contains( "user!login.action" ))
        {
            chain.doFilter( req, res );
            return;
        }
        System.out.println("【请求拦截.】");
        HttpSession session = request.getSession();
        if(session.getAttribute( "sessionuser") != null)
        {
            if(SessionLocal.getUser() == null)
            {
                System.out.println("【当前线程"+Thread.currentThread().getName()+"中用户信息为空,从session中set到ThreadLocal.】");
                SessionLocal.setUser( (User)session.getAttribute( "sessionuser") );
            }
        } else 
        {
            System.out.println("【用户会话失效,跳转到首页.】");
            HttpServletResponse response = (HttpServletResponse)res;
            response.sendRedirect( "index.jsp" );
            return;
        }
        chain.doFilter( req, res );
    }

    @Override
    public void init( FilterConfig arg0 ) throws ServletException
    {
        // TODO Auto-generated method stub
        
    }

}

再将上面的SessionFilter配置到web.xml,注意顺序一定要在struts2映射之前,不然Filter会没有效果


		sessionFilter
		com.shen.usersession.SessionFilter
	
	
		sessionFilter
		*.action
	

经过上面的配置,不论线程怎么切换,我们都可以在任何方法中很方便的获取用户信息了,不用传任何参数:

User user = SessionLocal.getUser();

以上是通过filter实现请求过滤,在springMVC中,我们可以通过HandlerInterceptor来实现,定义一个类去实现这个HandlerInterceptor接口,在preHandle中去调用SessionLocal中的setUser(user)来设置用户信息,在xml配置这个interceptor的时候,我们可以通过 很方便的配置要排除的URL。

你可能感兴趣的:(java)