zoukankan      html  css  js  c++  java
  • 国际化之struts2实现研究

    国际化之Struts2实现研究

    一、基本原理

    先不提Struts这一工具,也不用其他现成的工具,如何实现国际化?

    最基本的实现就是,根据不同的Locale读取不同的文本。

    例如有两个资源文件:

    第一个:ApplicationResources_zh_CN.properties

    第二个:ApplicationResources_en_US.properties

    当Locale=zh_CN时,就去第一个文件查找;当Locale=en_US时,就去第二个文件查找。

    二、自己写方案去实现

    明白这个原理后,我们可以自己编写一套工具类,去实现国际化。通常,为了方便,我们需要自定义一个页面标签,类似于<s:text>那种,可以根据Locale获取相应语言的字符串。

    三、借助Struts2

    其实,Struts2也是通过这个原理去实现国际化的。我们何必再重复造轮子?

    Struts2是开源的,源代码全都有,如果你的项目没有用到Struts2,也没有其他简便的国际化工具,我想你照搬Struts2那一套也不难。

    四、Struts2国际化研究

    本人查阅了很多网上的资料,其实Struts2国际化,有个问题。

    Struts2的页面国际化,默认要走action才行,也就是如果你直接访问jsp文件,它是没有国际化效果的,除非每个jsp都通过action去访问(这也是Struts2推荐的方式)。

    通常,大家都会写一个通用Action,去转发所有jsp。

    比如我有个通用I18nAction,名为i18n,现在要从index.jsp直接跳转到main.jsp,如果写成

    href="main.jsp"

    这样跳转过去,main.jsp是没有国际化效果的,因为它没有经过action处理,所有要写成:

    href="i18n.action?jsp=main.jsp"

    我们将jsp的路径以一个参数的形式,交给action,action再去转发。

    但是我们不想这么麻烦,每次都要写i18n.action,所以,高手们想,是不是能够编写一个过滤器(Filter) 自动实现此功能?当然可以!

    我们编写一个Filter,拦截所有的jsp访问,然后转交给i18n.action去处理。

    OK,这算是一种方法,不过,网上能够找到这种教程,所以我不再多讲,有兴趣可以baidu或google。

    这种方法有个劣势,就是如果你直接访问jsp,那还是没有国际化效果。而且拦截器可能带来一些问题,因为它拦截了所有jsp,有时我们并不希望这样做。

    我要讲另外一种方法,可以直接访问jsp,而无需经过action,当然也就无需拦截器。

    我们从基本原理入手,从问题的根源入手,Struts2国际化是怎么实现的呢,其入口不是action,而是<s:text>标签,只要我们能找出<s:text>标签实现的源码,并稍作修改,就可以使其按照我们的模式去工作。我就是这么做的。

    <s:text>标签的源码是怎样一个逻辑呢?请看下面的代码段:

                                 

    (取自com.opensymphony.xwork2.util.LocalizedTextUtil.java)

    就是根据Locale去寻找aTextName对应的value,

    直接访问jsp时,之所以没有国际化效果,是因为Locale设置有问题。

    Struts2获取text时默认取Loacle的方式为:

    ActionContext.getContext().getLocale()

    getLocale()方法源码如下:

     

    这个getLocale()方法,是从

    Map<String,Object> context里面去找

    key= "com.opensymphony.xwork2.ActionContext.locale"的Object,这个Object就是Locale对象。

    我们需要明确一点:

    LocaledefultLocale=Locale.getDefault();

    是获取操作系统的locale,这个值我们不应该改变(一改就会涉及到所有用户),也不推荐使用。

    我们要根据浏览器去设置LOCALE值?怎么办呢?

    打开IE的语言设置,我们可以看到,可以设置多个语言,所以说实际上浏览器端的Locale是一个列表。通过request可以获得它:

       Enumeration locales=request.getLocales();

       while(locales.hasMoreElements())

       {

          LocaleclientLocale=(Locale)locales.nextElement();

          out.println("国别:"+clientLocale.getDisplayCountry()+"<br>");

          out.println("语言:"+clientLocale.getDisplayLanguage()+"<br>");

       }

    另外,获取客户端用户设置的第一个locale:

    Localefirst=request.getLocale();

    如此,我们有了浏览器端的Locale,但是每次都去request里面取,是不是有些麻烦?稍后,可以改进一下。

    这还不够,我们要做到的是根据用户的选择,去切换语言类型。

       不同的浏览器、不同的访问应该有不同的Locale,所以应该把Locale放在HttpSession中。所以说切换语言其实很简单,将Locale存入Session中,然后国际化的时候从Session中去寻找Locale就行了。

    综上,总结出国际化的步骤:

    第一点:默认情况下(用户没有切换语言),则Session里面没有Locale值,此时从用户请求的浏览器端读取,并设置到Session中。

    第二点:用户选择了切换语言,则将切换后的语言设置到Session中。

    第一点Struts2是做到了,每次访问一个jsp,或Action,Struts都会new一个新的Map<String, Object> context,如下源码所示:

     

    (取自org.apache.struts2.dispatcher. Dispatcher.java)

    但是第二点,Struts的处理方式就不是我想要的形式,Struts是怎样切换语言环境的呢?

    是在action后面加request_locale参数,例如

    changeLan.action?request_locale=en_US

    执行每个action时,它都会去检查是否有request_locale这个参数,如果有就会将Locale设置到session里面,其key为:"WW_TRANS_I18N_LOCALE"

    同时执行:

    ActionContext.setLocale(locale);改变ActionContext里面的Map<String, Object>context值

    我之前说了:

    Struts2使用Locale时,默认是从ActionContext中取:

    ActionContext.getContext().getLocale()

    那是不是以后取出的Locale都是第一次设置的locale呢?

    答案是否定的。实际上每次访问一个jsp或Action时,Struts都会new一个新的Map<String, Object> context,并且如果是访问的Action,还会额外的经过一个名叫I18nInterceptor的拦截器,当session里面不存在Locale时,它会添加进去,如果存在就不添加。最后重新设置context里面的Locale(这就说明,每次访问Action时,ActionContext里面的Locale都是新的)。见下面的源码:

     

     

    看到没有,这个拦截器会拦截所有Action,当locale==null(也即requested_locale==null)时就会去session中取Locale(如果没有,则取ActionContext.getLocale),且最后,始终会执行saveLocale方法,这个方法调用了ActionContext.setLocale(locale),重新设置Locale。

    所有说,按Struts的模式(action后面加request_locale参数)切换了语言后,当访问Action时,locale实际上是从session里面取出来的,但是当访问jsp时,因为I18nInterceptor拦截器不会执行,而ActionContext里面的Map<String, Object>context又是新new出来的,且在new context时,用的是request.getLocale()(见上面我摘取的Dispatcher.java源码),所以访问jsp时,locale不是从session中取到的,而是读取的浏览器Locale。

    好了,我们知道struts2的这个毛病之后,应该怎样改进呢?

    很简单的逻辑:访问jsp时,如果session里面的Locale不为空,就应该以它为准,而不是以浏览器的Locale为准(除非session里面的Locale为空)。

    显然,我们不希望每个jsp都通过action,进而通过I18nInterceptor拦截器。

    我们希望直接访问jsp就能实现我们想要的那种效果。

    用户选择切换语言时,session里面一定是有我们想要的那个Locale的(我们可以设置进去)。

    关键是<s:text>标签获取Locale时出了问题,上面我已经说过,它是从

    ActionContext.getContext().getLocale()

    里面去拿的,在直接访问jsp的情况下,它的Locale值是浏览器端的语言。我们将其换成session里面的值不就行了?Yes!

    下面就是我改造后的getLocale()方法:

     

    或者换一个更标准的写法:

     

    OK,如此一来,国际化就完美了,我们做一个changeLocale.jsp,嵌入到指定页面,只要用户一切换语言,访问其他jsp时就能正确的国际化了,不需要通过action,不需要拦截器。此时整个环境也都是用户选择的那种Locale,所以即使在java代码中,也能正确的识别并做国际化处理。

       另外,当用户不切换语言时,我们能识别用户使用的浏览器语言,因为我们默认设置的是request.getLocale(),当用户切换语言后,session里面有Locale了,以后就用从session里面读取的Locale。

    另外,补充一点在国际化研究中,实践得出的一些关于Session的理解:

    服务器重启,session仍然有效

    浏览器重启,session失效

    可见session应该是双向的,服务器存一个,浏览器端也存一个。如果

    服务器重启,它的session没有丢失(应该是保存在了磁盘上),而

    浏览器重启,则session丢失(应该是保存在浏览器端的内存中)

    进一步研究得出结论:

    session是服务器端和浏览器端双向交互的。不过浏览器端存的是sessionid(Tomcat下Java程序通常是一个32位字符串,这个id是存在cookie中的,名为JSESSIONID,如下图是我在FireFox中捕捉到的),而服务器端存的是session对象,当浏览器访问服务器时,如果是以jsp、action等非静态访问形式,第一次连接时,服务器会新建一个session对象,以后的访问中,只要浏览器没有重启或者session没有过期或销毁,那么这个session是不会变的,也就是说后面用的session都是第一次建立的那个。

     

    实际上,访问jsp时,会调用如下方法:

    HttpServletRequestgetSession(true);

    该方法,如果session为空,则会new一个session,否则,返回已有的session。官方解释为:

    publicHttpSession getSession(boolean arg)

       Returns the current HttpSession associatedwith this request or, if if there is no current session and arg is true,returns a new session.

       If arg is false and the request has no validHttpSession, this method returns null.

    Tomcat默认是启用了session持久化技术的(session persistence),也就是说服务器关闭后,session会存在磁盘上(文件名为session.ser),重启服务器时,只要session没过期,仍然可以用。

    (提醒一点,存入session的类建议实现序列化接口,比如User什么的)

    在tomcat的配置文件context.xml中有一个<Manager ... />标签,可以配置session的持久化。

    在JSP页面,我们可以设置

    <%@ page session="false"%>

    这样设置呢,不是不让页面创建Session,而是在此JSP页面无法使用session,可以减少网络数据传输。

    另外补充一个“URL重写技术”:

       通常session id是保存在浏览器的cookie中的,由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为

       http://...../xxx;jsessionid=ByOK3vjFD75aPnrF788764

    另一种是作为查询字符串附加在URL后面,表现形式为

       http://...../xxx?jsessionid=ByOK3vjFD75aPnrF88764

       这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。

       为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个sessionid。

    还有一个问题,是不是关闭浏览器后session就消失了呢?

    回答:session是在服务器端的,你关不关浏览器对它没有影响,因为你关闭浏览器时,只是浏览器端的session id丢了,但是浏览器并会主动通知服务器说“我已经关闭了,你将session注销吧”。

    最后一个问题,session如何过期的呢?

    1)主动注销

    服务器会check session object 是不是valid的,如果是无效的。如果invalid,则先throw IllegalStateException,然后开始后续处理(从map中移除,通知listener等)

    代码片段如下:

    /**  
     * Perform the internal processing required to invalidate this session,  
     * without triggering an exception if the session has already expired.  
     *  
     * @param notify Should we notify listeners about the demise of  
     *  this session?  
     */  
    public void expire(boolean notify) {   
      
        // Check to see if expire is in progress or has previously been called   
        if (expiring || !isValid)   
            return;   
      
        synchronized (this) {   
            // Check again, now we are inside the sync so this code only runs once   
            // Double check locking - expiring and isValid need to be volatile   
            if (expiring || !isValid)   
                return;   
      
            expiring = true;   
            setValid(false);   
            manager.remove(this, true);//在管理对象中讲这个session object删除(内部也是map实现的)   
      
            // 此处nofity标示是否在注销session的时候发送Evnet给listener,典型的观察者pattern   
            if (notify) {   
                fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);   
            }   
      
            expiring = false;   
      
            // Unbind any objects associated with this session   
            String keys[] = keys();   
            for (int i = 0; i < keys.length; i++)   
                removeAttributeInternal(keys[i], notify);   
      
        }   
      
    } 
    


    2)超时注销

    如果浏览器端一直有操作(即一直有请求),那么session就不会过期,是什么原理呢?

    其实,有一个守护线程去检查session到期时间,每两次访问的时间间隔,如果超过timeout时间,则执行销毁工作。所以,想让session永不过期,可以在timeout时间内,一直保持有request。

    不过session可能会意外丢失,这个就不是我们能控制的了。

    好了,国际化涉及到的难题基本已经讲解完了,不懂的多看几遍,理解理解。我也是花了很多时间实践和分析才得出的,若有不正确之处,还望赐教。

    附:Struts2国际化的DEMO项目

    下载地址:http://dl.vmall.com/c04g39g2q7

    (struts的jar包需自己添加,2.3以上的版本均可,需要把xwork-core-2.3.4.1.jar里面的com/opensymphony/xwork2/ActionContext.class删掉,因为我重写了这个类)

    • 作者:zollty(接收技术咨询和工作机会)
    • 出处:http://www.cnblogs.com/zollty/
    • 本文版权归作者和博客园共有,欢迎转载,但请在文章页面明显位置给出原文连接,否则视为侵权。
    • 关于博主,85后程序员,擅长复杂分布式系统架构,中间件设计开发,关注效率/性能/稳定性。
    • 对于自己,博文只是总结。在总结的过程发现问题,解决问题。
    • 对于他人,在此过程如果还能附带帮助他人,那就再好不过了。
    • 感谢您的阅读。如果文章对您有用,那么请轻轻点个赞,以资鼓励。
  • 相关阅读:
    DataTable 中各种计算(笔记)
    超级强悍开源图表控件ZedGraph,请高手指教数据绑定问题!!!
    ArcGIS Engine 9.3项目迁移到ArcGIS Engine 10 报错的解决方案
    c# ,vb.net代码相互转换
    ArcGIS扩展开发(一)为 ArcGIS Desktop建立Addin插件
    设计
    正则表达式
    JSON入门
    关于Unity中资源导入
    关于Unity中的ShaderLab
  • 原文地址:https://www.cnblogs.com/zollty/p/3029544.html
Copyright © 2011-2022 走看看