Theme, click to change a theme for the site, I believe we have used a similar function, this is actually very similar to the internationalization function, the code is actually very similar, today we will run through it.

Considering that some of you may not have used Theme yet, let’s talk about the usage here first, and then we will do the source code analysis.

1. One click to switch themes

Let’s do a simple requirement, suppose I have three buttons on the page, after clicking on them, I can switch themes with one click, like the following.

image

blue.css:

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

green.css:

1
2
3
body{
    background-color: #aaff9c;
}

red.css:

1
2
3
body{
    background-color: #ff0721;
}

The definition of a theme is often a set of styles, so we generally configure the styles of the same theme together in a properties file, which makes it easier to load later.

So next we create a new theme directory in the resources directory, and then create three files in the theme directory with the following contents.

blue.properties:

1
index.body=/css/blue.css

green.properties:

1
index.body=/css/green.css

red.properties:

1
index.body=/css/red.css

Introduce different styles in different properties configuration files, but the key of the style definition is index.body, so that it is convenient to introduce in the page later.

Next, configure the three beans in the SpringMVC container as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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. First configure the interceptor ThemeChangeInterceptor, this interceptor is used to parse the theme parameter, the key of the parameter is theme, for example, the request address is /index?theme=blue, the interceptor will automatically set the system theme to blue. Provide an interface to modify the theme, and then manually modify the theme, similar to the following.

    1
    2
    3
    4
    5
    6
    7
    
    @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 is the new theme name, just configure it to themeResolver.

  2. Next configure ResourceBundleThemeSource, this bean is mainly for loading theme files, you need to configure a basenamePrefix property, if our theme files are placed in a folder, the value of this basenamePrefix is folderName.

  3. There are three kinds of theme resolvers, namely CookieThemeResolver, FixedThemeResolver, SessionThemeResolver, here we use SessionThemeResolver, the theme information will be saved in Session As long as the Session does not change, the theme will always be valid.These three theme resolvers will be carefully analyzed in the next subsection.

Once the configuration is complete, we will then provide a test page, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<%@ 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>

The most critical point is.

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

The css styles are not written directly, but refer to the index.body we defined in the properties file, so that a different css file will be loaded depending on the current theme.

Finally a handler is provided, as follows.

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

This one is very simple, nothing more to say.

Finally start the project for testing, you can see the picture given at the beginning of our article, click on the different buttons to achieve the background switch.

Is it very easy!

2. Principle analysis

The main thing involved in this piece of theme is the theme resolver, which is very similar to the internationalized resolver we mentioned earlier, but simpler than it, so let’s analyze it together.

First, let’s look at the ThemeResolver interface.

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

There are just two methods in this interface.

  1. resolveThemeName: resolve the name of the theme from the current request.
  2. setThemeName: Set the current theme.

ThemeResolver has three main implementation classes with the following inheritance relationships.

ThemeResolver

Let’s analyze these implementation classes one by one.

2.1 CookieThemeResolver

Let’s go straight to the source code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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);
	}
}

Let’s start with the resolveThemeName method.

  1. First it tries to get the theme name directly from the request, if it gets it, it returns it directly.
  2. If the theme name is not retrieved in the first step, then it will try to get the theme name from the cookie, which is also extracted from the current request and parsed using WebUtils tool.
  3. If the theme name is not retrieved, the default theme name is used, and the developer can configure the default theme name, if not, it is theme.
  4. Save the parsed theme to the request for subsequent use.

Let’s look at the setThemeName method.

  1. If themeName exists, set it, and add themeName to the cookie.
  2. If themeName does not exist, set a default theme name and remove the cookie from the response.

As you can see, the whole idea is very simple.

2.2 AbstractThemeResolver

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 mainly provides the ability to configure the default theme.

2.3 FixedThemeResolver

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 just uses the default theme name and doesn’t allow to modify the theme.

2.4 SessionThemeResolver

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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: retrieve the theme name from the session and return it, or return the default theme name if the theme name in the session is null.
  • setThemeName: configure the theme into the request.

Don’t want to say more because it’s simple.

2.5 ThemeChangeInterceptor

Finally, let’s take a look at the ThemeChangeInterceptor interceptor, which automatically extracts the theme parameters from the request and sets them to the request, with the core part in the preHandle method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@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;
}

Extracts the theme parameter from the request and sets it to the themeResolver.

Reference https://juejin.cn/post/6976499960065818655