【SSM 框架 | day27 spring MVC】
一、SSM 整合:从配置到底层协同原理(深化代码 + 底层剖析)
SSM 整合的核心是 Spring IoC 容器对三层组件的统一管控,其底层依赖组件扫描、动态代理、切面织入等机制。以下结合核心代码拆解原理,并补充关键避坑点。
1. 组件扫描与 Bean 生命周期管控(代码深化 + 避坑)
(1)Bean 重复加载的底层规避(代码细化 + 调试技巧)
原文的配置类可进一步补充注解解析逻辑,并添加调试手段,同时点明易踩的扫描范围坑。
java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
/*** Spring 核心配置类* 避坑点:扫描范围若写"com"会导致第三方包被误扫描,建议精确到项目业务包*/
@Configuration
// 精确扫描业务根包,排除Controller避免与SpringMVC重复加载
@ComponentScan(value = "com.itheima",excludeFilters = {// 排除Controller注解@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class),// 可选:排除测试类等非核心组件@ComponentScan.Filter(type = FilterType.REGEX,pattern = "com.itheima.*.test.*")}
)
public class SpringConfig {// 可添加BeanPostProcessor打印Bean加载日志,排查重复加载/*@Beanpublic BeanPostProcessor myBeanPostProcessor() {return new BeanPostProcessor() {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) {System.out.println("加载Bean:" + beanName);return bean;}};}*/
}对应的 SpringMVC 配置类需明确扫描 Controller,形成容器分工:
java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/*** SpringMVC配置类,仅扫描Controller*/
@Configuration
@EnableWebMvc
@ComponentScan(value = "com.itheima", includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)
})
public class SpringMvcConfig {
}避坑建议
扫描范围忌过宽:若 Spring 配置类扫描范围写
com,可能加载到依赖包中的 Bean,导致内存溢出或冲突,必须精确到项目包(如com.itheima)。避免注解混用:若 Controller 同时标注
@Service,即使排除@Controller,仍会被 Spring 容器扫描,需严格遵循分层注解规范。
(2)MyBatis 代理对象生成与注入(完整代码 + 源码关联)
Mapper 接口的动态代理依赖 MapperScannerConfigurer,以下补充完整配置,并关联 MyBatis 与 Spring 的衔接代码。
配置 Mapper 扫描
java
import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisConfig {@Beanpublic MapperScannerConfigurer mapperScannerConfigurer() {MapperScannerConfigurer scanner = new MapperScannerConfigurer();// 扫描Mapper接口所在包scanner.setBasePackage("com.itheima.mapper");// 关联SqlSessionFactory(Spring会自动注入匹配的Bean)scanner.setSqlSessionFactoryBeanName("sqlSessionFactory");return scanner;} }底层代理逻辑拆解
Mapper 代理的核心是
MapperFactoryBean
,其
getObject()
方法最终调用 MyBatis 的
MapperProxyFactory
生成代理对象,简化源码逻辑如下:
java
// MyBatis-Spring 核心类简化逻辑 public class MapperFactoryBean<T> implements FactoryBean<T> {private Class<T> mapperInterface;private SqlSession sqlSession; @Overridepublic T getObject() throws Exception {// 生成Mapper代理对象return sqlSession.getMapper(mapperInterface);} @Overridepublic Class<T> getObjectType() {return mapperInterface;} }
避坑建议
接口命名忌冲突:若两个 Mapper 接口同名(不同包),
MapperScannerConfigurer会优先注册后扫描到的 Bean,导致前一个被覆盖,建议按 “业务模块 + 功能” 命名(如BookMapper、UserMapper)。禁用默认接口方法:Java 8+ 的接口默认方法无法被 MyBatis 代理,若在 Mapper 中写
default void test() {},调用时会抛出UnsupportedOperationException。
2. 事务管理的底层实现(多场景代码 + 异常案例)
@Transactional 的底层是 AOP 动态代理,以下补充不同场景的事务配置,并剖析常见失效案例。
(1)完整事务配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement // 开启事务管理,注册事务切面
public class TransactionConfig {// 事务管理器依赖数据源@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}(2)多场景事务代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
// 场景1:嵌套事务,子事务独立提交/回滚@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)public void updateBookStock(Integer bookId, Integer num) throws Exception {// 库存扣减逻辑if (num < 0) {throw new Exception("库存不能为负数"); // 触发回滚}// jdbcTemplate.update("update book set stock = stock - ? where id = ?", num, bookId);}
// 场景2:只读事务,优化查询性能@Transactional(readOnly = true)public Object getBookById(Integer id) {// 仅查询操作,设置readOnly=true后数据库会关闭写锁return null;}
}(3)高频事务失效案例与修复
| 失效场景 | 错误代码 | 修复方案 |
|---|---|---|
| 非 public 方法 | @Transactional private void deductStock() {} | 改为 public 方法:@Transactional public void deductStock() {} |
| 同类方法调用 | public void buyBook() { // 同类调用,AOP无法拦截 deductStock(); } @Transactional public void deductStock() {} | 通过 AopContext 获取代理对象:public void buyBook() { ((BookService)AopContext.currentProxy()).deductStock(); }需开启暴露代理:@EnableAspectJAutoProxy(exposeProxy = true) |
| 异常类型不匹配 | @Transactional public void deductStock() { try { // 异常被捕获 } catch (Exception e) {} } | 1. 抛出 RuntimeException 或指定异常:@Transactional(rollbackFor = Exception.class)2. 捕获后手动抛出:catch (Exception e) {throw new RuntimeException(e);} |
3. 整合坑点与排查技巧(补充日志与调试手段)
| 问题 | 底层原因 | 进阶排查方案 |
|---|---|---|
| Mapper 注入失败 | Mapper 代理对象未注册到容器 | 1. 打印容器中 Bean 名称:applicationContext.getBeanDefinitionNames()2. 检查 Mapper 接口是否加 @Mapper 或扫描配置正确 |
| 数据源连接超时 | 连接池参数不合理,如最大连接数过小 | 1. 配置 Druid 监控:spring.datasource.druid.stat-view-servlet.enabled=true2. 访问 http://localhost:8080/druid 查看连接池状态 |
| 事务提交后数据未更新 | 多数据源场景下事务管理器未指定 | 明确绑定数据源:@Transactional(transactionManager = "bookDataSourceTransactionManager") |
二、统一结果封装:序列化定制与多场景适配(代码细化 + 避坑)
统一结果类需兼顾序列化规范、类型安全与多端适配,以下补充完整工具类及序列化异常处理。
1. 序列化定制(完整 Result 类 + 全局配置)
(1)增强版 Result 类(含状态码枚举)
java
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.time.LocalDateTime;
/*** 全局统一结果类* 避坑点:泛型不可省略,否则前端解析会出现类型混乱*/
@Data
public class Result<T> {// 状态码枚举,避免硬编码public enum Code {SUCCESS(20000, "操作成功"),GET_ERR(20040, "查询失败"),SYSTEM_ERR(50000, "系统异常");
private final Integer code;private final String msg;
Code(Integer code, String msg) {this.code = code;this.msg = msg;}}
private Integer code;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime timestamp;private T data;@JsonInclude(JsonInclude.Include.NON_NULL) // 空值不返回private String msg;
// 私有构造,通过静态方法创建private Result(Integer code, T data, String msg) {this.code = code;this.data = data;this.msg = msg;this.timestamp = LocalDateTime.now();}
// 成功响应(带数据)public static <T> Result<T> success(T data) {return new Result<>(Code.SUCCESS.code, data, Code.SUCCESS.msg);}
// 失败响应(自定义消息)public static <T> Result<T> error(Integer code, String msg) {return new Result<>(code, null, msg);}
}(2)全局序列化配置(解决 LocalDateTime 序列化异常)
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Configuration
public class JacksonConfig {@Beanpublic MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {ObjectMapper objectMapper = new ObjectMapper();JavaTimeModule timeModule = new JavaTimeModule();// 定制LocalDateTime序列化格式timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));objectMapper.registerModule(timeModule);// 禁用空对象序列化objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS);return new MappingJackson2HttpMessageConverter(objectMapper);}
}避坑建议
禁用空对象序列化:若 Result 中 data 为某个空对象(如
new Book()),未禁用FAIL_ON_EMPTY_BEANS会抛出序列化异常,需在配置中显式关闭。时间戳时区统一:前端若显示时间偏差,大概率是未指定
timezone = "GMT+8",或服务器时区与数据库时区不一致,建议统一设置为 UTC+8。
2. 泛型适配与 Swagger 联动(完整接口示例)
java
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
public class BookController {
@Operation(summary = "根据ID查询图书")@ApiResponses({@ApiResponse(responseCode = "20000", description = "查询成功",content = @Content(schema = @Schema(implementation = Result.class))),@ApiResponse(responseCode = "20040", description = "图书不存在")})@GetMapping("/{id}")public Result<Book> getById(@PathVariable Integer id) {// 模拟查询Book book = new Book(id, "Spring实战", 59.9);return book != null ? Result.success(book) : Result.error(Result.Code.GET_ERR.code, "图书不存在");}
// 静态内部类,模拟Book实体@Datastatic class Book {private Integer id;private String name;private Double price;
public Book(Integer id, String name, Double price) {this.id = id;this.name = name;this.price = price;}}
}避坑建议
Swagger 需指定泛型类型:若直接标注
@Schema(implementation = Result.class),文档中 data 字段类型会显示为 Object,需结合@Schema(type = "object", ref = "#/components/schemas/Book")明确类型。避免泛型擦除问题:若返回
Result而非Result,前端解析时可能无法识别 data 结构,需严格指定泛型。
三、统一异常处理:异常体系与预警机制(代码补全 + 预警优化)
完善分级异常体系,并补充预警机制的异常处理、日志规范,避免预警功能引发次生问题。
1. 完整分级异常体系
java
/*** 顶级异常,封装错误码和消息*/
public abstract class BaseException extends RuntimeException {private final Integer code;
public BaseException(Integer code, String message) {super(message);this.code = code;}
public Integer getCode() {return code;}
}
/*** 系统异常:数据库、缓存等底层问题*/
public class SystemException extends BaseException {public SystemException(Integer code, String message) {super(code, message);}
// 预定义常见系统异常public static SystemException DB_CONN_ERR() {return new SystemException(50001, "数据库连接失败");}
public static SystemException CACHE_ERR() {return new SystemException(50002, "缓存服务异常");}
}
/*** 业务异常:用户操作、参数校验等问题*/
public class BusinessException extends BaseException {public BusinessException(Integer code, String message) {super(code, message);}
}2. 异常处理器(含预警重试机制)
java
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestControllerAdvice
@Slf4j
public class ProjectExceptionAdvice {private final RestTemplate restTemplate = new RestTemplate();private final ObjectMapper objectMapper = new ObjectMapper();private static final String DING_TALK_URL = "https://oapi.dingtalk.com/robot/send?access_token=xxx";
// 处理系统异常@ExceptionHandler(SystemException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Result<Void> handleSystemException(SystemException ex) {log.error("系统异常:{}", ex.getMessage(), ex); // 打印堆栈,便于排查// 发送预警,带重试机制sendDingTalkMsgWithRetry("系统异常:" + ex.getMessage());return Result.error(ex.getCode(), ex.getMessage());}
// 处理业务异常@ExceptionHandler(BusinessException.class)public Result<Void> handleBusinessException(BusinessException ex) {log.warn("业务异常:{}", ex.getMessage()); // 警告级别,无需堆栈return Result.error(ex.getCode(), ex.getMessage());}
// 处理未捕获的通用异常@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Result<Void> handleException(Exception ex) {log.error("未知异常:", ex); // 必须打印完整堆栈sendDingTalkMsgWithRetry("未知异常:" + ex.getMessage());return Result.error(Result.Code.SYSTEM_ERR.code, "系统繁忙,请稍后再试");}
// 预警重试机制,避免网络抖动导致预警失败private void sendDingTalkMsgWithRetry(String content) {int retryCount = 3;for (int i = 0; i < retryCount; i++) {try {Map<String, Object> msg = new HashMap<>();msg.put("msgtype", "text");Map<String, String> text = new HashMap<>();text.put("content", content);msg.put("text", text);restTemplate.postForObject(DING_TALK_URL, msg, String.class);return;} catch (Exception e) {log.error("预警发送失败,重试第{}次", i + 1, e);try {TimeUnit.SECONDS.sleep(1); // 重试间隔1秒} catch (InterruptedException ignored) {Thread.currentThread().interrupt();}}}log.error("预警发送失败,已达最大重试次数");}
}避坑建议
日志级别区分:业务异常用
warn级别,系统异常用error级别,避免日志刷屏;未知异常必须打印完整堆栈,否则无法定位问题。预警加重试与熔断:若钉钉接口临时不可用,无重试机制会导致预警丢失,重试次数建议控制在 3 次内,避免引发连锁超时。
四、拦截器:进阶用法与底层机制(代码补全 + 执行流程可视化)
深化拦截器执行流程的代码验证,并补充分布式限流、注解式拦截的完整实现,规避拦截范围与执行顺序的坑。
1. 拦截器执行流程(带日志打印)
通过日志直观展示 preHandle、postHandle、afterCompletion 的执行顺序:
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {log.info("【LogInterceptor】preHandle:请求路径{}", request.getRequestURI());return true; // 返回true才会继续执行后续流程}
@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) {log.info("【LogInterceptor】postHandle:处理器执行完成");}
@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {log.info("【LogInterceptor】afterCompletion:视图渲染完成,异常信息{}", ex);}
}注册拦截器:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**") // 拦截所有路径.excludePathPatterns("/login"); // 排除登录接口}
}执行日志示例
plaintext
【LogInterceptor】preHandle:请求路径/books/1 【LogInterceptor】postHandle:处理器执行完成 【LogInterceptor】afterCompletion:视图渲染完成,异常信息null
避坑建议
afterCompletion必执行:即使preHandle返回 false,已执行的拦截器的afterCompletion仍会执行,需避免在此方法中做业务逻辑处理。排除静态资源:若拦截器拦截
/**且未排除.js、.css等静态资源,会导致前端资源加载失败,需通过excludePathPatterns排除。
2. 分布式限流(Redis+Lua 脚本)
本地限流无法适配分布式系统,以下是 Redis+Lua 的分布式限流实现,避免并发问题:
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
public class RateLimitInterceptor implements HandlerInterceptor {private final StringRedisTemplate redisTemplate;private final ObjectMapper objectMapper;// 限流脚本:1分钟内最多100次请求private static final String RATE_LIMIT_SCRIPT = """local key = KEYS[1]local limit = tonumber(ARGV[1])local expire = tonumber(ARGV[2])local current = redis.call('incr', key)if current == 1 thenredis.call('expire', key, expire)endreturn current <= limit""";
public RateLimitInterceptor(StringRedisTemplate redisTemplate, ObjectMapper objectMapper) {this.redisTemplate = redisTemplate;this.objectMapper = objectMapper;}
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String ip = request.getRemoteAddr();String uri = request.getRequestURI();String key = "rate_limit:" + ip + ":" + uri;Integer limit = 100;Integer expire = 60;
// 执行Lua脚本Boolean allowed = redisTemplate.execute(new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Boolean.class),Collections.singletonList(key),limit.toString(),expire.toString());
if (Boolean.FALSE.equals(allowed)) {response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(Result.error(60001, "请求过于频繁")));return false;}return true;}
}避坑建议
用 Lua 脚本保证原子性:若分两次执行
incr和expire,高并发下会出现超限流问题,Lua 脚本可确保两个操作原子执行。避免 Redis 雪崩:限流键设置过期时间,防止键堆积占用内存;同时给 Redis 配置哨兵模式,避免单点故障。
五、前后台联调:跨域、文件传输与接口安全(问题复现 + 修复)
针对联调中高频问题,复现错误配置并给出完整修复方案,确保生产环境可用。
1. 跨域问题(错误配置复现 + 修复)
(1)常见错误配置
java
// 错误配置:allowedOrigins为*时,不能允许credentials(cookie等)
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*") // 通配符.allowCredentials(true); // 冲突,会导致跨域失败}
}(2)正确配置
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("http://localhost:8080") // 明确前端地址.allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*").allowCredentials(true).maxAge(3600);}
}避坑建议
避免通配符与凭证冲突:
allowedOrigins("*")和allowCredentials(true)不能同时配置,浏览器会拦截响应。排查网关跨域:若项目有 Nginx 或 Spring Cloud Gateway,需确保跨域配置仅在一处生效,否则会因重复设置响应头导致失败。
2. 文件上传(完整实现 + 异常处理)
java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@RestController
public class FileUploadController {
private static final String UPLOAD_PATH = "D:/uploads/";
@PostMapping("/upload")public Result<String> upload(@RequestParam("file") MultipartFile file) {// 1. 校验文件是否为空if (file.isEmpty()) {return Result.error(60002, "上传文件不能为空");}// 2. 校验文件格式String originalFilename = file.getOriginalFilename();if (!originalFilename.endsWith(".png") && !originalFilename.endsWith(".jpg")) {return Result.error(60003, "仅支持png和jpg格式");}// 3. 生成唯一文件名,避免覆盖String filename = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));File destFile = new File(UPLOAD_PATH + filename);// 4. 创建目录(若不存在)if (!destFile.getParentFile().exists()) {destFile.getParentFile().mkdirs();}try {file.transferTo(destFile);return Result.success("/uploads/" + filename);} catch (IOException e) {log.error("文件上传失败", e);return Result.error(60004, "文件上传失败");}}
}避坑建议
生成唯一文件名:若直接用原文件名,同名文件会被覆盖,建议用 UUID 或 “时间戳 + 原文件名” 命名。
避免本地存储:生产环境不要存储文件到服务器本地,建议用 MinIO 或阿里云 OSS,同时记录文件存储路径到数据库。
3. JWT Token 校验(完整工具类 + 过期处理)
(1)JWT 工具类
java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
public class JwtUtil {// 密钥(生产环境需配置在配置中心,避免硬编码)private static final String SECRET = "itheima-secret-key-32bytes-long-123456";// 过期时间:2小时private static final long EXPIRATION = 7200000;
// 生成Tokenpublic static String generateToken(String username) {SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));return Jwts.builder().setSubject(username).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)).signWith(key).compact();}
// 验证Token并获取用户名public static String verifyToken(String token) {SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();// 校验过期时间if (claims.getExpiration().before(new Date())) {throw new RuntimeException("Token已过期");}return claims.getSubject();}
}(2)JWT 拦截器完善
java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
public class JwtInterceptor implements HandlerInterceptor {private final ObjectMapper objectMapper;
public JwtInterceptor(ObjectMapper objectMapper) {this.objectMapper = objectMapper;}
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("Authorization");if (token == null || !token.startsWith("Bearer ")) {response.getWriter().write(objectMapper.writeValueAsString(Result.error(401, "Token不存在")));return false;}try {JwtUtil.verifyToken(token.substring(7));} catch (RuntimeException e) {response.getWriter().write(objectMapper.writeValueAsString(Result.error(401, e.getMessage())));return false;}return true;}
}避坑建议
密钥长度足够:JWT 若使用 HS256 算法,密钥长度至少 256 位(32 个字符),否则会抛出
KeyLengthException。处理 Token 过期:前端需监听 401 响应,跳转至登录页重新获取 Token;服务端可生成刷新 Token,避免频繁登录。
总结
SSM 框架的企业级开发,核心在于吃透底层机制、规范代码编写、提前规避坑点。本文通过补充完整代码示例、拆解底层逻辑、复现高频错误,从 SSM 整合、统一结果 / 异常处理、拦截器拓展到前后端联调,形成一套完整的实战体系。开发中需重点关注容器协同、事务一致性、序列化规范、接口安全等核心问题,同时养成日志分级、异常捕获、配置规范的习惯,才能构建出高可用、易维护的 Web 应用。
面试题 1(SSM 整合底层)
题目:SSM 整合时,Controller 为何会出现重复加载的问题?底层原因是什么?请给出两种规避方案并说明各自的适用场景。
考察点:Spring 与 SpringMVC 容器关系、组件扫描机制、企业级配置规范
参考答案
底层原因
本质是 Spring 容器(RootApplicationContext)与 SpringMVC 容器(ServletApplicationContext)的父子容器作用域冲突。Spring 容器通常管理 Service、Mapper 等全局 Bean,SpringMVC 容器管理 Controller;若两者扫描范围重叠(如均扫描com.itheima),Controller 会被两个容器分别实例化,导致重复加载。
规避方案及适用场景
// Spring配置类排除Controller @ComponentScan(value = "com.itheima",excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)) // SpringMVC配置类仅扫描Controller @ComponentScan(value = "com.itheima",includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class))
方案 1:扫描范围精准划分。Spring 配置类扫描根包并排除@Controller,SpringMVC 配置类仅扫描 Controller。适用于分层清晰的中小型项目,配置简单易维护。代码示例:
方案 2:按包路径分层扫描。将 Controller 统一放在com.itheima.controller子包,Spring 配置类扫描com.itheima.service、com.itheima.mapper,SpringMVC 扫描com.itheima.controller。适用于大型项目,避免扫描范围泄露导致的冲突。
面试题 2(事务进阶)
题目:使用@Transactional注解时,列举 3 种常见的事务失效场景并说明底层原因,针对 “同类方法调用导致事务失效” 给出两种企业级解决方案。
考察点:Spring 事务底层代理机制、实战坑点排查、分布式场景适配
参考答案
3 种事务失效场景及底层原因
场景 1:注解标注在非 public 方法。底层 Spring 事务通过 JDK 动态代理或 CGLIB 代理实现,代理逻辑仅拦截 public 方法,非 public 方法无法触发切面。
场景 2:同类方法内部调用。同类方法调用时,直接通过 this 调用而非代理对象调用,跳过 AOP 拦截链,事务切面未执行。
场景 3:未指定rollbackFor且抛出检查型异常。默认@Transactional仅对RuntimeException和Error回滚,检查型异常(如IOException)不会触发回滚。
同类方法调用的解决方案
@EnableAspectJAutoProxy(exposeProxy = true)
public class BookService {public void buyBook() {((BookService)AopContext.currentProxy()).deductStock();}@Transactionalpublic void deductStock() {}
}方案 1:通过AopContext获取代理对象。开启暴露代理配置,调用代理对象的目标方法触发切面。适用于单体项目,代码侵入性低。
方案 2:依赖注入自身 Bean。通过构造器注入 Service 自身,调用注入对象的方法。适用于无法使用 AopContext的场景(如低版本 Spring),需注意避免循环依赖风险。
面试题 3(拦截器与 AOP)
题目:SpringMVC 的拦截器和 Spring AOP 都能实现功能增强,请从执行时机、底层原理、适用场景三个维度对比两者的差异,并说明为何接口限流更适合用拦截器实现。
考察点:拦截器执行流程、AOP 底层机制、场景化技术选型
参考答案
三者维度差异表
| 对比维度 | SpringMVC 拦截器 | Spring AOP |
|---|---|---|
| 执行时机 | 围绕 DispatcherServlet 的处理器执行链,仅作用于 Controller 请求 | 可作用于所有 Spring Bean 的方法,执行时机贯穿 Bean 方法生命周期 |
| 底层原理 | 基于 Java 回调机制,通过HandlerExecutionChain构建执行链 | 基于动态代理(JDK/CGLIB)和切面织入,通过Advisor和Advice定义增强逻辑 |
| 适用场景 | 请求级别的功能增强(如登录校验、跨域处理、接口限流) | 业务级别的功能增强(如事务控制、日志记录、权限细化校验) |
接口限流适合用拦截器的原因
拦截器聚焦请求流程,可在请求进入 Controller 前直接拦截非法请求,减少无效的业务逻辑执行,比 AOP 更高效。
拦截器可直接获取HttpServletRequest对象,轻松获取 IP、请求 URI 等限流关键信息,无需额外注入上下文。
拦截器支持快速响应(如直接向HttpServletResponse写入限流提示),无需等待业务方法执行,用户体验更优。
面试题 4(统一异常处理)
题目:请设计一套企业级的分级异常体系,说明设计思路,同时说明如何避免异常处理过程中出现的预警次生问题。
考察点:异常体系设计、日志规范、生产环境稳定性保障
参考答案
分级异常体系设计
// 顶级抽象异常
public abstract class BaseException extends RuntimeException {private final Integer code;public BaseException(Integer code, String message) {super(message);this.code = code;}// getter
}
// 系统异常(数据库、缓存等底层问题)
public class SystemException extends BaseException {public static SystemException DB_CONN_ERR() {return new SystemException(50001, "数据库连接失败");}
}
// 业务异常(用户操作、参数校验等)
public class BusinessException extends BaseException {public BusinessException(Integer code, String message) {super(code, message);}
}设计思路:基于异常影响范围和类型,划分顶级抽象异常 + 细分异常,统一封装错误码和消息,便于前端适配和问题定位。
代码实现:
避免预警次生问题的方案
预警重试机制:发送钉钉 / 企业微信预警时,添加 3 次内重试逻辑,间隔 1 秒,避免网络抖动导致预警丢失。
日志分级:业务异常用warn级别(无需堆栈),系统异常用error级别(打印完整堆栈),避免日志刷屏。
熔断保护:若预警接口持续失败,暂停预警 1 分钟,防止预警逻辑拖垮业务系统。
面试题 5(前后端联调)
题目:前后端分离项目中,同时遇到 JWT Token 失效和跨域两个问题,请分别说明问题原因,并给出一套可落地的整合解决方案,确保生产环境安全可用。
考察点:JWT 机制、跨域底层原理、生产环境安全配置
参考答案
问题原因
JWT Token 失效:Token 过期、密钥篡改或 Token 格式错误,导致服务端校验失败;
跨域:浏览器的同源策略限制,前端请求的协议、域名、端口与后端不一致,触发跨域拦截。
整合解决方案
JWT Token 失效处理
public String verifyToken(String token) {SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes());try {return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();} catch (Exception e) {throw new RuntimeException("Token无效或已过期");}
}服务端:开发 JWT 工具类,统一处理 Token 生成、校验,校验失败时返回 401 状态码;
前端:通过请求拦截器携带 Token,响应拦截器监听 401,跳转至登录页重新获取 Token。
跨域解决方案
配置全局跨域,明确允许的前端地址,避免通配符与凭证冲突;
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("https://xxx-frontend.com").allowedMethods("GET","POST").allowCredentials(true).maxAge(3600);}
}生产环境安全增强
JWT 密钥存储在配置中心(如 Nacos),避免硬编码;
跨域仅允许生产环境的前端域名,禁用本地测试域名;
对敏感接口(如支付),除 Token 外额外添加签名校验。
