Skip to content

Spring MVC 高级特性

1. 视图技术

1.1 Thymeleaf 集成

添加依赖

xml
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring6</artifactId>
    <version>3.1.0.RELEASE</version>
</dependency>

配置视图解析器

java
@Configuration
public class ThymeleafConfig {
    
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
    
    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        engine.setEnableSpringELCompiler(true);
        return engine;
    }
    
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
}

Thymeleaf 模板示例

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>用户列表</title>
</head>
<body>
    <h1>用户列表</h1>
    
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>用户名</th>
                <th>邮箱</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="user : ${users}">
                <td th:text="${user.id}">1</td>
                <td th:text="${user.username}">张三</td>
                <td th:text="${user.email}">zhangsan@example.com</td>
            </tr>
        </tbody>
    </table>
    
    <a th:href="@{/users/add}">添加用户</a>
    <a th:href="@{/users/{id}(id=${user.id})}">查看详情</a>
</body>
</html>

1.2 常用 Thymeleaf 语法

html
<!-- 文本显示 -->
<span th:text="${user.name}">默认值</span>
<span th:utext="${htmlContent}">HTML内容</span>

<!-- 条件判断 -->
<div th:if="${user != null}">
    欢迎您,<span th:text="${user.name}"></span>
</div>
<div th:unless="${user == null}">
    请先登录
</div>

<!-- 循环 -->
<tr th:each="item, stat : ${items}">
    <td th:text="${stat.index}">0</td>
    <td th:text="${item.name}">名称</td>
</tr>

<!-- 链接 -->
<a th:href="@{/users}">用户列表</a>
<a th:href="@{/users/{id}(id=${user.id})}">用户详情</a>
<a th:href="@{/search(keyword=${keyword})}">搜索</a>

<!-- 表单 -->
<form th:action="@{/users}" th:object="${user}" method="post">
    <input type="text" th:field="*{username}" />
    <input type="email" th:field="*{email}" />
    <button type="submit">提交</button>
</form>

<!-- 日期格式化 -->
<span th:text="${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01</span>

<!-- 数字格式化 -->
<span th:text="${#numbers.formatDecimal(price, 1, 2)}">99.99</span>

<!-- 包含片段 -->
<div th:replace="~{fragments/header :: header}"></div>
<div th:insert="~{fragments/footer :: footer}"></div>

1.3 片段定义与复用

html
<!-- fragments/header.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
    <meta charset="UTF-8">
    <title th:text="${title}">默认标题</title>
</head>
<body>
    <nav th:fragment="header">
        <a href="/">首页</a>
        <a href="/users">用户</a>
    </nav>
</body>
</html>

<!-- 使用片段 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/header :: head('用户管理')}"></head>
<body>
    <nav th:replace="~{fragments/header :: header}"></nav>
    
    <!-- 页面内容 -->
    
    <footer th:insert="~{fragments/footer :: footer}"></footer>
</body>
</html>

2. 内容协商

2.1 基于扩展名

java
@GetMapping(value = "/users/{id}", produces = {
    MediaType.APPLICATION_JSON_VALUE,
    MediaType.APPLICATION_XML_VALUE
})
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

2.2 基于 Accept 头

java
@GetMapping(value = "/users")
public ResponseEntity<List<User>> getUsers() {
    return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(userService.findAll());
}

2.3 配置内容协商

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true)
            .parameterName("format")
            .ignoreAcceptHeader(false)
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML);
    }
}

3. 跨域处理

3.1 @CrossOrigin 注解

java
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000", maxAge = 3600)
public class ApiController {
    
    @GetMapping("/users")
    @CrossOrigin(origins = "*")
    public List<User> getUsers() {
        return userService.findAll();
    }
}

3.2 全局配置

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000", "http://example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

3.3 CorsFilter

java
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        
        return new CorsFilter(source);
    }
}

4. 国际化

4.1 配置国际化

java
@Configuration
public class I18nConfig {
    
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasenames("i18n/messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }
    
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return resolver;
    }
    
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }
}

4.2 消息资源文件

properties
# messages_zh_CN.properties
user.welcome=欢迎,{0}!
user.login=登录
user.logout=退出
user.profile=个人中心

# messages_en_US.properties
user.welcome=Welcome, {0}!
user.login=Login
user.logout=Logout
user.profile=Profile

4.3 使用国际化消息

java
@Controller
public class UserController {
    
    @Autowired
    private MessageSource messageSource;
    
    @GetMapping("/welcome")
    public String welcome(Locale locale, Model model) {
        String message = messageSource.getMessage(
            "user.welcome", 
            new Object[]{"张三"}, 
            locale
        );
        model.addAttribute("message", message);
        return "welcome";
    }
}
html
<!-- Thymeleaf 中使用 -->
<h1 th:text="#{user.welcome(${user.name})}">欢迎!</h1>
<button th:text="#{user.login}">登录</button>

5. 类型转换

5.1 自定义类型转换器

java
public class StringToDateConverter implements Converter<String, Date> {
    
    private static final DateTimeFormatter FORMATTER = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd");
    
    @Override
    public Date convert(String source) {
        LocalDate localDate = LocalDate.parse(source, FORMATTER);
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
}

public class StringToEnumConverter implements Converter<String, UserStatus> {
    
