通常在项目中,用户登录后,我们会将用户的信息存到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。