SpringMVC 源碼分析系列最后一篇,和大家聊一聊 Theme。
Theme,就是主題,點一下就給網站更換一個主題,相信大家都用過類似功能,這個其實和前面所說的國際化功能很像,代碼其實也很像,今天我們就來捋一捋。
考慮到有的小伙伴可能還沒用過 Theme,所以這里松哥先來說下用法,然后我們再進行源碼分析。
1.一鍵換膚
來做一個簡單的需求,假設我的頁面上有三個按鈕,點擊之后就能一鍵換膚,像下面這樣:
我們來看下這個需求怎么實現。
首先三個按鈕分別對應了三個不同的樣式,我們先把這三個不同的樣式定義出來,分別如下:
blue.css:
- body{
- background-color: #05e1ff;
- }
green.css:
- body{
- background-color: #aaff9c;
- }
red.css:
- body{
- background-color: #ff0721;
- }
主題的定義,往往是一組樣式,因此我們一般都是在一個 properties 文件中將同一主題的樣式配置在一起,這樣方便后期加載。
所以接下來我們在 resources 目錄下新建 theme 目錄,然后在 theme 目錄中創建三個文件,內容如下:
blue.properties:
- index.body=/css/blue.css
green.properties:
- index.body=/css/green.css
red.properties:
- index.body=/css/red.css
在不同的 properties 配置文件中引入不同的樣式,但是樣式定義的 key 都是 index.body,這樣方便后期在頁面中引入。
接下來在 SpringMVC 容器中配置三個 Bean,如下:
- <mvc:interceptors>
- <mvc:interceptor>
- <mvc:mapping path="/**"/>
- <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor">
- <property name="paramName" value="theme"/>
- </bean>
- </mvc:interceptor>
- </mvc:interceptors>
- <bean id="themeSource" class="org.springframework.ui.context.support.ResourceBundleThemeSource">
- <property name="basenamePrefix" value="theme."/>
- </bean>
- <bean id="themeResolver" class="org.springframework.web.servlet.theme.SessionThemeResolver">
- <property name="defaultThemeName" value="blue"/>
- </bean>
首先配置攔截器 ThemeChangeInterceptor,這個攔截器用來解析主題參數,參數的 key 為 theme,例如請求地址是 /index?theme=blue,該攔截器就會自動設置系統主題為 blue。當然也可以不配置攔截器,如果不配置的話,就可以單獨提供一個修改主題的接口,然后手動修改主題,類似下面這樣:
- @Autowired
- private ThemeResolver themeResolver;
- @RequestMapping(path = "/01/{theme}",method = RequestMethod.GET)
- public String theme1(@PathVariable("theme") String themeStr, HttpServletRequest request, HttpServletResponse response){
- themeResolver.setThemeName(request,response, themeStr);
- return "redirect:/01";
- }
themeStr 就是新的主題名稱,將其配置給 themeResolver 即可。
接下來配置 ResourceBundleThemeSource,這個 Bean 主要是為了加載主題文件,需要配置一個 basenamePrefix 屬性,如果我們的主題文件放在文件夾中,這個 basenamePrefix 的值就是 文件夾名稱.。
接下來配置主題解析器,主題解析器有三種,分別是 CookieThemeResolver、FixedThemeResolver、SessionThemeResolver,這里我們使用的是 SessionThemeResolver,主題信息將被保存在 Session 中,只要 Session 不變,主題就一直有效。這三個主題解析器松哥會在下一小節中和大家仔細分析。
配置完成后,我們再來提供一個測試頁面,如下:
- <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
- <%@ page contentType="text/html;charset=UTF-8" language="java" %>
- <html>
- <head>
- <title>Title</title>
- <link rel="stylesheet" href="<spring:theme code="index.body" />" >
- </head>
- <body>
- <div>
- 一鍵切換主題:<br/>
- <a href="/index?theme=blue">托帕藍</a>
- <a href="/index?theme=red">多巴胺紅</a>
- <a href="/index?theme=green">石竹青</a>
- </div>
- <br/>
- </body>
- </html>
最關鍵的是:
- <link rel="stylesheet" href="<spring:theme code="index.body" />" >
css 樣式不直接寫,而是引用我們在 properties 文件中定義的 index.body,這樣將根據當前主題加載不同的 css 文件。
最后再提供一個處理器,如下:
- @GetMapping(path = "/index")
- public String getPage(){
- return "index";
- }
這個就很簡單了,沒啥好說的。
最后啟動項目進行測試,大家就可以看到我們文章一開始給出的圖片了,點擊不同的按鈕就可以實現背景的切換。
是不是非常 Easy!
2.原理分析
主題這塊涉及到的東西主要就是主題解析器,主題解析器和我們前面所說的國際化的解析器非常類似,但是比它更簡單,我們一起來分析下。
先來看下 ThemeResolver 接口:
- public interface ThemeResolver {
- String resolveThemeName(HttpServletRequest request);
- void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName);
- }
這個接口中就兩個方法:
- resolveThemeName:從當前請求中解析出主題的名字。
- setThemeName:設置當前主題。
ThemeResolver 主要有三個實現類,繼承關系如下:
接下來我們對這幾個實現類來逐個分析。
2.1 CookieThemeResolver
直接上源碼吧:
- @Override
- public String resolveThemeName(HttpServletRequest request) {
- String themeName = (String) request.getAttribute(THEME_REQUEST_ATTRIBUTE_NAME);
- if (themeName != null) {
- return themeName;
- }
- String cookieName = getCookieName();
- if (cookieName != null) {
- Cookie cookie = WebUtils.getCookie(request, cookieName);
- if (cookie != null) {
- String value = cookie.getValue();
- if (StringUtils.hasText(value)) {
- themeName = value;
- }
- }
- }
- if (themeName == null) {
- themeName = getDefaultThemeName();
- }
- request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName);
- return themeName;
- }
- @Override
- public void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) {
- if (StringUtils.hasText(themeName)) {
- request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName);
- addCookie(response, themeName);
- } else {
- request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, getDefaultThemeName());
- removeCookie(response);
- }
- }
先來看 resolveThemeName 方法:
- 首先會嘗試直接從請求中獲取主題名稱,如果獲取到了,就直接返回。
- 如果第一步沒有獲取到主題名稱,接下來就嘗試從 Cookie 中獲取主題名稱,Cookie 也是從當前請求中提取,利用 WebUtils 工具進行解析,如果解析到了主題名稱,就賦值給 themeName 變量。
- 如果前面沒有獲取到主題名稱,就使用默認的主題名稱,開發者可以自行配置默認的主題名稱,如果不配置,就是 theme。
- 將解析出來的 theme 保存到 request 中,以備后續使用。
再來看 setThemeName 方法:
- 如果存在 themeName 就進行設置,同時將 themeName 添加到 Cookie 中。
- 如果不存在 themeName,就設置一個默認的主題名,同時從 response 中移除 Cookie。
可以看到,整個實現思路還是非常簡單的。
2.2 AbstractThemeResolver
- public abstract class AbstractThemeResolver implements ThemeResolver {
- public static final String ORIGINAL_DEFAULT_THEME_NAME = "theme";
- private String defaultThemeName = ORIGINAL_DEFAULT_THEME_NAME;
- public void setDefaultThemeName(String defaultThemeName) {
- this.defaultThemeName = defaultThemeName;
- }
- public String getDefaultThemeName() {
- return this.defaultThemeName;
- }
- }
AbstractThemeResolver 主要提供了配置默認主題的能力。
2.3 FixedThemeResolver
- public class FixedThemeResolver extends AbstractThemeResolver {
- @Override
- public String resolveThemeName(HttpServletRequest request) {
- return getDefaultThemeName();
- }
- @Override
- public void setThemeName(
- HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) {
- throw new UnsupportedOperationException("Cannot change theme - use a different theme resolution strategy");
- }
- }
FixedThemeResolver 就是使用默認的主題名稱,并且不允許修改主題。
2.4 SessionThemeResolver
- public class SessionThemeResolver extends AbstractThemeResolver {
- public static final String THEME_SESSION_ATTRIBUTE_NAME = SessionThemeResolver.class.getName() + ".THEME";
- @Override
- public String resolveThemeName(HttpServletRequest request) {
- String themeName = (String) WebUtils.getSessionAttribute(request, THEME_SESSION_ATTRIBUTE_NAME);
- return (themeName != null ? themeName : getDefaultThemeName());
- }
- @Override
- public void setThemeName(
- HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) {
- WebUtils.setSessionAttribute(request, THEME_SESSION_ATTRIBUTE_NAME,
- (StringUtils.hasText(themeName) ? themeName : null));
- }
- }
resolveThemeName:從 session 中取出主題名稱并返回,如果 session 中的主題名稱為 null,就返回默認的主題名稱。
setThemeName:將主題配置到請求中。
不想多說,因為很簡單。
2.5 ThemeChangeInterceptor
最后我們再來看一看 ThemeChangeInterceptor 攔截器,這個攔截器會自動從請求中提取出主題參數,并設置到請求中,核心部分在 preHandle 方法中:
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws ServletException {
- String newTheme = request.getParameter(this.paramName);
- if (newTheme != null) {
- ThemeResolver themeResolver = RequestContextUtils.getThemeResolver(request);
- if (themeResolver == null) {
- throw new IllegalStateException("No ThemeResolver found: not in a DispatcherServlet request?");
- }
- themeResolver.setThemeName(request, response, newTheme);
- }
- return true;
- }
從請求中提取出 theme 參數,并設置到 themeResolver 中。
3.小結
好啦,這就是今天和小伙伴們分享的一鍵換膚!無論是功能性還是源碼,都和國際化非常類似,但是比國際化簡單很多,不知道小伙伴們有沒有 GET 到呢?
原文地址:https://mp.weixin.qq.com/s/epVIGcCAZdW3vS80XG_duQ