    @Override
    public UserStatus convert(String source) {
        return UserStatus.valueOf(source.toUpperCase());
    }
}

5.2 注册转换器

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToDateConverter());
        registry.addConverter(new StringToEnumConverter());
    }
}

5.3 自定义格式化器

java
public class DateFormatter implements Formatter<Date> {
    
    private static final DateTimeFormatter FORMATTER = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    @Override
    public String print(Date date, Locale locale) {
        return date.toInstant()
                   .atZone(ZoneId.systemDefault())
                   .format(FORMATTER);
    }
    
    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        LocalDate localDate = LocalDate.parse(text, FORMATTER);
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
}

6. 异步处理

6.1 Callable 返回

java
@GetMapping("/async")
public Callable<String> asyncRequest() {
    return () -> {
        Thread.sleep(2000);
        return "async-result";
    };
}

6.2 DeferredResult

java
@GetMapping("/deferred")
public DeferredResult<String> deferredRequest() {
    DeferredResult<String> result = new DeferredResult<>(5000L, "timeout");
    
    CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(2000);
            result.setResult("deferred-result");
        } catch (InterruptedException e) {
            result.setErrorResult(e);
        }
    });
    
    return result;
}

6.3 ResponseBodyEmitter

java
@GetMapping("/events")
public ResponseBodyEmitter handleEvents() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter(60000L);
    
    CompletableFuture.runAsync(() -> {
        try {
            for (int i = 0; i < 10; i++) {
                emitter.send("Event " + i + "\n");
                Thread.sleep(1000);
            }
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    
    return emitter;
}

6.4 Server-Sent Events

java
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleSse() {
    SseEmitter emitter = new SseEmitter(60000L);
    
    CompletableFuture.runAsync(() -> {
        try {
            for (int i = 0; i < 10; i++) {
                SseEmitter.SseEventBuilder event = SseEmitter.event()
                    .data("Event data " + i)
                    .id(String.valueOf(i))
                    .name("message");
                emitter.send(event);
                Thread.sleep(1000);
            }
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    
    return emitter;
}

7. WebSocket

7.1 添加依赖

xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>7.0.0</version>
</dependency>

7.2 WebSocket 配置

java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler(), "/chat")
                .setAllowedOrigins("*");
    }
    
    @Bean
    public ChatHandler chatHandler() {
        return new ChatHandler();
    }
}

7.3 WebSocket 处理器

java
public class ChatHandler extends TextWebSocketHandler {
    
    private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        broadcast("用户加入: " + session.getId());
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        broadcast("用户 " + session.getId() + ": " + message.getPayload());
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        broadcast("用户离开: " + session.getId());
    }
    
    private void broadcast(String message) throws IOException {
        for (WebSocketSession session : sessions) {
            session.sendMessage(new TextMessage(message));
        }
    }
}

7.4 STOMP 协议

java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
}
java
@Controller
public class ChatController {
    
    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public ChatMessage send(ChatMessage message) {
        return new ChatMessage(
            message.sender(),
            message.content(),
            LocalDateTime.now()
        );
    }
    
    @MessageMapping("/private")
    @SendToUser("/queue/messages")
    public ChatMessage sendPrivate(ChatMessage message, Principal principal) {
        return new ChatMessage(
            principal.getName(),
            message.content(),
            LocalDateTime.now()
        );
    }
}

public record ChatMessage(
    String sender,
    String content,
    LocalDateTime timestamp
) {}

8. 过滤器

8.1 定义过滤器

java
@Component
public class RequestLoggingFilter implements Filter {
    
    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        logger.info("Request: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());
        
        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        long duration = System.currentTimeMillis() - startTime;
        
        logger.info("Response: {} ms", duration);
    }
}

8.2 过滤器注册

java
@Configuration
public class FilterConfig {
    
    @Bean
    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
        FilterRegistrationBean<RequestLoggingFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new RequestLoggingFilter());
        registration.addUrlPatterns("/api/*");
        registration.setOrder(1);
        return registration;
    }
}

9. 请求限流

9.1 基于 Guava RateLimiter

xml
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>
java
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    
    private final RateLimiter rateLimiter = RateLimiter.create(10.0);
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response,
                            Object handler) throws Exception {
        if (!rateLimiter.tryAcquire()) {
            response.setStatus(429);
            response.getWriter().write("请求过于频繁,请稍后再试");
            return false;
        }
        return true;
    }
}

9.2 基于 IP 限流

java
@Component
public class IpRateLimitInterceptor implements HandlerInterceptor {
    
    private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response,
                            Object handler) throws Exception {
        String ip = getClientIp(request);
        RateLimiter limiter = limiters.computeIfAbsent(ip, k -> RateLimiter.create(5.0));
        
        if (!limiter.tryAcquire()) {
            response.setStatus(429);
            response.getWriter().write("请求过于频繁");
            return false;
        }
        return true;
    }
    
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip.split(",")[0].trim();
    }
}

10. 小结

本章学习了 Spring MVC 的高级特性:

特性应用场景
Thymeleaf服务端渲染,模板引擎
内容协商多格式响应,JSON/XML
跨域处理前后端分离,API 调用
国际化多语言支持
类型转换参数解析,格式化
异步处理长时间请求,实时推送
WebSocket实时通信,聊天室
过滤器请求日志,编码处理
请求限流防止滥用,保护服务

下一章将学习 Spring Boot 4 的快速入门。