1. 概述

Web 应用的外观和体验(即 主题)直接影响用户体验、可访问性,甚至品牌形象的建立。

本文将系统讲解如何在 Spring MVC 应用中配置和使用主题功能,实现灵活的界面风格切换。✅

2. 使用场景

主题本质上是一组静态资源(如 CSS 和图片),用于控制页面的整体视觉风格。常见用途包括:

  • 统一风格(Fixed Theme):全站使用固定主题,保证品牌一致性
  • 多租户定制(Branding Theme):SaaS 场景下,不同客户可拥有独立 UI 风格
  • 无障碍优化(Usability Theme):提供暗色模式、高对比度等主题,提升特殊用户群体的可访问性

这些需求在实际项目中非常普遍,尤其在中后台系统或平台型产品中,主题机制是标配功能。

3. Maven 依赖

首先引入核心依赖。本文示例基于 Spring WebMVC 和 JSP 技术栈。

Spring WebMVC 核心

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

JSP 相关依赖

由于使用 JSP 作为视图层,需添加以下依赖:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
     <groupId>javax.servlet.jsp</groupId>
     <artifactId>javax.servlet.jsp-api</artifactId>
     <version>2.3.3</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

⚠️ 注意:若使用 Thymeleaf 等现代模板引擎,可忽略 JSP 相关依赖。

4. Spring 主题配置

4.1 主题属性文件

定义两个主题:lightdark,分别对应亮色和暗色模式。

创建 light.properties

styleSheet=themes/white.css
background=white

创建 dark.properties

styleSheet=themes/black.css
background=black

📌 说明:

  • styleSheet 对应 CSS 文件路径
  • background 对应 HTML 的 bgcolor 属性值
  • 所有 .properties 文件需放在 classpath:/themes/ 目录下

4.2 ResourceHandler 配置

静态资源需通过 ResourceHandler 暴露,否则无法访问。

