用过struts2的人都知道,struts2有个很大的特点就是可以不再面向Servlet API编程,从Action的方法签名就可以看出,其execute方法不接收任何参数,返回值也仅仅是String.从而实现与Servlet API的解耦,语法层面上脱离了Web容器。
当要在Web层即控制器向视图层传递数据时,传统做法都是存储在HttpServletRequest、HttpServletSession、ServletContext对象中。
而在struts2中使用其提供的API就可以操作request,session,application这些用于向页面传递数据的对象,这里说的request,session,application是不同于上面所讲的,它们是Map对象,所以才实现与Servlet API的解耦。当然还有parameters对象,还有一个stuts2扩展的attr对象,下面就详细讲解这其中的原理。
在讲解之前须要知道ActionContext是如何创建而来的,所以先把这个讲明白:
struts2的核心分发器Dispatcher中有两重载的方法:createContextMap,下面是这两个方法的源码:
public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping, ServletContext context) { // request map wrapping the http request objects Map requestMap = new RequestMap(request);//将request封装成一个RequestMap对象 // parameters map wrapping the http parameters. ActionMapping parameters are now handled and applied separately Map params = new HashMap(request.getParameterMap());//将请求参数放在HashMap中 // session map wrapping the http session Map session = new SessionMap(request);//将session封装在一个SessionMap中(通过request.getSession可获取session) // application map wrapping the ServletContext Map application = new ApplicationMap(context);//把ServeltContext封装在成一个ApplicationMap对象 Map<String,Object> extraContext = createContextMap(requestMap, params, session, application, request, response, context); if (mapping != null) { extraContext.put(ServletActionContext.ACTION_MAPPING, mapping); } return extraContext; }
public HashMap<String,Object> createContextMap(Map requestMap, Map parameterMap, Map sessionMap, Map applicationMap, HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) { HashMap<String,Object> extraContext = new HashMap<String,Object>(); extraContext.put(ActionContext.PARAMETERS, new HashMap(parameterMap)); extraContext.put(ActionContext.SESSION, sessionMap); extraContext.put(ActionContext.APPLICATION, applicationMap); Locale locale; if (defaultLocale != null) { locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale()); } else { locale = request.getLocale(); } extraContext.put(ActionContext.LOCALE, locale); //extraContext.put(ActionContext.DEV_MODE, Boolean.valueOf(devMode)); extraContext.put(StrutsStatics.HTTP_REQUEST, request); extraContext.put(StrutsStatics.HTTP_RESPONSE, response); extraContext.put(StrutsStatics.SERVLET_CONTEXT, servletContext); // helpers to get access to request/session/application scope extraContext.put("request", requestMap); extraContext.put("session", sessionMap); extraContext.put("application", applicationMap); extraContext.put("parameters", parameterMap); AttributeMap attrMap = new AttributeMap(extraContext); extraContext.put("attr", attrMap); return extraContext; }
首先要说明的是createContextMap方法返回的Map中的数据就是要放到ActionContext中的数据。
第一个createContextMap所做的工作看注释应该都知道了,但有一个细节不知道大家注意到没有,为什么parameters是存放在一个HashMap中,而不是在一个ParametersMap中,当然ParametersMap这个类是不存在的,sturts2开发者没有为请求参数开发一个用于封装请求参数的类,而是直接存放在HashMap中。但是像request,session,application都有对应的封装Map类。这是因为对于请求参数来说在请求到来的时候就已经确定了,不会再变化,而request,session,application这些对象到了Action或者Interceptor中我们会向其添加数据,封装后利于在Action中与Servlet API解耦。
第一个createContextMap调用了第二个createContextMap方法,把Servlet原生的与封装好的request,session,application都传了进去,第二个createContextMap方法所做的工作也很简单就是把Servlet原生的与封装好的对象都放在了一个HashMap(extraContext)中,然后返回,注意这个大的HashMap中的所有数据都会放到ActionContext中。
还有一个要先讲的就是在struts2中,所使用的request是struts2经过包装的StrutsRequestWrapper类,继承自javax.servlet.http.HttpServletRequestWrapper,并且覆盖了getAttribute方法,下面是该方法源码:
public Object getAttribute(String s) { if (s != null && s.startsWith("javax.servlet")) { // don't bother with the standard javax.servlet attributes, we can short-circuit this // see WW-953 and the forums post linked in that issue for more info return super.getAttribute(s);//先去父类中找,即在HttpServletRequest中查找 } ActionContext ctx = ActionContext.getContext(); Object attribute = super.getAttribute(s);//这里同上 if (ctx != null) { if (attribute == null) {//如果没有找到 boolean alreadyIn = false; Boolean b = (Boolean) ctx.get("__requestWrapper.getAttribute"); if (b != null) { alreadyIn = b.booleanValue(); } // note: we don't let # come through or else a request for // #attr.foo or #request.foo could cause an endless loop if (!alreadyIn && s.indexOf("#") == -1) { try { // If not found, then try the ValueStack ctx.put("__requestWrapper.getAttribute", Boolean.TRUE); ValueStack stack = ctx.getValueStack(); if (stack != null) { attribute = stack.findValue(s);//如果在HttpServletRequest没有找到则在ValueStack查找 } } finally { ctx.put("__requestWrapper.getAttribute", Boolean.FALSE); } } } } return attribute; }
正如注释中所说的如果在HttpServletRequest中没有找到则在ValueStack中查找。这一点是非重要的,到后面的讲解中就能体会得到。
做完上面的准备工作可以进行正题了:
在Action中我们向View中传递数据是这样做的:
ActionContext ctx = ActionContext.getContext(); //获取ActionContext对象 ctx.put(key, value); //往request中存储数据 ctx.getSession().put(key, value); //往session中存储数据 ctx.getApplication().put(key, value); //往application中存储数据
如果我们紧接着在上面代码写这样的代码:
ServletActionContext.getRequest().getAttribute(key); //在HttpServletRequest中获取上面存储的数据 ServletActionContext.getRequest().getSession().getAttribute(key); //在HttpSession中获取上面存储的数据 ServletActionContext.getServletContext().getAttribute(key); //在ServletContext中获取上面存储的数据
你会“奇怪”地发现立即就能获取到上面存储的数据,数据很快地在Map中与Servlet对象中同步了,这是为什么呢?
因为存储在request中的原理更复杂所以把它放到后面,先讲session与application。
session数据同步原理:
ctx.getSession()看方法签名返回值是一个Map,其真正的实现类就是前面所讲的SessionMap,其实现了java.util.Map接口在SessionMap创建的时候struts2把HttpServletRequest对象传递了进去,这样当然就可以操作HttpSession对象了。下面是SessionMap的put方法源码:
public V put(K key, V value) { synchronized (this) { if (session == null) { session = request.getSession(true); } } synchronized (session) { entries = null; session.setAttribute(key.toString(), value); return get(key); } }
大家可以看到该方法内部就是把我们要存储的值放到了HttpSession中。
下面是SessionMap的get方法源码:
public V get(Object key) { if (session == null) { return null; } synchronized (session) { return (V) session.getAttribute(key.toString()); } }
获取的时候理所当然就在HttpSesion中获取了,这里在HttpSession中地行存放与读取数据中加了synchronized关键字,这是因为对于有些浏览器可能存在两个进程操作的是同一个HttpSession的情况。
application数据同步原理:
这与session数据同步原理基本上是一样的,ctx.getApplication()返回的真实对象是一个ApplicationMap对象,在该对象创建的时候传入了ServletContext,当然其内部就是操作的ServletContext对象,这个就不说源码了,大家看一下应该都能明白了。
最后讲request数据同步原理:
当我们调用ctx.put方法时其实调用的是ActionContext内部维护的一个名为context对象的put方法,这个context对象的类型为ognl.OgnlContext,即OGNL表达式上下文。下面是OgnlContext的put方法的源码:
public Object put(Object key, Object value) { Object result; if (RESERVED_KEYS.containsKey(key)) { //这里省略了很多判断代码... } else { result = _values.put(key, value); } return result; }
当我们存储普通数据的时候都是存储在了一个叫_values的对象中,翻一下源码就知道_values其实就是一个OgnlContext内部维护的HashMap。现在我们知道了,当我们调用ActionContext.getContext().put(key,value)方法的时候其实就是存放在一个OgnlContext内部维护的HashMap中。
再看我们调用HttpServletRequest.getAttribute(key)是如何获取到通过调用ActionContext.getContext().put(key,value)存储的值的。这里就要用到上面的结论了,我们获取的HttpServletRequest对象其实是经过struts2包装后的StrutsRequestWrapper对象,其覆盖了getAtrribute方法,当在HttpServletRequest中找不到指定key的值的时候就会去ValueStack中查找。
通过上面我们可以知道当我们调用ActionContext.getContext().put(key,value)方法存储数据时根本就没有存储在HttpServletRequest对象中,所以在HttpServletRequest中是根据找不到数据的,所以会在ValueStack中查找。
即会执行StrutsRequestWrapper中的getAtrribute方法中的这句代码:
attribute = stack.findValue(s);
ValueStack是一个接口,sturts2使用的其实现类是OgnlValueStack,下面是OgnlValueStack.findValues方法源码
public Object findValue(String expr) { return findValue(expr, false); }
这里又调用了另一个重载的findValue方法,进去看看
public Object findValue(String expr, boolean throwExceptionOnFailure) { try { if (expr == null) { return null; } if ((overrides != null) && overrides.containsKey(expr)) { expr = (String) overrides.get(expr); } if (defaultType != null) { return findValue(expr, defaultType); } Object value = ognlUtil.getValue(expr, context, root); //在ValueStack的root对象中查找,当然是找不到的 if (value != null) { return value; } else { checkForInvalidProperties(expr, throwExceptionOnFailure, throwExceptionOnFailure); return findInContext(expr); //在OgnlContext中查找,看,这里又绕回来了,我们存的时候就是存在OgnlContext中 } } catch (OgnlException e) { checkForInvalidProperties(expr, throwExceptionOnFailure, throwExceptionOnFailure); return findInContext(expr); } catch (Exception e) { logLookupFailure(expr, e); if (throwExceptionOnFailure) throw new XWorkException(e); return findInContext(expr); } finally { ReflectionContextState.clear(context); } }
我们存的时候并不是存在ValueStack的root对象中,而是存在OgnlContext中,这里的查找顺序则好,先去root对象中查找,如果找不到再去OgnlContext中查找,即findInContext(expr)方法,我们进放该方法:
private Object findInContext(String name) { return getContext().get(name); }
这里getContext()返回的就是ognl.OgnlContext对象,再调用了其get方法,再进入get方法:
public Object get(Object key) { Object result; if (RESERVED_KEYS.containsKey(key)) { //这里省略了很多判断代码... 翻开源码就会发现get与put方法那些判断条件都是一样的,我们存储的普通数据不会进入这里 } else { result = _values.get(key); } return result; }
到这里相信大家都明白了,最终调用的OgnlContext.get(Object key)方法中,内部就是在其内部维护的_values对象中查找,而我们存储的时候就是存放在这个_values对象中,然后返回,当然也就找到了我们存储的数据。
还记得上面在第一个createContextMap方法中,struts2把HttpServletRequest对象封装成了一个RequestMap对象,并存放在ActionContext中,key为"request":
extraContext.put("request", requestMap);
但是ActionContext中并没有直接提供一个API(getRequest())去获取RequestMap对象,其原因我想就是因为通过上面的机制就已经实现request数据同步功能。当然你也可以调用ActionContext.getContext().get("request")去获取RequestMap对象,然后在RequestMap中获取存储的数据,因为RequestMap中查找时就是在HttpServletRequest中查找,当然这个HttpServletRequest是struts2包装过的StrutsRequestWrapper对象,查找时又进行上述流程,最终查找到我们存储的数据。
在页面中获取数据的时候我们是通过OGNL表达式,这个就不用多说了,当然就是在OgnlContext与ValuesStack中查找的。
通过上面所讲的大家想想:通过EL表达是不是也能访问到request,session,application范围中存储的数据呢?
至此呢,struts2数据同步的原理就到这了,如有错误之处,尽请指正。