利用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<User> local = new ThreadLocal<User>();

    /**

    * 设置用户信息

    *

    * @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会没有效果

<filter>

<filter-name>sessionFilter</filter-name>

<filter-class>com.shen.usersession.SessionFilter</filter-class>

</filter>

<filter-mapping>

<filter-name>sessionFilter</filter-name>

<url-pattern>*.action</url-pattern>

</filter-mapping>

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

User user = SessionLocal.getUser();

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容