Spring国际化解析Locale的原理

2,304 阅读5分钟

1.LocaleContext

    org.springframework.context.i18n.LocaleContext是一个接口,只有一个方法getLocale(),就是用来获取当前的Locale的,下面看下整体类图。

    从类图中,我们可以看到LocaleContext有三个子类:其中TimeZoneAwareLocaleContext是一个子接口,该接口提供了一个getTimeZone()方法来获取当前时区了;SimepleLocaleContext是对LocaleContext接口的一个简单实现;主要看下SimpleTimeZoneAwareLocaleContext类,这个类继承了SimpleLocaleContext,实现了TimeZoneAwareLocaleContext接口,这也就说该类可以同时获取Locale和TimeZone,这个类也是我们常用的,看下他的实现:

public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implements TimeZoneAwareLocaleContext {

	private final TimeZone timeZone;


	/**
	 * Create a new SimpleTimeZoneAwareLocaleContext that exposes the specified
	 * Locale and TimeZone. Every {@link #getLocale()} call will return the given
	 * Locale, and every {@link #getTimeZone()} call will return the given TimeZone.
	 * @param locale the Locale to expose
	 * @param timeZone the TimeZone to expose
	 */
	/**
	 * 通过构造方法设置当前的Locale和TimeZone
	 * @param locale
	 * @param timeZone
	 */
	public SimpleTimeZoneAwareLocaleContext(Locale locale, TimeZone timeZone) {
		super(locale);
		this.timeZone = timeZone;
	}


	public TimeZone getTimeZone() {
		return this.timeZone;
	}

	@Override
	public String toString() {
		return super.toString() + " " + (this.timeZone != null ? this.timeZone.toString() : "-");
	}

}

    整个LocaleContext的设计目的是为了保存了整个应用的Locale和TimeZone。看完下面的LocaleResolver,你就会明白LocaleContext的作用。

2.LocaleResolver

    首先看下org.springframework.web.servlet.LocaleResolver的整体类图:

    上面这个类图中有两个主要的接口:org.springframework.web.servlet.LocaleResolver和org.springframework.web.servlet.LocaleContextResolver,这两个接口的设计思想和上面的LocaleContext与TimeZoneLocaleContext是一致的。LocaleResolver接口提供了对Locale操作的两个方法:

public interface LocaleResolver {

    /**
     * Resolve the current locale via the given request.
     * Can return a default locale as fallback in any case.
     * @param request the request to resolve the locale for
     * @return the current locale (never {@code null})
     */
    /**
     * 根据当前请求解析Locale
     *
     * @param request
     * @return
     */
    Locale resolveLocale(HttpServletRequest request);

    /**
     * Set the current locale to the given one.
     * @param request the request to be used for locale modification
     * @param response the response to be used for locale modification
     * @param locale the new locale, or {@code null} to clear the locale
     * @throws UnsupportedOperationException if the LocaleResolver
     * implementation does not support dynamic changing of the locale
     */
    /**
     * 设置语言环境,该方法的本质就是调用LocaleContextResolver的setLocaleContext方法,来设置应用的Locale和TimeZone,
     * 那也就意味着如果没有实现LocaleResolverContext接口的类,就一定是不能设置Locale和TimeZone,
     * 实现了LocaleContextResolver接口的类也不一定可以设置TimeZone和Locale,如FixLocaleResolver,是根据业务需求不允许设置的
     * @param request
     * @param response
     * @param locale
     */
    void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);

}

    LocaleContextResolver提供了对LocaleContext的两个操作方法:

public interface LocaleContextResolver extends LocaleResolver {

	/**
	 * Resolve the current locale context via the given request.
	 * <p>This is primarily intended for framework-level processing; consider using
	 * {@link org.springframework.web.servlet.support.RequestContextUtils} or
	 * {@link org.springframework.web.servlet.support.RequestContext} for
	 * application-level access to the current locale and/or time zone.
	 * <p>The returned context may be a
	 * {@link org.springframework.context.i18n.TimeZoneAwareLocaleContext},
	 * containing a locale with associated time zone information.
	 * Simply apply an {@code instanceof} check and downcast accordingly.
	 * <p>Custom resolver implementations may also return extra settings in
	 * the returned context, which again can be accessed through downcasting.
	 * @param request the request to resolve the locale context for
	 * @return the current locale context (never {@code null}
	 * @see #resolveLocale(HttpServletRequest)
	 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
	 * @see org.springframework.web.servlet.support.RequestContextUtils#getTimeZone
	 */
	LocaleContext resolveLocaleContext(HttpServletRequest request);

