Spring Boot一键换肤,so easy!

SpringMVC 源码分析系列最后一篇,和大家聊一聊 Theme。

Theme,就是主题,点一下就给网站更换一个主题,相信大家都用过类似功能,这个其实和前面所说的国际化功能很像,代码其实也很像,今天我们就来捋一捋。

考虑到有的小伙伴可能还没用过 Theme,所以这里松哥先来说下用法,然后我们再进行源码分析。

1.一键换肤

来做一个简单的需求,假设我的页面上有三个按钮,点击之后就能一键换肤,像下面这样:

image

我们来看下这个需求怎么实现。

首先三个按钮分别对应了三个不同的样式,我们先把这三个不同的样式定义出来,分别如下:

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>
  1. 首先配置拦截器 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 即可。

  1. 接下来配置 ResourceBundleThemeSource,这个 Bean 主要是为了加载主题文件,需要配置一个 basenamePrefix 属性,如果我们的主题文件放在文件夹中,这个 basenamePrefix 的值就是 文件夹名称.
  2. 接下来配置主题解析器,主题解析器有三种,分别是 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);
}

这个接口中就两个方法:

  1. resolveThemeName:从当前请求中解析出主题的名字。
  2. setThemeName:设置当前主题。

ThemeResolver 主要有三个实现类,继承关系如下:

image

接下来我们对这几个实现类来逐个分析。

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 方法:

  1. 首先会尝试直接从请求中获取主题名称,如果获取到了,就直接返回。
  2. 如果第一步没有获取到主题名称,接下来就尝试从 Cookie 中获取主题名称,Cookie 也是从当前请求中提取,利用 WebUtils 工具进行解析,如果解析到了主题名称,就赋值给 themeName 变量。
  3. 如果前面没有获取到主题名称,就使用默认的主题名称,开发者可以自行配置默认的主题名称,如果不配置,就是 theme。
  4. 将解析出来的 theme 保存到 request 中,以备后续使用。

再来看 setThemeName 方法:

  1. 如果存在 themeName 就进行设置,同时将 themeName 添加到 Cookie 中。
  2. 如果不存在 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 到呢?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容