Spring Boot一键换肤,so easy!

时间:2022-01-16 09:16:35

Spring Boot一键换肤,so easy!

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

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

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

1.一键换肤

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

Spring Boot一键换肤,so easy!

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

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

blue.css:

  1. body{ 
  2.     background-color: #05e1ff; 

green.css:

  1. body{ 
  2.     background-color: #aaff9c; 

red.css:

  1. body{ 
  2.     background-color: #ff0721; 

主题的定义,往往是一组样式,因此我们一般都是在一个 properties 文件中将同一主题的样式配置在一起,这样方便后期加载。

所以接下来我们在 resources 目录下新建 theme 目录,然后在 theme 目录中创建三个文件,内容如下:

blue.properties:

  1. index.body=/css/blue.css 

green.properties:

  1. index.body=/css/green.css 

red.properties:

  1. index.body=/css/red.css 

在不同的 properties 配置文件中引入不同的样式,但是样式定义的 key 都是 index.body,这样方便后期在页面中引入。

接下来在 SpringMVC 容器中配置三个 Bean,如下:

  1. <mvc:interceptors> 
  2.     <mvc:interceptor> 
  3.         <mvc:mapping path="/**"/> 
  4.         <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"
  5.             <property name="paramName" value="theme"/> 
  6.         </bean> 
  7.     </mvc:interceptor> 
  8. </mvc:interceptors> 
  9. <bean id="themeSource" class="org.springframework.ui.context.support.ResourceBundleThemeSource"
  10.     <property name="basenamePrefix" value="theme."/> 
  11. </bean> 
  12. <bean id="themeResolver" class="org.springframework.web.servlet.theme.SessionThemeResolver"
  13.     <property name="defaultThemeName" value="blue"/> 
  14. </bean> 

 

 

 

 

 

首先配置拦截器 ThemeChangeInterceptor,这个拦截器用来解析主题参数,参数的 key 为 theme,例如请求地址是 /index?theme=blue,该拦截器就会自动设置系统主题为 blue。当然也可以不配置拦截器,如果不配置的话,就可以单独提供一个修改主题的接口,然后手动修改主题,类似下面这样:

  1. @Autowired 
  2. private ThemeResolver themeResolver; 
  3. @RequestMapping(path = "/01/{theme}",method = RequestMethod.GET) 
  4. public String theme1(@PathVariable("theme") String themeStr, HttpServletRequest request, HttpServletResponse response){ 
  5.     themeResolver.setThemeName(request,response, themeStr); 
  6.     return "redirect:/01"

themeStr 就是新的主题名称,将其配置给 themeResolver 即可。

接下来配置 ResourceBundleThemeSource,这个 Bean 主要是为了加载主题文件,需要配置一个 basenamePrefix 属性,如果我们的主题文件放在文件夹中,这个 basenamePrefix 的值就是 文件夹名称.。

接下来配置主题解析器,主题解析器有三种,分别是 CookieThemeResolver、FixedThemeResolver、SessionThemeResolver,这里我们使用的是 SessionThemeResolver,主题信息将被保存在 Session 中,只要 Session 不变,主题就一直有效。这三个主题解析器松哥会在下一小节中和大家仔细分析。

配置完成后,我们再来提供一个测试页面,如下:

  1. <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 
  2. <%@ page contentType="text/html;charset=UTF-8" language="java" %> 
  3. <html> 
  4. <head> 
  5.     <title>Title</title> 
  6.     <link rel="stylesheet" href="<spring:theme code="index.body" />" > 
  7. </head> 
  8. <body> 
  9. <div> 
  10.     一键切换主题:<br/> 
  11.     <a href="/index?theme=blue">托帕蓝</a> 
  12.     <a href="/index?theme=red">多巴胺红</a> 
  13.     <a href="/index?theme=green">石竹青</a> 
  14. </div> 
  15. <br/> 
  16. </body> 
  17. </html> 

最关键的是:

  1. <link rel="stylesheet" href="<spring:theme code="index.body" />" > 

css 样式不直接写,而是引用我们在 properties 文件中定义的 index.body,这样将根据当前主题加载不同的 css 文件。

最后再提供一个处理器,如下:

  1. @GetMapping(path = "/index"
  2. public  String getPage(){ 
  3.     return "index"

这个就很简单了,没啥好说的。

最后启动项目进行测试,大家就可以看到我们文章一开始给出的图片了,点击不同的按钮就可以实现背景的切换。

是不是非常 Easy!

2.原理分析

主题这块涉及到的东西主要就是主题解析器,主题解析器和我们前面所说的国际化的解析器非常类似,但是比它更简单,我们一起来分析下。

先来看下 ThemeResolver 接口:

  1. public interface ThemeResolver { 
  2.  String resolveThemeName(HttpServletRequest request); 
  3.  void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName); 

这个接口中就两个方法:

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

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

Spring Boot一键换肤,so easy!

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

2.1 CookieThemeResolver

直接上源码吧:

  1. @Override 
  2. public String resolveThemeName(HttpServletRequest request) { 
  3.  String themeName = (String) request.getAttribute(THEME_REQUEST_ATTRIBUTE_NAME); 
  4.  if (themeName != null) { 
  5.   return themeName; 
  6.  } 
  7.  String cookieName = getCookieName(); 
  8.  if (cookieName != null) { 
  9.   Cookie cookie = WebUtils.getCookie(request, cookieName); 
  10.   if (cookie != null) { 
  11.    String value = cookie.getValue(); 
  12.    if (StringUtils.hasText(value)) { 
  13.     themeName = value; 
  14.    } 
  15.   } 
  16.  } 
  17.  if (themeName == null) { 
  18.   themeName = getDefaultThemeName(); 
  19.  } 
  20.  request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName); 
  21.  return themeName; 
  22. @Override 
  23. public void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) { 
  24.  if (StringUtils.hasText(themeName)) { 
  25.   request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName); 
  26.   addCookie(response, themeName); 
  27.  } else { 
  28.   request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, getDefaultThemeName()); 
  29.   removeCookie(response); 
  30.  } 

先来看 resolveThemeName 方法:

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

再来看 setThemeName 方法:

  • 如果存在 themeName 就进行设置,同时将 themeName 添加到 Cookie 中。
  • 如果不存在 themeName,就设置一个默认的主题名,同时从 response 中移除 Cookie。

可以看到,整个实现思路还是非常简单的。

2.2 AbstractThemeResolver

  1. public abstract class AbstractThemeResolver implements ThemeResolver { 
  2.  public static final String ORIGINAL_DEFAULT_THEME_NAME = "theme"
  3.  private String defaultThemeName = ORIGINAL_DEFAULT_THEME_NAME; 
  4.  public void setDefaultThemeName(String defaultThemeName) { 
  5.   this.defaultThemeName = defaultThemeName; 
  6.  } 
  7.  public String getDefaultThemeName() { 
  8.   return this.defaultThemeName; 
  9.  } 

AbstractThemeResolver 主要提供了配置默认主题的能力。

2.3 FixedThemeResolver

  1. public class FixedThemeResolver extends AbstractThemeResolver { 
  2.  
  3.  @Override 
  4.  public String resolveThemeName(HttpServletRequest request) { 
  5.   return getDefaultThemeName(); 
  6.  } 
  7.  
  8.  @Override 
  9.  public void setThemeName( 
  10.    HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) { 
  11.  
  12.   throw new UnsupportedOperationException("Cannot change theme - use a different theme resolution strategy"); 
  13.  } 
  14.  

FixedThemeResolver 就是使用默认的主题名称,并且不允许修改主题。

2.4 SessionThemeResolver

  1. public class SessionThemeResolver extends AbstractThemeResolver { 
  2.  public static final String THEME_SESSION_ATTRIBUTE_NAME = SessionThemeResolver.class.getName() + ".THEME"
  3.  @Override 
  4.  public String resolveThemeName(HttpServletRequest request) { 
  5.   String themeName = (String) WebUtils.getSessionAttribute(request, THEME_SESSION_ATTRIBUTE_NAME); 
  6.   return (themeName != null ? themeName : getDefaultThemeName()); 
  7.  } 
  8.  @Override 
  9.  public void setThemeName( 
  10.    HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) { 
  11.   WebUtils.setSessionAttribute(request, THEME_SESSION_ATTRIBUTE_NAME, 
  12.     (StringUtils.hasText(themeName) ? themeName : null)); 
  13.  } 

resolveThemeName:从 session 中取出主题名称并返回,如果 session 中的主题名称为 null,就返回默认的主题名称。

setThemeName:将主题配置到请求中。

不想多说,因为很简单。

2.5 ThemeChangeInterceptor

最后我们再来看一看 ThemeChangeInterceptor 拦截器,这个拦截器会自动从请求中提取出主题参数,并设置到请求中,核心部分在 preHandle 方法中:

  1. @Override 
  2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
  3.   throws ServletException { 
  4.  String newTheme = request.getParameter(this.paramName); 
  5.  if (newTheme != null) { 
  6.   ThemeResolver themeResolver = RequestContextUtils.getThemeResolver(request); 
  7.   if (themeResolver == null) { 
  8.    throw new IllegalStateException("No ThemeResolver found: not in a DispatcherServlet request?"); 
  9.   } 
  10.   themeResolver.setThemeName(request, response, newTheme); 
  11.  } 
  12.  return true

从请求中提取出 theme 参数,并设置到 themeResolver 中。

3.小结

好啦,这就是今天和小伙伴们分享的一键换肤!无论是功能性还是源码,都和国际化非常类似,但是比国际化简单很多,不知道小伙伴们有没有 GET 到呢?

原文地址:https://mp.weixin.qq.com/s/epVIGcCAZdW3vS80XG_duQ