Spring Boot一鍵換膚,so easy!
松哥原創(chuàng)的 Spring Boot 視頻教程已經(jīng)殺青,感興趣的小伙伴戳這里-->Spring Boot+Vue+微人事視頻教程
SpringMVC 源碼分析系列最后一篇,和大家聊一聊 Theme。
Theme,就是主題,點(diǎn)一下就給網(wǎng)站更換一個(gè)主題,相信大家都用過(guò)類(lèi)似功能,這個(gè)其實(shí)和前面所說(shuō)的國(guó)際化功能很像,代碼其實(shí)也很像,今天我們就來(lái)捋一捋。
考慮到有的小伙伴可能還沒(méi)用過(guò) Theme,所以這里松哥先來(lái)說(shuō)下用法,然后我們?cè)龠M(jìn)行源碼分析。
1.一鍵換膚
來(lái)做一個(gè)簡(jiǎn)單的需求,假設(shè)我的頁(yè)面上有三個(gè)按鈕,點(diǎn)擊之后就能一鍵換膚,像下面這樣:

我們來(lái)看下這個(gè)需求怎么實(shí)現(xiàn)。
首先三個(gè)按鈕分別對(duì)應(yīng)了三個(gè)不同的樣式,我們先把這三個(gè)不同的樣式定義出來(lái),分別如下:
blue.css:
body{
background-color: #05e1ff;
}
green.css:
body{
background-color: #aaff9c;
}
red.css:
body{
background-color: #ff0721;
}
主題的定義,往往是一組樣式,因此我們一般都是在一個(gè) properties 文件中將同一主題的樣式配置在一起,這樣方便后期加載。
所以接下來(lái)我們?cè)?resources 目錄下新建 theme 目錄,然后在 theme 目錄中創(chuàng)建三個(gè)文件,內(nèi)容如下:
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,這樣方便后期在頁(yè)面中引入。
接下來(lái)在 SpringMVC 容器中配置三個(gè) 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,這個(gè)攔截器用來(lái)解析主題參數(shù),參數(shù)的 key 為 theme,例如請(qǐng)求地址是 /index?theme=blue,該攔截器就會(huì)自動(dòng)設(shè)置系統(tǒng)主題為 blue。當(dāng)然也可以不配置攔截器,如果不配置的話,就可以單獨(dú)提供一個(gè)修改主題的接口,然后手動(dòng)修改主題,類(lèi)似下面這樣:
@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 就是新的主題名稱(chēng),將其配置給 themeResolver 即可。
接下來(lái)配置 ResourceBundleThemeSource,這個(gè) Bean 主要是為了加載主題文件,需要配置一個(gè) basenamePrefix 屬性,如果我們的主題文件放在文件夾中,這個(gè) basenamePrefix 的值就是 文件夾名稱(chēng).。接下來(lái)配置主題解析器,主題解析器有三種,分別是 CookieThemeResolver、FixedThemeResolver、SessionThemeResolver,這里我們使用的是 SessionThemeResolver,主題信息將被保存在 Session 中,只要 Session 不變,主題就一直有效。這三個(gè)主題解析器松哥會(huì)在下一小節(jié)中和大家仔細(xì)分析。
配置完成后,我們?cè)賮?lái)提供一個(gè)測(cè)試頁(yè)面,如下:
<%@ 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">托帕藍(lán)</a>
<a href="/index?theme=red">多巴胺紅</a>
<a href="/index?theme=green">石竹青</a>
</div>
<br/>
</body>
</html>
最關(guān)鍵的是:
<link rel="stylesheet" href="<spring:theme code="index.body" />" >
css 樣式不直接寫(xiě),而是引用我們?cè)?properties 文件中定義的 index.body,這樣將根據(jù)當(dāng)前主題加載不同的 css 文件。
最后再提供一個(gè)處理器,如下:
@GetMapping(path = "/index")
public String getPage(){
return "index";
}
這個(gè)就很簡(jiǎn)單了,沒(méi)啥好說(shuō)的。
最后啟動(dòng)項(xiàng)目進(jìn)行測(cè)試,大家就可以看到我們文章一開(kāi)始給出的圖片了,點(diǎn)擊不同的按鈕就可以實(shí)現(xiàn)背景的切換。
是不是非常 Easy!
2.原理分析
主題這塊涉及到的東西主要就是主題解析器,主題解析器和我們前面所說(shuō)的國(guó)際化的解析器非常類(lèi)似,但是比它更簡(jiǎn)單,我們一起來(lái)分析下。
先來(lái)看下 ThemeResolver 接口:
public interface ThemeResolver {
String resolveThemeName(HttpServletRequest request);
void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName);
}
這個(gè)接口中就兩個(gè)方法:
resolveThemeName:從當(dāng)前請(qǐng)求中解析出主題的名字。 setThemeName:設(shè)置當(dāng)前主題。
ThemeResolver 主要有三個(gè)實(shí)現(xiàn)類(lèi),繼承關(guān)系如下:

接下來(lái)我們對(duì)這幾個(gè)實(shí)現(xiàn)類(lèi)來(lái)逐個(gè)分析。
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);
}
}
先來(lái)看 resolveThemeName 方法:
首先會(huì)嘗試直接從請(qǐng)求中獲取主題名稱(chēng),如果獲取到了,就直接返回。 如果第一步?jīng)]有獲取到主題名稱(chēng),接下來(lái)就嘗試從 Cookie 中獲取主題名稱(chēng),Cookie 也是從當(dāng)前請(qǐng)求中提取,利用 WebUtils 工具進(jìn)行解析,如果解析到了主題名稱(chēng),就賦值給 themeName 變量。 如果前面沒(méi)有獲取到主題名稱(chēng),就使用默認(rèn)的主題名稱(chēng),開(kāi)發(fā)者可以自行配置默認(rèn)的主題名稱(chēng),如果不配置,就是 theme。 將解析出來(lái)的 theme 保存到 request 中,以備后續(xù)使用。
再來(lái)看 setThemeName 方法:
如果存在 themeName 就進(jìn)行設(shè)置,同時(shí)將 themeName 添加到 Cookie 中。 如果不存在 themeName,就設(shè)置一個(gè)默認(rèn)的主題名,同時(shí)從 response 中移除 Cookie。
可以看到,整個(gè)實(shí)現(xiàn)思路還是非常簡(jiǎn)單的。
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 主要提供了配置默認(rèn)主題的能力。
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 就是使用默認(rèn)的主題名稱(chēng),并且不允許修改主題。
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 中取出主題名稱(chēng)并返回,如果 session 中的主題名稱(chēng)為 null,就返回默認(rèn)的主題名稱(chēng)。 setThemeName:將主題配置到請(qǐng)求中。
不想多說(shuō),因?yàn)楹芎?jiǎn)單。
2.5 ThemeChangeInterceptor
最后我們?cè)賮?lái)看一看 ThemeChangeInterceptor 攔截器,這個(gè)攔截器會(huì)自動(dòng)從請(qǐng)求中提取出主題參數(shù),并設(shè)置到請(qǐng)求中,核心部分在 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;
}
從請(qǐng)求中提取出 theme 參數(shù),并設(shè)置到 themeResolver 中。
3.小結(jié)
好啦,這就是今天和小伙伴們分享的一鍵換膚!無(wú)論是功能性還是源碼,都和國(guó)際化非常類(lèi)似,但是比國(guó)際化簡(jiǎn)單很多,不知道小伙伴們有沒(méi)有 GET 到呢?
