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=Profile4.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 的快速入门。