	/**
	 * Set the current locale context to the given one,
	 * potentially including a locale with associated time zone information.
	 * @param request the request to be used for locale modification
	 * @param response the response to be used for locale modification
	 * @param localeContext the new locale context, or {@code null} to clear the locale
	 * @throws UnsupportedOperationException if the LocaleResolver implementation
	 * does not support dynamic changing of the locale or time zone
	 * @see #setLocale(HttpServletRequest, HttpServletResponse, Locale)
	 * @see org.springframework.context.i18n.SimpleLocaleContext
	 * @see org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext
	 */
	void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext);
}

    从上面的类图中,我们可以看到主要有四个实现类:     1.org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver         该实现类相当于LocaleResolver的默认实现,由于它只实现了LocaleResolver接口,因此只能解析Locale,不能设置Locale,该类在DispatcherServlet调用initLocaleResolver()方法的时候,会判断IOC容器中是由有一个叫localeResolver的Bean,如果这个Bean不存在,就会初始化该类作为默认的LocaleResolver。这个类是通过判断HTTP Header中的Accept-Language字段的值来决定当前应用的Locale和TimeZone。

      /**
	 * 通过Http Header的Accept-Language字段判断Locale
	 * @param request
	 * @return
	 */
	@Override
	public Locale resolveLocale(HttpServletRequest request) {
		Locale defaultLocale = getDefaultLocale();
		if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
			return defaultLocale;
		}
		Locale requestLocale = request.getLocale();
		List<Locale> supportedLocales = getSupportedLocales();
		if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
			return requestLocale;
		}
		Locale supportedLocale = findSupportedLocale(request, supportedLocales);
		if (supportedLocale != null) {
			return supportedLocale;
		}
		return (defaultLocale != null ? defaultLocale : requestLocale);
	}

      /**
	 * 通过Http Header的Accept-Language字段判断Locale
	 * @param request
	 * @return
	 */
	@Override
	public Locale resolveLocale(HttpServletRequest request) {
		Locale defaultLocale = getDefaultLocale();
		if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
			return defaultLocale;
		}
		Locale requestLocale = request.getLocale();
		List<Locale> supportedLocales = getSupportedLocales();
		if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
			return requestLocale;
		}
		Locale supportedLocale = findSupportedLocale(request, supportedLocales);
		if (supportedLocale != null) {
			return supportedLocale;
		}
		return (defaultLocale != null ? defaultLocale : requestLocale);
	}

    2.org.springframework.web.servlet.i18n.CookieLocaleResolver         该类是通过应用设置的Cookie来判断当前需要的Locale的,我们只需要给定CookieName,它会自动读取对应的value,设置Locale。

      //Locale 的Cookie
	private String localeAttributeName = LOCALE_SESSION_ATTRIBUTE_NAME;
    //TimeZone 的Cookie
	private String timeZoneAttributeName = TIME_ZONE_SESSION_ATTRIBUTE_NAME;

    @Override
	public Locale resolveLocale(HttpServletRequest request) {
		//该方法从Cookie中获取Locale
		parseLocaleCookieIfNecessary(request);
		return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
	}

	@Override
	public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
		parseLocaleCookieIfNecessary(request);
		return new TimeZoneAwareLocaleContext() {
			@Override
			public Locale getLocale() {
				return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
			}
			@Override
			public TimeZone getTimeZone() {
				return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
			}
		};
	}

    @Override
	public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
		setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
	}

	@Override
	public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
		Locale locale = null;
		TimeZone timeZone = null;
		if (localeContext != null) {
			locale = localeContext.getLocale();
			if (localeContext instanceof TimeZoneAwareLocaleContext) {
				timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
			}
			addCookie(response,
					(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
		}
		else {
			removeCookie(response);
		}
		request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
				(locale != null ? locale : determineDefaultLocale(request)));
		request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
				(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
	}

    3.org.springframework.web.servlet.i18nSessionLocaleResolver         由名字可知,该类是通过设置Session来实现的,实现原理和CookieLocaleResolver大差不差。

        //Locale 的Session
	private String localeAttributeName = LOCALE_SESSION_ATTRIBUTE_NAME;
    //TimeZone 的Session
	private String timeZoneAttributeName = TIME_ZONE_SESSION_ATTRIBUTE_NAME;

        @Override
	public Locale resolveLocale(HttpServletRequest request) {
		//通过sssion获取
		Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);
		if (locale == null) {
			locale = determineDefaultLocale(request);
		}
		return locale;
	}

	@Override
	public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
		return new TimeZoneAwareLocaleContext() {
			@Override
			public Locale getLocale() {
				Locale locale = (Locale) WebUtils.getSessionAttribute(request, localeAttributeName);
				if (locale == null) {
					locale = determineDefaultLocale(request);
				}
				return locale;
			}
			@Override
			public TimeZone getTimeZone() {
				TimeZone timeZone = (TimeZone) WebUtils.getSessionAttribute(request, timeZoneAttributeName);
				if (timeZone == null) {
					timeZone = determineDefaultTimeZone(request);
				}
				return timeZone;
			}
		};
	}

    //setLocale方法是在抽象类:org.springframework.web.servlet.i18n.AbstractLocaleContextResolver中,调用了子类的实现,也就是该方法
    @Override
	public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
		Locale locale = null;
		TimeZone timeZone = null;
		if (localeContext != null) {
			locale = localeContext.getLocale();
			if (localeContext instanceof TimeZoneAwareLocaleContext) {
				timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
			}
		}
		WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);
		WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
	}

    4.org.springframework.web.servlet.i18n.FixLocaleResolver         该类从名字就可以知道是一个固定的LocaleResolver,也就是说该类一旦设置了默认的Locale和TimeZone,就不可更改,更改会抛出异常。

private Locale defaultLocale;
private TimeZone defaultTimeZone;

    @Override
	public Locale resolveLocale(HttpServletRequest request) {
		Locale locale = getDefaultLocale();
		if (locale == null) {
			locale = Locale.getDefault();
		}
		return locale;
	}

	@Override
	public LocaleContext resolveLocaleContext(HttpServletRequest request) {
		return new TimeZoneAwareLocaleContext() {
			@Override
			public Locale getLocale() {
				return getDefaultLocale();
			}
			@Override
			public TimeZone getTimeZone() {
				return getDefaultTimeZone();
			}
		};
	}

	/**
	 * 更改直接抛出异常
	 * @param request the request to be used for locale modification
	 * @param response the response to be used for locale modification
	 * @param localeContext the new locale context, or {@code null} to clear the locale
	 */
	@Override
	public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
		throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
	}

    总结一下,常用的是CookieLocaleResolver和SessionLocaleResolver。LocaleResolver的初始化是在DispatcherServlet的initLocaleResolver方法中进行的。 无论是使用哪个实现类,Bean的id一定要申明为localeResolver,否则DIspatcherServlet读取不到,将会初始化默认的AcceptHeaderLocaleResolver。