@Override 
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/themes/**")
            .addResourceLocations("classpath:/themes/");
}

这样 /themes/black.css 请求就能正确映射到类路径下的资源。

4.3 ThemeSource 配置

使用 ResourceBundleThemeSource 加载主题属性文件:

@Bean
public ResourceBundleThemeSource resourceBundleThemeSource() {
    return new ResourceBundleThemeSource();
}

它会自动加载 classpath 下的 light.propertiesdark.properties

4.4 ThemeResolver 配置

ThemeResolver 负责决定当前使用哪个主题。

使用 CookieThemeResolver(推荐入门)

@Bean
public ThemeResolver themeResolver() {
    CookieThemeResolver resolver = new CookieThemeResolver();
    resolver.setDefaultThemeName("light");
    return resolver;
}

✅ 优点:用户切换后持久化到 Cookie,刷新不丢失
❌ 缺点:换设备不生效

其他内置实现

  • FixedThemeResolver:固定主题,不可切换
  • SessionThemeResolver:主题仅在当前会话有效,退出即重置

4.5 视图层集成(JSP)

在 JSP 中使用 Spring 标签库读取主题属性。

引入标签库

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

使用主题属性

<link rel="stylesheet" href="<spring:theme code='styleSheet'/>"/>
<body bgcolor="<spring:theme code='background'/>">

完整 index.jsp 示例

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" href="<spring:theme code='styleSheet'/>"/>
        <title>Themed Application</title>
    </head>
    <body>
        <header>
            <h1>Themed Application</h1>
            <hr />
        </header>
        <section>
            <h2>Spring MVC Theme Demo</h2>
            <form action="<c:url value='/'/>" method="POST" name="themeChangeForm">
                <div><h4>Change Theme</h4></div>
                <select id="theme" name="theme" onChange="submitForm()">
                    <option value="">Reset</option>
                    <option value="light">Light</option>
                    <option value="dark">Dark</option>
                </select>
            </form>
        </section>
        <script type="text/javascript">
            function submitForm() {
                document.themeChangeForm.submit();
            }
        </script>
    </body>
</html>

此时访问页面默认显示亮色主题。

4.6 ThemeChangeInterceptor

为了让用户能切换主题,需配置 ThemeChangeInterceptor 拦截主题变更请求。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(themeChangeInterceptor());
}

@Bean
public ThemeChangeInterceptor themeChangeInterceptor() {
    ThemeChangeInterceptor interceptor = new ThemeChangeInterceptor();
    interceptor.setParamName("theme"); // 监听名为 theme 的请求参数
    return interceptor;
}

📌 工作流程:

  1. 用户选择下拉框 → 提交表单
  2. 请求携带 theme=dark
  3. ThemeChangeInterceptor 捕获参数,调用 ThemeResolver.setThemeName() 更新主题
  4. 下次请求读取新主题

5. 进阶依赖(数据库持久化)

若需将主题偏好保存到数据库(如用户换设备也能同步),需引入以下依赖:

Spring Security(用户身份识别)

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

数据持久化(JPA + HSQLDB)

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.9.Final</version>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.5.0</version>
</dependency>

6. 自定义 ThemeResolver

实现 ThemeResolver 接口,将用户偏好存入数据库。

用户偏好实体

@Entity
@Table(name = "preferences")
public class UserPreference {
    @Id
    private String username;

    private String theme;
    
    // getter & setter
}

自定义解析器:UserPreferenceThemeResolver

public class UserPreferenceThemeResolver implements ThemeResolver {

    @Autowired
    private UserPreferenceRepository userPreferenceRepository;

    private String defaultThemeName = "light";

    @Override
    public String resolveThemeName(HttpServletRequest request) {
        String themeName = findThemeFromRequest(request)
            .orElse(findUserPreferredTheme().orElse(getDefaultThemeName()));
        request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName);
        return themeName;
    }

    @Override
    public void setThemeName(HttpServletRequest request, HttpServletResponse response, String theme) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (isAuthenticated(authentication)) {
            request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, theme);
            User user = (User) authentication.getPrincipal();
            UserPreference pref = getUserPreference(authentication).orElse(new UserPreference());
            pref.setUsername(user.getUsername());
            pref.setTheme(StringUtils.hasText(theme) ? theme : null);
            userPreferenceRepository.save(pref);
        }
    }

    private Optional<String> findThemeFromRequest(HttpServletRequest request) {
        return Optional.ofNullable((String) request.getAttribute(THEME_REQUEST_ATTRIBUTE_NAME));
    }

    private Optional<String> findUserPreferredTheme() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return getUserPreference(auth).map(UserPreference::getTheme);
    }

    private Optional<UserPreference> getUserPreference(Authentication auth) {
        if (isAuthenticated(auth)) {
            String username = ((User) auth.getPrincipal()).getUsername();
            return userPreferenceRepository.findById(username);
        }
        return Optional.empty();
    }

    private boolean isAuthenticated(Authentication authentication) {
        return authentication != null && authentication.isAuthenticated() &&
               authentication.getPrincipal() instanceof User;
    }

    public void setDefaultThemeName(String defaultThemeName) {
        this.defaultThemeName = defaultThemeName;
    }

    public String getDefaultThemeName() {
        return defaultThemeName;
    }
}

替换默认 ThemeResolver

@Bean 
public ThemeResolver themeResolver() { 
    UserPreferenceThemeResolver resolver = new UserPreferenceThemeResolver();
    resolver.setDefaultThemeName("light");
    return resolver;
}

优势:用户主题偏好持久化,跨设备同步
⚠️ 注意:需确保 SecurityContext 可获取当前用户

7. 总结

Spring MVC 的主题机制通过 ThemeSource + ThemeResolver + ThemeChangeInterceptor 三者协作,实现灵活的主题切换。

核心要点:

  • ✅ 主题属性文件命名规则:{themeName}.properties
  • ✅ 使用 <spring:theme code='xxx'/> 在 JSP 中读取主题值
  • CookieThemeResolver 适合简单场景,自定义 ThemeResolver 可对接数据库
  • ThemeChangeInterceptor 是用户主动切换的关键

完整代码示例可参考:GitHub - spring-mvc-views

📌 踩坑提醒

  • 确保 ResourceHandler 正确映射静态资源路径
  • 若使用自定义 ThemeResolver,务必实现 setThemeName 逻辑
  • JSP 标签库导入不可遗漏,否则 <spring:theme> 会报错

原始标题:Spring MVC Themes

« 上一篇: Apache Tapestry 介绍